Pallavee: Building a Real-Time AI Colleague
The Inspiration
Static chatbots operate on a fundamentally flawed premise: wait your turn, type your query, wait for a wall of text. Real human conversation doesn't work that way. It's messy, simultaneous, and relies heavily on shared visual context.
The moment I realized how transformative it would be to combine three critical elements—ultra-low latency voice generation, instantaneous barge-in interruption handling, and active hardware-camera vision analysis—into a single unified stream, the concept for Pallavee was born. I wanted to build an agent that felt less like a terminal prompt and more like a knowledgeable colleague actively sitting beside me, seeing what I see and responding naturally to my thoughts.
How I Built It
Architecture Overview
Pallavee's core foundation was rapid-prototyped using the powerful Google ADK (Agent Development Kit), which drastically simplified defining agent scopes, tool mounting, and session lifecycle management. This allowed me to focus on the unique real-time challenges rather than reinventing agent infrastructure.
For the primary inferencing framework, I leveraged the bleeding-edge Gemini Live API (gemini-2.5-flash-native-audio-latest). By establishing a StreamingMode.BIDI (Bidirectional) context, the agent accepts raw bytes of audio and Base64 encoded JPEG vision data directly into its modality buffers while simultaneously streaming generated 24kHz PCM audio outward.
Backend Infrastructure
To handle this massive real-time data flow, the backend was built on FastAPI and WebSockets. The Python server manages persistent asynchronous connections with the client, orchestrating the flow of multi-modal data with precision timing.
The architecture distributes work across three layers:
- WebSocket Layer — Establishes persistent connections and manages binary frame chunking for continuous PCM streams
- Session Manager — Coordinates Gemini Live API sessions and tool integrations (Google Search)
- Audio Processing Pipeline — Handles real-time synthesis and VAD coordination
Frontend Implementation
On the frontend, a vanilla JavaScript client uses the Web Audio API and an advanced ScriptProcessorNode to capture raw 16kHz microphone data. The client calculates RMS (Root Mean Square) for local Voice Activity Detection (VAD) using:
$$\text{RMS} = \sqrt{\frac{1}{N}\sum_{i=0}^{N-1} x_i^2}$$
where $x_i$ represents audio sample amplitudes and $N$ is the window size. This local VAD reduces bandwidth consumption by transmitting only high-confidence audio frames with zero-gap PCM data to the server.
Concurrently, the frontend captures 2 FPS camera frames as Base64-encoded JPEG payloads, providing continuous visual context without overwhelming the network.
Deployment Pipeline
The entire backend pipeline is containerized using Docker and built through Google Cloud Build, enabling seamless, autoscaling deployment to Google Cloud Run. To ensure production-grade reliability and billing management, the agent intelligently falls back to Vertex AI when running in the cloud environment, leveraging enterprise-grade infrastructure.
Major Challenges
Challenge 1: Audio Resampling
Modern browsers default to 44.1kHz or 48kHz audio capture, but the Gemini Live API specifically demands highly-tuned 16kHz Int16 sample rates. This creates a sample rate conversion problem:
$$\text{New Sample Rate} = \text{Original Sample Rate} \times \frac{\text{Target Rate}}{\text{Original Rate}}$$
Porting native Python 3.13 packages to handle low-latency bit-crushing (audioop-lts) and dynamic rate conversion was a significant hurdle. The naive approach of linear interpolation introduced artifacts at the streaming boundary conditions, requiring careful windowing and anti-aliasing filters to prevent audio distortion.
Solution: Implemented a hybrid approach using FFmpeg for high-quality sample rate conversion while maintaining sub-100ms latency through streaming buffers and overlap-add windowing.
Challenge 2: True Barge-In Without Audio Stuttering
Implementing true interruption capability—where the user can cut off the agent mid-response—required solving a complex state synchronization problem. Without careful state tracking, audio queues would become misaligned, causing stuttering or missing audio chunks.
The problem domain involves:
- Frontend: Web Audio API playback nodes with their own internal buffering
- Backend: ADK
SessionManagermanaging both request and response queues - Network: WebSocket frames that may arrive out of order
Solution: Developed a precise state tracking mechanism between frontend and backend:
- Each audio frame carries a monotonically-increasing sequence number
- The frontend maintains a "playback sequence marker" that updates as audio plays
- Upon interruption detection, the backend receives a
CANCELmessage with the current playback position - The backend stops generating new tokens and clears queued responses up to that point
- New user audio is immediately processed without waiting for the cancel to fully propagate
This ensures zero audio stuttering and seamless pivot to new context.
Challenge 3: WebSocket Binary Frame Chunking
Raw PCM audio streaming over WebSockets exposed unexpected complexities. Browsers have framework overhead, and WebSocket frames have their own fragmentation semantics. Naively sending every audio chunk as a separate frame caused header bloat and increased context switching overhead.
The optimal frame size follows principles from communication theory—balancing latency against throughput efficiency. Through empirical testing, 4KB frames proved optimal for 24kHz PCM audio with sub-50ms latency.
What I Learned
Deep Architectural Insights
Mastering the ADK LiveRequestQueue: Understanding how the ADK schedules concurrent multi-modal jobs was crucial. The queue uses a priority-based scheduler where voice queries always preempt vision-only requests, ensuring responsive conversation even during heavy visual processing.
WebSocket Binary Frame Semantics: The extreme intricacies of WebSocket binary frame chunking for continuous raw PCM audio streams taught me that network protocols designed for text messaging require careful adaptation for streaming media. Proper buffering strategies and frame boundary management are essential for maintaining quality of service.
Authentication Architecture: The structural authentication differences between rapid local prototyping via API keys and production-grade IAM deployments running on Vertex AI revealed how security and scalability are intertwined. Using OAuth2 with Workload Identity in production required rethinking the entire credential lifecycle.
Technical Breakthroughs
- Achieved sub-500ms voice response latency using bare-metal PCM transfers over WSS protocols
- Built a highly resilient frontend canvas-stealing loop for invisible, clean camera context injection
- Implemented robust server-side transcript echoing and deduplication to ensure a perfectly synced, clean UI
- Reached zero hallucinations during factual queries explicitly through robust ADK Google Search integration
Broader Systems Thinking
This project reinforced that real-time systems require thinking in terms of end-to-end latency, not component latency. A 100ms improvement in backend API latency is worthless if the frontend capture introduces 300ms of buffering. Every millisecond matters when aiming for human-like responsiveness.
Future Directions
- Persistent Memory: Utilizing Vector databases to remember user preferences across distinct WebSocket sessions
- Multi-Language Support: Expanding the ADK configurations to support diverse global tongues dynamically
- Mobile App Wrapper: Porting the vanilla HTML/JS logic over to React Native for native iOS and Android experiences
- Advanced Context: Integrating document understanding and screen analysis for truly contextual help
Built with: Google ADK • Gemini Live API • Vertex AI • Cloud Run • FastAPI • Python • WebSockets • Web Audio API • Docker • Cloud Build
Built With
- cloud-build
- cloud-run
- docker
- fastapi
- gemini-live-api
- google-adk
- python
- vertex-ai
- web-audio-api
- websockets
Log in or sign up for Devpost to join the conversation.