This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

character / src / gemini-live.ts
3.0 kB 97 lines
1import type { ChatMessage, ResponseProvider } from './runtime'; 2import { GoogleGenAI, Modality, type LiveServerMessage } from '@google/genai'; 3 4type GeminiLiveOptions = { 5 apiKey?: string; 6 model?: string; 7 systemPrompt?: string; 8}; 9 10export class GeminiLiveProvider implements ResponseProvider { 11 private readonly apiKey?: string; 12 private readonly model: string; 13 private readonly systemPrompt: string; 14 private client?: GoogleGenAI; 15 16 constructor(options: GeminiLiveOptions = {}) { 17 this.apiKey = options.apiKey ?? process.env.GEMINI_API_KEY; 18 this.model = options.model ?? 'gemini-2.0-flash-live-001'; 19 this.systemPrompt = options.systemPrompt ?? 'You are a concise, friendly streaming VTuber agent replying to live chat.'; 20 } 21 22 async generateReply(message: ChatMessage): Promise<string> { 23 if (!this.apiKey) { 24 throw new Error('GEMINI_API_KEY is not set.'); 25 } 26 27 this.client ??= new GoogleGenAI({ apiKey: this.apiKey }); 28 29 const textChunks: string[] = []; 30 const queue: LiveServerMessage[] = []; 31 32 const session = await this.client.live.connect({ 33 model: this.model.startsWith('models/') ? this.model : `models/${this.model}`, 34 config: { 35 responseModalities: [Modality.TEXT], 36 systemInstruction: this.systemPrompt, 37 }, 38 callbacks: { 39 onmessage: (msg) => { 40 queue.push(msg); 41 42 const anyMsg = msg as any; 43 const serverContent = anyMsg.serverContent; 44 const directText = anyMsg.text; 45 if (typeof directText === 'string' && directText.trim()) { 46 textChunks.push(directText); 47 } 48 49 const modelTurn = serverContent?.modelTurn; 50 const parts = modelTurn?.parts ?? []; 51 for (const part of parts) { 52 if (typeof part?.text === 'string' && part.text.trim()) { 53 textChunks.push(part.text); 54 } 55 } 56 57 const outputTranscription = serverContent?.outputTranscription?.text; 58 if (typeof outputTranscription === 'string' && outputTranscription.trim()) { 59 textChunks.push(outputTranscription); 60 } 61 }, 62 onerror: (e) => { 63 throw e; 64 }, 65 }, 66 }); 67 68 try { 69 await session.sendClientContent({ 70 turns: `${message.author}: ${message.text}`, 71 } as any); 72 73 const started = Date.now(); 74 while (Date.now() - started < 15000) { 75 const done = queue.find((msg: any) => msg.serverContent?.turnComplete); 76 if (done) break; 77 await new Promise((resolve) => setTimeout(resolve, 50)); 78 } 79 80 const reply = textChunks.join(' ').replace(/\s+/g, ' ').trim(); 81 if (!reply) { 82 return `[gemini-live empty:${this.model}] ${message.author}: ${message.text}`; 83 } 84 return reply; 85 } finally { 86 session.close(); 87 } 88 } 89 90 describeSession() { 91 return { 92 model: this.model, 93 systemPrompt: this.systemPrompt, 94 transport: 'to-be-implemented' 95 }; 96 } 97}