@kuralle-syrinx/browser-client 4.4.0 → 4.5.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/dist/agent-state.d.ts +35 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +76 -0
- package/dist/agent-state.js.map +1 -0
- package/dist/audio.d.ts +61 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +276 -0
- package/dist/audio.js.map +1 -0
- package/dist/browser-opus.d.ts +41 -0
- package/dist/browser-opus.d.ts.map +1 -0
- package/dist/browser-opus.js +81 -0
- package/dist/browser-opus.js.map +1 -0
- package/dist/index.d.ts +270 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +744 -0
- package/dist/index.js.map +1 -0
- package/dist/session-metrics.d.ts +28 -0
- package/dist/session-metrics.d.ts.map +1 -0
- package/dist/session-metrics.js +60 -0
- package/dist/session-metrics.js.map +1 -0
- package/dist/session-record.d.ts +112 -0
- package/dist/session-record.d.ts.map +1 -0
- package/dist/session-record.js +181 -0
- package/dist/session-record.js.map +1 -0
- package/dist/transport.d.ts +16 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +3 -0
- package/dist/transport.js.map +1 -0
- package/dist/turn-recorder.d.ts +56 -0
- package/dist/turn-recorder.d.ts.map +1 -0
- package/dist/turn-recorder.js +168 -0
- package/dist/turn-recorder.js.map +1 -0
- package/dist/turn-timeline.d.ts +51 -0
- package/dist/turn-timeline.d.ts.map +1 -0
- package/dist/turn-timeline.js +92 -0
- package/dist/turn-timeline.js.map +1 -0
- package/dist/websocket-transport.d.ts +20 -0
- package/dist/websocket-transport.d.ts.map +1 -0
- package/dist/websocket-transport.js +92 -0
- package/dist/websocket-transport.js.map +1 -0
- package/package.json +35 -11
- package/src/agent-state.test.ts +143 -0
- package/src/audio.test.ts +130 -0
- package/src/index.test.ts +842 -0
- package/src/jitter-buffer.test.ts +275 -0
- package/src/session-metrics.test.ts +102 -0
- package/src/session-record.test.ts +219 -0
- package/src/studio-page.test.ts +69 -0
- package/src/transport.test.ts +233 -0
- package/src/turn-recorder.test.ts +138 -0
- package/src/turn-timeline.test.ts +151 -0
- package/index.html +0 -1073
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// TurnAudioRecorder — keeps the microphone PCM you already streamed, sliced per turn,
|
|
4
|
+
// so a turn can be saved as a fixture. Without this the studio sends audio to the
|
|
5
|
+
// server and keeps none of it, and the "capture a fixture" loop cannot close.
|
|
6
|
+
//
|
|
7
|
+
// Dependency-free and DOM-free, exported from the `/turn-recorder` subpath: it takes
|
|
8
|
+
// Int16 frames and returns WAV bytes, so the same code runs in a browser, in Node, and
|
|
9
|
+
// in CI. It holds no socket and no AudioContext — you push it the frames you were
|
|
10
|
+
// sending anyway, and the server's own messages drive the turn boundaries.
|
|
11
|
+
export const DEFAULT_RECORDER_OPTIONS = {
|
|
12
|
+
maxTurns: 10,
|
|
13
|
+
preRollMs: 300,
|
|
14
|
+
maxTurnMs: 60_000,
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_SAMPLE_RATE_HZ = 16_000;
|
|
17
|
+
/** Minimal RIFF/WAVE header + samples. Mono PCM16 — the format every STT accepts. */
|
|
18
|
+
export function encodeWav(samples, sampleRateHz) {
|
|
19
|
+
if (!Number.isFinite(sampleRateHz) || sampleRateHz <= 0) {
|
|
20
|
+
throw new Error(`encodeWav: invalid sampleRateHz ${String(sampleRateHz)}`);
|
|
21
|
+
}
|
|
22
|
+
const dataBytes = samples.length * 2;
|
|
23
|
+
const out = new Uint8Array(44 + dataBytes);
|
|
24
|
+
const view = new DataView(out.buffer);
|
|
25
|
+
const ascii = (offset, text) => {
|
|
26
|
+
for (let i = 0; i < text.length; i += 1)
|
|
27
|
+
out[offset + i] = text.charCodeAt(i);
|
|
28
|
+
};
|
|
29
|
+
ascii(0, "RIFF");
|
|
30
|
+
view.setUint32(4, 36 + dataBytes, true);
|
|
31
|
+
ascii(8, "WAVE");
|
|
32
|
+
ascii(12, "fmt ");
|
|
33
|
+
view.setUint32(16, 16, true); // PCM chunk size
|
|
34
|
+
view.setUint16(20, 1, true); // PCM
|
|
35
|
+
view.setUint16(22, 1, true); // mono
|
|
36
|
+
view.setUint32(24, sampleRateHz, true);
|
|
37
|
+
view.setUint32(28, sampleRateHz * 2, true); // byte rate
|
|
38
|
+
view.setUint16(32, 2, true); // block align
|
|
39
|
+
view.setUint16(34, 16, true); // bits per sample
|
|
40
|
+
ascii(36, "data");
|
|
41
|
+
view.setUint32(40, dataBytes, true);
|
|
42
|
+
// Copy through a DataView so the result is little-endian on every host, rather than
|
|
43
|
+
// inheriting the platform's byte order via Int16Array.set.
|
|
44
|
+
for (let i = 0; i < samples.length; i += 1) {
|
|
45
|
+
view.setInt16(44 + i * 2, samples[i] ?? 0, true);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export class TurnAudioRecorder {
|
|
50
|
+
options;
|
|
51
|
+
sampleRateHz = DEFAULT_SAMPLE_RATE_HZ;
|
|
52
|
+
/** Rolling pre-roll, trimmed to preRollMs on every push. */
|
|
53
|
+
preRoll = [];
|
|
54
|
+
preRollLength = 0;
|
|
55
|
+
open;
|
|
56
|
+
/** Sealed but not yet attributed to a turnId — turnId arrives with a later message. */
|
|
57
|
+
pending;
|
|
58
|
+
turns = new Map();
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this.options = { ...DEFAULT_RECORDER_OPTIONS, ...options };
|
|
61
|
+
}
|
|
62
|
+
/** Push the uplink frame you are already sending. Cheap: retains the reference, copies on seal. */
|
|
63
|
+
pushFrame(frame) {
|
|
64
|
+
if (frame.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
if (this.open) {
|
|
67
|
+
const cap = Math.floor((this.options.maxTurnMs / 1000) * this.sampleRateHz);
|
|
68
|
+
if (this.open.length >= cap) {
|
|
69
|
+
this.open.truncated = true;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.open.samples.push(frame);
|
|
73
|
+
this.open.length += frame.length;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.preRoll.push(frame);
|
|
77
|
+
this.preRollLength += frame.length;
|
|
78
|
+
const keep = Math.floor((this.options.preRollMs / 1000) * this.sampleRateHz);
|
|
79
|
+
while (this.preRollLength - (this.preRoll[0]?.length ?? 0) >= keep && this.preRoll.length > 1) {
|
|
80
|
+
this.preRollLength -= this.preRoll.shift()?.length ?? 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Feed the same messages you feed the SessionRecord. Turn boundaries come from the server. */
|
|
84
|
+
onMessage(message) {
|
|
85
|
+
const type = message.type;
|
|
86
|
+
if (type === "ready") {
|
|
87
|
+
const rate = message.audio?.inputSampleRateHz;
|
|
88
|
+
if (typeof rate === "number" && Number.isFinite(rate) && rate > 0)
|
|
89
|
+
this.sampleRateHz = rate;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const turnId = typeof message.turnId === "string"
|
|
93
|
+
? message.turnId
|
|
94
|
+
: undefined;
|
|
95
|
+
if (type === "speech_started") {
|
|
96
|
+
// Start from the pre-roll so the onset is present.
|
|
97
|
+
this.open = { samples: [...this.preRoll], length: this.preRollLength, truncated: false };
|
|
98
|
+
this.preRoll = [];
|
|
99
|
+
this.preRollLength = 0;
|
|
100
|
+
if (turnId !== undefined)
|
|
101
|
+
this.attributeOnSeal = turnId;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (type === "speech_ended" || type === "stt_output" || type === "turn_complete") {
|
|
105
|
+
this.seal(turnId);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
attributeOnSeal;
|
|
110
|
+
seal(turnId) {
|
|
111
|
+
if (this.open) {
|
|
112
|
+
const merged = new Int16Array(this.open.length);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const chunk of this.open.samples) {
|
|
115
|
+
merged.set(chunk, offset);
|
|
116
|
+
offset += chunk.length;
|
|
117
|
+
}
|
|
118
|
+
this.pending = { samples: merged, truncated: this.open.truncated };
|
|
119
|
+
this.open = undefined;
|
|
120
|
+
}
|
|
121
|
+
if (!this.pending)
|
|
122
|
+
return;
|
|
123
|
+
const id = turnId ?? this.attributeOnSeal;
|
|
124
|
+
if (id === undefined)
|
|
125
|
+
return; // wait for a message that names the turn
|
|
126
|
+
this.attributeOnSeal = undefined;
|
|
127
|
+
this.turns.set(id, this.pending);
|
|
128
|
+
this.pending = undefined;
|
|
129
|
+
// Bounded by construction: audio blobs, not the records, are the memory risk.
|
|
130
|
+
while (this.turns.size > this.options.maxTurns) {
|
|
131
|
+
const oldest = this.turns.keys().next().value;
|
|
132
|
+
if (oldest === undefined)
|
|
133
|
+
break;
|
|
134
|
+
this.turns.delete(oldest);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** WAV bytes for a turn, or undefined if it was never captured or has been evicted. */
|
|
138
|
+
getWav(turnId) {
|
|
139
|
+
const entry = this.turns.get(turnId);
|
|
140
|
+
return entry ? encodeWav(entry.samples, this.sampleRateHz) : undefined;
|
|
141
|
+
}
|
|
142
|
+
/** What audio is retained right now, oldest first. */
|
|
143
|
+
list() {
|
|
144
|
+
return [...this.turns.entries()].map(([turnId, entry]) => ({
|
|
145
|
+
turnId,
|
|
146
|
+
sampleRateHz: this.sampleRateHz,
|
|
147
|
+
durationMs: Math.round((entry.samples.length / this.sampleRateHz) * 1000),
|
|
148
|
+
byteLength: entry.samples.length * 2,
|
|
149
|
+
truncated: entry.truncated,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
/** Total retained audio in bytes — what a memory readout should show. */
|
|
153
|
+
retainedBytes() {
|
|
154
|
+
let total = 0;
|
|
155
|
+
for (const entry of this.turns.values())
|
|
156
|
+
total += entry.samples.length * 2;
|
|
157
|
+
return total;
|
|
158
|
+
}
|
|
159
|
+
reset() {
|
|
160
|
+
this.preRoll = [];
|
|
161
|
+
this.preRollLength = 0;
|
|
162
|
+
this.open = undefined;
|
|
163
|
+
this.pending = undefined;
|
|
164
|
+
this.attributeOnSeal = undefined;
|
|
165
|
+
this.turns.clear();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=turn-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-recorder.js","sourceRoot":"","sources":["../src/turn-recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { TurnRecord } from "./session-record.js";
|
|
2
|
+
export interface TimelineSegment {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
/** Plain language. The reader does not know what `eos.turn_complete` is. */
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly startMs: number;
|
|
7
|
+
readonly durationMs: number;
|
|
8
|
+
/** True for the longest segment — where the time actually went. */
|
|
9
|
+
readonly slowest: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface TurnTimeline {
|
|
12
|
+
readonly turnId: string;
|
|
13
|
+
readonly segments: readonly TimelineSegment[];
|
|
14
|
+
readonly totalMs: number;
|
|
15
|
+
/** Why there is nothing to draw, when there is nothing to draw. */
|
|
16
|
+
readonly unavailable?: "no-metrics" | "insufficient-marks";
|
|
17
|
+
/**
|
|
18
|
+
* Set when the agent replied implausibly fast, which almost always means the
|
|
19
|
+
* endpointer fired while the caller was still speaking. Most tools celebrate
|
|
20
|
+
* this number; it is usually a defect.
|
|
21
|
+
*/
|
|
22
|
+
readonly suspiciouslyFast?: {
|
|
23
|
+
readonly totalMs: number;
|
|
24
|
+
readonly floorMs: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Below this, a reply is not "fast" — the endpointer almost certainly cut the
|
|
29
|
+
* caller off. Asserting a floor as well as a ceiling is the inversion no
|
|
30
|
+
* surveyed framework supports.
|
|
31
|
+
*/
|
|
32
|
+
export declare const FAST_TURN_FLOOR_MS = 700;
|
|
33
|
+
export declare function buildTurnTimeline(turn: TurnRecord): TurnTimeline;
|
|
34
|
+
export declare function buildTimelines(turns: readonly TurnRecord[]): readonly TurnTimeline[];
|
|
35
|
+
export type EndpointingMarkerKind = "typed" | "endpoint" | "unknown";
|
|
36
|
+
export interface EndpointingMarker {
|
|
37
|
+
readonly kind: EndpointingMarkerKind;
|
|
38
|
+
/** Plain language — never a packet or message name (`eos.turn_complete`, raw `provider_stt`, …). */
|
|
39
|
+
readonly text: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Turn an endpointing decision into the words a person reads. Internal type
|
|
43
|
+
* values (`provider_stt`, `smart_turn`, `timer`, `text`, `force_finalized`, …)
|
|
44
|
+
* stay internal; this is the only place they cross into user-facing text.
|
|
45
|
+
*
|
|
46
|
+
* A genuinely unknown decision is returned as `kind: "unknown"` rather than
|
|
47
|
+
* guessed — a debugging surface that fabricates a cause is worse than one that
|
|
48
|
+
* says it does not know.
|
|
49
|
+
*/
|
|
50
|
+
export declare function endpointingMarker(owner?: string, reason?: string): EndpointingMarker;
|
|
51
|
+
//# sourceMappingURL=turn-timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-timeline.d.ts","sourceRoot":"","sources":["../src/turn-timeline.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,qBAAqB,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,YAAY,GAAG,oBAAoB,CAAC;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACpF;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAiBtC,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CA+BhE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,YAAY,EAAE,CAEpF;AASD,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,oGAAoG;IACpG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA0BpF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
//
|
|
3
|
+
// Turn timeline — the per-turn latency waterfall, computed from the `metrics`
|
|
4
|
+
// message the server already sends.
|
|
5
|
+
//
|
|
6
|
+
// "It cut me off" is not actionable; a lane showing speech ending at 2.1s and the
|
|
7
|
+
// endpoint firing at 2.16s is. This is the view no competing playground ships,
|
|
8
|
+
// and the data has been arriving in the browser all along.
|
|
9
|
+
//
|
|
10
|
+
// Pure and DOM-free, so a timeline can be rendered from a recorded fixture in a
|
|
11
|
+
// test rather than only from a live provider.
|
|
12
|
+
/**
|
|
13
|
+
* Below this, a reply is not "fast" — the endpointer almost certainly cut the
|
|
14
|
+
* caller off. Asserting a floor as well as a ceiling is the inversion no
|
|
15
|
+
* surveyed framework supports.
|
|
16
|
+
*/
|
|
17
|
+
export const FAST_TURN_FLOOR_MS = 700;
|
|
18
|
+
// Ordered marks. Each segment spans one mark to the next, so a missing mark
|
|
19
|
+
// collapses its segment rather than corrupting the ones around it.
|
|
20
|
+
const MARKS = [
|
|
21
|
+
{ field: "speechEndMs", label: "You stopped speaking" },
|
|
22
|
+
{ field: "textReadyMs", label: "Deciding you're done, transcribing, and thinking" },
|
|
23
|
+
// Segments are labelled by the mark that ENDS them, so this one covers
|
|
24
|
+
// textReady -> firstAudioByte: the reply text already exists and we are waiting on
|
|
25
|
+
// speech. That is time-to-first-audio, which `session-metrics` reports as
|
|
26
|
+
// "Voice (to first audio)" from the same field. Calling it "Thinking" made the two
|
|
27
|
+
// panels name one quantity two different things.
|
|
28
|
+
{ field: "firstAudioByteMs", label: "Voice (to first audio)" },
|
|
29
|
+
{ field: "firstAudioPlayedMs", label: "First audio out" },
|
|
30
|
+
{ field: "lastAudioPlayedMs", label: "Agent speaking" },
|
|
31
|
+
];
|
|
32
|
+
export function buildTurnTimeline(turn) {
|
|
33
|
+
const t = turn.timings;
|
|
34
|
+
if (!t) {
|
|
35
|
+
// The Workers path emits no `metrics` at all (proven by LDT-18). Say so
|
|
36
|
+
// rather than rendering an empty lane that looks like a zero-latency turn.
|
|
37
|
+
return { turnId: turn.turnId, segments: [], totalMs: 0, unavailable: "no-metrics" };
|
|
38
|
+
}
|
|
39
|
+
const points = MARKS.map(({ field, label }) => ({ label, at: t[field] })).filter((p) => typeof p.at === "number");
|
|
40
|
+
if (points.length < 2) {
|
|
41
|
+
return { turnId: turn.turnId, segments: [], totalMs: t.e2eMs ?? 0, unavailable: "insufficient-marks" };
|
|
42
|
+
}
|
|
43
|
+
const origin = points[0]?.at ?? 0;
|
|
44
|
+
const raw = points.slice(1).map((p, i) => {
|
|
45
|
+
const prev = points[i];
|
|
46
|
+
const startMs = (prev?.at ?? origin) - origin;
|
|
47
|
+
return { key: p.label, label: p.label, startMs, durationMs: Math.max(0, p.at - (prev?.at ?? origin)) };
|
|
48
|
+
});
|
|
49
|
+
const longest = raw.reduce((max, s) => Math.max(max, s.durationMs), 0);
|
|
50
|
+
const segments = raw.map((s) => ({ ...s, slowest: longest > 0 && s.durationMs === longest }));
|
|
51
|
+
const totalMs = t.e2eMs ?? (points[points.length - 1]?.at ?? origin) - origin;
|
|
52
|
+
const suspiciouslyFast = totalMs > 0 && totalMs < FAST_TURN_FLOOR_MS ? { totalMs, floorMs: FAST_TURN_FLOOR_MS } : undefined;
|
|
53
|
+
return { turnId: turn.turnId, segments, totalMs, ...(suspiciouslyFast ? { suspiciouslyFast } : {}) };
|
|
54
|
+
}
|
|
55
|
+
export function buildTimelines(turns) {
|
|
56
|
+
return turns.map(buildTurnTimeline);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Turn an endpointing decision into the words a person reads. Internal type
|
|
60
|
+
* values (`provider_stt`, `smart_turn`, `timer`, `text`, `force_finalized`, …)
|
|
61
|
+
* stay internal; this is the only place they cross into user-facing text.
|
|
62
|
+
*
|
|
63
|
+
* A genuinely unknown decision is returned as `kind: "unknown"` rather than
|
|
64
|
+
* guessed — a debugging surface that fabricates a cause is worse than one that
|
|
65
|
+
* says it does not know.
|
|
66
|
+
*/
|
|
67
|
+
export function endpointingMarker(owner, reason) {
|
|
68
|
+
if (owner === "text") {
|
|
69
|
+
return {
|
|
70
|
+
kind: "typed",
|
|
71
|
+
text: "You typed this turn — nothing transcribed you and nothing judged when you finished, so no endpointer ran.",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const who = owner === "provider_stt"
|
|
75
|
+
? "The speech-to-text provider"
|
|
76
|
+
: owner === "smart_turn"
|
|
77
|
+
? "The Smart Turn model"
|
|
78
|
+
: owner === "timer"
|
|
79
|
+
? "A turn timer"
|
|
80
|
+
: undefined;
|
|
81
|
+
if (who === undefined) {
|
|
82
|
+
return {
|
|
83
|
+
kind: "unknown",
|
|
84
|
+
text: "This backend did not report what ended the turn, so the cause is unknown.",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const text = reason === "force_finalized"
|
|
88
|
+
? `${who} force-finalized the transcript after a timeout, not a natural end of speech.`
|
|
89
|
+
: `${who} decided you had finished speaking.`;
|
|
90
|
+
return { kind: "endpoint", text };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=turn-timeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-timeline.js","sourceRoot":"","sources":["../src/turn-timeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ClientTransport, ClientTransportHandlers } from "./transport.js";
|
|
2
|
+
export interface WebSocketClientTransportOptions {
|
|
3
|
+
readonly protocols?: string | readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare class WebSocketClientTransport implements ClientTransport {
|
|
6
|
+
private readonly options;
|
|
7
|
+
private socket;
|
|
8
|
+
private pendingUrl;
|
|
9
|
+
private handlers;
|
|
10
|
+
constructor(options?: WebSocketClientTransportOptions);
|
|
11
|
+
get connected(): boolean;
|
|
12
|
+
setHandlers(handlers: ClientTransportHandlers): void;
|
|
13
|
+
connect(url: string): void;
|
|
14
|
+
disconnect(code?: number, reason?: string): void;
|
|
15
|
+
sendAudio(data: Uint8Array | ArrayBuffer): void;
|
|
16
|
+
sendJson(value: unknown): void;
|
|
17
|
+
private requireOpenSocket;
|
|
18
|
+
private dispatchMessage;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=websocket-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-transport.d.ts","sourceRoot":"","sources":["../src/websocket-transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAE/E,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CACjD;AAED,qBAAa,wBAAyB,YAAW,eAAe;IAKlD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAA+B;gBAElB,OAAO,GAAE,+BAAoC;IAE1E,IAAI,SAAS,IAAI,OAAO,CAGvB;IAED,WAAW,CAAC,QAAQ,EAAE,uBAAuB,GAAG,IAAI;IAIpD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAqC1B,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAKhD,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAM/C,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI9B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,eAAe;CAiBxB"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export class WebSocketClientTransport {
|
|
3
|
+
options;
|
|
4
|
+
socket = null;
|
|
5
|
+
pendingUrl = null;
|
|
6
|
+
handlers = {};
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
get connected() {
|
|
11
|
+
const state = this.socket?.readyState;
|
|
12
|
+
return state === WebSocket.OPEN;
|
|
13
|
+
}
|
|
14
|
+
setHandlers(handlers) {
|
|
15
|
+
this.handlers = handlers;
|
|
16
|
+
}
|
|
17
|
+
connect(url) {
|
|
18
|
+
const state = this.socket?.readyState;
|
|
19
|
+
if (state === WebSocket.OPEN)
|
|
20
|
+
return;
|
|
21
|
+
if (state === WebSocket.CLOSING) {
|
|
22
|
+
this.pendingUrl = url;
|
|
23
|
+
const closingSocket = this.socket;
|
|
24
|
+
const onClose = () => {
|
|
25
|
+
closingSocket.removeEventListener("close", onClose);
|
|
26
|
+
if (this.socket !== closingSocket)
|
|
27
|
+
return;
|
|
28
|
+
const nextUrl = this.pendingUrl;
|
|
29
|
+
this.pendingUrl = null;
|
|
30
|
+
this.socket = null;
|
|
31
|
+
if (nextUrl)
|
|
32
|
+
this.connect(nextUrl);
|
|
33
|
+
};
|
|
34
|
+
closingSocket.addEventListener("close", onClose);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (state === WebSocket.CONNECTING)
|
|
38
|
+
return;
|
|
39
|
+
this.pendingUrl = null;
|
|
40
|
+
const socket = new WebSocket(url, this.options.protocols);
|
|
41
|
+
socket.binaryType = "arraybuffer";
|
|
42
|
+
this.socket = socket;
|
|
43
|
+
socket.addEventListener("open", () => {
|
|
44
|
+
this.handlers.onOpen?.();
|
|
45
|
+
});
|
|
46
|
+
socket.addEventListener("close", (event) => {
|
|
47
|
+
this.handlers.onClose?.(event.code, event.reason);
|
|
48
|
+
});
|
|
49
|
+
socket.addEventListener("error", (event) => {
|
|
50
|
+
this.handlers.onError?.(event);
|
|
51
|
+
});
|
|
52
|
+
socket.addEventListener("message", (event) => {
|
|
53
|
+
this.dispatchMessage(event.data);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
disconnect(code, reason) {
|
|
57
|
+
this.pendingUrl = null;
|
|
58
|
+
this.socket?.close(code, reason);
|
|
59
|
+
}
|
|
60
|
+
sendAudio(data) {
|
|
61
|
+
const socket = this.requireOpenSocket();
|
|
62
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
63
|
+
socket.send(bytes);
|
|
64
|
+
}
|
|
65
|
+
sendJson(value) {
|
|
66
|
+
this.requireOpenSocket().send(JSON.stringify(value));
|
|
67
|
+
}
|
|
68
|
+
requireOpenSocket() {
|
|
69
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
70
|
+
throw new Error("ClientTransport WebSocket is not open");
|
|
71
|
+
}
|
|
72
|
+
return this.socket;
|
|
73
|
+
}
|
|
74
|
+
dispatchMessage(data) {
|
|
75
|
+
if (typeof data === "string") {
|
|
76
|
+
this.handlers.onMessage?.(data);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (data instanceof Blob) {
|
|
80
|
+
void data.arrayBuffer().then((buffer) => {
|
|
81
|
+
this.handlers.onAudio?.(buffer);
|
|
82
|
+
}).catch((err) => {
|
|
83
|
+
this.handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (data instanceof ArrayBuffer) {
|
|
88
|
+
this.handlers.onAudio?.(data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=websocket-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-transport.js","sourceRoot":"","sources":["../src/websocket-transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/browser-client",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.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
6
|
"keywords": [
|
|
@@ -23,28 +23,52 @@
|
|
|
23
23
|
"url": "https://github.com/kuralle/syrinx/issues"
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
|
-
"main": "./
|
|
27
|
-
"types": "./
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
28
|
"exports": {
|
|
29
|
-
".":
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"./
|
|
34
|
-
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./record": {
|
|
34
|
+
"types": "./dist/session-record.d.ts",
|
|
35
|
+
"default": "./dist/session-record.js"
|
|
36
|
+
},
|
|
37
|
+
"./agent-state": {
|
|
38
|
+
"types": "./dist/agent-state.d.ts",
|
|
39
|
+
"default": "./dist/agent-state.js"
|
|
40
|
+
},
|
|
41
|
+
"./turn-timeline": {
|
|
42
|
+
"types": "./dist/turn-timeline.d.ts",
|
|
43
|
+
"default": "./dist/turn-timeline.js"
|
|
44
|
+
},
|
|
45
|
+
"./session-metrics": {
|
|
46
|
+
"types": "./dist/session-metrics.d.ts",
|
|
47
|
+
"default": "./dist/session-metrics.js"
|
|
48
|
+
},
|
|
49
|
+
"./turn-recorder": {
|
|
50
|
+
"types": "./dist/turn-recorder.d.ts",
|
|
51
|
+
"default": "./dist/turn-recorder.js"
|
|
52
|
+
}
|
|
35
53
|
},
|
|
36
54
|
"dependencies": {
|
|
37
55
|
"@evan/opus": "1.0.3",
|
|
38
|
-
"@kuralle-syrinx/core": "4.
|
|
56
|
+
"@kuralle-syrinx/core": "4.5.0"
|
|
39
57
|
},
|
|
40
58
|
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
41
60
|
"typescript": "^5.7.0",
|
|
42
61
|
"vite": "^6.4.3",
|
|
43
62
|
"vitest": "^3.2.6"
|
|
44
63
|
},
|
|
64
|
+
"files": [
|
|
65
|
+
"dist",
|
|
66
|
+
"src",
|
|
67
|
+
"README.md"
|
|
68
|
+
],
|
|
45
69
|
"scripts": {
|
|
46
70
|
"dev": "vite --port 5173",
|
|
47
|
-
"build": "
|
|
71
|
+
"build": "tsc -p tsconfig.build.json",
|
|
48
72
|
"preview": "vite preview",
|
|
49
73
|
"typecheck": "tsc --noEmit",
|
|
50
74
|
"test": "vitest run"
|