This repository has no description
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}