@micdrop/server 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -35,24 +35,28 @@ interface ConversationMessage<Data = any> {
35
35
  }
36
36
 
37
37
  declare const END_INTERVIEW = "END_INTERVIEW";
38
+ interface Processing {
39
+ aborted: boolean;
40
+ }
38
41
  declare class CallSocket {
39
42
  socket: WebSocket | null;
40
43
  config: CallConfig | null;
41
44
  private startTime;
42
45
  private lastDebug;
43
- private abortAnswer;
46
+ private processing?;
44
47
  private isSpeaking;
45
48
  private chunks;
46
49
  private conversation;
47
50
  private speakerStreamingEnabled;
48
51
  constructor(socket: WebSocket, config: CallConfig);
49
52
  resetConversation(conversation: Conversation): void;
53
+ private abortProcessing;
50
54
  private addMessage;
51
55
  private sendAudio;
52
56
  private onClose;
53
57
  private onMessage;
54
58
  private onStopSpeaking;
55
- answer(message: string | ConversationMessage): Promise<void>;
59
+ answer(message: string | ConversationMessage, processing?: Processing): Promise<void>;
56
60
  private log;
57
61
  }
58
62
 
package/dist/index.d.ts CHANGED
@@ -35,24 +35,28 @@ interface ConversationMessage<Data = any> {
35
35
  }
36
36
 
37
37
  declare const END_INTERVIEW = "END_INTERVIEW";
38
+ interface Processing {
39
+ aborted: boolean;
40
+ }
38
41
  declare class CallSocket {
39
42
  socket: WebSocket | null;
40
43
  config: CallConfig | null;
41
44
  private startTime;
42
45
  private lastDebug;
43
- private abortAnswer;
46
+ private processing?;
44
47
  private isSpeaking;
45
48
  private chunks;
46
49
  private conversation;
47
50
  private speakerStreamingEnabled;
48
51
  constructor(socket: WebSocket, config: CallConfig);
49
52
  resetConversation(conversation: Conversation): void;
53
+ private abortProcessing;
50
54
  private addMessage;
51
55
  private sendAudio;
52
56
  private onClose;
53
57
  private onMessage;
54
58
  private onStopSpeaking;
55
- answer(message: string | ConversationMessage): Promise<void>;
59
+ answer(message: string | ConversationMessage, processing?: Processing): Promise<void>;
56
60
  private log;
57
61
  }
58
62
 
