@memberjunction/ai-realtime-client 5.48.0 → 5.49.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/README.md CHANGED
@@ -163,3 +163,14 @@ If you write a new driver, follow the same shape: every wire/hardware boundary b
163
163
  - `@memberjunction/ai-agents` — `RealtimeSessionRunner`, `RealtimeToolBroker`, `RealtimeClientSessionService`
164
164
  - `@memberjunction/ng-conversations` — the Angular host (overlay, channels, session review)
165
165
  - `@memberjunction/ng-whiteboard` — the generic whiteboard the Whiteboard channel surfaces
166
+
167
+ ## OpenAI-protocol client family architecture
168
+
169
+ The OpenAI-protocol drivers share two stacked layers in `generic/openAIProtocolClient.ts`:
170
+
171
+ - **`OpenAIProtocolRealtimeClient`** — the transport-agnostic protocol brain: inbound event dispatch (GA + beta frame names), the response state machine, narration-kind tagging, tool-result queueing, and the outbound actions (`SendText`, `SendToolResult`, `SendContextNote`, `RequestSpokenUpdate`, `CancelActiveResponse`, `SetMuted`).
172
+ - **`OpenAIProtocolWebSocketRealtimeClient`** — the websocket + client-owned PCM transport: socket lifecycle with a **connect-phase deadline** (`connectTimeoutMs`, default 15s, covering open AND the optional `session.created` wait — socket death, consumer `Disconnect`, and the deadline all reject the awaited `Connect`), the shared mic-capture/playout plumbing, and open-gated sends.
173
+
174
+ `OpenAIRealtimeClient` (WebRTC) extends the brain directly; `xAIRealtimeClient` and `HuggingFaceRealtimeClient` are thin subclasses of the websocket layer.
175
+
176
+ **Turn-discipline guarantees** (all drivers): a cancelled turn's trailing `response.done` cannot release the busy lock out from under a locally-initiated replacement response (stale-done protection); a TRUE user barge-in drops the queued tool-result auto-trigger — the result item is already in the conversation and the user's next turn voices it, so the model never speaks over a user who just took the floor.
@@ -1,48 +1,11 @@
1
- import { ClientRealtimeSessionConfig } from '@memberjunction/ai';
2
- import { BaseRealtimeClient } from '../generic/baseRealtimeClient.js';
3
- import { IRealtimePcmPlayback } from '../audio/pcmPlayback.js';
4
- import { IPcmMicCapture } from '../audio/micCapture.js';
1
+ import { ClientRealtimeSessionConfig, JSONObject } from '@memberjunction/ai';
2
+ import { OpenAIProtocolWebSocketRealtimeClient, IOpenAIProtocolClientSocket } from '../generic/openAIProtocolClient.js';
5
3
  /**
6
4
  * Default PCM16 sample rate (mono) both directions, used when the server pact omits `sampleRate`.
7
5
  * HuggingFace's speech-to-speech cascade is natively **16 kHz**, so capture AND playout default to that
8
- * (a wrong rate pitch/speed-distorts audio). Matches the server driver's {@link HUGGINGFACE_DEFAULT_PCM_SAMPLE_RATE}.
6
+ * (a wrong rate pitch/speed-distorts audio). Matches the server driver's `HUGGINGFACE_DEFAULT_PCM_SAMPLE_RATE`.
9
7
  */
10
8
  export declare const HUGGINGFACE_DEFAULT_PCM_SAMPLE_RATE = 16000;
