@tw2gem/server
Version:
WebSocket server for receiving real-time audio streams from Twilio calls and sending them to the Gemini Live API for processing.
86 lines (70 loc) • 3.37 kB
text/typescript
import { TwilioMediaEvent, TwilioServerOptions, TwilioWebSocketServer } from '@tw2gem/twilio-server';
import { BidiGenerateContentServerContent, GeminiLiveClient } from '@tw2gem/gemini-live-client';
import { Tw2GemGeminiEvents, Tw2GemServerOptions, Tw2GemSocket } from './server.dto';
import { AudioConverter } from '@tw2gem/audio-converter';
export class Tw2GemServer extends TwilioWebSocketServer {
public onNewCall?: (socket: Tw2GemSocket) => void;
public geminiLive = new Tw2GemGeminiEvents();
constructor(options: Tw2GemServerOptions) {
super(options.serverOptions);
const twilioServerOptions = <TwilioServerOptions>options.serverOptions;
twilioServerOptions.handlers = {
onStart: (socket: Tw2GemSocket, event) => {
this.onNewCall?.(socket);
const geminiClient = new GeminiLiveClient(options.geminiOptions);
socket.twilioStreamSid = event.streamSid;
geminiClient.onReady = () => {
socket.geminiClient = geminiClient;
this.geminiLive.onReady?.(socket);
};
geminiClient.onClose = () => {
socket.close();
this.geminiLive.onClose?.(socket);
};
geminiClient.onError = (error) => {
this.onError?.(socket, error);
};
geminiClient.onServerContent = (serverContent) => {
this.onServerContent?.(socket, serverContent);
};
socket.onclose = (event) => {
if (socket?.geminiClient) {
socket.geminiClient.close();
delete socket.geminiClient;
}
this.onClose?.(socket, event);
};
},
onMedia: this.onMedia
};
}
public onMedia(socket: Tw2GemSocket, event: TwilioMediaEvent) {
if (!socket.geminiClient || event.media?.track !== 'inbound' || !event.media.payload)
return;
const base64MulawAudio = event.media.payload;
const base64PCM16k = AudioConverter.convertBase64MuLawToBase64PCM16k(base64MulawAudio);
socket.geminiClient.sendRealTime({
audio: {
mimeType: 'audio/pcm;rate=16000',
data: base64PCM16k
}
});
}
public onServerContent(socket: Tw2GemSocket, serverContent: BidiGenerateContentServerContent) {
if (!socket.twilioStreamSid || !socket.geminiClient || !serverContent.modelTurn?.parts?.length)
return;
const parts = serverContent.modelTurn?.parts;
const inlineData = parts.flatMap(part => part.inlineData)?.filter(item => item?.mimeType === 'audio/pcm;rate=24000' && item?.data);
if (!inlineData?.length)
return;
const base64Mulaws = inlineData.map(lineData => AudioConverter.convertBase64PCM24kToBase64MuLaw8k(lineData!.data));
for (const audios of base64Mulaws) {
socket.sendMedia({
streamSid: socket.twilioStreamSid,
media: {
payload: audios
}
});
}
}
}