- Yes. For Gemini Live, that appears to be exactly...
# ask-ai
r
• Yes. For Gemini Live, that appears to be exactly the billing model: if Gemini is listening, input audio tokens are charged, even when Gemini does not respond. Google’s Live API docs say output audio is only charged when Gemini responds or speaks, but input audio is charged while it is listening. For Chicky, the important repo detail is that in normal conversation mode we enable the LiveKit microphone immediately after room connect: /C:/Users/phano/ CascadeProjects/chickytutor/hooks/useLiveKitAgent.ts:1938. In the default Gemini Live path, STT/TTS are built into Gemini and vad_component starts as None, so we are not obviously gating Gemini input through a separate server- side VAD before Live receives audio. We do use browser noise suppression and LiveKit BVC noise cancellation, but that is not the same as “do not send audio to Gemini”: So the practical answer is: - If the user is connected and mic is unmuted, quiet time can still create billable input audio. - Background noise or room tone may also count as listening/input, though noise cancellation may reduce what reaches the model. - If Gemini stays silent, you avoid output audio cost, but not necessarily input audio cost. - If the mic is actually disabled/muted via setMicrophoneEnabled(false), then we should not be publishing user mic audio, so input audio cost should drop for that period. Is there a way to not send silence to gemini live?
✅ 1
t
Thanks for reaching out, Phanos. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a great summary and your conclusions are correct. I’ll address “Is there a way to not send silence to Gemini Live?” and give practical options you can implement in Chicky. Acknowledgement • You’re right: Gemini Live charges for input audio while it is listening, even if it doesn’t respond. With your current flow (mic enabled immediately after room connect and no server-side VAD gating), silence/room tone can still be sent and billed. What you can do (practical options) • Disable the mic when not needed ◦ Call
setMicrophoneEnabled(false)
(or
room.localParticipant.setMicrophoneEnabled(false)
) whenever the user is not expected to speak. This stops publishing audio to the room and prevents input billing during that time. • Use client-side VAD to toggle the mic automatically ◦ Run a local VAD (e.g., WebAudio-based or a lightweight VAD library) in the browser. Enable the mic only when speech is detected and disable it after speech ends or after a short timeout. This avoids sending room tone while preserving hands-free UX. • Gate before sending to Gemini (server-side VAD or audio gate) ◦ If you need to keep the track published to LiveKit for other reasons, add a server-side or intermediary VAD that forwards audio to Gemini only when voice activity is detected. Note: your current Gemini Live path does not do this by default. • Reduce unnecessary publish during agent output ◦ Disable user mic while the agent is speaking or during known quiet windows (e.g., during Gemini TTS playback). Re-enable for user turns. Trade-offs to consider • UX vs cost: aggressive muting/VAD reduces costs but may add latency or miss short user utterances. Tune VAD sensitivity and timeouts. • Noise cancellation vs gating: browser and LiveKit noise cancellation reduce what the model hears but do not guarantee zero billing if audio is published while mic is enabled. • Implementation complexity: client-side VAD + mic toggling is simpler to implement and avoids changes to the Gemini path; server-side gating requires routing audio through additional logic. Suggested next step • If you want, I can: ◦ Suggest a lightweight client-side VAD approach and code pattern to toggle
setMicrophoneEnabled
, or ◦ Outline how to integrate a server-side gate if you prefer that architecture. Which option would you like help implementing? Sources: Gemini Live API plugin | LiveKit Documentation | https://livekit.com/pricing.md| | Turns overview | LiveKit Documentation | Understanding "silence detected on local audio track" warning | Text and transcriptions | LiveKit Documentation, with interactive elements
r
◦ Suggest a lightweight client-side VAD approach and code pattern to toggle
setMicrophoneEnabled
, or ◦
t
Thanks — I’ll give a concise, practical client-side VAD approach you can drop into Chicky and a TypeScript/React pattern that toggles
setMicrophoneEnabled
. This will reduce sending silence to Gemini Live while keeping UX responsive. Summary • Use a lightweight VAD running in the browser (WebAssembly or WebAudio + small model) to detect speech locally. • When VAD reports speech, enable the mic (
setMicrophoneEnabled(true)
); when VAD reports silence for a short timeout, disable the mic (
setMicrophoneEnabled(false)
). • Tune sensitivity and silence timeout to balance missed starts vs. cost. Recommended options for browser VAD • Silero VAD compiled to WebAssembly (recommended for accuracy). • A tiny WebAudio/TensorFlow.js VAD (lower accuracy but simpler). (Choose one depending on complexity you want; below uses a generic WebAudio + WASM VAD pattern.) High-level pattern 1. Capture a short raw audio stream locally (AudioWorklet or ScriptProcessor) and feed frames into the VAD. 2. Maintain a speech state machine: ◦ On speech start -> call
room.localParticipant.setMicrophoneEnabled(true)
(if not already). ◦ On prolonged silence (e.g., 500–800 ms) -> call
room.localParticipant.setMicrophoneEnabled(false)
. 1. Optional: while mic is disabled you can still keep a muted track for playback sync or UI; ensure you’re not publishing audio when disabled. Example TypeScript/React pattern (concise) 1) Install/prepare VAD (pseudo-install for WASM or TF.js) • If using a WASM VAD, include the compiled WASM and a small JS wrapper. 2) Hook to microphone and VAD in a React hook:
Copy code
import { useEffect, useRef } from "react";

