{"slug": "app-santa-cruz-js", "title": "app-santa-cruz.js", "summary": "A developer built a real-time voice assistant for the 'Build with AI de Santa Cruz' hackathon using Google's Gemini 3.1 Flash Live Preview model. The assistant, named Jarvis, processes audio input and output via Web Audio API and communicates with the Gemini API to provide conversational guidance to student participants.", "body_md": "| import { GoogleGenAI, Modality } from \"https://esm.sh/@google/genai\"; | |\n| import { key } from './_key.js' | |\n| const API_KEY = key; | |\n| let audioContext; | |\n| let nextStartTime = 0; | |\n| const audioSources = new Set(); | |\n| let audioInputContext; | |\n| let micStream; | |\n| let audioProcessor; | |\n| const genAI = new GoogleGenAI({ | |\n| apiKey: API_KEY, | |\n| }); | |\n| const instructions = ` | |\n| Eres el asistente oficial del evento 'Build with AI de Santa Cruz'. Tu propósito es guiar, inspirar y ayudar a los estudiantes de la universidad que participan en esta hackathon. Al iniciar la conexión, debes saludar con entusiasmo, darles la bienvenida formal al 'Build with AI de Santa Cruz', explicar brevemente quién eres (el copiloto de IA de la hackathon) y preguntarles si tienen alguna duda o idea que quieran empezar a pelotear. | |\n| REGLA CRÍTICA PARA VOZ: Mantén tus respuestas muy cortas, conversacionales y directas (máximo 2 o 3 oraciones por intervención). No uses listas con viñetas ni texto que suene raro al ser leído en voz alta. Habla con un tono amigable, tecnológico y motivador | |\n| `; | |\n| const config = { | |\n| responseModalities: [Modality.AUDIO], | |\n| systemInstruction: instructions, | |\n| outputAudioTranscription: {}, | |\n| inputAudioTranscription: {}, | |\n| }; | |\n| const session = await genAI.live.connect({ | |\n| model: 'gemini-3.1-flash-live-preview', | |\n| config: config, | |\n| callbacks: { | |\n| onmessage: async (message) => { | |\n| const { setupComplete, serverContent } = message; | |\n| if (setupComplete) { | |\n| console.log('Conectado'); | |\n| sendTextToIa(\"Empieza a hablar\"); | |\n| startMic(); | |\n| } | |\n| if (serverContent?.outputTranscription) { | |\n| const newText = serverContent.outputTranscription.text; | |\n| console.log(`Jarvis dice: [${newText}]`); | |\n| } | |\n| if (serverContent?.inputTranscription) { | |\n| const newText = serverContent.inputTranscription.text; | |\n| console.log(`User dice: [${newText}]`); | |\n| } | |\n| const audio = serverContent?.modelTurn?.parts[0]?.inlineData; | |\n| if (audio) { | |\n| playAIAudio(audio); | |\n| } | |\n| }, | |\n| onerror: async (e) => { | |\n| console.error('Session Error:', e); | |\n| }, | |\n| onclose: async (e) => { | |\n| console.warn('Session Closed:', e); | |\n| }, | |\n| }, | |\n| }); | |\n| window.sendTextToIa = (text) => { | |\n| session.sendRealtimeInput({ | |\n| text: text, | |\n| }); | |\n| } | |\n| function playAIAudio(audio) { | |\n| if (!audioContext) { | |\n| audioContext = new (window.AudioContext || window.webkitAudioContext)({ | |\n| sampleRate: 24000 | |\n| }); | |\n| nextStartTime = audioContext.currentTime; | |\n| } | |\n| const rawBytes = base64Decode(audio.data); | |\n| const aiFloat32_24k = getFloat32FromPCM(rawBytes); | |\n| nextStartTime = Math.max(nextStartTime, audioContext.currentTime); | |\n| const audioBuffer = audioContext.createBuffer(1, aiFloat32_24k.length, 24000); | |\n| audioBuffer.getChannelData(0).set(aiFloat32_24k); | |\n| const source = audioContext.createBufferSource(); | |\n| source.buffer = audioBuffer; | |\n| source.connect(audioContext.destination); | |\n| source.addEventListener('ended', () => audioSources.delete(source)); | |\n| source.start(nextStartTime); | |\n| nextStartTime += audioBuffer.duration; | |\n| audioSources.add(source); | |\n| } | |\n| function getFloat32FromPCM(bytesCrudos) { | |\n| const dataInt16 = new Int16Array(bytesCrudos.buffer); | |\n| const dataFloat32 = new Float32Array(dataInt16.length); | |\n| for (let i = 0; i < dataInt16.length; i++) { | |\n| dataFloat32[i] = dataInt16[i] / 32768.0; | |\n| } | |\n| return dataFloat32; | |\n| } | |\n| function base64Decode(base64) { | |\n| const binaryString = atob(base64); | |\n| const len = binaryString.length; | |\n| const bytes = new Uint8Array(len); | |\n| for (let i = 0; i < len; i++) { | |\n| bytes[i] = binaryString.charCodeAt(i); | |\n| } | |\n| return bytes; | |\n| } | |\n| window.startMic = async () => { | |\n| try { | |\n| micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |\n| audioInputContext = new (window.AudioContext || window.webkitAudioContext)({ | |\n| sampleRate: 16000 | |\n| }); | |\n| const source = audioInputContext.createMediaStreamSource(micStream); | |\n| audioProcessor = audioInputContext.createScriptProcessor(4096, 1, 1); | |\n| audioProcessor.onaudioprocess = (e) => { | |\n| const float32Data = e.inputBuffer.getChannelData(0); | |\n| const pcm16Data = floatTo16BitPCM(float32Data); | |\n| const base64Audio = arrayBufferToBase64(pcm16Data); | |\n| session.sendRealtimeInput({ | |\n| audio: { | |\n| data: base64Audio, | |\n| mimeType: `audio/pcm;rate=16000` | |\n| }, | |\n| }) | |\n| }; | |\n| source.connect(audioProcessor); | |\n| audioProcessor.connect(audioInputContext.destination); | |\n| console.log('Micrófono conectado'); | |\n| } | |\n| catch (error) { | |\n| console.warn(\"Mic error:\", error); | |\n| } | |\n| } | |\n| function floatTo16BitPCM(float32Array) { | |\n| const buffer = new ArrayBuffer(float32Array.length * 2); | |\n| const view = new DataView(buffer); | |\n| let offset = 0; | |\n| for (let i = 0; i < float32Array.length; i++, offset += 2) { | |\n| let s = Math.max(-1, Math.min(1, float32Array[i])); | |\n| view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); | |\n| } | |\n| return buffer; | |\n| } | |\n| function arrayBufferToBase64(buffer) { | |\n| let binary = ''; | |\n| const bytes = new Uint8Array(buffer); | |\n| const len = bytes.byteLength; | |\n| for (let i = 0; i < len; i++) { | |\n| binary += String.fromCharCode(bytes[i]); | |\n| } | |\n| return window.btoa(binary); | |\n| } |", "url": "https://wpnews.pro/news/app-santa-cruz-js", "canonical_source": "https://gist.github.com/briansalvattore/6c21d23f9f3a575c4993088bb4030c12", "published_at": "2026-05-30 10:46:56+00:00", "updated_at": "2026-06-27 16:33:21.791467+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "generative-ai", "ai-tools", "developer-tools"], "entities": ["Google", "Gemini 3.1 Flash Live Preview", "GoogleGenAI", "Web Audio API", "Jarvis", "Santa Cruz"], "alternates": {"html": "https://wpnews.pro/news/app-santa-cruz-js", "markdown": "https://wpnews.pro/news/app-santa-cruz-js.md", "text": "https://wpnews.pro/news/app-santa-cruz-js.txt", "jsonld": "https://wpnews.pro/news/app-santa-cruz-js.jsonld"}}