@micdrop/server 1.4.0 → 1.6.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
@@ -161,92 +161,80 @@ interface CallConfig {
161
161
  }
162
162
  ```
163
163
 
164
- ### Message Types
165
-
166
- ```typescript
167
- interface ConversationMessage {
168
- role: 'system' | 'user' | 'assistant'
169
- content: string
170
- metadata?: any
171
- }
172
-
173
- type Conversation = ConversationMessage[]
174
-
175
- interface CallSummary {
176
- conversation: Conversation
177
- duration: number
178
- }
179
- ```
180
-
181
164
  ## WebSocket Protocol
182
165
 
183
166
  The server implements a specific protocol for client-server communication:
184
167
 
185
168
  ### Client Commands
186
169
 
187
- ```typescript
188
- enum CallClientCommands {
189
- StartSpeaking = 'startSpeaking',
190
- StopSpeaking = 'stopSpeaking',
191
- Mute = 'mute',
192
- }
193
- ```
170
+ The client can send the following commands to the server:
171
+
172
+ - `CallClientCommands.StartSpeaking` - The user starts speaking
173
+ - `CallClientCommands.StopSpeaking` - The user stops speaking
174
+ - `CallClientCommands.Mute` - The user mutes the microphone
194
175
 
195
176
  ### Server Commands
196
177
 
197
- ```typescript
198
- enum CallServerCommands {
199
- Message = 'message',
200
- CancelLastAssistantMessage = 'cancelLastAssistantMessage',
201
- EndInterview = 'endInterview',
202
- }
203
- ```
178
+ The server can send the following commands to the client:
179
+
180
+ - `CallServerCommands.Message` - A message from the assistant.
181
+ - `CallServerCommands.CancelLastAssistantMessage` - Cancel the last assistant message.
182
+ - `CallServerCommands.CancelLastUserMessage` - Cancel the last user message.
183
+ - `CallServerCommands.SkipAnswer` - Notify that the last generated answer was ignored, it's listening again.
184
+ - `CallServerCommands.EnableSpeakerStreaming` - Enable speaker streaming.
185
+ - `CallServerCommands.EndCall` - End the call.
204
186
 
205
187
  ### Message Flow
206
188
 
207
189
  1. Client connects to WebSocket server
208
- 2. Server sends initial assistant message (if configured)
190
+ 2. Server sends initial assistant message (generated if not provided)
209
191
  3. Client sends audio chunks when user speaks
210
- 4. Server processes audio and responds with text/audio
211
- 5. Process continues until interview ends
192
+ 4. Server processes audio and responds with text+audio
193
+ 5. Process continues until call ends
194
+
195
+ See detailed protocol in [README.md](../README.md).
212
196
 
213
197
  ## Ending the call
214
198
 
215
199
  The call has two ways to end:
216
200
 
217
201
  - When the client closes the websocket connection.
218
- - When the generated answer contains the keyword "END_INTERVIEW".
202
+ - When the generated answer contains the metadata command `endCall: true`.
219
203
 
220
- You can prompt it like this:
204
+ Example:
221
205
 
222
206
  ```typescript
223
- import { END_INTERVIEW } from '@micdrop/server'
224
-
207
+ const END_CALL = 'END_CALL'
225
208
  const systemPrompt = `
226
209
  You are a voice assistant interviewing the user.
227
- To end the interview, briefly thank the user and say good bye, then say "${END_INTERVIEW}".
210
+ To end the interview, briefly thank the user and say good bye, then say ${END_CALL}.
228
211
  `
229
- ```
230
-
231
- ## Error Handling
232
212
 
233
- The server implements standardized error handling with specific codes:
213
+ async function generateAnswer(
214
+ conversation: ConversationMessage[]
215
+ ): Promise<ConversationMessage> {
216
+ const response = await openai.chat.completions.create({
217
+ model: 'gpt-4o',
218
+ messages: conversation,
219
+ temperature: 0.5,
220
+ max_tokens: 250,
221
+ })
222
+
223
+ let text = response.choices[0].message.content
224
+ if (!text) throw new Error('Empty response')
225
+
226
+ // Add metadata
227
+ const metadata: CallMetadata = {}
228
+ if (text.includes(END_CALL)) {
229
+ text = text.replace(END_CALL, '').trim()
230
+ metadata.commands = { endCall: true }
231
+ }
234
232
 
235
- ```typescript
236
- enum CallErrorCode {
237
- BadRequest = 4400,
238
- Unauthorized = 4401,
239
- NotFound = 4404,
233
+ return { role: 'assistant', content: text, metadata }
240
234
  }
241
235
  ```
242
236
 
243
- Common error scenarios:
244
-
245
- - Invalid WebSocket messages
246
- - Authentication failures
247
- - Missing or invalid parameters
248
- - Audio processing errors
249
- - Connection timeouts
237
+ See demo [system prompt](../demo-server/src/call.ts) and [generateAnswer](../demo-server/src/ai/generateAnswer.ts) for a complete example.
250
238
 
251
239
  ## Integration Example
252
240
 
@@ -271,7 +259,7 @@ server.get('/call', { websocket: true }, (socket) => {
271
259
  server.listen({ port: 8080 })
272
260
  ```
273
261
 
274
- See [@micdrop/demo-server](../demo-server/src/ai/index.ts) for a complete example using OpenAI and ElevenLabs.
262
+ See [@micdrop/demo-server](../demo-server/src/call.ts) for a complete example using OpenAI and ElevenLabs.
275
263
 
276
264
  ## Debug Mode
277
265
 
@@ -282,6 +270,8 @@ The server includes a debug mode that can:
282
270
  - Track conversation state
283
271
  - Monitor WebSocket events
284
272
 
