@ariaflowagents/livekit-plugin-transport-ws 0.9.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.
@@ -0,0 +1,192 @@
1
+ // ─── PCM Conversion Utilities ──────────────────────────────────────────────
2
+ /**
3
+ * Convert float32 PCM samples to int16 PCM bytes (Uint8Array).
4
+ * GeminiLiveSession and RealtimeRuntime expect int16 PCM.
5
+ * LiveKit AudioFrame uses float32 samples internally.
6
+ */
7
+ export function float32ToInt16Bytes(float32) {
8
+ const int16 = new Int16Array(float32.length);
9
+ for (let i = 0; i < float32.length; i++) {
10
+ const clamped = Math.max(-1, Math.min(1, float32[i] ?? 0));
11
+ int16[i] = Math.round(clamped * 0x7fff);
12
+ }
13
+ return new Uint8Array(int16.buffer);
14
+ }
15
+ /**
16
+ * Convert int16 PCM bytes (Uint8Array) to float32 PCM samples.
17
+ */
18
+ export function int16BytesToFloat32(pcmBytes) {
19
+ const int16View = new Int16Array(pcmBytes.buffer, pcmBytes.byteOffset, Math.floor(pcmBytes.byteLength / 2));
20
+ const float32 = new Float32Array(int16View.length);
21
+ for (let i = 0; i < int16View.length; i++) {
22
+ float32[i] = (int16View[i] ?? 0) / 0x7fff;
23
+ }
24
+ return float32;
25
+ }
26
+ // ─── Raw WebSocket Bridge ──────────────────────────────────────────────────
27
+ /**
28
+ * Bridge a raw WebSocket connection to a RealtimeTransportSession.
29
+ *
30
+ * This is the primary bridge used by WebSocketAgentServer.startNativeSession().
31
+ * Binary WS messages are passed through directly as Uint8Array (int16 PCM)
32
+ * without intermediate float32 conversion.
33
+ *
34
+ * The WebSocket client and the model client must agree on sample rate and
35
+ * encoding. No resampling is performed in this bridge.
36
+ */
37
+ export function bridgeWebSocketToRealtimeTransport(ws, options) {
38
+ const audioHandlers = [];
39
+ const closeHandlers = [];
40
+ let closed = false;
41
+ function fireClose() {
42
+ if (closed)
43
+ return;
44
+ closed = true;
45
+ for (const handler of closeHandlers) {
46
+ handler();
47
+ }
48
+ }
49
+ // Named handlers so they can be removed on close()
50
+ const onMessage = (data, isBinary) => {
51
+ if (!isBinary || closed)
52
+ return;
53
+ const uint8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
54
+ for (const handler of audioHandlers) {
55
+ handler(uint8);
56
+ }
57
+ };
58
+ const onClose = () => fireClose();
59
+ const onError = () => fireClose();
60
+ ws.on('message', onMessage);
61
+ ws.on('close', onClose);
62
+ ws.on('error', onError);
63
+ function removeListeners() {
64
+ ws.removeListener('message', onMessage);
65
+ ws.removeListener('close', onClose);
66
+ ws.removeListener('error', onError);
67
+ }
68
+ const interruptHandlers = [];
69
+ return {
70
+ sendAudio(data) {
71
+ if (closed || ws.readyState !== ws.OPEN)
72
+ return;
73
+ try {
74
+ ws.send(Buffer.from(data), { binary: true });
75
+ }
76
+ catch {
77
+ // WS already closed
78
+ }
79
+ },
80
+ onAudio(handler) {
81
+ audioHandlers.push(handler);
82
+ },
83
+ onClose(handler) {
84
+ closeHandlers.push(handler);
85
+ },
86
+ onInterrupted(handler) {
87
+ interruptHandlers.push(handler);
88
+ },
89
+ clearAudioBuffer() {
90
+ // Signal the client to stop playback.
91
+ // Send a JSON control message that the frontend can act on.
92
+ if (closed || ws.readyState !== ws.OPEN)
93
+ return;
94
+ try {
95
+ ws.send(JSON.stringify({ type: 'clear_audio' }));
96
+ }
97
+ catch {
98
+ // WS closed
99
+ }
100
+ for (const handler of interruptHandlers) {
101
+ handler();
102
+ }
103
+ },
104
+ close() {
105
+ if (closed)
106
+ return;
107
+ closed = true;
108
+ removeListeners();
109
+ if (ws.readyState === ws.OPEN) {
110
+ ws.close(1000, 'Session ended');
111
+ }
112
+ },
113
+ };
114
+ }
115
+ // ─── Adapter-Based Bridge ──────────────────────────────────────────────────
116
+ /**
117
+ * Bridge a WebSocketTransportAdapter to a RealtimeTransportSession.
118
+ *
119
+ * This variant works at the adapter level, converting between LiveKit's
120
+ * float32 AudioFrame format and RealtimeRuntime's int16 PCM Uint8Array.
121
+ *
122
+ * Use this when you already have a WebSocketTransportAdapter instance
123
+ * (e.g., from the server's onConnection callback) and want to route it
124
+ * through RealtimeRuntime instead of AriaFlowVoiceSession.
125
+ *
126
+ * Note: This bridge subscribes to the raw WS binary messages directly
127
+ * (same as the simple bridge) because WebSocketAudioInput doesn't expose
128
+ * a frame event. Output is sent as raw binary on the WS, bypassing
129
+ * WebSocketAudioOutput's AudioFrame machinery.
130
+ */
131
+ export function bridgeAdapterToRealtimeTransport(adapter) {
132
+ const audioHandlers = [];
133
+ const closeHandlers = [];
134
+ let closed = false;
135
+ // Use the public rawSocket getter to access the underlying WebSocket.
136
+ // The adapter's audioInput already listens on the WS for binary messages
137
+ // and converts to AudioFrames. For the realtime bridge, we need the raw
138
+ // int16 PCM bytes. We attach a second binary listener on the same WS.
139
+ const ws = adapter.rawSocket;
140
+ function fireClose() {
141
+ if (closed)
142
+ return;
143
+ closed = true;
144
+ for (const handler of closeHandlers) {
145
+ handler();
146
+ }
147
+ }
148
+ const onMessage = (data, isBinary) => {
149
+ if (!isBinary || closed)
150
+ return;
151
+ const uint8 = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
152
+ for (const handler of audioHandlers) {
153
+ handler(uint8);
154
+ }
155
+ };
156
+ const onClose = () => fireClose();
157
+ const onError = () => fireClose();
158
+ ws.on('message', onMessage);
159
+ ws.on('close', onClose);
160
+ ws.on('error', onError);
161
+ function removeListeners() {
162
+ ws.removeListener('message', onMessage);
163
+ ws.removeListener('close', onClose);
164
+ ws.removeListener('error', onError);
165
+ }
166
+ return {
167
+ sendAudio(data) {
168
+ if (closed || !adapter.isOpen)
169
+ return;
170
+ try {
171
+ ws.send(data, { binary: true });
172
+ }
173
+ catch {
174
+ // WS closed
175
+ }
176
+ },
177
+ onAudio(handler) {
178
+ audioHandlers.push(handler);
179
+ },
180
+ onClose(handler) {
181
+ closeHandlers.push(handler);
182
+ },
183
+ close() {
184
+ if (closed)
185
+ return;
186
+ closed = true;
187
+ removeListeners();
188
+ adapter.close().catch(() => { });
189
+ },
190
+ };
191
+ }
192
+ //# sourceMappingURL=realtime_bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime_bridge.js","sourceRoot":"","sources":["../src/realtime_bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { SessionManager, type AriaFlowVoiceSession } from '@ariaflow/livekit-plugin';
2
+ import type { voice } from '@livekit/agents';
3
+ import type { RealtimeRuntime, RealtimeAudioClient, RealtimeSessionHandle } from '@ariaflowagents/core/realtime';
4
+ import { WebSocketTransportAdapter } from './transport_adapter.js';
5
+ import type { WebSocketServerOptions } from './types.js';
6
+ /**
7
+ * Options for starting a native audio session through RealtimeRuntime.
8
+ */
9
+ export interface NativeSessionOptions {
10
+ /** The RealtimeRuntime instance to route audio through. */
11
+ runtime: RealtimeRuntime;
12
+ /** Factory that creates a RealtimeAudioClient for each session. */
13
+ createModelClient: () => RealtimeAudioClient;
14
+ /** Optional session ID (generated if not provided). */
15
+ sessionId?: string;
16
+ /** Optional user ID for session scoping. */
17
+ userId?: string;
18
+ /** Optional agent ID override. */
19
+ agentId?: string;
20
+ }
21
+ /**
22
+ * A WebSocket server that accepts connections and creates agent sessions.
23
+ *
24
+ * Usage:
25
+ * const server = new WebSocketAgentServer({ port: 8080 });
26
+ *
27
+ * server.onConnection(async (transport) => {
28
+ * const voiceSession = new AriaFlowVoiceSession({
29
+ * runtime: runtime,
30
+ * stt: new GeminiLiveSTT(),
31
+ * tts: new GeminiLiveTTS(),
32
+ * });
33
+ * await server.startSession(transport, voiceSession);
34
+ * });
35
+ *
36
+ * await server.listen();
37
+ */
38
+ export declare class WebSocketAgentServer {
39
+ private options;
40
+ private wss;
41
+ private sessionManager;
42
+ private nativeSessions;
43
+ private connectionHandler;
44
+ constructor(options?: WebSocketServerOptions);
45
+ onConnection(handler: (adapter: WebSocketTransportAdapter) => void | Promise<void>): void;
46
+ startSession(adapter: WebSocketTransportAdapter, voiceSession: AriaFlowVoiceSession): Promise<voice.AgentSession>;
47
+ /**
48
+ * Start a native audio session that routes audio directly through
49
+ * RealtimeRuntime instead of the cascaded STT→LLM→TTS pipeline.
50
+ *
51
+ * This uses the WS-to-RealtimeTransport bridge to convert the raw
52
+ * WebSocket connection into a RealtimeTransportSession, then starts
53
+ * a session via RealtimeRuntime.startSession().
54
+ *
55
+ * The model client handles STT, reasoning, and TTS in a single
56
+ * persistent connection (e.g., Gemini Live, OpenAI Realtime).
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * server.onConnection(async (transport) => {
61
+ * await server.startNativeSession(transport, {
62
+ * runtime: realtimeRuntime,
63
+ * createModelClient: () => new GeminiLiveSession({ apiKey, model }),
64
+ * });
65
+ * });
66
+ * ```
67
+ */
68
+ startNativeSession(adapter: WebSocketTransportAdapter, options: NativeSessionOptions): Promise<RealtimeSessionHandle>;
69
+ listen(): Promise<void>;
70
+ get sessions(): SessionManager;
71
+ close(): Promise<void>;
72
+ }
73
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,OAAO,EAAE,eAAe,CAAC;IACzB,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;IAC7C,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,oBAAoB;IAQnB,OAAO,CAAC,OAAO;IAP3B,OAAO,CAAC,GAAG,CAAyB;IACpC,OAAO,CAAC,cAAc,CAAwC;IAC9D,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,iBAAiB,CAET;gBAEI,OAAO,GAAE,sBAA2B;IAExD,YAAY,CACV,OAAO,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACpE,IAAI;IAID,YAAY,CAChB,OAAO,EAAE,yBAAyB,EAClC,YAAY,EAAE,oBAAoB,GACjC,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC;IAI9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,kBAAkB,CACtB,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA4C3B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAqJ7B,IAAI,QAAQ,IAAI,cAAc,CAE7B;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAc7B"}
package/dist/server.js ADDED
@@ -0,0 +1,247 @@
1
+ import { WebSocketServer as WSServer } from 'ws';
2
+ import { SessionManager } from '@ariaflow/livekit-plugin';
3
+ import { WebSocketTransportAdapter } from './transport_adapter.js';
4
+ import { bridgeWebSocketToRealtimeTransport } from './realtime_bridge.js';
5
+ import { parseClientMessage, serializeServerMessage } from './protocol.js';
6
+ /**
7
+ * A WebSocket server that accepts connections and creates agent sessions.
8
+ *
9
+ * Usage:
10
+ * const server = new WebSocketAgentServer({ port: 8080 });
11
+ *
12
+ * server.onConnection(async (transport) => {
13
+ * const voiceSession = new AriaFlowVoiceSession({
14
+ * runtime: runtime,
15
+ * stt: new GeminiLiveSTT(),
16
+ * tts: new GeminiLiveTTS(),
17
+ * });
18
+ * await server.startSession(transport, voiceSession);
19
+ * });
20
+ *
21
+ * await server.listen();
22
+ */
23
+ export class WebSocketAgentServer {
24
+ options;
25
+ wss = null;
26
+ sessionManager = new SessionManager();
27
+ nativeSessions = new Map();
28
+ connectionHandler = null;
29
+ constructor(options = {}) {
30
+ this.options = options;
31
+ }
32
+ onConnection(handler) {
33
+ this.connectionHandler = handler;
34
+ }
35
+ async startSession(adapter, voiceSession) {
36
+ return this.sessionManager.startSession(adapter, voiceSession);
37
+ }
38
+ /**
39
+ * Start a native audio session that routes audio directly through
40
+ * RealtimeRuntime instead of the cascaded STT→LLM→TTS pipeline.
41
+ *
42
+ * This uses the WS-to-RealtimeTransport bridge to convert the raw
43
+ * WebSocket connection into a RealtimeTransportSession, then starts
44
+ * a session via RealtimeRuntime.startSession().
45
+ *
46
+ * The model client handles STT, reasoning, and TTS in a single
47
+ * persistent connection (e.g., Gemini Live, OpenAI Realtime).
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * server.onConnection(async (transport) => {
52
+ * await server.startNativeSession(transport, {
53
+ * runtime: realtimeRuntime,
54
+ * createModelClient: () => new GeminiLiveSession({ apiKey, model }),
55
+ * });
56
+ * });
57
+ * ```
58
+ */
59
+ async startNativeSession(adapter, options) {
60
+ const ws = adapter.rawSocket;
61
+ const realtimeTransport = bridgeWebSocketToRealtimeTransport(ws, {
62
+ sessionId: adapter.id,
63
+ });
64
+ const modelClient = options.createModelClient();
65
+ const handle = await options.runtime.startSession({
66
+ modelClient,
67
+ transport: realtimeTransport,
68
+ sessionId: options.sessionId ?? adapter.id,
69
+ userId: options.userId,
70
+ agentId: options.agentId,
71
+ });
72
+ this.nativeSessions.set(adapter.id, handle);
73
+ // Wire cleanup on WS close
74
+ ws.on('close', () => {
75
+ const nativeHandle = this.nativeSessions.get(adapter.id);
76
+ if (nativeHandle) {
77
+ this.nativeSessions.delete(adapter.id);
78
+ nativeHandle.stop().catch((err) => {
79
+ console.error('[WebSocketServer] native session stop error:', {
80
+ error: err instanceof Error ? err.message : String(err),
81
+ adapterId: adapter.id,
82
+ timestamp: new Date().toISOString(),
83
+ });
84
+ });
85
+ }
86
+ });
87
+ console.info('[WebSocketServer] native session started', {
88
+ adapterId: adapter.id,
89
+ sessionId: handle.sessionId,
90
+ callId: handle.callId,
91
+ timestamp: new Date().toISOString(),
92
+ });
93
+ return handle;
94
+ }
95
+ async listen() {
96
+ const port = this.options.port ?? 8080;
97
+ const host = this.options.host ?? '0.0.0.0';
98
+ this.wss = new WSServer({ port, host });
99
+ this.wss.on('connection', async (ws, req) => {
100
+ const connectedAt = Date.now();
101
+ let binaryMessageCount = 0;
102
+ let firstBinaryAt = null;
103
+ // Authentication
104
+ if (this.options.authenticate) {
105
+ const allowed = await this.options.authenticate(req);
106
+ if (!allowed) {
107
+ ws.close(4001, 'Unauthorized');
108
+ return;
109
+ }
110
+ }
111
+ const sampleRate = this.options.defaultSampleRate ?? 24000;
112
+ const numChannels = this.options.defaultNumChannels ?? 1;
113
+ const adapter = new WebSocketTransportAdapter(ws, {
114
+ sampleRate,
115
+ numChannels,
116
+ });
117
+ console.info('[WebSocketServer] client connected', {
118
+ adapterId: adapter.id,
119
+ remoteAddress: req.socket.remoteAddress,
120
+ timestamp: new Date().toISOString(),
121
+ });
122
+ // Send session started
123
+ ws.send(serializeServerMessage({
124
+ type: 'session_started',
125
+ sessionId: adapter.id,
126
+ config: {
127
+ sampleRate,
128
+ numChannels,
129
+ encoding: 'pcm_s16le',
130
+ },
131
+ }));
132
+ console.info('[WebSocketServer] session_started sent', {
133
+ adapterId: adapter.id,
134
+ sampleRate,
135
+ numChannels,
136
+ timestamp: new Date().toISOString(),
137
+ });
138
+ // Handle text and control messages
139
+ ws.on('message', (data, isBinary) => {
140
+ if (isBinary) {
141
+ binaryMessageCount += 1;
142
+ if (!firstBinaryAt) {
143
+ firstBinaryAt = Date.now();
144
+ console.info('[WebSocketServer] first binary audio packet received', {
145
+ adapterId: adapter.id,
146
+ bytes: data.byteLength,
147
+ msSinceConnect: firstBinaryAt - connectedAt,
148
+ timestamp: new Date().toISOString(),
149
+ });
150
+ }
151
+ else if (binaryMessageCount % 100 === 0) {
152
+ console.debug('[WebSocketServer] binary audio packet progress', {
153
+ adapterId: adapter.id,
154
+ count: binaryMessageCount,
155
+ bytes: data.byteLength,
156
+ timestamp: new Date().toISOString(),
157
+ });
158
+ }
159
+ return;
160
+ }
161
+ const msg = parseClientMessage(data.toString());
162
+ if (!msg) {
163
+ console.warn('[WebSocketServer] unrecognized client message', {
164
+ adapterId: adapter.id,
165
+ payloadPreview: data.toString().slice(0, 120),
166
+ timestamp: new Date().toISOString(),
167
+ });
168
+ return;
169
+ }
170
+ if (msg.type === 'end_of_audio') {
171
+ console.info('[WebSocketServer] end_of_audio received', {
172
+ adapterId: adapter.id,
173
+ binaryMessageCount,
174
+ timestamp: new Date().toISOString(),
175
+ });
176
+ adapter.audioInput.endOfAudio();
177
+ }
178
+ else if (msg.type === 'user_text') {
179
+ console.info('[WebSocketServer] user_text received', {
180
+ adapterId: adapter.id,
181
+ chars: msg.text.length,
182
+ timestamp: new Date().toISOString(),
183
+ });
184
+ const session = this.sessionManager.getSession(adapter.id);
185
+ if (session) {
186
+ try {
187
+ const handle = session.generateReply({ userInput: msg.text });
188
+ handle.addDoneCallback(() => {
189
+ // Intentionally noop: callback is used to attach completion lifecycle
190
+ // in one place for future telemetry hooks.
191
+ });
192
+ }
193
+ catch (err) {
194
+ console.error('[WebSocketServer] generateReply error:', {
195
+ error: err instanceof Error ? err.message : String(err),
196
+ adapterId: adapter.id,
197
+ timestamp: new Date().toISOString(),
198
+ });
199
+ }
200
+ }
201
+ }
202
+ });
203
+ // Handle disconnect
204
+ ws.on('close', (code, reasonBuffer) => {
205
+ const disconnectedAt = Date.now();
206
+ console.info('[WebSocketServer] client disconnected', {
207
+ adapterId: adapter.id,
208
+ code,
209
+ reason: reasonBuffer.toString(),
210
+ binaryMessageCount,
211
+ sessionDurationMs: disconnectedAt - connectedAt,
212
+ timestamp: new Date().toISOString(),
213
+ });
214
+ this.sessionManager.closeSession(adapter.id).catch((err) => {
215
+ console.error('[WebSocketServer] Error closing session:', {
216
+ error: err instanceof Error ? err.message : String(err),
217
+ adapterId: adapter.id,
218
+ timestamp: new Date().toISOString(),
219
+ });
220
+ });
221
+ });
222
+ if (this.connectionHandler) {
223
+ await this.connectionHandler(adapter);
224
+ }
225
+ });
226
+ return new Promise((resolve) => {
227
+ this.wss.on('listening', () => {
228
+ resolve();
229
+ });
230
+ });
231
+ }
232
+ get sessions() {
233
+ return this.sessionManager;
234
+ }
235
+ async close() {
236
+ // Stop all native sessions
237
+ const nativeStops = Array.from(this.nativeSessions.values()).map((handle) => handle.stop().catch(() => { }));
238
+ await Promise.allSettled(nativeStops);
239
+ this.nativeSessions.clear();
240
+ // Close all cascaded sessions
241
+ await this.sessionManager.closeAll();
242
+ if (this.wss) {
243
+ this.wss.close();
244
+ }
245
+ }
246
+ }
247
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { TextOutput, type TimedString } from '@ariaflow/livekit-plugin';
2
+ import type { WebSocket } from 'ws';
3
+ /**
4
+ * Sends agent transcription text to the WebSocket client as JSON messages.
5
+ */
6
+ export declare class WebSocketTextOutput extends TextOutput {
7
+ private ws;
8
+ private contextLabel;
9
+ private closed;
10
+ private segmentIndex;
11
+ private segmentStartedAt;
12
+ constructor(ws: WebSocket, contextLabel?: string, nextInChain?: TextOutput);
13
+ captureText(text: string | TimedString): Promise<void>;
14
+ flush(): void;
15
+ close(): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=text_output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text_output.d.ts","sourceRoot":"","sources":["../src/text_output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAiB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAGpC;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,UAAU;IAM/C,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,YAAY;IANtB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,gBAAgB,CAAuB;gBAGrC,EAAE,EAAE,SAAS,EACb,YAAY,GAAE,MAAkB,EACxC,WAAW,CAAC,EAAE,UAAU;IAKpB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC5D,KAAK,IAAI,IAAI;IA6BP,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,77 @@
1
+ import { TextOutput, isTimedString } from '@ariaflow/livekit-plugin';
2
+ import { serializeServerMessage } from './protocol.js';
3
+ /**
4
+ * Sends agent transcription text to the WebSocket client as JSON messages.
5
+ */
6
+ export class WebSocketTextOutput extends TextOutput {
7
+ ws;
8
+ contextLabel;
9
+ closed = false;
10
+ segmentIndex = 0;
11
+ segmentStartedAt = null;
12
+ constructor(ws, contextLabel = 'unknown', nextInChain) {
13
+ super(nextInChain);
14
+ this.ws = ws;
15
+ this.contextLabel = contextLabel;
16
+ }
17
+ async captureText(text) {
18
+ if (this.closed)
19
+ return;
20
+ const textContent = isTimedString(text) ? text.text : text;
21
+ if (textContent.length > 0 && this.segmentStartedAt === null) {
22
+ this.segmentIndex += 1;
23
+ this.segmentStartedAt = Date.now();
24
+ console.info('[WebSocketTextOutput] first chunk', {
25
+ context: this.contextLabel,
26
+ segmentIndex: this.segmentIndex,
27
+ chars: textContent.length,
28
+ timestamp: new Date().toISOString(),
29
+ });
30
+ }
31
+ const msg = {
32
+ type: 'agent_text',
33
+ text: textContent,
34
+ isFinal: false,
35
+ };
36
+ try {
37
+ this.ws.send(serializeServerMessage(msg));
38
+ }
39
+ catch {
40
+ // WebSocket closed
41
+ }
42
+ if (this.nextInChain) {
43
+ await this.nextInChain.captureText(text);
44
+ }
45
+ }
46
+ flush() {
47
+ if (!this.closed) {
48
+ if (this.segmentStartedAt !== null) {
49
+ console.info('[WebSocketTextOutput] segment flushed', {
50
+ context: this.contextLabel,
51
+ segmentIndex: this.segmentIndex,
52
+ durationMs: Date.now() - this.segmentStartedAt,
53
+ timestamp: new Date().toISOString(),
54
+ });
55
+ }
56
+ this.segmentStartedAt = null;
57
+ const msg = {
58
+ type: 'agent_text',
59
+ text: '',
60
+ isFinal: true,
61
+ };
62
+ try {
63
+ this.ws.send(serializeServerMessage(msg));
64
+ }
65
+ catch {
66
+ // WebSocket closed
67
+ }
68
+ }
69
+ if (this.nextInChain) {
70
+ this.nextInChain.flush();
71
+ }
72
+ }
73
+ async close() {
74
+ this.closed = true;
75
+ }
76
+ }
77
+ //# sourceMappingURL=text_output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text_output.js","sourceRoot":"","sources":["../src/text_output.ts"],"names":[],"mappings":"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"}