package/dist/index.js CHANGED
@@ -67,8 +67,6 @@ var CallSocket = class {
67
67
  this.config = null;
68
68
  this.startTime = Date.now();
69
69
  this.lastDebug = Date.now();
70
- // An answer can be aborted if user is speaking
71
- this.abortAnswer = false;
72
70
  // When user is speaking, we're waiting to chunks or to stop
73
71
  this.isSpeaking = false;
74
72
  // Chunks of user speech since user started speaking
@@ -95,16 +93,21 @@ var CallSocket = class {
95
93
  this.log("Reset conversation");
96
94
  this.conversation = conversation;
97
95
  }
96
+ abortProcessing() {
97
+ if (!this.processing) return;
98
+ this.processing.aborted = true;
99
+ this.processing = void 0;
100
+ }
98
101
  addMessage(message) {
99
102
  if (!this.socket || !this.config) return;
100
103
  this.conversation.push(message);
101
104
  this.socket.send(`${"message" /* Message */} ${JSON.stringify(message)}`);
102
105
  this.config.onMessage?.(message);
103
106
  }
104
- async sendAudio(audio, abort) {
107
+ async sendAudio(audio, processing, onAbort) {
105
108
  if (!this.socket) return;
106
- if (this.abortAnswer) {
107
- abort?.();
109
+ if (processing.aborted) {
110
+ onAbort?.();
108
111
  return;
109
112
  }
110
113
  if (Buffer.isBuffer(audio) || audio instanceof ArrayBuffer) {
@@ -116,8 +119,8 @@ var CallSocket = class {
116
119
  this.speakerStreamingEnabled = true;
117
120
  }
118
121
  for await (const chunk of audio) {
119
- if (this.abortAnswer) {
120
- abort?.();
122
+ if (processing.aborted) {
123
+ onAbort?.();
121
124
  return;
122
125
  }
123
126
  this.log(`Send audio chunk (${chunk.length} bytes)`);
@@ -130,7 +133,7 @@ var CallSocket = class {
130
133
  onClose() {
131
134
  if (!this.config) return;
132
135
  this.log("Connection closed");
133
- this.abortAnswer = true;
136
+ this.abortProcessing();
134
137
  const duration = Math.round((Date.now() - this.startTime) / 1e3);
135
138
  this.config.onEnd?.({
136
139
  conversation: this.conversation.slice(1),
@@ -150,9 +153,9 @@ var CallSocket = class {
150
153
  this.log(`Command: ${cmd}`);
151
154
  if (cmd === "startSpeaking" /* StartSpeaking */) {
152
155
  this.isSpeaking = true;
153
- this.abortAnswer = true;
156
+ this.abortProcessing();
154
157
  } else if (cmd === "mute" /* Mute */) {
155
- this.abortAnswer = true;
158
+ this.abortProcessing();
156
159
  } else if (cmd === "stopSpeaking" /* StopSpeaking */) {
157
160
  this.isSpeaking = false;
158
161
  await this.onStopSpeaking();
@@ -165,7 +168,8 @@ var CallSocket = class {
165
168
  async onStopSpeaking() {
166
169
  if (!this.config) return;
167
170
  if (this.chunks.length === 0) return;
168
- this.abortAnswer = false;
171
+ this.abortProcessing();
172
+ const processing = this.processing = { aborted: false };
169
173
  const blob = new Blob(this.chunks, { type: "audio/ogg" });
170
174
  this.chunks.length = 0;
171
175
  try {
@@ -184,23 +188,27 @@ var CallSocket = class {
184
188
  }
185
189
  this.log("User transcript:", transcript);
186
190
  this.addMessage({ role: "user", content: transcript });
187
- if (this.abortAnswer) {
191
+ if (processing.aborted) {
188
192
  this.log("Answer aborted, no answer generated");
189
193
  return;
190
194
  }
191
195
  const answer = await this.config.generateAnswer(this.conversation);
192
- if (this.abortAnswer) {
196
+ if (processing.aborted) {
193
197
  this.log("Answer aborted, ignoring answer");
194
198
  return;
195
199
  }
196
- await this.answer(answer);
200
+ await this.answer(answer, processing);
197
201
  } catch (error) {
198
202
  console.error("[CallSocket]", error);
199
203
  }
200
204
  }
201
205
  // Add assistant message and send to client with audio (TTS)
202
- async answer(message) {
206
+ async answer(message, processing) {
203
207
  if (!this.socket || !this.config) return;
208
+ if (!processing) {
209
+ this.abortProcessing();
210
+ processing = this.processing = { aborted: false };
211
+ }
204
212
  let isEnd = false;
205
213
  let content = typeof message === "string" ? message : message.content;
206
214
  const metadata = typeof message === "string" ? void 0 : message.metadata;
@@ -213,8 +221,7 @@ var CallSocket = class {
213
221
  this.addMessage({ role: "assistant", content, metadata });
214
222
  if (!this.config.disableTTS) {
215
223
  try {
216
- const audio = await this.config.text2Speech(content);
217
- const abort = () => {
224
+ const onAbort = () => {
218
225
  this.log("Answer aborted, removing last assistant message");
219
226
  const lastMessage = this.conversation[this.conversation.length - 1];
220
227
  if (lastMessage?.role === "assistant") {
@@ -222,7 +229,12 @@ var CallSocket = class {
222
229
  this.socket?.send("cancelLastAssistantMessage" /* CancelLastAssistantMessage */);
223
230
  }
224
231
  };
225
- await this.sendAudio(audio, abort);
232
+ if (processing.aborted) {
233
+ onAbort();
234
+ return;
235
+ }
236
+ const audio = await this.config.text2Speech(content);
237
+ await this.sendAudio(audio, processing, onAbort);
226
238
  } catch (error) {
227
239
  console.error("[CallSocket]", error);
228
240
  }
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\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 abortAnswer = false\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 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 abort?: () => void\n ) {\n if (!this.socket) return\n if (this.abortAnswer) {\n abort?.()\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 (this.abortAnswer) {\n abort?.()\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.abortAnswer = true\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.abortAnswer = true\n } else if (cmd === CallClientCommands.Mute) {\n // User muted the call\n // Abort answer if there is generation in progress\n this.abortAnswer = true\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.abortAnswer = 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 (this.abortAnswer) {\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 (this.abortAnswer) {\n this.log('Answer aborted, ignoring answer')\n return\n }\n\n await this.answer(answer)\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(message: string | ConversationMessage) {\n if (!this.socket || !this.config) return\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 const audio = await this.config.text2Speech(content)\n\n // Remove last assistant message if aborted\n const abort = () => {\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 // Send audio to client\n await this.sendAudio(audio, abort)\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\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"]}
package/dist/index.mjs CHANGED
@@ -24,8 +24,6 @@ var CallSocket = class {
24
24
  this.config = null;
25
25
  this.startTime = Date.now();
26
26
  this.lastDebug = Date.now();
27
- // An answer can be aborted if user is speaking
28
- this.abortAnswer = false;
29
27
  // When user is speaking, we're waiting to chunks or to stop
30
28
  this.isSpeaking = false;
31
29
  // Chunks of user speech since user started speaking
@@ -52,16 +50,21 @@ var CallSocket = class {
52
50
  this.log("Reset conversation");
53
51
  this.conversation = conversation;
54
52
  }
53
+ abortProcessing() {
54
+ if (!this.processing) return;
55
+ this.processing.aborted = true;
56
+ this.processing = void 0;
57
+ }
55
58
  addMessage(message) {
56
59
  if (!this.socket || !this.config) return;
57
60
  this.conversation.push(message);
58
61
  this.socket.send(`${"message" /* Message */} ${JSON.stringify(message)}`);
59
62
  this.config.onMessage?.(message);
60
63
  }
61
- async sendAudio(audio, abort) {
64
+ async sendAudio(audio, processing, onAbort) {
62
65
  if (!this.socket) return;
63
- if (this.abortAnswer) {
64
- abort?.();
66
+ if (processing.aborted) {
67
+ onAbort?.();
65
68
  return;
66
69
  }
67
70
  if (Buffer.isBuffer(audio) || audio instanceof ArrayBuffer) {
@@ -73,8 +76,8 @@ var CallSocket = class {
73
76
  this.speakerStreamingEnabled = true;
74
77
  }
75
78
  for await (const chunk of audio) {
76
- if (this.abortAnswer) {
77
- abort?.();
79
+ if (processing.aborted) {
80
+ onAbort?.();
78
81
  return;
79
82
  }
80
83
  this.log(`Send audio chunk (${chunk.length} bytes)`);
@@ -87,7 +90,7 @@ var CallSocket = class {
87
90
  onClose() {
88
91
  if (!this.config) return;
89
92
  this.log("Connection closed");
90
- this.abortAnswer = true;
93
+ this.abortProcessing();
91
94
  const duration = Math.round((Date.now() - this.startTime) / 1e3);
92
95
  this.config.onEnd?.({
93
96
  conversation: this.conversation.slice(1),
@@ -107,9 +110,9 @@ var CallSocket = class {
107
110
  this.log(`Command: ${cmd}`);
108
111
  if (cmd === "startSpeaking" /* StartSpeaking */) {
109
112
  this.isSpeaking = true;
110
- this.abortAnswer = true;
113
+ this.abortProcessing();
111
114
  } else if (cmd === "mute" /* Mute */) {
112
- this.abortAnswer = true;
115
+ this.abortProcessing();
113
116
  } else if (cmd === "stopSpeaking" /* StopSpeaking */) {
114
117
  this.isSpeaking = false;
115
118
  await this.onStopSpeaking();
@@ -122,7 +125,8 @@ var CallSocket = class {
122
125
  async onStopSpeaking() {
123
126
  if (!this.config) return;
124
127
  if (this.chunks.length === 0) return;
125
- this.abortAnswer = false;
128
+ this.abortProcessing();
129
+ const processing = this.processing = { aborted: false };
126
130
  const blob = new Blob(this.chunks, { type: "audio/ogg" });
127
131
  this.chunks.length = 0;
128
132
  try {
@@ -141,23 +145,27 @@ var CallSocket = class {
141
145
  }
142
146
  this.log("User transcript:", transcript);
143
147
  this.addMessage({ role: "user", content: transcript });
144
- if (this.abortAnswer) {
148
+ if (processing.aborted) {
145
149
  this.log("Answer aborted, no answer generated");
146
150
  return;
147
151
  }
148
152
  const answer = await this.config.generateAnswer(this.conversation);
149
- if (this.abortAnswer) {
153
+ if (processing.aborted) {
150
154
  this.log("Answer aborted, ignoring answer");
151
155
  return;
152
156
  }
153
- await this.answer(answer);
157
+ await this.answer(answer, processing);
154
158
  } catch (error) {
155
159
  console.error("[CallSocket]", error);
156
160
  }
157
161
  }
158
162
  // Add assistant message and send to client with audio (TTS)
159
- async answer(message) {
163
+ async answer(message, processing) {
160
164
  if (!this.socket || !this.config) return;
165
+ if (!processing) {
166
+ this.abortProcessing();
167
+ processing = this.processing = { aborted: false };
168
+ }
161
169
  let isEnd = false;
162
170
  let content = typeof message === "string" ? message : message.content;
163
171
  const metadata = typeof message === "string" ? void 0 : message.metadata;
@@ -170,8 +178,7 @@ var CallSocket = class {
170
178
  this.addMessage({ role: "assistant", content, metadata });
171
179
  if (!this.config.disableTTS) {
172
180
  try {
173
- const audio = await this.config.text2Speech(content);
174
- const abort = () => {
181
+ const onAbort = () => {
175
182
  this.log("Answer aborted, removing last assistant message");
176
183
  const lastMessage = this.conversation[this.conversation.length - 1];
177
184
  if (lastMessage?.role === "assistant") {
@@ -179,7 +186,12 @@ var CallSocket = class {
179
186
  this.socket?.send("cancelLastAssistantMessage" /* CancelLastAssistantMessage */);
180
187
  }
181
188
  };
182
- await this.sendAudio(audio, abort);
189
+ if (processing.aborted) {
190
+ onAbort();
191
+ return;
192
+ }
193
+ const audio = await this.config.text2Speech(content);
194
+ await this.sendAudio(audio, processing, onAbort);
183
195
  } catch (error) {
184
196
  console.error("[CallSocket]", error);
185
197
  }
@@ -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\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 abortAnswer = false\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 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 abort?: () => void\n ) {\n if (!this.socket) return\n if (this.abortAnswer) {\n abort?.()\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 (this.abortAnswer) {\n abort?.()\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.abortAnswer = true\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.abortAnswer = true\n } else if (cmd === CallClientCommands.Mute) {\n // User muted the call\n // Abort answer if there is generation in progress\n this.abortAnswer = true\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.abortAnswer = 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 (this.abortAnswer) {\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 (this.abortAnswer) {\n this.log('Answer aborted, ignoring answer')\n return\n }\n\n await this.answer(answer)\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(message: string | ConversationMessage) {\n if (!this.socket || !this.config) return\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 const audio = await this.config.text2Speech(content)\n\n // Remove last assistant message if aborted\n const abort = () => {\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 // Send audio to client\n await this.sendAudio(audio, abort)\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\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micdrop/server",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",