273
+ See `debugLog`, `debugSaveSpeech` and `disableTTS` options in [CallConfig](#callconfig).
274
+
285
275
  ## Browser Support
286
276
 
287
277
  The server is designed to work with any WebSocket client, but is specifically tested with:
package/dist/index.d.mts CHANGED
@@ -1,15 +1,17 @@
1
1
  import WebSocket$1, { WebSocket } from 'ws';
2
2
 
3
3
  declare enum CallClientCommands {
4
- StartSpeaking = "startSpeaking",
5
- StopSpeaking = "stopSpeaking",
6
- Mute = "mute"
4
+ StartSpeaking = "StartSpeaking",
5
+ StopSpeaking = "StopSpeaking",
6
+ Mute = "Mute"
7
7
  }
8
8
  declare enum CallServerCommands {
9
- Message = "message",
10
- CancelLastAssistantMessage = "cancelLastAssistantMessage",
11
- EnableSpeakerStreaming = "enableSpeakerStreaming",
12
- EndInterview = "endInterview"
9
+ Message = "Message",
10
+ CancelLastAssistantMessage = "CancelLastAssistantMessage",
11
+ CancelLastUserMessage = "CancelLastUserMessage",
12
+ SkipAnswer = "SkipAnswer",
13
+ EnableSpeakerStreaming = "EnableSpeakerStreaming",
14
+ EndCall = "EndCall"
13
15
  }
14
16
  interface CallConfig {
15
17
  systemPrompt: string;
@@ -28,13 +30,20 @@ interface CallSummary {
28
30
  duration: number;
29
31
  }
30
32
  type Conversation = ConversationMessage[];
31
- interface ConversationMessage<Data = any> {
33
+ type CallMetadata = {
34
+ commands?: {
35
+ endCall?: boolean;
36
+ cancelLastUserMessage?: boolean;
37
+ skipAnswer?: boolean;
38
+ };
39
+ [key: string]: any;
40
+ };
41
+ interface ConversationMessage<Data extends CallMetadata = CallMetadata> {
32
42
  role: 'system' | 'user' | 'assistant';
33
43
  content: string;
34
44
  metadata?: Data;
35
45
  }
36
46
 
37
- declare const END_INTERVIEW = "END_INTERVIEW";
38
47
  interface Processing {
39
48
  aborted: boolean;
40
49
  }
@@ -73,4 +82,4 @@ declare function handleError(socket: WebSocket$1, error: unknown): void;
73
82
 
74
83
  declare function waitForParams<CallParams>(socket: WebSocket, validate: (params: any) => CallParams): Promise<CallParams>;
75
84
 
76
- export { CallClientCommands, type CallConfig, CallError, CallErrorCode, CallServerCommands, CallSocket, type CallSummary, type Conversation, type ConversationMessage, END_INTERVIEW, handleError, waitForParams };
85
+ export { CallClientCommands, type CallConfig, CallError, CallErrorCode, type CallMetadata, CallServerCommands, CallSocket, type CallSummary, type Conversation, type ConversationMessage, handleError, waitForParams };
package/dist/index.d.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  import WebSocket$1, { WebSocket } from 'ws';
2
2
 
3
3
  declare enum CallClientCommands {
4
- StartSpeaking = "startSpeaking",
5
- StopSpeaking = "stopSpeaking",
6
- Mute = "mute"
4
+ StartSpeaking = "StartSpeaking",
5
+ StopSpeaking = "StopSpeaking",
6
+ Mute = "Mute"
7
7
  }
8
8
  declare enum CallServerCommands {
9
- Message = "message",
10
- CancelLastAssistantMessage = "cancelLastAssistantMessage",
11
- EnableSpeakerStreaming = "enableSpeakerStreaming",
12
- EndInterview = "endInterview"
9
+ Message = "Message",
10
+ CancelLastAssistantMessage = "CancelLastAssistantMessage",
11
+ CancelLastUserMessage = "CancelLastUserMessage",
12
+ SkipAnswer = "SkipAnswer",
13
+ EnableSpeakerStreaming = "EnableSpeakerStreaming",
14
+ EndCall = "EndCall"
13
15
  }
14
16
  interface CallConfig {
15
17
  systemPrompt: string;
@@ -28,13 +30,20 @@ interface CallSummary {
28
30
  duration: number;
29
31
  }
30
32
  type Conversation = ConversationMessage[];
31
- interface ConversationMessage<Data = any> {
33
+ type CallMetadata = {
34
+ commands?: {
35
+ endCall?: boolean;
36
+ cancelLastUserMessage?: boolean;
37
+ skipAnswer?: boolean;
38
+ };
39
+ [key: string]: any;
40
+ };
41
+ interface ConversationMessage<Data extends CallMetadata = CallMetadata> {
32
42
  role: 'system' | 'user' | 'assistant';
33
43
  content: string;
34
44
  metadata?: Data;
35
45
  }
36
46
 
37
- declare const END_INTERVIEW = "END_INTERVIEW";
38
47
  interface Processing {
39
48
  aborted: boolean;
40
49
  }
@@ -73,4 +82,4 @@ declare function handleError(socket: WebSocket$1, error: unknown): void;
73
82
 
74
83
  declare function waitForParams<CallParams>(socket: WebSocket, validate: (params: any) => CallParams): Promise<CallParams>;
75
84
 
76
- export { CallClientCommands, type CallConfig, CallError, CallErrorCode, CallServerCommands, CallSocket, type CallSummary, type Conversation, type ConversationMessage, END_INTERVIEW, handleError, waitForParams };
85
+ export { CallClientCommands, type CallConfig, CallError, CallErrorCode, type CallMetadata, CallServerCommands, CallSocket, type CallSummary, type Conversation, type ConversationMessage, handleError, waitForParams };
package/dist/index.js CHANGED
@@ -35,7 +35,6 @@ __export(index_exports, {
35
35
  CallErrorCode: () => CallErrorCode,
36
36
  CallServerCommands: () => CallServerCommands,
37
37
  CallSocket: () => CallSocket,
38
- END_INTERVIEW: () => END_INTERVIEW,
39
38
  handleError: () => handleError,
40
39
  waitForParams: () => waitForParams
41
40
  });
@@ -46,21 +45,22 @@ var fs = __toESM(require("fs"));
46
45
 
47
46
  // src/types.ts
48
47
  var CallClientCommands = /* @__PURE__ */ ((CallClientCommands2) => {
49
- CallClientCommands2["StartSpeaking"] = "startSpeaking";
50
- CallClientCommands2["StopSpeaking"] = "stopSpeaking";
51
- CallClientCommands2["Mute"] = "mute";
48
+ CallClientCommands2["StartSpeaking"] = "StartSpeaking";
49
+ CallClientCommands2["StopSpeaking"] = "StopSpeaking";
50
+ CallClientCommands2["Mute"] = "Mute";
52
51
  return CallClientCommands2;
53
52
  })(CallClientCommands || {});
54
53
  var CallServerCommands = /* @__PURE__ */ ((CallServerCommands2) => {
55
- CallServerCommands2["Message"] = "message";
56
- CallServerCommands2["CancelLastAssistantMessage"] = "cancelLastAssistantMessage";
57
- CallServerCommands2["EnableSpeakerStreaming"] = "enableSpeakerStreaming";
58
- CallServerCommands2["EndInterview"] = "endInterview";
54
+ CallServerCommands2["Message"] = "Message";
55
+ CallServerCommands2["CancelLastAssistantMessage"] = "CancelLastAssistantMessage";
56
+ CallServerCommands2["CancelLastUserMessage"] = "CancelLastUserMessage";
57
+ CallServerCommands2["SkipAnswer"] = "SkipAnswer";
58
+ CallServerCommands2["EnableSpeakerStreaming"] = "EnableSpeakerStreaming";
59
+ CallServerCommands2["EndCall"] = "EndCall";
59
60
  return CallServerCommands2;
60
61
  })(CallServerCommands || {});
61
62
 
62
63
  // src/CallSocket.ts
63
- var END_INTERVIEW = "END_INTERVIEW";
64
64
  var CallSocket = class {
65
65
  constructor(socket, config) {
66
66
  this.socket = null;
@@ -101,7 +101,7 @@ var CallSocket = class {
101
101
  addMessage(message) {
102
102
  if (!this.socket || !this.config) return;
103
103
  this.conversation.push(message);
104
- this.socket.send(`${"message" /* Message */} ${JSON.stringify(message)}`);
104
+ this.socket.send(`${"Message" /* Message */} ${JSON.stringify(message)}`);
105
105
  this.config.onMessage?.(message);
106
106
  }
107
107
  async sendAudio(audio, processing, onAbort) {
@@ -115,7 +115,7 @@ var CallSocket = class {
115
115
  this.socket.send(audio);
116
116
  } else if ("paused" in audio) {
117
117
  if (!this.speakerStreamingEnabled) {
118
- this.socket.send("enableSpeakerStreaming" /* EnableSpeakerStreaming */);
118
+ this.socket.send("EnableSpeakerStreaming" /* EnableSpeakerStreaming */);
119
119
  this.speakerStreamingEnabled = true;
120
120
  }
121
121
  for await (const chunk of audio) {
@@ -151,12 +151,12 @@ var CallSocket = class {
151
151
  if (message.byteLength < 15) {
152
152
  const cmd = message.toString();
153
153
  this.log(`Command: ${cmd}`);
154
- if (cmd === "startSpeaking" /* StartSpeaking */) {
154
+ if (cmd === "StartSpeaking" /* StartSpeaking */) {
155
155
  this.isSpeaking = true;
156
156
  this.abortProcessing();
157
- } else if (cmd === "mute" /* Mute */) {
157
+ } else if (cmd === "Mute" /* Mute */) {
158
158
  this.abortProcessing();
159
- } else if (cmd === "stopSpeaking" /* StopSpeaking */) {
159
+ } else if (cmd === "StopSpeaking" /* StopSpeaking */) {
160
160
  this.isSpeaking = false;
161
161
  await this.onStopSpeaking();
162
162
  }
@@ -184,6 +184,7 @@ var CallSocket = class {
184
184
  );
185
185
  if (!transcript) {
186
186
  this.log("Ignoring empty transcript");
187
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
187
188
  return;
188
189
  }
189
190
  this.log("User transcript:", transcript);
@@ -200,6 +201,7 @@ var CallSocket = class {
200
201
  await this.answer(answer, processing);
201
202
  } catch (error) {
202
203
  console.error("[CallSocket]", error);
204
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
203
205
  }
204
206
  }
205
207
  // Add assistant message and send to client with audio (TTS)
@@ -209,40 +211,48 @@ var CallSocket = class {
209
211
  this.abortProcessing();
210
212
  processing = this.processing = { aborted: false };
211
213
  }
212
- let isEnd = false;
213
214
  let content = typeof message === "string" ? message : message.content;
214
215
  const metadata = typeof message === "string" ? void 0 : message.metadata;
215
- if (content.includes(END_INTERVIEW)) {
216
- content = content.replace(END_INTERVIEW, "").trim();
217
- isEnd = true;
216
+ if (!content.length || metadata?.commands?.cancelLastUserMessage) {
217
+ this.log("Cancelling last user message");
218
+ const lastMessage = this.conversation[this.conversation.length - 1];
219
+ if (lastMessage?.role === "user") {
220
+ this.conversation.pop();
221
+ this.socket?.send("CancelLastUserMessage" /* CancelLastUserMessage */);
222
+ }
223
+ return;
218
224
  }
219
- if (content.length) {
220
- this.log("Assistant message:", message);
221
- this.addMessage({ role: "assistant", content, metadata });
222
- if (!this.config.disableTTS) {
223
- try {
224
- const onAbort = () => {
225
- this.log("Answer aborted, removing last assistant message");
226
- const lastMessage = this.conversation[this.conversation.length - 1];
227
- if (lastMessage?.role === "assistant") {
228
- this.conversation.pop();
229
- this.socket?.send("cancelLastAssistantMessage" /* CancelLastAssistantMessage */);
230
- }
231
- };
232
- if (processing.aborted) {
233
- onAbort();
234
- return;
225
+ if (metadata?.commands?.skipAnswer) {
226
+ this.log("Skipping answer");
227
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
228
+ return;
229
+ }
230
+ this.log("Assistant message:", message);
231
+ this.addMessage({ role: "assistant", content, metadata });
232
+ if (!this.config.disableTTS) {
233
+ try {
234
+ const onAbort = () => {
235
+ this.log("Answer aborted, removing last assistant message");
236
+ const lastMessage = this.conversation[this.conversation.length - 1];
237
+ if (lastMessage?.role === "assistant") {
238
+ this.conversation.pop();
239
+ this.socket?.send("CancelLastAssistantMessage" /* CancelLastAssistantMessage */);
235
240
  }
236
- const audio = await this.config.text2Speech(content);
237
- await this.sendAudio(audio, processing, onAbort);
238
- } catch (error) {
239
- console.error("[CallSocket]", error);
241
+ };
242
+ if (processing.aborted) {
243
+ onAbort();
244
+ return;
240
245
  }
246
+ const audio = await this.config.text2Speech(content);
247
+ await this.sendAudio(audio, processing, onAbort);
248
+ } catch (error) {
249
+ console.error("[CallSocket]", error);
250
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
241
251
  }
242
252
  }
243
- if (isEnd) {
244
- this.log("Interview ended");
245
- this.socket.send("endInterview" /* EndInterview */);
253
+ if (metadata?.commands?.endCall) {
254
+ this.log("Call ended");
255
+ this.socket.send("EndCall" /* EndCall */);
246
256
  }
247
257
  }
248
258
  log(...message) {
@@ -303,7 +313,6 @@ async function waitForParams(socket, validate) {
303
313
  CallErrorCode,
304
314
  CallServerCommands,
305
315
  CallSocket,
306
- END_INTERVIEW,
307
316
  handleError,
308
317
  waitForParams
309
318
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/CallSocket.ts","../src/types.ts","../src/errors.ts","../src/waitForParams.ts"],"sourcesContent":["export * from './CallSocket'\nexport * from './errors'\nexport * from './types'\nexport * from './waitForParams'\n","import * as fs from 'fs'\nimport { WebSocket } from 'ws'\nimport {\n CallClientCommands,\n CallConfig,\n CallServerCommands,\n Conversation,\n ConversationMessage,\n} from './types'\n\nexport const END_INTERVIEW = 'END_INTERVIEW'\n\ninterface Processing {\n aborted: boolean\n}\n\nexport class CallSocket {\n public socket: WebSocket | null = null\n public config: CallConfig | null = null\n\n private startTime = Date.now()\n private lastDebug = Date.now()\n\n // An answer can be aborted if user is speaking\n private processing?: Processing\n\n // When user is speaking, we're waiting to chunks or to stop\n private isSpeaking = false\n\n // Chunks of user speech since user started speaking\n private chunks: Buffer[] = []\n\n // Conversation history\n private conversation: Conversation\n\n // Enable speaker streaming\n private speakerStreamingEnabled = false\n\n constructor(socket: WebSocket, config: CallConfig) {\n this.socket = socket\n this.config = config\n this.conversation = [{ role: 'system', content: config.systemPrompt }]\n this.log(`Call started`)\n\n // Assistant speaks first\n if (config.firstMessage) {\n this.answer(config.firstMessage)\n } else {\n this.config\n .generateAnswer(this.conversation)\n .then((answer) => this.answer(answer))\n .catch((error) => {\n console.error('[CallSocket]', error)\n socket?.close()\n // TODO: Implement retry\n })\n }\n\n // Listen to events\n socket.on('close', this.onClose.bind(this))\n socket.on('message', this.onMessage.bind(this))\n }\n\n // Reset conversation\n public resetConversation(conversation: Conversation) {\n this.log('Reset conversation')\n this.conversation = conversation\n }\n\n private abortProcessing() {\n if (!this.processing) return\n this.processing.aborted = true\n this.processing = undefined\n }\n\n private addMessage(message: ConversationMessage) {\n if (!this.socket || !this.config) return\n this.conversation.push(message)\n this.socket.send(`${CallServerCommands.Message} ${JSON.stringify(message)}`)\n this.config.onMessage?.(message)\n }\n\n private async sendAudio(\n audio: ArrayBuffer | NodeJS.ReadableStream,\n processing: Processing,\n onAbort?: () => void\n ) {\n if (!this.socket) return\n if (processing.aborted) {\n onAbort?.()\n return\n }\n\n if (Buffer.isBuffer(audio) || audio instanceof ArrayBuffer) {\n // Whole audio as a single buffer\n this.log(`Send audio: (${audio.byteLength} bytes)`)\n this.socket.send(audio)\n } else if ('paused' in audio) {\n // Enable speaker streaming if not already enabled\n if (!this.speakerStreamingEnabled) {\n this.socket.send(CallServerCommands.EnableSpeakerStreaming)\n this.speakerStreamingEnabled = true\n }\n\n // Audio as a stream\n for await (const chunk of audio) {\n if (processing.aborted) {\n onAbort?.()\n return\n }\n this.log(`Send audio chunk (${chunk.length} bytes)`)\n this.socket.send(chunk)\n }\n } else {\n this.log(`Unknown audio type: ${audio}`)\n }\n }\n\n private onClose() {\n if (!this.config) return\n this.log('Connection closed')\n this.abortProcessing()\n const duration = Math.round((Date.now() - this.startTime) / 1000)\n\n // End call callback\n this.config.onEnd?.({\n conversation: this.conversation.slice(1), // Remove system message\n duration,\n })\n\n // Unset params\n this.socket = null\n this.config = null\n }\n\n private async onMessage(message: Buffer) {\n if (!Buffer.isBuffer(message)) {\n console.warn(`[CallSocket] Message is not a buffer`)\n return\n }\n\n // Commands\n if (message.byteLength < 15) {\n const cmd = message.toString()\n this.log(`Command: ${cmd}`)\n\n if (cmd === CallClientCommands.StartSpeaking) {\n // User started speaking\n this.isSpeaking = true\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.Mute) {\n // User muted the call\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.StopSpeaking) {\n // User stopped speaking\n this.isSpeaking = false\n await this.onStopSpeaking()\n }\n }\n\n // Audio chunk\n else if (Buffer.isBuffer(message) && this.isSpeaking) {\n this.log(`Received chunk (${message.byteLength} bytes)`)\n this.chunks.push(message)\n }\n }\n\n private async onStopSpeaking() {\n if (!this.config) return\n\n // Do nothing if there is no chunk\n if (this.chunks.length === 0) return\n\n this.abortProcessing()\n const processing = (this.processing = { aborted: false })\n\n // Combine audio blob\n const blob = new Blob(this.chunks, { type: 'audio/ogg' })\n\n // Reset chunks for next user speech\n this.chunks.length = 0\n\n try {\n // Save file to disk\n if (this.config.debugSaveSpeech) {\n const filename = `speech-${Date.now()}.ogg`\n fs.writeFileSync(filename, Buffer.from(await blob.arrayBuffer()))\n this.log(`Saved speech: ${filename}`)\n }\n\n // STT: Get transcript and send to client\n const transcript = await this.config.speech2Text(\n blob,\n this.conversation[this.conversation.length - 1]?.content\n )\n if (!transcript) {\n this.log('Ignoring empty transcript')\n return\n }\n\n this.log('User transcript:', transcript)\n\n // Send transcript to client\n this.addMessage({ role: 'user', content: transcript })\n\n if (processing.aborted) {\n this.log('Answer aborted, no answer generated')\n return\n }\n\n // LLM: Generate answer\n const answer = await this.config.generateAnswer(this.conversation)\n if (processing.aborted) {\n this.log('Answer aborted, ignoring answer')\n return\n }\n\n await this.answer(answer, processing)\n } catch (error) {\n console.error('[CallSocket]', error)\n // TODO: Implement retry\n }\n }\n\n // Add assistant message and send to client with audio (TTS)\n public async answer(\n message: string | ConversationMessage,\n processing?: Processing\n ) {\n if (!this.socket || !this.config) return\n\n if (!processing) {\n this.abortProcessing()\n processing = this.processing = { aborted: false }\n }\n\n let isEnd = false\n\n let content = typeof message === 'string' ? message : message.content\n const metadata = typeof message === 'string' ? undefined : message.metadata\n\n // Detect end of interview\n if (content.includes(END_INTERVIEW)) {\n content = content.replace(END_INTERVIEW, '').trim()\n isEnd = true\n }\n\n if (content.length) {\n // Send answer to client\n this.log('Assistant message:', message)\n this.addMessage({ role: 'assistant', content, metadata })\n\n // TTS: Generate answer audio\n if (!this.config.disableTTS) {\n try {\n // Remove last assistant message if aborted\n const onAbort = () => {\n this.log('Answer aborted, removing last assistant message')\n const lastMessage = this.conversation[this.conversation.length - 1]\n if (lastMessage?.role === 'assistant') {\n this.conversation.pop()\n this.socket?.send(CallServerCommands.CancelLastAssistantMessage)\n }\n }\n\n if (processing.aborted) {\n onAbort()\n return\n }\n\n const audio = await this.config.text2Speech(content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallSocket]', error)\n // TODO: Implement retry\n }\n }\n }\n\n // End of call\n if (isEnd) {\n this.log('Interview ended')\n this.socket.send(CallServerCommands.EndInterview)\n }\n }\n\n private log(...message: any[]) {\n if (!this.config?.debugLog) return\n const now = Date.now()\n const delta = now - this.lastDebug\n this.lastDebug = now\n console.log(`[Debug +${delta}ms]`, ...message)\n }\n}\n","export enum CallClientCommands {\n StartSpeaking = 'startSpeaking',\n StopSpeaking = 'stopSpeaking',\n Mute = 'mute',\n}\n\nexport enum CallServerCommands {\n Message = 'message',\n CancelLastAssistantMessage = 'cancelLastAssistantMessage',\n EnableSpeakerStreaming = 'enableSpeakerStreaming',\n EndInterview = 'endInterview',\n}\n\nexport interface CallConfig {\n systemPrompt: string\n firstMessage?: string\n debugLog?: boolean\n debugSaveSpeech?: boolean\n disableTTS?: boolean\n generateAnswer(\n conversation: Conversation\n ): Promise<string | ConversationMessage>\n speech2Text(audioBlob: Blob, prevMessage?: string): Promise<string>\n text2Speech(text: string): Promise<ArrayBuffer | NodeJS.ReadableStream>\n onMessage?(message: ConversationMessage): void\n onEnd?(call: CallSummary): void\n}\n\nexport interface CallSummary {\n conversation: Conversation\n duration: number\n}\n\nexport type Conversation = ConversationMessage[]\n\nexport interface ConversationMessage<Data = any> {\n role: 'system' | 'user' | 'assistant'\n content: string\n metadata?: Data\n}\n","import WebSocket from 'ws'\n\nexport enum CallErrorCode {\n BadRequest = 4400,\n Unauthorized = 4401,\n NotFound = 4404,\n}\n\nexport class CallError extends Error {\n code: number\n\n constructor(code: number, message: string) {\n super(message)\n this.code = code\n }\n}\n\nexport function handleError(socket: WebSocket, error: unknown) {\n if (error instanceof CallError) {\n socket.close(error.code, error.message)\n } else {\n console.error(error)\n socket.close(1011)\n }\n socket.terminate()\n}\n","import { WebSocket } from 'ws'\nimport { CallError, CallErrorCode } from './errors'\n\nexport async function waitForParams<CallParams>(\n socket: WebSocket,\n validate: (params: any) => CallParams\n): Promise<CallParams> {\n return new Promise<CallParams>((resolve, reject) => {\n // Handle timeout\n const timeout = setTimeout(() => {\n reject(new CallError(CallErrorCode.BadRequest, 'Missing params'))\n }, 3000)\n\n const onParams = (payload: string) => {\n // Clear timeout and listener\n clearTimeout(timeout)\n socket.off('message', onParams)\n\n try {\n // Parse JSON payload\n const params = validate(JSON.parse(payload))\n resolve(params)\n } catch (error) {\n reject(new CallError(CallErrorCode.BadRequest, 'Invalid params'))\n }\n }\n\n // Listen for params\n socket.on('message', onParams)\n })\n}\n"],"mappings":"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","names":["CallClientCommands","CallServerCommands","CallErrorCode"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/CallSocket.ts","../src/types.ts","../src/errors.ts","../src/waitForParams.ts"],"sourcesContent":["export * from './CallSocket'\nexport * from './errors'\nexport * from './types'\nexport * from './waitForParams'\n","import * as fs from 'fs'\nimport { WebSocket } from 'ws'\nimport {\n CallClientCommands,\n CallConfig,\n CallServerCommands,\n Conversation,\n ConversationMessage,\n} from './types'\n\ninterface Processing {\n aborted: boolean\n}\n\nexport class CallSocket {\n public socket: WebSocket | null = null\n public config: CallConfig | null = null\n\n private startTime = Date.now()\n private lastDebug = Date.now()\n\n // An answer can be aborted if user is speaking\n private processing?: Processing\n\n // When user is speaking, we're waiting to chunks or to stop\n private isSpeaking = false\n\n // Chunks of user speech since user started speaking\n private chunks: Buffer[] = []\n\n // Conversation history\n private conversation: Conversation\n\n // Enable speaker streaming\n private speakerStreamingEnabled = false\n\n constructor(socket: WebSocket, config: CallConfig) {\n this.socket = socket\n this.config = config\n this.conversation = [{ role: 'system', content: config.systemPrompt }]\n this.log(`Call started`)\n\n // Assistant speaks first\n if (config.firstMessage) {\n this.answer(config.firstMessage)\n } else {\n this.config\n .generateAnswer(this.conversation)\n .then((answer) => this.answer(answer))\n .catch((error) => {\n console.error('[CallSocket]', error)\n socket?.close()\n // TODO: Implement retry\n })\n }\n\n // Listen to events\n socket.on('close', this.onClose.bind(this))\n socket.on('message', this.onMessage.bind(this))\n }\n\n // Reset conversation\n public resetConversation(conversation: Conversation) {\n this.log('Reset conversation')\n this.conversation = conversation\n }\n\n private abortProcessing() {\n if (!this.processing) return\n this.processing.aborted = true\n this.processing = undefined\n }\n\n private addMessage(message: ConversationMessage) {\n if (!this.socket || !this.config) return\n this.conversation.push(message)\n this.socket.send(`${CallServerCommands.Message} ${JSON.stringify(message)}`)\n this.config.onMessage?.(message)\n }\n\n private async sendAudio(\n audio: ArrayBuffer | NodeJS.ReadableStream,\n processing: Processing,\n onAbort?: () => void\n ) {\n if (!this.socket) return\n if (processing.aborted) {\n onAbort?.()\n return\n }\n\n if (Buffer.isBuffer(audio) || audio instanceof ArrayBuffer) {\n // Whole audio as a single buffer\n this.log(`Send audio: (${audio.byteLength} bytes)`)\n this.socket.send(audio)\n } else if ('paused' in audio) {\n // Enable speaker streaming if not already enabled\n if (!this.speakerStreamingEnabled) {\n this.socket.send(CallServerCommands.EnableSpeakerStreaming)\n this.speakerStreamingEnabled = true\n }\n\n // Audio as a stream\n for await (const chunk of audio) {\n if (processing.aborted) {\n onAbort?.()\n return\n }\n this.log(`Send audio chunk (${chunk.length} bytes)`)\n this.socket.send(chunk)\n }\n } else {\n this.log(`Unknown audio type: ${audio}`)\n }\n }\n\n private onClose() {\n if (!this.config) return\n this.log('Connection closed')\n this.abortProcessing()\n const duration = Math.round((Date.now() - this.startTime) / 1000)\n\n // End call callback\n this.config.onEnd?.({\n conversation: this.conversation.slice(1), // Remove system message\n duration,\n })\n\n // Unset params\n this.socket = null\n this.config = null\n }\n\n private async onMessage(message: Buffer) {\n if (!Buffer.isBuffer(message)) {\n console.warn(`[CallSocket] Message is not a buffer`)\n return\n }\n\n // Commands\n if (message.byteLength < 15) {\n const cmd = message.toString()\n this.log(`Command: ${cmd}`)\n\n if (cmd === CallClientCommands.StartSpeaking) {\n // User started speaking\n this.isSpeaking = true\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.Mute) {\n // User muted the call\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.StopSpeaking) {\n // User stopped speaking\n this.isSpeaking = false\n await this.onStopSpeaking()\n }\n }\n\n // Audio chunk\n else if (Buffer.isBuffer(message) && this.isSpeaking) {\n this.log(`Received chunk (${message.byteLength} bytes)`)\n this.chunks.push(message)\n }\n }\n\n private async onStopSpeaking() {\n if (!this.config) return\n\n // Do nothing if there is no chunk\n if (this.chunks.length === 0) return\n\n this.abortProcessing()\n const processing = (this.processing = { aborted: false })\n\n // Combine audio blob\n const blob = new Blob(this.chunks, { type: 'audio/ogg' })\n\n // Reset chunks for next user speech\n this.chunks.length = 0\n\n try {\n // Save file to disk\n if (this.config.debugSaveSpeech) {\n const filename = `speech-${Date.now()}.ogg`\n fs.writeFileSync(filename, Buffer.from(await blob.arrayBuffer()))\n this.log(`Saved speech: ${filename}`)\n }\n\n // STT: Get transcript and send to client\n const transcript = await this.config.speech2Text(\n blob,\n this.conversation[this.conversation.length - 1]?.content\n )\n if (!transcript) {\n this.log('Ignoring empty transcript')\n this.socket?.send(CallServerCommands.SkipAnswer)\n return\n }\n\n this.log('User transcript:', transcript)\n\n // Send transcript to client\n this.addMessage({ role: 'user', content: transcript })\n\n if (processing.aborted) {\n this.log('Answer aborted, no answer generated')\n return\n }\n\n // LLM: Generate answer\n const answer = await this.config.generateAnswer(this.conversation)\n if (processing.aborted) {\n this.log('Answer aborted, ignoring answer')\n return\n }\n\n await this.answer(answer, processing)\n } catch (error) {\n console.error('[CallSocket]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // Add assistant message and send to client with audio (TTS)\n public async answer(\n message: string | ConversationMessage,\n processing?: Processing\n ) {\n if (!this.socket || !this.config) return\n\n if (!processing) {\n this.abortProcessing()\n processing = this.processing = { aborted: false }\n }\n\n let content = typeof message === 'string' ? message : message.content\n const metadata = typeof message === 'string' ? undefined : message.metadata\n\n // Cancel last user message\n if (!content.length || metadata?.commands?.cancelLastUserMessage) {\n this.log('Cancelling last user message')\n const lastMessage = this.conversation[this.conversation.length - 1]\n if (lastMessage?.role === 'user') {\n this.conversation.pop()\n this.socket?.send(CallServerCommands.CancelLastUserMessage)\n }\n return\n }\n\n // Skip answer\n if (metadata?.commands?.skipAnswer) {\n this.log('Skipping answer')\n this.socket?.send(CallServerCommands.SkipAnswer)\n return\n }\n\n // Send answer to client\n this.log('Assistant message:', message)\n this.addMessage({ role: 'assistant', content, metadata })\n\n // TTS: Generate answer audio\n if (!this.config.disableTTS) {\n try {\n // Remove last assistant message if aborted\n const onAbort = () => {\n this.log('Answer aborted, removing last assistant message')\n const lastMessage = this.conversation[this.conversation.length - 1]\n if (lastMessage?.role === 'assistant') {\n this.conversation.pop()\n this.socket?.send(CallServerCommands.CancelLastAssistantMessage)\n }\n }\n\n if (processing.aborted) {\n onAbort()\n return\n }\n\n const audio = await this.config.text2Speech(content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallSocket]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // End of call\n if (metadata?.commands?.endCall) {\n this.log('Call ended')\n this.socket.send(CallServerCommands.EndCall)\n }\n }\n\n private log(...message: any[]) {\n if (!this.config?.debugLog) return\n const now = Date.now()\n const delta = now - this.lastDebug\n this.lastDebug = now\n console.log(`[Debug +${delta}ms]`, ...message)\n }\n}\n","export enum CallClientCommands {\n StartSpeaking = 'StartSpeaking',\n StopSpeaking = 'StopSpeaking',\n Mute = 'Mute',\n}\n\nexport enum CallServerCommands {\n Message = 'Message',\n CancelLastAssistantMessage = 'CancelLastAssistantMessage',\n CancelLastUserMessage = 'CancelLastUserMessage',\n SkipAnswer = 'SkipAnswer',\n EnableSpeakerStreaming = 'EnableSpeakerStreaming',\n EndCall = 'EndCall',\n}\n\nexport interface CallConfig {\n systemPrompt: string\n firstMessage?: string\n debugLog?: boolean\n debugSaveSpeech?: boolean\n disableTTS?: boolean\n generateAnswer(\n conversation: Conversation\n ): Promise<string | ConversationMessage>\n speech2Text(audioBlob: Blob, prevMessage?: string): Promise<string>\n text2Speech(text: string): Promise<ArrayBuffer | NodeJS.ReadableStream>\n onMessage?(message: ConversationMessage): void\n onEnd?(call: CallSummary): void\n}\n\nexport interface CallSummary {\n conversation: Conversation\n duration: number\n}\n\nexport type Conversation = ConversationMessage[]\n\nexport type CallMetadata = {\n commands?: {\n endCall?: boolean\n cancelLastUserMessage?: boolean\n skipAnswer?: boolean\n }\n [key: string]: any\n}\n\nexport interface ConversationMessage<Data extends CallMetadata = CallMetadata> {\n role: 'system' | 'user' | 'assistant'\n content: string\n metadata?: Data\n}\n","import WebSocket from 'ws'\n\nexport enum CallErrorCode {\n BadRequest = 4400,\n Unauthorized = 4401,\n NotFound = 4404,\n}\n\nexport class CallError extends Error {\n code: number\n\n constructor(code: number, message: string) {\n super(message)\n this.code = code\n }\n}\n\nexport function handleError(socket: WebSocket, error: unknown) {\n if (error instanceof CallError) {\n socket.close(error.code, error.message)\n } else {\n console.error(error)\n socket.close(1011)\n }\n socket.terminate()\n}\n","import { WebSocket } from 'ws'\nimport { CallError, CallErrorCode } from './errors'\n\nexport async function waitForParams<CallParams>(\n socket: WebSocket,\n validate: (params: any) => CallParams\n): Promise<CallParams> {\n return new Promise<CallParams>((resolve, reject) => {\n // Handle timeout\n const timeout = setTimeout(() => {\n reject(new CallError(CallErrorCode.BadRequest, 'Missing params'))\n }, 3000)\n\n const onParams = (payload: string) => {\n // Clear timeout and listener\n clearTimeout(timeout)\n socket.off('message', onParams)\n\n try {\n // Parse JSON payload\n const params = validate(JSON.parse(payload))\n resolve(params)\n } catch (error) {\n reject(new CallError(CallErrorCode.BadRequest, 'Invalid params'))\n }\n }\n\n // Listen for params\n socket.on('message', onParams)\n })\n}\n"],"mappings":"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","names":["CallClientCommands","CallServerCommands","CallErrorCode"]}
package/dist/index.mjs CHANGED
@@ -3,21 +3,22 @@ import * as fs from "fs";
3
3
 
4
4
  // src/types.ts
5
5
  var CallClientCommands = /* @__PURE__ */ ((CallClientCommands2) => {
6
- CallClientCommands2["StartSpeaking"] = "startSpeaking";
7
- CallClientCommands2["StopSpeaking"] = "stopSpeaking";
8
- CallClientCommands2["Mute"] = "mute";
6
+ CallClientCommands2["StartSpeaking"] = "StartSpeaking";
7
+ CallClientCommands2["StopSpeaking"] = "StopSpeaking";
8
+ CallClientCommands2["Mute"] = "Mute";
9
9
  return CallClientCommands2;
10
10
  })(CallClientCommands || {});
11
11
  var CallServerCommands = /* @__PURE__ */ ((CallServerCommands2) => {
12
- CallServerCommands2["Message"] = "message";
13
- CallServerCommands2["CancelLastAssistantMessage"] = "cancelLastAssistantMessage";
14
- CallServerCommands2["EnableSpeakerStreaming"] = "enableSpeakerStreaming";
15
- CallServerCommands2["EndInterview"] = "endInterview";
12
+ CallServerCommands2["Message"] = "Message";
13
+ CallServerCommands2["CancelLastAssistantMessage"] = "CancelLastAssistantMessage";
14
+ CallServerCommands2["CancelLastUserMessage"] = "CancelLastUserMessage";
15
+ CallServerCommands2["SkipAnswer"] = "SkipAnswer";
16
+ CallServerCommands2["EnableSpeakerStreaming"] = "EnableSpeakerStreaming";
17
+ CallServerCommands2["EndCall"] = "EndCall";
16
18
  return CallServerCommands2;
17
19
  })(CallServerCommands || {});
18
20
 
19
21
  // src/CallSocket.ts
20
- var END_INTERVIEW = "END_INTERVIEW";
21
22
  var CallSocket = class {
22
23
  constructor(socket, config) {
23
24
  this.socket = null;
@@ -58,7 +59,7 @@ var CallSocket = class {
58
59
  addMessage(message) {
59
60
  if (!this.socket || !this.config) return;
60
61
  this.conversation.push(message);
61
- this.socket.send(`${"message" /* Message */} ${JSON.stringify(message)}`);
62
+ this.socket.send(`${"Message" /* Message */} ${JSON.stringify(message)}`);
62
63
  this.config.onMessage?.(message);
63
64
  }
64
65
  async sendAudio(audio, processing, onAbort) {
@@ -72,7 +73,7 @@ var CallSocket = class {
72
73
  this.socket.send(audio);
73
74
  } else if ("paused" in audio) {
74
75
  if (!this.speakerStreamingEnabled) {
75
- this.socket.send("enableSpeakerStreaming" /* EnableSpeakerStreaming */);
76
+ this.socket.send("EnableSpeakerStreaming" /* EnableSpeakerStreaming */);
76
77
  this.speakerStreamingEnabled = true;
77
78
  }
78
79
  for await (const chunk of audio) {
@@ -108,12 +109,12 @@ var CallSocket = class {
108
109
  if (message.byteLength < 15) {
109
110
  const cmd = message.toString();
110
111
  this.log(`Command: ${cmd}`);
111
- if (cmd === "startSpeaking" /* StartSpeaking */) {
112
+ if (cmd === "StartSpeaking" /* StartSpeaking */) {
112
113
  this.isSpeaking = true;
113
114
  this.abortProcessing();
114
- } else if (cmd === "mute" /* Mute */) {
115
+ } else if (cmd === "Mute" /* Mute */) {
115
116
  this.abortProcessing();
116
- } else if (cmd === "stopSpeaking" /* StopSpeaking */) {
117
+ } else if (cmd === "StopSpeaking" /* StopSpeaking */) {
117
118
  this.isSpeaking = false;
118
119
  await this.onStopSpeaking();
119
120
  }
@@ -141,6 +142,7 @@ var CallSocket = class {
141
142
  );
142
143
  if (!transcript) {
143
144
  this.log("Ignoring empty transcript");
145
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
144
146
  return;
145
147
  }
146
148
  this.log("User transcript:", transcript);
@@ -157,6 +159,7 @@ var CallSocket = class {
157
159
  await this.answer(answer, processing);
158
160
  } catch (error) {
159
161
  console.error("[CallSocket]", error);
162
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
160
163
  }
161
164
  }
162
165
  // Add assistant message and send to client with audio (TTS)
@@ -166,40 +169,48 @@ var CallSocket = class {
166
169
  this.abortProcessing();
167
170
  processing = this.processing = { aborted: false };
168
171
  }
169
- let isEnd = false;
170
172
  let content = typeof message === "string" ? message : message.content;
171
173
  const metadata = typeof message === "string" ? void 0 : message.metadata;
172
- if (content.includes(END_INTERVIEW)) {
173
- content = content.replace(END_INTERVIEW, "").trim();
174
- isEnd = true;
174
+ if (!content.length || metadata?.commands?.cancelLastUserMessage) {
175
+ this.log("Cancelling last user message");
176
+ const lastMessage = this.conversation[this.conversation.length - 1];
177
+ if (lastMessage?.role === "user") {
178
+ this.conversation.pop();
179
+ this.socket?.send("CancelLastUserMessage" /* CancelLastUserMessage */);
180
+ }
181
+ return;
175
182
  }
176
- if (content.length) {
177
- this.log("Assistant message:", message);
178
- this.addMessage({ role: "assistant", content, metadata });
179
- if (!this.config.disableTTS) {
180
- try {
181
- const onAbort = () => {
182
- this.log("Answer aborted, removing last assistant message");
183
- const lastMessage = this.conversation[this.conversation.length - 1];
184
- if (lastMessage?.role === "assistant") {
185
- this.conversation.pop();
186
- this.socket?.send("cancelLastAssistantMessage" /* CancelLastAssistantMessage */);
187
- }
188
- };
189
- if (processing.aborted) {
190
- onAbort();
191
- return;
183
+ if (metadata?.commands?.skipAnswer) {
184
+ this.log("Skipping answer");
185
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
186
+ return;
187
+ }
188
+ this.log("Assistant message:", message);
189
+ this.addMessage({ role: "assistant", content, metadata });
190
+ if (!this.config.disableTTS) {
191
+ try {
192
+ const onAbort = () => {
193
+ this.log("Answer aborted, removing last assistant message");
194
+ const lastMessage = this.conversation[this.conversation.length - 1];
195
+ if (lastMessage?.role === "assistant") {
196
+ this.conversation.pop();
197
+ this.socket?.send("CancelLastAssistantMessage" /* CancelLastAssistantMessage */);
192
198
  }
193
- const audio = await this.config.text2Speech(content);
194
- await this.sendAudio(audio, processing, onAbort);
195
- } catch (error) {
196
- console.error("[CallSocket]", error);
199
+ };
200
+ if (processing.aborted) {
201
+ onAbort();
202
+ return;
197
203
  }
204
+ const audio = await this.config.text2Speech(content);
205
+ await this.sendAudio(audio, processing, onAbort);
206
+ } catch (error) {
207
+ console.error("[CallSocket]", error);
208
+ this.socket?.send("SkipAnswer" /* SkipAnswer */);
198
209
  }
199
210
  }
200
- if (isEnd) {
201
- this.log("Interview ended");
202
- this.socket.send("endInterview" /* EndInterview */);
211
+ if (metadata?.commands?.endCall) {
212
+ this.log("Call ended");
213
+ this.socket.send("EndCall" /* EndCall */);
203
214
  }
204
215
  }
205
216
  log(...message) {
@@ -259,7 +270,6 @@ export {
259
270
  CallErrorCode,
260
271
  CallServerCommands,
261
272
  CallSocket,
262
- END_INTERVIEW,
263
273
  handleError,
264
274
  waitForParams
265
275
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/CallSocket.ts","../src/types.ts","../src/errors.ts","../src/waitForParams.ts"],"sourcesContent":["import * as fs from 'fs'\nimport { WebSocket } from 'ws'\nimport {\n CallClientCommands,\n CallConfig,\n CallServerCommands,\n Conversation,\n ConversationMessage,\n} from './types'\n\nexport const END_INTERVIEW = 'END_INTERVIEW'\n\ninterface Processing {\n aborted: boolean\n}\n\nexport class CallSocket {\n public socket: WebSocket | null = null\n public config: CallConfig | null = null\n\n private startTime = Date.now()\n private lastDebug = Date.now()\n\n // An answer can be aborted if user is speaking\n private processing?: Processing\n\n // When user is speaking, we're waiting to chunks or to stop\n private isSpeaking = false\n\n // Chunks of user speech since user started speaking\n private chunks: Buffer[] = []\n\n // Conversation history\n private conversation: Conversation\n\n // Enable speaker streaming\n private speakerStreamingEnabled = false\n\n constructor(socket: WebSocket, config: CallConfig) {\n this.socket = socket\n this.config = config\n this.conversation = [{ role: 'system', content: config.systemPrompt }]\n this.log(`Call started`)\n\n // Assistant speaks first\n if (config.firstMessage) {\n this.answer(config.firstMessage)\n } else {\n this.config\n .generateAnswer(this.conversation)\n .then((answer) => this.answer(answer))\n .catch((error) => {\n console.error('[CallSocket]', error)\n socket?.close()\n // TODO: Implement retry\n })\n }\n\n // Listen to events\n socket.on('close', this.onClose.bind(this))\n socket.on('message', this.onMessage.bind(this))\n }\n\n // Reset conversation\n public resetConversation(conversation: Conversation) {\n this.log('Reset conversation')\n this.conversation = conversation\n }\n\n private abortProcessing() {\n if (!this.processing) return\n this.processing.aborted = true\n this.processing = undefined\n }\n\n private addMessage(message: ConversationMessage) {\n if (!this.socket || !this.config) return\n this.conversation.push(message)\n this.socket.send(`${CallServerCommands.Message} ${JSON.stringify(message)}`)\n this.config.onMessage?.(message)\n }\n\n private async sendAudio(\n audio: ArrayBuffer | NodeJS.ReadableStream,\n processing: Processing,\n onAbort?: () => void\n ) {\n if (!this.socket) return\n if (processing.aborted) {\n onAbort?.()\n return\n }\n\n if (Buffer.isBuffer(audio) || audio instanceof ArrayBuffer) {\n // Whole audio as a single buffer\n this.log(`Send audio: (${audio.byteLength} bytes)`)\n this.socket.send(audio)\n } else if ('paused' in audio) {\n // Enable speaker streaming if not already enabled\n if (!this.speakerStreamingEnabled) {\n this.socket.send(CallServerCommands.EnableSpeakerStreaming)\n this.speakerStreamingEnabled = true\n }\n\n // Audio as a stream\n for await (const chunk of audio) {\n if (processing.aborted) {\n onAbort?.()\n return\n }\n this.log(`Send audio chunk (${chunk.length} bytes)`)\n this.socket.send(chunk)\n }\n } else {\n this.log(`Unknown audio type: ${audio}`)\n }\n }\n\n private onClose() {\n if (!this.config) return\n this.log('Connection closed')\n this.abortProcessing()\n const duration = Math.round((Date.now() - this.startTime) / 1000)\n\n // End call callback\n this.config.onEnd?.({\n conversation: this.conversation.slice(1), // Remove system message\n duration,\n })\n\n // Unset params\n this.socket = null\n this.config = null\n }\n\n private async onMessage(message: Buffer) {\n if (!Buffer.isBuffer(message)) {\n console.warn(`[CallSocket] Message is not a buffer`)\n return\n }\n\n // Commands\n if (message.byteLength < 15) {\n const cmd = message.toString()\n this.log(`Command: ${cmd}`)\n\n if (cmd === CallClientCommands.StartSpeaking) {\n // User started speaking\n this.isSpeaking = true\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.Mute) {\n // User muted the call\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.StopSpeaking) {\n // User stopped speaking\n this.isSpeaking = false\n await this.onStopSpeaking()\n }\n }\n\n // Audio chunk\n else if (Buffer.isBuffer(message) && this.isSpeaking) {\n this.log(`Received chunk (${message.byteLength} bytes)`)\n this.chunks.push(message)\n }\n }\n\n private async onStopSpeaking() {\n if (!this.config) return\n\n // Do nothing if there is no chunk\n if (this.chunks.length === 0) return\n\n this.abortProcessing()\n const processing = (this.processing = { aborted: false })\n\n // Combine audio blob\n const blob = new Blob(this.chunks, { type: 'audio/ogg' })\n\n // Reset chunks for next user speech\n this.chunks.length = 0\n\n try {\n // Save file to disk\n if (this.config.debugSaveSpeech) {\n const filename = `speech-${Date.now()}.ogg`\n fs.writeFileSync(filename, Buffer.from(await blob.arrayBuffer()))\n this.log(`Saved speech: ${filename}`)\n }\n\n // STT: Get transcript and send to client\n const transcript = await this.config.speech2Text(\n blob,\n this.conversation[this.conversation.length - 1]?.content\n )\n if (!transcript) {\n this.log('Ignoring empty transcript')\n return\n }\n\n this.log('User transcript:', transcript)\n\n // Send transcript to client\n this.addMessage({ role: 'user', content: transcript })\n\n if (processing.aborted) {\n this.log('Answer aborted, no answer generated')\n return\n }\n\n // LLM: Generate answer\n const answer = await this.config.generateAnswer(this.conversation)\n if (processing.aborted) {\n this.log('Answer aborted, ignoring answer')\n return\n }\n\n await this.answer(answer, processing)\n } catch (error) {\n console.error('[CallSocket]', error)\n // TODO: Implement retry\n }\n }\n\n // Add assistant message and send to client with audio (TTS)\n public async answer(\n message: string | ConversationMessage,\n processing?: Processing\n ) {\n if (!this.socket || !this.config) return\n\n if (!processing) {\n this.abortProcessing()\n processing = this.processing = { aborted: false }\n }\n\n let isEnd = false\n\n let content = typeof message === 'string' ? message : message.content\n const metadata = typeof message === 'string' ? undefined : message.metadata\n\n // Detect end of interview\n if (content.includes(END_INTERVIEW)) {\n content = content.replace(END_INTERVIEW, '').trim()\n isEnd = true\n }\n\n if (content.length) {\n // Send answer to client\n this.log('Assistant message:', message)\n this.addMessage({ role: 'assistant', content, metadata })\n\n // TTS: Generate answer audio\n if (!this.config.disableTTS) {\n try {\n // Remove last assistant message if aborted\n const onAbort = () => {\n this.log('Answer aborted, removing last assistant message')\n const lastMessage = this.conversation[this.conversation.length - 1]\n if (lastMessage?.role === 'assistant') {\n this.conversation.pop()\n this.socket?.send(CallServerCommands.CancelLastAssistantMessage)\n }\n }\n\n if (processing.aborted) {\n onAbort()\n return\n }\n\n const audio = await this.config.text2Speech(content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallSocket]', error)\n // TODO: Implement retry\n }\n }\n }\n\n // End of call\n if (isEnd) {\n this.log('Interview ended')\n this.socket.send(CallServerCommands.EndInterview)\n }\n }\n\n private log(...message: any[]) {\n if (!this.config?.debugLog) return\n const now = Date.now()\n const delta = now - this.lastDebug\n this.lastDebug = now\n console.log(`[Debug +${delta}ms]`, ...message)\n }\n}\n","export enum CallClientCommands {\n StartSpeaking = 'startSpeaking',\n StopSpeaking = 'stopSpeaking',\n Mute = 'mute',\n}\n\nexport enum CallServerCommands {\n Message = 'message',\n CancelLastAssistantMessage = 'cancelLastAssistantMessage',\n EnableSpeakerStreaming = 'enableSpeakerStreaming',\n EndInterview = 'endInterview',\n}\n\nexport interface CallConfig {\n systemPrompt: string\n firstMessage?: string\n debugLog?: boolean\n debugSaveSpeech?: boolean\n disableTTS?: boolean\n generateAnswer(\n conversation: Conversation\n ): Promise<string | ConversationMessage>\n speech2Text(audioBlob: Blob, prevMessage?: string): Promise<string>\n text2Speech(text: string): Promise<ArrayBuffer | NodeJS.ReadableStream>\n onMessage?(message: ConversationMessage): void\n onEnd?(call: CallSummary): void\n}\n\nexport interface CallSummary {\n conversation: Conversation\n duration: number\n}\n\nexport type Conversation = ConversationMessage[]\n\nexport interface ConversationMessage<Data = any> {\n role: 'system' | 'user' | 'assistant'\n content: string\n metadata?: Data\n}\n","import WebSocket from 'ws'\n\nexport enum CallErrorCode {\n BadRequest = 4400,\n Unauthorized = 4401,\n NotFound = 4404,\n}\n\nexport class CallError extends Error {\n code: number\n\n constructor(code: number, message: string) {\n super(message)\n this.code = code\n }\n}\n\nexport function handleError(socket: WebSocket, error: unknown) {\n if (error instanceof CallError) {\n socket.close(error.code, error.message)\n } else {\n console.error(error)\n socket.close(1011)\n }\n socket.terminate()\n}\n","import { WebSocket } from 'ws'\nimport { CallError, CallErrorCode } from './errors'\n\nexport async function waitForParams<CallParams>(\n socket: WebSocket,\n validate: (params: any) => CallParams\n): Promise<CallParams> {\n return new Promise<CallParams>((resolve, reject) => {\n // Handle timeout\n const timeout = setTimeout(() => {\n reject(new CallError(CallErrorCode.BadRequest, 'Missing params'))\n }, 3000)\n\n const onParams = (payload: string) => {\n // Clear timeout and listener\n clearTimeout(timeout)\n socket.off('message', onParams)\n\n try {\n // Parse JSON payload\n const params = validate(JSON.parse(payload))\n resolve(params)\n } catch (error) {\n reject(new CallError(CallErrorCode.BadRequest, 'Invalid params'))\n }\n }\n\n // Listen for params\n socket.on('message', onParams)\n })\n}\n"],"mappings":"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","names":["CallClientCommands","CallServerCommands","CallErrorCode"]}
1
+ {"version":3,"sources":["../src/CallSocket.ts","../src/types.ts","../src/errors.ts","../src/waitForParams.ts"],"sourcesContent":["import * as fs from 'fs'\nimport { WebSocket } from 'ws'\nimport {\n CallClientCommands,\n CallConfig,\n CallServerCommands,\n Conversation,\n ConversationMessage,\n} from './types'\n\ninterface Processing {\n aborted: boolean\n}\n\nexport class CallSocket {\n public socket: WebSocket | null = null\n public config: CallConfig | null = null\n\n private startTime = Date.now()\n private lastDebug = Date.now()\n\n // An answer can be aborted if user is speaking\n private processing?: Processing\n\n // When user is speaking, we're waiting to chunks or to stop\n private isSpeaking = false\n\n // Chunks of user speech since user started speaking\n private chunks: Buffer[] = []\n\n // Conversation history\n private conversation: Conversation\n\n // Enable speaker streaming\n private speakerStreamingEnabled = false\n\n constructor(socket: WebSocket, config: CallConfig) {\n this.socket = socket\n this.config = config\n this.conversation = [{ role: 'system', content: config.systemPrompt }]\n this.log(`Call started`)\n\n // Assistant speaks first\n if (config.firstMessage) {\n this.answer(config.firstMessage)\n } else {\n this.config\n .generateAnswer(this.conversation)\n .then((answer) => this.answer(answer))\n .catch((error) => {\n console.error('[CallSocket]', error)\n socket?.close()\n // TODO: Implement retry\n })\n }\n\n // Listen to events\n socket.on('close', this.onClose.bind(this))\n socket.on('message', this.onMessage.bind(this))\n }\n\n // Reset conversation\n public resetConversation(conversation: Conversation) {\n this.log('Reset conversation')\n this.conversation = conversation\n }\n\n private abortProcessing() {\n if (!this.processing) return\n this.processing.aborted = true\n this.processing = undefined\n }\n\n private addMessage(message: ConversationMessage) {\n if (!this.socket || !this.config) return\n this.conversation.push(message)\n this.socket.send(`${CallServerCommands.Message} ${JSON.stringify(message)}`)\n this.config.onMessage?.(message)\n }\n\n private async sendAudio(\n audio: ArrayBuffer | NodeJS.ReadableStream,\n processing: Processing,\n onAbort?: () => void\n ) {\n if (!this.socket) return\n if (processing.aborted) {\n onAbort?.()\n return\n }\n\n if (Buffer.isBuffer(audio) || audio instanceof ArrayBuffer) {\n // Whole audio as a single buffer\n this.log(`Send audio: (${audio.byteLength} bytes)`)\n this.socket.send(audio)\n } else if ('paused' in audio) {\n // Enable speaker streaming if not already enabled\n if (!this.speakerStreamingEnabled) {\n this.socket.send(CallServerCommands.EnableSpeakerStreaming)\n this.speakerStreamingEnabled = true\n }\n\n // Audio as a stream\n for await (const chunk of audio) {\n if (processing.aborted) {\n onAbort?.()\n return\n }\n this.log(`Send audio chunk (${chunk.length} bytes)`)\n this.socket.send(chunk)\n }\n } else {\n this.log(`Unknown audio type: ${audio}`)\n }\n }\n\n private onClose() {\n if (!this.config) return\n this.log('Connection closed')\n this.abortProcessing()\n const duration = Math.round((Date.now() - this.startTime) / 1000)\n\n // End call callback\n this.config.onEnd?.({\n conversation: this.conversation.slice(1), // Remove system message\n duration,\n })\n\n // Unset params\n this.socket = null\n this.config = null\n }\n\n private async onMessage(message: Buffer) {\n if (!Buffer.isBuffer(message)) {\n console.warn(`[CallSocket] Message is not a buffer`)\n return\n }\n\n // Commands\n if (message.byteLength < 15) {\n const cmd = message.toString()\n this.log(`Command: ${cmd}`)\n\n if (cmd === CallClientCommands.StartSpeaking) {\n // User started speaking\n this.isSpeaking = true\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.Mute) {\n // User muted the call\n // Abort answer if there is generation in progress\n this.abortProcessing()\n } else if (cmd === CallClientCommands.StopSpeaking) {\n // User stopped speaking\n this.isSpeaking = false\n await this.onStopSpeaking()\n }\n }\n\n // Audio chunk\n else if (Buffer.isBuffer(message) && this.isSpeaking) {\n this.log(`Received chunk (${message.byteLength} bytes)`)\n this.chunks.push(message)\n }\n }\n\n private async onStopSpeaking() {\n if (!this.config) return\n\n // Do nothing if there is no chunk\n if (this.chunks.length === 0) return\n\n this.abortProcessing()\n const processing = (this.processing = { aborted: false })\n\n // Combine audio blob\n const blob = new Blob(this.chunks, { type: 'audio/ogg' })\n\n // Reset chunks for next user speech\n this.chunks.length = 0\n\n try {\n // Save file to disk\n if (this.config.debugSaveSpeech) {\n const filename = `speech-${Date.now()}.ogg`\n fs.writeFileSync(filename, Buffer.from(await blob.arrayBuffer()))\n this.log(`Saved speech: ${filename}`)\n }\n\n // STT: Get transcript and send to client\n const transcript = await this.config.speech2Text(\n blob,\n this.conversation[this.conversation.length - 1]?.content\n )\n if (!transcript) {\n this.log('Ignoring empty transcript')\n this.socket?.send(CallServerCommands.SkipAnswer)\n return\n }\n\n this.log('User transcript:', transcript)\n\n // Send transcript to client\n this.addMessage({ role: 'user', content: transcript })\n\n if (processing.aborted) {\n this.log('Answer aborted, no answer generated')\n return\n }\n\n // LLM: Generate answer\n const answer = await this.config.generateAnswer(this.conversation)\n if (processing.aborted) {\n this.log('Answer aborted, ignoring answer')\n return\n }\n\n await this.answer(answer, processing)\n } catch (error) {\n console.error('[CallSocket]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // Add assistant message and send to client with audio (TTS)\n public async answer(\n message: string | ConversationMessage,\n processing?: Processing\n ) {\n if (!this.socket || !this.config) return\n\n if (!processing) {\n this.abortProcessing()\n processing = this.processing = { aborted: false }\n }\n\n let content = typeof message === 'string' ? message : message.content\n const metadata = typeof message === 'string' ? undefined : message.metadata\n\n // Cancel last user message\n if (!content.length || metadata?.commands?.cancelLastUserMessage) {\n this.log('Cancelling last user message')\n const lastMessage = this.conversation[this.conversation.length - 1]\n if (lastMessage?.role === 'user') {\n this.conversation.pop()\n this.socket?.send(CallServerCommands.CancelLastUserMessage)\n }\n return\n }\n\n // Skip answer\n if (metadata?.commands?.skipAnswer) {\n this.log('Skipping answer')\n this.socket?.send(CallServerCommands.SkipAnswer)\n return\n }\n\n // Send answer to client\n this.log('Assistant message:', message)\n this.addMessage({ role: 'assistant', content, metadata })\n\n // TTS: Generate answer audio\n if (!this.config.disableTTS) {\n try {\n // Remove last assistant message if aborted\n const onAbort = () => {\n this.log('Answer aborted, removing last assistant message')\n const lastMessage = this.conversation[this.conversation.length - 1]\n if (lastMessage?.role === 'assistant') {\n this.conversation.pop()\n this.socket?.send(CallServerCommands.CancelLastAssistantMessage)\n }\n }\n\n if (processing.aborted) {\n onAbort()\n return\n }\n\n const audio = await this.config.text2Speech(content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallSocket]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // End of call\n if (metadata?.commands?.endCall) {\n this.log('Call ended')\n this.socket.send(CallServerCommands.EndCall)\n }\n }\n\n private log(...message: any[]) {\n if (!this.config?.debugLog) return\n const now = Date.now()\n const delta = now - this.lastDebug\n this.lastDebug = now\n console.log(`[Debug +${delta}ms]`, ...message)\n }\n}\n","export enum CallClientCommands {\n StartSpeaking = 'StartSpeaking',\n StopSpeaking = 'StopSpeaking',\n Mute = 'Mute',\n}\n\nexport enum CallServerCommands {\n Message = 'Message',\n CancelLastAssistantMessage = 'CancelLastAssistantMessage',\n CancelLastUserMessage = 'CancelLastUserMessage',\n SkipAnswer = 'SkipAnswer',\n EnableSpeakerStreaming = 'EnableSpeakerStreaming',\n EndCall = 'EndCall',\n}\n\nexport interface CallConfig {\n systemPrompt: string\n firstMessage?: string\n debugLog?: boolean\n debugSaveSpeech?: boolean\n disableTTS?: boolean\n generateAnswer(\n conversation: Conversation\n ): Promise<string | ConversationMessage>\n speech2Text(audioBlob: Blob, prevMessage?: string): Promise<string>\n text2Speech(text: string): Promise<ArrayBuffer | NodeJS.ReadableStream>\n onMessage?(message: ConversationMessage): void\n onEnd?(call: CallSummary): void\n}\n\nexport interface CallSummary {\n conversation: Conversation\n duration: number\n}\n\nexport type Conversation = ConversationMessage[]\n\nexport type CallMetadata = {\n commands?: {\n endCall?: boolean\n cancelLastUserMessage?: boolean\n skipAnswer?: boolean\n }\n [key: string]: any\n}\n\nexport interface ConversationMessage<Data extends CallMetadata = CallMetadata> {\n role: 'system' | 'user' | 'assistant'\n content: string\n metadata?: Data\n}\n","import WebSocket from 'ws'\n\nexport enum CallErrorCode {\n BadRequest = 4400,\n Unauthorized = 4401,\n NotFound = 4404,\n}\n\nexport class CallError extends Error {\n code: number\n\n constructor(code: number, message: string) {\n super(message)\n this.code = code\n }\n}\n\nexport function handleError(socket: WebSocket, error: unknown) {\n if (error instanceof CallError) {\n socket.close(error.code, error.message)\n } else {\n console.error(error)\n socket.close(1011)\n }\n socket.terminate()\n}\n","import { WebSocket } from 'ws'\nimport { CallError, CallErrorCode } from './errors'\n\nexport async function waitForParams<CallParams>(\n socket: WebSocket,\n validate: (params: any) => CallParams\n): Promise<CallParams> {\n return new Promise<CallParams>((resolve, reject) => {\n // Handle timeout\n const timeout = setTimeout(() => {\n reject(new CallError(CallErrorCode.BadRequest, 'Missing params'))\n }, 3000)\n\n const onParams = (payload: string) => {\n // Clear timeout and listener\n clearTimeout(timeout)\n socket.off('message', onParams)\n\n try {\n // Parse JSON payload\n const params = validate(JSON.parse(payload))\n resolve(params)\n } catch (error) {\n reject(new CallError(CallErrorCode.BadRequest, 'Invalid params'))\n }\n }\n\n // Listen for params\n socket.on('message', onParams)\n })\n}\n"],"mappings":"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","names":["CallClientCommands","CallServerCommands","CallErrorCode"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micdrop/server",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "A lib for Node.js that helps to use the mic and speaker for voice conversation",
5
5
  "author": "Lonestone",
6
6
  "license": "MIT",