11
- /** A parsed inbound frame from the OpenAI-Realtime-compatible endpoint (discriminated by `type`). */
12
- export interface HuggingFaceClientServerEvent {
13
- type?: string;
14
- /** `response.output_audio.delta` / `*_audio_transcript.delta` / input-transcription delta. */
15
- delta?: string;
16
- /** `*_audio_transcript.done` — the finalized assistant transcript. */
17
- transcript?: string;
18
- /** `response.function_call_arguments.done` — the tool name. */
19
- name?: string;
20
- /** `response.function_call_arguments.done` — correlation id the result must echo. */
21
- call_id?: string;
22
- /** `response.function_call_arguments.done` — JSON-encoded tool arguments (a STRING). */
23
- arguments?: string;
24
- /** `response.done` — usage payload for the completed response. */
25
- response?: {
26
- usage?: {
27
- input_tokens?: number;
28
- output_tokens?: number;
29
- };
30
- };
31
- /** `error` — the provider error payload. */
32
- error?: {
33
- message?: string;
34
- code?: string;
35
- };
36
- }
37
- /** The minimal websocket surface this client depends on (mirrors the other websocket drivers). */
38
- export interface IHuggingFaceClientSocket {
39
- onopen: (() => void) | null;
40
- onmessage: ((data: string) => void) | null;
41
- onerror: ((message: string) => void) | null;
42
- onclose: (() => void) | null;
43
- send(data: string): void;
44
- close(): void;
45
- }
46
9
  /**
47
10
  * HuggingFace speech-to-speech implementation of {@link BaseRealtimeClient}: a **browser-direct**
48
11
  * websocket that speaks the OpenAI-Realtime wire protocol over the shared PCM audio plane.
@@ -54,118 +17,34 @@ export interface IHuggingFaceClientSocket {
54
17
  * — the credential IS the URL): the browser connects to MJAPI's realtime proxy, which tunnels
55
18
  * transparently to the internal self-hosted endpoint. The client never learns the internal endpoint.
56
19
  *
57
- * OpenAI ordering: the session config is applied via `session.update` only AFTER the endpoint's
58
- * `session.created` frame (sending it earlier is dropped), and `'listening'` is reported at that point
59
- * (obligation #7). Because the audio plane is a raw websocket (not WebRTC), the client OWNS playback —
60
- * `IsAudioPlaying` reflects the local playout clock, and barge-in/cancel flush it locally.
20
+ * Because HuggingFace speaks the OpenAI wire protocol over a websocket with a client-owned PCM
21
+ * plane, nearly everything lives in the shared layers (protocol brain + websocket transport in
22
+ * {@link OpenAIProtocolWebSocketRealtimeClient}). This class supplies only the HuggingFace
23
+ * specifics: the proxy-URL connect, the `{ session, sampleRate }` server-pact parsing, the
24
+ * `session.created` readiness gate (OpenAI ordering — a `session.update` sent earlier is
25
+ * dropped), and the benign close semantics of a self-hosted proxy hop.
61
26
  */
