@micdrop/server 1.7.0 → 1.7.1
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.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -155,6 +155,8 @@ var CallServer = class {
|
|
|
155
155
|
this.isSpeaking = true;
|
|
156
156
|
this.abortProcessing();
|
|
157
157
|
} else if (cmd === "Mute" /* Mute */) {
|
|
158
|
+
this.isSpeaking = false;
|
|
159
|
+
this.chunks.length = 0;
|
|
158
160
|
this.abortProcessing();
|
|
159
161
|
} else if (cmd === "StopSpeaking" /* StopSpeaking */) {
|
|
160
162
|
this.isSpeaking = false;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/CallServer.ts","../src/types.ts","../src/errors.ts","../src/waitForParams.ts"],"sourcesContent":["export * from './CallServer'\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 CallServer {\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('[CallServer]', 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(`[CallServer] 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('[CallServer]', 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 if (typeof message === 'string') {\n message = { role: 'assistant', content: message }\n }\n\n // Cancel last user message\n if (message.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 (!message.content.length || message.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(message)\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(message.content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallServer]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // End of call\n if (message.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 AnswerCommands = {\n endCall?: boolean\n cancelLastUserMessage?: boolean\n skipAnswer?: boolean\n}\n\nexport type AnswerMetadata = {\n [key: string]: any\n}\n\nexport interface ConversationMessage<\n Data extends AnswerMetadata = AnswerMetadata,\n> {\n role: 'system' | 'user' | 'assistant'\n content: string\n commands?: AnswerCommands\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/CallServer.ts","../src/types.ts","../src/errors.ts","../src/waitForParams.ts"],"sourcesContent":["export * from './CallServer'\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 CallServer {\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('[CallServer]', 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(`[CallServer] 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 this.isSpeaking = false\n this.chunks.length = 0\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('[CallServer]', 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 if (typeof message === 'string') {\n message = { role: 'assistant', content: message }\n }\n\n // Cancel last user message\n if (message.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 (!message.content.length || message.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(message)\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(message.content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallServer]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // End of call\n if (message.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 AnswerCommands = {\n endCall?: boolean\n cancelLastUserMessage?: boolean\n skipAnswer?: boolean\n}\n\nexport type AnswerMetadata = {\n [key: string]: any\n}\n\nexport interface ConversationMessage<\n Data extends AnswerMetadata = AnswerMetadata,\n> {\n role: 'system' | 'user' | 'assistant'\n content: string\n commands?: AnswerCommands\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
|
@@ -113,6 +113,8 @@ var CallServer = class {
|
|
|
113
113
|
this.isSpeaking = true;
|
|
114
114
|
this.abortProcessing();
|
|
115
115
|
} else if (cmd === "Mute" /* Mute */) {
|
|
116
|
+
this.isSpeaking = false;
|
|
117
|
+
this.chunks.length = 0;
|
|
116
118
|
this.abortProcessing();
|
|
117
119
|
} else if (cmd === "StopSpeaking" /* StopSpeaking */) {
|
|
118
120
|
this.isSpeaking = false;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/CallServer.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 CallServer {\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('[CallServer]', 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(`[CallServer] 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('[CallServer]', 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 if (typeof message === 'string') {\n message = { role: 'assistant', content: message }\n }\n\n // Cancel last user message\n if (message.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 (!message.content.length || message.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(message)\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(message.content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallServer]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // End of call\n if (message.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 AnswerCommands = {\n endCall?: boolean\n cancelLastUserMessage?: boolean\n skipAnswer?: boolean\n}\n\nexport type AnswerMetadata = {\n [key: string]: any\n}\n\nexport interface ConversationMessage<\n Data extends AnswerMetadata = AnswerMetadata,\n> {\n role: 'system' | 'user' | 'assistant'\n content: string\n commands?: AnswerCommands\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/CallServer.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 CallServer {\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('[CallServer]', 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(`[CallServer] 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 this.isSpeaking = false\n this.chunks.length = 0\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('[CallServer]', 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 if (typeof message === 'string') {\n message = { role: 'assistant', content: message }\n }\n\n // Cancel last user message\n if (message.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 (!message.content.length || message.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(message)\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(message.content)\n\n // Send audio to client\n await this.sendAudio(audio, processing, onAbort)\n } catch (error) {\n console.error('[CallServer]', error)\n this.socket?.send(CallServerCommands.SkipAnswer)\n // TODO: Implement retry\n }\n }\n\n // End of call\n if (message.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 AnswerCommands = {\n endCall?: boolean\n cancelLastUserMessage?: boolean\n skipAnswer?: boolean\n}\n\nexport type AnswerMetadata = {\n [key: string]: any\n}\n\nexport interface ConversationMessage<\n Data extends AnswerMetadata = AnswerMetadata,\n> {\n role: 'system' | 'user' | 'assistant'\n content: string\n commands?: AnswerCommands\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"]}
|