// Assume `room` is your LiveKit Room instance available in component scope
// and `createVAD` is a small wrapper that returns {processFrame: Float32Array=>bool, dispose}

export function useClientVAD(room: any, enabled = true) {
  const vadRef = useRef<any>(null);
  const audioCtxRef = useRef<AudioContext | null>(null);
  const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
  const workletNodeRef = useRef<AudioWorkletNode | null>(null);
  const silenceTimerRef = useRef<number | null>(null);
  const speakingRef = useRef(false);

  useEffect(() => {
    if (!enabled) return;
    let mounted = true;

    async function start() {
      audioCtxRef.current = new AudioContext({ sampleRate: 16000 });
      // request mic but do NOT publish until we explicitly enable via LiveKit SDK
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      sourceRef.current = audioCtxRef.current.createMediaStreamSource(stream);

      // load VAD WASM / model
      vadRef.current = await createVAD(); // implement or import your VAD wrapper

      // simple ScriptProcessor for small demo (or use AudioWorklet)
      const bufferSize = 2048;
      const processor = audioCtxRef.current.createScriptProcessor(bufferSize, 1, 1);
      processor.onaudioprocess = (e) => {
        const input = e.inputBuffer.getChannelData(0);
        const isSpeech = vadRef.current.processFrame(input); // returns bool
        if (isSpeech) {
          // cancel pending silence timeout
          if (silenceTimerRef.current) {
            window.clearTimeout(silenceTimerRef.current);
            silenceTimerRef.current = null;
          }
          if (!speakingRef.current) {
            speakingRef.current = true;
            // enable publishing to LiveKit so audio is sent to Gemini
            room.localParticipant.setMicrophoneEnabled(true);
          }
        } else {
          // start or reset silence timeout (e.g., 700ms)
          if (!silenceTimerRef.current) {
            silenceTimerRef.current = window.setTimeout(() => {
              silenceTimerRef.current = null;
              if (speakingRef.current) {
                speakingRef.current = false;
                room.localParticipant.setMicrophoneEnabled(false);
              }
            }, 700);
          }
        }
      };

      sourceRef.current.connect(processor);
      processor.connect(audioCtxRef.current.destination);
      workletNodeRef.current = processor;
    }

    start();

    return () => {
      mounted = false;
      // cleanup
      if (silenceTimerRef.current) window.clearTimeout(silenceTimerRef.current);
      if (workletNodeRef.current) {
        workletNodeRef.current.disconnect();
        workletNodeRef.current = null;
      }
      if (sourceRef.current) {
        sourceRef.current.disconnect();
        sourceRef.current = null;
      }
      if (audioCtxRef.current) {
        audioCtxRef.current.close();
        audioCtxRef.current = null;
      }
      if (vadRef.current) vadRef.current.dispose?.();
      // ensure mic publishing disabled on unmount
      room.localParticipant.setMicrophoneEnabled(false);
    };
  }, [room, enabled]);
}
Notes & tuning • Buffer size & sample rate: convert frames to the rate your VAD expects (common choice 16 kHz). Use an offline converter if needed. • Silence timeout: 500–800 ms is a common starting point. Lower values risk chopping words; higher values increase billing. • Warm-up: start VAD in a muted state, enable only when speech is detected to avoid initial billing. • Edge cases: If you need to detect very short interjections (“uh-huh”), lower timeout but be aware of false positives. • UX: show a mic LED/state so users know when mic is enabled. Allow manual override (user mute button) that forces mic off regardless of VAD. If you want, I can: • Provide a small concrete VAD implementation suggestion (Silero WebAssembly wrapper or a TF.js-compatible tiny model) and example
createVAD()
implementation, or • Tailor buffer sizes and timeouts to your existing audio chain in Chicky. Which would you prefer? Sources: Silero VAD plugin | LiveKit Documentation, with interactive elements