62
- export declare class HuggingFaceRealtimeClient extends BaseRealtimeClient {
63
- protected socket: IHuggingFaceClientSocket | null;
64
- private playback;
65
- private micCapture;
66
- private micStream;
67
- /** The OpenAI-Realtime `session` object applied via `session.update` on `session.created`. */
68
- private sessionObject;
69
- /** Resolver for the in-flight Connect's `session.created` wait (null outside Connect). */
70
- private onSessionCreated;
71
- private pendingAssistantText;
72
- private responseActive;
73
- private pendingResultResponse;
74
- private pendingNarrationKind;
75
- private activeResponseKind;
76
- private currentState;
77
- private closedByConsumer;
78
- /**
79
- * Opens the browser-direct websocket to the proxy URL (`config.EphemeralToken`), waits for the
80
- * endpoint's `session.created`, applies the server-built session config, builds the PCM audio plane
81
- * at the pact's sample rate, and reports `'listening'`.
82
- */
83
- Connect(config: ClientRealtimeSessionConfig, micStream: MediaStream): Promise<void>;
84
- /** Tears down the audio plane + socket and resets the response state machine. */
85
- Disconnect(): Promise<void>;
86
- /**
87
- * Injects typed text as a user-role message, then triggers a reply through the same collision-safe
88
- * path tool results use. SendText implies barge-in — an active spoken response is cancelled first.
89
- */
90
- SendText(text: string): void;
91
- /**
92
- * Cancels the model's in-flight response (only when one is active) and flushes local playback so
93
- * already-buffered speech stops immediately. Preserves any queued tool-result trigger. No-op when idle.
94
- */
95
- CancelActiveResponse(): void;
96
- /** Injects a system-role context item the model can draw on next time it speaks, WITHOUT a reply. */
97
- SendContextNote(text: string): void;
98
- /**
99
- * Triggers ONE short spoken update; marks the upcoming response `'narration'` so its transcripts are
100
- * ephemeral. Skipped while a response is in flight (narration is disposable by contract).
101
- */
102
- RequestSpokenUpdate(instructions: string): void;
27
+ export declare class HuggingFaceRealtimeClient extends OpenAIProtocolWebSocketRealtimeClient {
28
+ /** @inheritdoc — used in the shared diagnostics + close messages. */
29
+ protected get providerDebugLabel(): string;
30
+ /** @inheritdoc — the proxy URL IS the credential (one-time ticket in the query string). */
31
+ protected openProviderSocket(config: ClientRealtimeSessionConfig): IOpenAIProtocolClientSocket;
32
+ /** @inheritdoc — the endpoint drops `session.update` until `session.created` confirms the session. */
33
+ protected get waitsForSessionCreated(): boolean;
34
+ /** @inheritdoc — extracts the wire-shaped `session` object from the `{ session, sampleRate }` pact. */
35
+ protected resolveSessionObject(config: ClientRealtimeSessionConfig): JSONObject;
36
+ /** @inheritdoc — the pact's `sampleRate`, defaulting to the HF-native 16 kHz. */
37
+ protected resolveSampleRate(config: ClientRealtimeSessionConfig): number;
103
38
  /**
104
- * Sends the tool result as a `function_call_output` item, then triggers a reply — immediately if idle,
105
- * otherwise queued until the current response finishes (so the model always voices delegated results).
39
+ * @inheritdoc
40
+ *
41
+ * A close the consumer didn't ask for is reported as a terminal `'closed'` — NOT the fatal
42
+ * error the cloud providers surface: the self-hosted proxy hop closes benignly (ticket
43
+ * expiry, upstream restart), and the host treats `'closed'` as the session-over signal.
106
44
  */
107
- SendToolResult(callID: string, outputJson: string): void;
108
- /** Mute / unmute by toggling the mic tracks' `enabled` flag (transport stays up). */
109
- SetMuted(muted: boolean): void;
110
- /** @inheritdoc */
111
- get IsBusy(): boolean;
112
- /** @inheritdoc */
113
- get IsAudioPlaying(): boolean;
45
+ protected handleSocketClose(): void;
114
46
  /** Creates the websocket to the given URL (the proxy URL). Production wraps the platform `WebSocket`. */
115
- protected createSocket(url: string): IHuggingFaceClientSocket;
116
- /** Creation seam for the mic-capture pipeline. Production delegates to the shared {@link createPcmMicCapture}. */
117
- protected createMicCapture(micStream: MediaStream, sampleRate: number, onPcmChunk: (base64Pcm16: string) => void): Promise<IPcmMicCapture>;
118
- /** Creation seam for the playout engine. Production returns the shared {@link RealtimePcmPlayback}. */
119
- protected createPlayback(sampleRate: number): IRealtimePcmPlayback;
120
- /** Resolver for the in-flight Connect's socket-open wait. */
121
- private onOpen;
122
- /** Extracts the wire-shaped `session` object from the server pact (`{ session, sampleRate }`). */
123
- private static parseSessionObject;
124
- /** Extracts the PCM sample rate from the server pact, defaulting to the HF-native 16 kHz. */
125
- private static parseSampleRate;
126
- /** Creates a one-shot signal promise, handing its resolver to the supplied setter. */
127
- private buildSignal;
128
- /** Sends the server-built session config once the session exists (skipped when empty). */
129
- private applySessionConfig;
130
- /** Streams one base64 PCM16 mic frame as an `input_audio_buffer.append`. */
131
- private sendInput;
132
- /** Parses an inbound frame and dispatches it. */
133
- private handleSocketMessage;
134
- /** Dispatches a typed OpenAI-Realtime server event to the appropriate behavior. */
135
- private handleEvent;
136
- /** Enqueues one base64 audio chunk for playout and reflects `'speaking'`. */
137
- private handleAudioDelta;
138
- /** Appends an assistant transcript delta and emits it (tagged with the active response kind). */
139
- private onAssistantDelta;
140
- /** Finalizes the assistant turn (empty turns emit nothing). */
141
- private onAssistantDone;
142
- /**
143
- * Surfaces a completed tool call. Silently leaves `'speaking'` (no emission) so a host busy indicator
144
- * isn't clobbered, and clears the busy flag (deadlock guard — obligation #2). `arguments` is already a
145
- * JSON string on the wire.
146
- */
147
- private onToolCallFrame;
148
- /**
149
- * True barge-in ONLY: the user started speaking over active model output (a response in flight or audio
150
- * audibly playing). Flushes local playback, emits the interruption, returns to `'listening'`.
151
- */
152
- private handleSpeechStarted;
153
- /** Response boundary: release the lock, emit usage, fire any queued tool-result reply, return to listening. */
154
- private handleResponseDone;
155
- /** Triggers a reply immediately when idle, else queues it until the current response finishes. */
156
- private requestResultResponse;
157
- /** On a turn completing, fire any queued tool-result response so the answer is spoken. */
158
- private flushPendingResultResponse;
159
- /** Resets the per-session response state machine (used on Disconnect). */
160
- private resetResponseState;
161
- /** A socket-level error is fatal — the transport is gone (obligation #6). */
162
- private handleSocketError;
163
- /** A close is silent when consumer-initiated; otherwise reported as a terminal `'closed'`. */
164
- private handleSocketClose;
165
- /** Updates the client's own state view and emits the change to the host. */
166
- private setState;
167
- /** JSON-serializes and sends one client frame (no-op when the socket is closed). */
168
- private sendFrame;
47
+ protected createSocket(url: string): IOpenAIProtocolClientSocket;
169
48
  }
170
49
  /**
171
50
  * Tree-shaking prevention: bundlers cannot see that {@link HuggingFaceRealtimeClient} is instantiated
@@ -1 +1 @@
1
- {"version":3,"file":"huggingFaceRealtimeClient.d.ts","sourceRoot":"","sources":["../../src/drivers/huggingFaceRealtimeClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"huggingFaceRealtimeClient.d.ts","sourceRoot":"","sources":["../../src/drivers/huggingFaceRealtimeClient.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,EACH,qCAAqC,EACrC,2BAA2B,EAC9B,MAAM,iCAAiC,CAAC;AAIzC;;;;GAIG;AACH,eAAO,MAAM,mCAAmC,QAAQ,CAAC;AAczD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,yBAA0B,SAAQ,qCAAqC;IAChF,qEAAqE;IACrE,cAAuB,kBAAkB,IAAI,MAAM,CAElD;IAID,2FAA2F;IAC3F,SAAS,CAAC,kBAAkB,CAAC,MAAM,EAAE,2BAA2B,GAAG,2BAA2B;IAI9F,sGAAsG;IACtG,cAAuB,sBAAsB,IAAI,OAAO,CAEvD;IAED,uGAAuG;cACpF,oBAAoB,CAAC,MAAM,EAAE,2BAA2B,GAAG,UAAU;IAMxF,iFAAiF;IACjF,SAAS,CAAC,iBAAiB,CAAC,MAAM,EAAE,2BAA2B,GAAG,MAAM;IAKxE;;;;;;OAMG;cACgB,iBAAiB,IAAI,IAAI;IAS5C,yGAAyG;IACzG,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,2BAA2B;CAoBnE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}