Build a Gemini Live Voice Chat
Build a browser voice chat that streams audio to Gemini and supports barge-in.
Introduction
30 Second Summary
Voice assistants feel awkward when every reply becomes a speech you have to sit through. Real conversation needs room for you to jump in.
In this project, you will build a single-page voice chat with Google's Gemini Live API. The page uses a WebSocket for a spoken conversation you can interrupt mid-sentence.
What You'll Build
Your finished page feels like a real conversation because the model goes quiet within a beat when you speak over its answer.
By the end of this project, you'll have:
- Watch a live byte counter climb while the Web Audio API streams your microphone audio.
- Hear the model answer out loud with the voice preset you selected.
- Interrupt the model during a long answer. Its playback stops within a beat.
- Secret Mission: Take on an optional challenge to push your voice app skills further.
Are there any prerequisites?
You'll need a Google AI Studio API key plus a browser with microphone access. Basic familiarity with HTML and JavaScript is enough because there is no framework or build step.
Before We Start
A live voice page depends on more than code. The browser needs a working microphone before it can send speech that the model can understand.
This project runs as one HTML page. This step checks the few local tools that page needs.
You will also set a clear safety boundary before any credentials enter the page. The key you create later is disposable.
In this step, get ready to:
- Confirm your desktop browser and local tools are ready.
- Set a safety rule for the API key you will create.
- Test that your microphone produces a clear input signal.
Prepare your browser and local tools
A static HTML page runs directly in your browser. Your code editor gives you a place to change that page.
- Keep this project open in a modern desktop browser.
- Use your operating system search to open the code editor you plan to use.
You should now see this project in your browser. You should also see your editor's main window.
- Use your operating system search to open your terminal application.
You should see a terminal window with a command prompt ready for input.
Your three-part workspace is ready. Each tool now has one clear job.
Missing a Local Tool?
- Install a desktop code editor that can save plain text files if you do not have one.
- Use the terminal application included with your operating system.
- Update your desktop browser if it no longer receives browser updates.
Get help checking whether my browser, code editor, and terminal are ready for a static HTML project.
Protect the API key you will create
Browser JavaScript is visible to anyone who can inspect the page. A key placed inside this prototype can therefore be copied.
That exposure can feel alarming. A disposable key keeps the experiment contained.
Use a Throwaway Key
The Google AI Studio API key you create next is only for this learning session. Keep every permanent key out of the page.
Rotate the throwaway key after the project. A small proxy must protect any long-lived key before you publish the page online.
- Reserve the new Google AI Studio key from the next step for this project only.
- Keep personal keys out of the prototype.
- Plan to rotate the throwaway key when you finish the project.
Test your microphone
The Web Audio API can capture useful audio only when your operating system receives a live microphone signal. This check confirms the hardware before your browser requests page-level access later.
macOS
- Open the Apple menu.
- Select System Settings.
- Select Sound in the sidebar.
- Select Input.
- Select the microphone you plan to use from the list of sound input devices.
Before you speak, predict what the input level meter will do.
- Speak normally for a few seconds.
You should see the input level meter move with your voice. That movement proves your microphone is producing a signal.
- Adjust Input volume if the meter barely moves.
Windows
- Open Start.
- Select Settings.
- Select System.
- Select Sound.
- Select your microphone under Input.
- Select the control to the right of your microphone to open its properties.
Before you start the test, predict whether the recorded sample will sound clear on the first try.
- Select Start test under Input settings.
- Speak normally for a few seconds.
- Select Stop test.
- Select Play.
You should hear your recorded voice through your selected output device. That playback proves your microphone is producing a usable signal.
- Adjust the Input volume slider if the recording is too quiet.
Microphone Test Not Working?
- Check that your intended microphone is selected as the input device.
- Raise the input volume if the signal stays low.
- Reconnect your external microphone if it does not appear in the device list.
Help me diagnose why my microphone test is not detecting my voice.
That is the hardware check done. Your microphone is now ready to carry your voice into the prototype.
Your browser workspace is ready. Next up, you will create a throwaway AI Studio key and confirm that it works.
Get a Live API Key
Your voice chat page needs permission to reach the Gemini API. An API key supplies that permission for every request.
The finished page places the key in browser code. This project therefore uses a fresh throwaway key that you verify before building around it.
In this step, get ready to:
- Sign in to Google AI Studio and create a throwaway API key.
- List the available models with curl to verify the key.
- Select a Live API model identifier from the response.
Create a throwaway API key
Google AI Studio manages each key through a Google Cloud project. New keys created in AI Studio are authorization keys for the Gemini API.
Credentials deserve care. This key is deliberately temporary because the browser prototype exposes it to anyone who can inspect the page.
- Open the Google AI Studio API Keys page in your browser.
- Complete the Google sign-in flow if prompted.
- Accept the Terms of Service if prompted.
- Click Create API key.
- Select a Google Cloud project you control in the dialog.
- Follow the dialog to add the new key-project pair.
- Copy the new key value.
You now have the one credential this prototype needs. The model-list check gives it a safe first job.
Unable to create a key?
- Choose another Google Cloud project you control if the creation control is unavailable.
- Ask the project administrator for key creation permission if the project belongs to an organization.
Help me fix API key creation in Google AI Studio. You can also ask in the Google AI Developers Forum.
Verify the key and choose a model
The models.list endpoint returns the model catalogue available to your project. A successful response proves that the API accepts your new key.
Seeing a key inside a terminal command can feel uncomfortable. This is the disposable key you created for that exact reason.
Before you run the request, predict whether your new key will return model metadata or an authentication failure.
- List the available models by running this command after substituting your throwaway key for YOUR_API_KEY:
curl https://generativelanguage.googleapis.com/v1beta/models?key=YOUR_API_KEY
What does this command do?
- The curl command sends the request from your terminal.
- The /v1beta/models endpoint requests the available model catalogue.
- Your replacement for YOUR_API_KEY authenticates the request.
You should see a JSON response containing a models collection. Each model entry includes a name value.
Good, your throwaway key is working. The response now gives you the model catalogue for your live connection.
No model list in the response?
- Check that you replaced the entire YOUR_API_KEY placeholder without adding spaces.
- Create another throwaway key if the copied value is incomplete or unavailable.
Help me diagnose my failed Gemini model-list request. You can also ask in the Google AI Developers Forum.
A Live API session needs a model identifier designed for real-time conversation. The current official WebSocket quickstart uses gemini-3.8-live.
- Locate the model entry containing gemini-3.8-live in the successful response.
- Record its identifier here: gemini-3.8-live.
- Scroll the terminal until the command containing your key is outside the visible area.
- Keep the model entry visible for your checkpoint.
Your successful response proves that the key can reach the Gemini API. The listed model entry gives you the identifier for the Live API connection.
Your throwaway key now reaches the Gemini API. Next, you will use the selected model to open a WebSocket connection and inspect the setup response.
Connect to the Live API
Your successful curl response proved that your key can reach Gemini. A voice conversation needs a connection that stays open while audio travels in both directions.
In this step, you will build the first browser page for the Gemini Live API. The page opens a WebSocket connection.
Its first message requests audio responses. The browser console logs the server's setup response.
In this step, get ready to:
- Create a dependency-free page with your Live API configuration.
- Serve the page from a local static web server.
- Confirm that the server accepts the session setup.
Create the connection page
The browser needs your throwaway key to authenticate the connection. It also needs the Live model identifier you selected earlier.
Use the throwaway key only
This is the credential-sensitive part. Keep the page limited to the throwaway API key from earlier.
Anyone who can read the page source can read that key. This page is suitable for local learning only.
- Use your code editor's folder controls to create a gemini-live-voice folder on your Desktop.
- Use the editor's file sidebar to create index.html inside gemini-live-voice.
- Store the selected Live model identifier for the page: your selected Live model identifier.
- Paste this scaffold into index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Live Connection</title>
</head>
<body>
<h1>Gemini Live Connection</h1>
<p>Open the browser console to inspect the setup response.</p>
<script>
// Store only the throwaway credential used by this browser demo.
const API_KEY = 'your-api-key-here';
// Store the selected Live model for the session setup.
const MODEL_NAME = '[[MODEL_NAME="your selected Live model identifier"]]';
// Build the authenticated address for the persistent session.
const WS_URL = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${API_KEY}`;
</script>
</body>
</html>
What does this scaffold prepare?
- The API_KEY value holds the disposable credential used to authenticate this browser session.
- The MODEL_NAME value identifies the Live model that handles the conversation.
- The WS_URL value points the browser at the authenticated Live API endpoint.
- Replace your-api-key-here with the throwaway API key you created earlier.
- Save index.html.
- Confirm that the editor sidebar lists index.html inside gemini-live-voice.
Can't find the page file?
Check that the editor opened the gemini-live-voice folder from your Desktop. The index.html file must sit directly inside that folder.
Help me check the location of my HTML file.
Serve the page locally
A static web server gives the page a local HTTP address. Python includes a small server that can serve the folder without adding project dependencies.
The server keeps control of this terminal until you stop it. Press Ctrl+C when you finish the project.
macOS
- Move into gemini-live-voice and start its static server by running these commands:
cd ~/Desktop/gemini-live-voice
python3 -m http.server 8000
What do these commands do?
The first command moves the terminal into the folder containing index.html. The second command serves that folder on port 8000.
Windows
- Move into gemini-live-voice and start its static server by running these commands:
cd ~/Desktop/gemini-live-voice
python -m http.server 8000
What do these commands do?
The first command moves the terminal into the folder containing index.html. The second command serves that folder on port 8000.
Your terminal reports that it is serving the folder on port 8000.
- Leave the server terminal running.
- Open http://localhost:8000 in your browser.
You'll see the Gemini Live Connection heading. The message underneath points you toward the browser console.
Local page not loading?
- Confirm that the server terminal is still running.
- Check that the terminal entered the Desktop copy of gemini-live-voice.
- Use another available port if port 8000 is already occupied.
Help me debug my local static server.
Confirm the Live API handshake
A Live API session begins with a setup message. The server returns setupComplete when it accepts that configuration.
- In index.html, find the line that begins with const WS_URL.
- Paste this connection logic immediately below that line:
// Open one persistent session with the Live API.
const websocket = new WebSocket(WS_URL);
websocket.onopen = () => {
// Configure audio responses before sending microphone input.
const setupMessage = {
setup: {
model: `models/${MODEL_NAME}`,
responseModalities: ['AUDIO']
}
};
websocket.send(JSON.stringify(setupMessage));
};
websocket.onmessage = (event) => {
// Log setupComplete now and future server messages.
const response = JSON.parse(event.data);
console.log('Received:', response);
};
What does this connection logic do?
- The WebSocket keeps one bidirectional session open with the Live API.
- The onopen handler sends setupMessage after the connection becomes available.
- The setup selects your model. It requests AUDIO responses for the later voice conversation.
- The onmessage handler parses every server message. It prints each message in the browser console.
- Save index.html.
- Return to the local page from earlier.
- Use your browser menu to open its developer tools.
- Select the Console panel.
Before you refresh, which server field do you expect to confirm that the setup finished?
- Refresh the local page.
You'll see Received: followed by an object containing setupComplete. That field confirms that the server accepted your audio session setup.
That's the networking layer in place: your browser can now start a Live API session without a framework.
No setup response in the console?
- Verify that API_KEY contains the same throwaway key that passed your earlier request.
- Verify that MODEL_NAME contains the Live model identifier from your successful model list.
- Confirm that the local server terminal remains running.
- Refresh the page after opening the browser console.
Help me debug my Live API setup handshake.
✔️ Awesome, I've got everything!
Great. Double-check that you saved index.html while the local server keeps running.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Live Connection</title>
</head>
<body>
<h1>Gemini Live Connection</h1>
<p>Open the browser console to inspect the setup response.</p>
<script>
// Store only the throwaway credential used by this browser demo.
const API_KEY = 'your-api-key-here';
// Store the selected Live model for the session setup.
const MODEL_NAME = '[[MODEL_NAME="your selected Live model identifier"]]';
// Build the authenticated address for the persistent session.
const WS_URL = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${API_KEY}`;
// Open one persistent session with the Live API.
const websocket = new WebSocket(WS_URL);
websocket.onopen = () => {
// Configure audio responses before sending microphone input.
const setupMessage = {
setup: {
model: `models/${MODEL_NAME}`,
responseModalities: ['AUDIO']
}
};
websocket.send(JSON.stringify(setupMessage));
};
websocket.onmessage = (event) => {
// Log setupComplete now and future server messages.
const response = JSON.parse(event.data);
console.log('Received:', response);
};
</script>
</body>
</html>
Your browser can now reach the Live API. Next up, you will stream microphone audio through this open session.
Stream Microphone Audio
Your page already opens a live WebSocket connection. The missing half is a steady stream from your microphone.
The Web Audio API gives your page microphone frames as floating-point samples. The Gemini Live API expects raw 16-bit PCM input at 16 kHz in little-endian order.
You will bridge those formats through one AudioContext. A byte counter gives you immediate proof that audio is leaving the browser.
In this step, get ready to:
- Add a live byte counter to the page.
- Convert microphone samples into 16-bit PCM audio.
- Stream microphone frames through the open WebSocket.
Add the byte counter
Audio frames move too quickly to inspect one by one. A running byte total turns that invisible stream into a visible signal.
- In index.html, find the existing script element.
- Place these elements immediately before that script element:
<button id="start-microphone" onclick="startMicrophone()">Start microphone</button>
<p id="byte-counter">Sent 0 bytes</p>
What does this markup do?
- The button starts microphone capture from a deliberate browser gesture.
- The counter begins at zero because no microphone frames have crossed the connection yet.
- Save index.html.
- Refresh the hosted page in your browser.
You should see a Start microphone button above a counter that reads Sent 0 bytes.
Do not see the counter?
Confirm that both elements sit inside the page body. They must appear before the opening script tag.
Check the browser console for an HTML parsing problem. Help me find why the microphone button or byte counter is missing from my page.
Encode browser samples as PCM
Browser audio buffers hold values between negative one and positive one. The conversion helper scales each value into a signed 16-bit sample.
The helper writes each sample in little-endian order. That byte order matches the audio format expected by the Live API.
- Inside the existing script element in index.html, add this code after the WebSocket event handlers:
const audioContext = new AudioContext({ sampleRate: 16000 });
let sentBytes = 0;
// Pack each browser float as a signed 16-bit little-endian sample.
function floatToPcm16(samples) {
const buffer = new ArrayBuffer(samples.length * 2);
const view = new DataView(buffer);
for (let index = 0; index < samples.length; index += 1) {
const sample = Math.max(-1, Math.min(1, samples[index]));
view.setInt16(index * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
}
return new Uint8Array(buffer);
}
What does this code do?
- The single audioContext requests a 16 kHz sample rate for Live API input.
- The floatToPcm16() helper clamps every browser sample to its valid range.
- The DataView writes two bytes per sample in little-endian order.
- The returned Uint8Array is ready for Base64 encoding.
- Save index.html.
- Refresh the hosted page.
- Return to the browser developer console from earlier.
You should see the setup response again without a JavaScript syntax error. This proves the browser loaded the conversion helper successfully.
Seeing a JavaScript problem?
Check that the helper sits inside the existing script element. Confirm that every opening brace has a matching closing brace.
Make sure floatToPcm16() appears once in the file. Help me debug my PCM conversion helper.
Capture and stream microphone frames
Microphone permission gives the page access to a live media stream. A processing node reads that stream in small frames so each frame can cross the socket immediately.
The permission prompt can feel intrusive. Your browser shares audio only with this hosted page after you choose Allow.
- Add this function directly below floatToPcm16() inside index.html:
// Capture microphone frames and stream them through the configured socket.
async function startMicrophone() {
document.getElementById("start-microphone").disabled = true;
await audioContext.resume();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
// Convert each frame only after the Live API connection is ready.
processor.addEventListener("audioprocess", (event) => {
if (websocket.readyState === WebSocket.OPEN) {
const samples = event.inputBuffer.getChannelData(0);
const pcmBytes = floatToPcm16(samples);
const audioMessage = {
realtimeInput: { audio: {
data: pcmBytes.toBase64(),
mimeType: "audio/pcm;rate=16000"
} }
};
websocket.send(JSON.stringify(audioMessage));
sentBytes += pcmBytes.byteLength;
document.getElementById("byte-counter").textContent = `Sent ${sentBytes} bytes`;
}
});
source.connect(processor);
processor.connect(audioContext.destination);
}
How does the stream move?
- The microphone becomes a source inside the existing audio context.
- The processor reads one input channel in frames of 4096 samples.
- Each frame becomes Base64-encoded PCM inside a realtimeInput message.
- The counter increases by the number of raw PCM bytes sent through the open WebSocket.
- Save index.html.
- Refresh the hosted page.
Before you start the microphone, do you expect the counter to stay at zero or climb while you speak?
- Click Start microphone.
- Choose Allow when your browser requests microphone access.
- Speak into your microphone for a few seconds.
You should see the number after Sent climb as microphone frames leave the browser. That is the tricky audio input path working.
Counter stuck at zero?
Confirm that the page still logs a successful setup response before you start the microphone. The counter only changes while the WebSocket is open.
Check the browser site permissions if the microphone prompt never appeared. Refresh the page before trying the button again.
If the console reports a browser compatibility problem during Base64 conversion, update your modern desktop browser. Help me debug why my microphone byte counter stays at zero.
✔️ Awesome, I've got everything!
Your file now opens the Live API connection. It also converts microphone frames into PCM audio while the visible byte counter tracks the stream.
ⓧ I'd like to double check the full code
- Compare this reference with index.html while keeping your existing API key value.
- Keep your selected Live model identifier in the model constant.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Live Voice Chat</title>
</head>
<body>
<h1>Gemini Live Voice Chat</h1>
<button id="start-microphone" onclick="startMicrophone()">Start microphone</button>
<p id="byte-counter">Sent 0 bytes</p>
<script>
// Keep this key disposable because browser code exposes it to visitors.
const API_KEY = "your-api-key-here";
const MODEL_NAME = "[[MODEL_NAME=\"your selected Live model identifier\"]]";
const WS_URL = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${API_KEY}`;
// Open one Live API connection for the audio conversation.
const websocket = new WebSocket(WS_URL);
websocket.onopen = () => {
console.log("WebSocket Connected");
// Configure the session to return spoken audio.
const setupMessage = {
setup: {
model: `models/${MODEL_NAME}`,
responseModalities: ["AUDIO"]
}
};
websocket.send(JSON.stringify(setupMessage));
console.log("Configuration sent");
};
websocket.onmessage = (event) => {
const response = JSON.parse(event.data);
console.log("Received:", response);
};
websocket.onerror = (error) => {
console.error("WebSocket Error:", error);
};
websocket.onclose = () => {
console.log("WebSocket Closed");
};
const audioContext = new AudioContext({ sampleRate: 16000 });
let sentBytes = 0;
// Pack each browser float as a signed 16-bit little-endian sample.
function floatToPcm16(samples) {
const buffer = new ArrayBuffer(samples.length * 2);
const view = new DataView(buffer);
for (let index = 0; index < samples.length; index += 1) {
const sample = Math.max(-1, Math.min(1, samples[index]));
view.setInt16(index * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
}
return new Uint8Array(buffer);
}
// Capture microphone frames and stream them through the configured socket.
async function startMicrophone() {
document.getElementById("start-microphone").disabled = true;
await audioContext.resume();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
// Convert each frame only after the Live API connection is ready.
processor.addEventListener("audioprocess", (event) => {
if (websocket.readyState === WebSocket.OPEN) {
const samples = event.inputBuffer.getChannelData(0);
const pcmBytes = floatToPcm16(samples);
const audioMessage = {
realtimeInput: { audio: {
data: pcmBytes.toBase64(),
mimeType: "audio/pcm;rate=16000"
} }
};
websocket.send(JSON.stringify(audioMessage));
sentBytes += pcmBytes.byteLength;
document.getElementById("byte-counter").textContent = `Sent ${sentBytes} bytes`;
}
});
source.connect(processor);
processor.connect(audioContext.destination);
}
</script>
</body>
</html>
Your voice now crosses the WebSocket as measurable audio frames. Next up, you will turn the model's returned PCM chunks into sound you can hear.
Play the Model's Voice
Your microphone stream now reaches the Gemini Live API. The rising byte counter proves the browser is sending real audio.
A voice conversation still needs a return path. In this step, you'll decode the model's PCM chunks. You'll schedule them through the existing AudioContext for continuous playback.
In this step, get ready to:
- Inspect model audio chunks in the WebSocket response.
- Decode the returned PCM data for browser playback.
- Schedule each chunk through the existing audio context.
Inspect a returned audio chunk
Each server message can carry several content parts. Inspecting every part ensures every model audio chunk reaches playback.
- Return to the browser tab serving index.html.
- Open the browser's developer tools.
- Select the active WebSocket in the network inspector.
Before you speak, predict whether the newest server message will contain audio for the model's reply.
- Ask the page Why is the sky blue? through your microphone.
- Inspect the newest received WebSocket frame after the model responds.
You'll find serverContent containing a modelTurn with one or more parts. An audio part carries its base64 payload in inlineData.data.
Why does the audio arrive in chunks?
Streaming lets playback begin before the model finishes its whole answer. Each payload holds the next small section of speech.
Your page must preserve the arrival order. Ordered playback turns those separate payloads into one continuous answer.
Complete the audio return path
The returned payload is raw little-endian 16-bit PCM at 24 kHz. Your playback path converts those integers into browser audio samples.
Each chunk must start after the previous chunk finishes. A playback cursor keeps the answer in order and prevents overlap.
- Switch back to index.html in your code editor.
- Extend the existing WebSocket response handler with one playback path that turns every inlineData.data payload into correctly ordered audio through the existing AudioContext.
- Keep the microphone capture attached to the same AudioContext.
- Save index.html.
Before you test, predict whether the returned chunks will play as one continuous voice.
- Return to the browser page from earlier.
- Refresh the page.
- Ask What causes the seasons on Earth? through your microphone.
You'll hear the model answer through your speakers. The speech should play in order without gaps or overlapping chunks.
Hear silence or distorted audio?
- Confirm the playback path interprets each sample as signed little-endian 16-bit PCM.
- Confirm the playback buffer uses one channel at 24 kHz.
- Confirm every scheduled audio source connects to the existing audio context output.
Ask for help with the exact symptom you hear: Help me debug silent or distorted PCM playback in my browser voice chat page.
That is the full spoken round trip working. Your page now turns the model's streamed chunks into audible speech.
Your model has a voice. Next, you'll add controls that let you shape what it says and how it sounds.
Choose a Prompt and Voice
Your page can already send a spoken question over a WebSocket. It can play Gemini's answer through the same audio path.
Now you can shape each Gemini Live API session before the conversation starts.
A system instruction guides the model's behavior. A voice preset gives the response a distinct vocal character.
In this step, get ready to:
- Add an optional system-prompt input to the page.
- Add a picker with two voice presets.
- Ask the same question with each voice preset.
Add the prompt and voice controls
These controls collect the choices used to configure a fresh conversation. Keeping them on the page makes each voice test repeatable.
- Switch back to index.html in your code editor.
- Add an optional text input labeled System prompt to the existing page controls.
- Save index.html.
- Refresh the browser page.
You should see the new System prompt input beside the controls from earlier.
- Add a drop-down picker labeled Voice preset with Kore and Puck as its options.
- Save index.html.
- Refresh the browser page.
You should now see the optional prompt input plus a voice picker containing both presets.
Controls missing after refresh?
Confirm that you edited the same index.html served by your static host. An unsaved copy does not reach the browser.
Check that both controls sit inside the visible page content. Help me find why my prompt or voice controls are missing.
Apply both choices to each session
The setup message defines the behavior of a WebSocket session. A changed prompt or voice takes effect when your page creates a fresh session.
- Add systemInstruction to the existing setup object with a parts array whose text value comes from the prompt input. Omit this configuration when the input is blank.
- Save index.html.
- Refresh the browser page.
- Enter Reply in one short sentence. in the System prompt input.
- Start a fresh conversation through the page's existing connection flow.
- Ask What can you hear? out loud.
You should hear a short spoken response. That result proves the prompt value reaches the new session.
How the system instruction travels
The systemInstruction field belongs inside setup. Its parts array carries the prompt through a text value.
The server applies that instruction for the fresh session. Leaving the input blank preserves the optional behavior.
- Return to the code that builds the existing setup message in index.html.
- Add speechConfig inside generationConfig with nested voiceConfig and prebuiltVoiceConfig objects. Set voiceName from the selected voice preset.
- Save index.html.
- Refresh the browser page.
- Select Kore in the Voice preset picker.
- Start a fresh conversation through the page's existing connection flow.
You should see the same successful connection state from earlier. The new session now carries the selected voice configuration.
How the voice preset travels
The selected preset becomes the voiceName inside prebuiltVoiceConfig. The surrounding voiceConfig and speechConfig objects place that choice in the audio generation settings.
Creating a fresh session after a selection change ensures the server receives the new voice.
Fresh session not connecting?
Check the nesting of speechConfig, voiceConfig, and prebuiltVoiceConfig. Each object must contain the next one.
Confirm that voiceName receives the selected preset. Help me debug my Live API voice setup.
Compare two voices with one question
A controlled comparison keeps the prompt unchanged while the voice preset changes. Any audible difference now comes from the selected voice.
- Enter You are a patient science tutor. Answer in no more than three sentences. in the System prompt input.
- Select Kore in the Voice preset picker.
- Start a fresh conversation through the page's existing connection flow.
- Ask Why is the sky blue? out loud.
- Listen to the full response.
You should hear the science answer delivered with the firm character of Kore.
- Select Puck in the Voice preset picker.
- Start a fresh conversation through the page's existing connection flow.
Before you repeat the question, which parts of the delivery do you expect to change?
- Ask Why is the sky blue? out loud again.
- Listen to the full response.
You should hear the same kind of answer with the more upbeat character of Puck. The contrast confirms that each fresh session uses the selected preset.
You have now made your voice app configurable. Its behavior and vocal character can change without rewriting the conversation logic.
Both responses sound the same?
Confirm that the picker value supplies voiceName when the setup message is created. Changing the picker after setup does not alter the current session.
Create a fresh session after choosing each preset. Help me find why both voice presets sound the same.
Your conversations now have a selectable role and voice. Next up, you will make the playback stop the moment you speak over the model.
Interrupt Model Playback
Your page already lets you choose a voice. It can speak a model response back through the browser.
Conversation still feels rigid when queued audio ignores your interruption. This step makes the response yield as soon as you speak.
In this step, get ready to:
- Expose the current playback overlap.
- Add immediate interruption to scheduled model audio.
- Interrupt a long answer while the sent-byte counter keeps climbing.
Expose the playback overlap
Your browser has already scheduled several audio chunks by the time the model starts speaking. Those queued chunks can continue playing after your microphone captures a new sentence.
Before you try it, do you expect the current model playback to stop when it hears your voice?
- Switch back to the voice-chat page from earlier.
- Ask the model to give you a long explanation of how airplanes fly.
- Begin speaking halfway through the answer by saying, "Stop there. I have another question.".
You'll hear queued model audio keep playing while you speak. This overlap is the intended shortfall in the current page.
What is barge-in?
Barge-in lets your voice interrupt active model playback. The browser must cancel audio it has already scheduled while microphone streaming continues.
Cancel queued model audio
The Web Audio API plays each returned chunk through a one-shot AudioBufferSourceNode. The page must retain each source until playback ends so it can cancel scheduled sound.
How barge-in works
- A collection retains every scheduled AudioBufferSourceNode until that source finishes.
- Each source removes itself from the collection through its ended event.
- A clearing helper calls stop() on every tracked source.
- The helper empties the collection after stopping playback.
- The helper resets the playback cursor to AudioContext.currentTime so future chunks start from the present.
- The microphone callback triggers the helper before sending a new frame whenever tracked playback exists.
- Open index.html in your code editor.
- Locate the existing logic that schedules decoded model PCM for playback.
- Implement the complete barge-in flow described above across the playback scheduler plus microphone callback.
- Save index.html.
- Refresh the browser page.
You'll see the existing system-prompt control. You'll also see the voice picker plus sent-byte counter.
Playback still overlaps your voice?
Confirm that every scheduled source enters the playback collection before it starts. Check that finished sources leave the collection through the ended event.
Confirm that the microphone callback invokes the clearing helper before it sends the new frame. Make sure the helper resets the playback cursor after calling stop().
Help me debug why my scheduled model audio does not stop when new microphone input arrives.
Prove the interruption works
A successful interruption silences every queued model-audio source. Your microphone stream stays active throughout the test.
Before you test it, do you expect the voice to stop before your next sentence finishes?
- Start a fresh conversation with your preferred voice preset.
- Ask the model for a detailed answer about a topic you know well.
- Wait until the model is halfway through its spoken answer.
- Speak over the model with a new question.
- Continue speaking briefly while watching the sent-byte counter.
You'll hear model playback stop within a beat of your voice. You'll see the sent-byte counter keep rising while you speak.
You got it. Your page now supports an interruption that feels like a real spoken conversation.
That immediate silence turns streaming audio into a conversation you can steer.
Secret mission
Stress-Test Your Barge-In
One clean interruption proves the idea. Run a three-point reliability test across a spoken answer. Confirm that model audio stops immediately while microphone streaming continues.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep index.html available, pause its static host, or remove the project completely. Ongoing costs depend on any external hosting plan or Gemini API usage you continue after this session.
Resources you used:
- Dependency-free voice-chat page in index.html.
- Throwaway Google AI Studio API key configured in index.html.
- Local static web server process or secure static hosting deployment serving index.html.
- Browser microphone permission for the origin serving index.html.
Keep everything running
No action is needed for index.html during this session. Choose this option if you are still developing the page.
- Continue using the current page only for this learning session.
- Return to the Google AI Studio API key management area from earlier when you finish.
- Locate the masked entry for the throwaway key.
- Use its deletion control.
- Confirm the throwaway key is no longer listed.
- Replace the live key in index.html with your-api-key-here in the code editor from earlier.
- Save index.html.
- Keep the page offline until a small proxy supplies credentials for any future hosted version.
Keep Browser Credentials Safe
Your working page carries the throwaway key in client-side code. Anyone with access to the page source can copy it.
Deleting the key keeps your saved index.html useful without leaving an active credential behind.
Pause - I'll come back to this later
Shut down the active host while keeping index.html for later. Remove the live key before you step away.
- Stop the local static web server in the terminal from earlier if you served index.html locally.
- Remove the deployment serving index.html from the secure static hosting location from earlier if you hosted it externally.
- Return to the Google AI Studio API key management area from earlier.
- Locate the masked entry for the throwaway key.
- Use its deletion control.
- Replace the live key in index.html with your-api-key-here in the code editor from earlier.
- Save index.html.
- Reload the voice-chat URL in your browser.
The throwaway key should no longer appear in the key list.
Your voice-chat page should no longer load from its hosted URL.
Delete - I don't want to use this again
Remove the active credential first. Then clear the hosted page, local file, and browser permission.
- Stop the local static web server in the terminal from earlier if you served index.html locally.
- Select the deployment serving index.html in the secure static hosting location from earlier if you hosted it externally.
- Use that host's deployment removal control.
- Return to the Google AI Studio API key management area from earlier.
- Locate the masked entry for the throwaway key.
- Use its deletion control.
- Confirm the throwaway key is no longer listed.
That is the sensitive part handled. The exposed browser credential can no longer authorize requests.
- Delete index.html from the code editor file tree.
- Use the browser site controls for the page origin to revoke microphone access.
- Reload the project URL in your browser.
You should see that the voice-chat page no longer loads. The code editor file tree should no longer list index.html.
Nice Work!
Nice Work!
You did it! Your dependency-free voice chat page now holds a spoken conversation through the Gemini Live API. Its barge-in behavior stops the model within a beat when you speak over it.
You've learned how to:
- Built a browser voice pipeline with the Web Audio API. It streams microphone PCM over a WebSocket. It schedules returned audio through the same AudioContext.
- Configured system instructions through an optional prompt. You also applied selectable voice presets to hear different voices answer the same question.
- Implemented barge-in interruption that immediately stops scheduled model audio when new microphone input arrives. You proved it works by cutting off a long spoken answer halfway through.
- Secret Mission: Completed an optional challenge that extends your voice chat page beyond the core conversation flow.
Ready to quiz yourself?