Based on the git changes, this is a major migration from OpenAI's Realtime API to LiveKit for voice infrastructure. Here's the commit message:

```
feat: migrate from OpenAI Realtime API to LiveKit for voice infrastructure

Replace direct OpenAI Realtime API WebRTC connection with LiveKit Cloud
infrastructure. Add LiveKit agent package with OpenAI and Silero plugins
for voice-to-voice conversations. Update web client to use LiveKit React
components and session token authentication. Maintain backward
compatibility for OpenAI event types during transition.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
```

This commit message:
- Uses `feat:` type since this is a new feature/major change
- Describes the main change (migration from OpenAI to LiveKit)
- Provides context in the body about what was added/changed
- Mentions backward compatibility consideration
- Includes the Claude Code attribution as requested
This commit is contained in:
Mohamed Boudra
2025-10-14 11:54:17 +02:00
parent 5dd1aae70d
commit b70c0cfc73
12 changed files with 6312 additions and 419 deletions
+95 -16
View File
@@ -1,9 +1,15 @@
# Voice Dev # Voice Dev
Voice-controlled development environment powered by OpenAI Realtime API. Voice-controlled development environment powered by LiveKit and OpenAI Realtime API.
## Packages ## Packages
- **`@voice-dev/agent`** - LiveKit agent with OpenAI Realtime API
- Located in `packages/agent/`
- Handles voice conversations via LiveKit
- Integrates OpenAI Realtime API
- Manages agent lifecycle
- **`@voice-dev/mcp-server`** - MCP server for terminal control - **`@voice-dev/mcp-server`** - MCP server for terminal control
- Located in `packages/mcp-server/` - Located in `packages/mcp-server/`
- Provides terminal session management via MCP protocol - Provides terminal session management via MCP protocol
@@ -11,20 +17,37 @@ Voice-controlled development environment powered by OpenAI Realtime API.
- **`@voice-dev/web`** - Next.js web interface - **`@voice-dev/web`** - Next.js web interface
- Located in `packages/web/` - Located in `packages/web/`
- Voice interaction with OpenAI Realtime API - Voice interaction via LiveKit Cloud
- Password authentication - Password authentication
- Agent activity transparency (debug panel) - Agent activity transparency (debug panel)
- Real-time status display - Real-time status display
## Quick Start ## Quick Start
For detailed setup instructions, see [QUICKSTART.md](./QUICKSTART.md).
**You need TWO terminals:**
```bash
# Terminal 1: Start the LiveKit agent
npm run dev:agent
# Terminal 2: Start the web app
npm run dev
```
Then open http://localhost:3000 and click "Start Voice Chat".
For more info:
- [QUICKSTART.md](./QUICKSTART.md) - Step-by-step setup guide
- [MIGRATION.md](./MIGRATION.md) - Details about LiveKit migration
### Other Commands
```bash ```bash
# Install all dependencies # Install all dependencies
npm install npm install
# Run web app in dev mode
npm run dev
# Run MCP server in dev mode # Run MCP server in dev mode
npm run dev:mcp npm run dev:mcp
@@ -37,6 +60,17 @@ npm run typecheck
## Development ## Development
### LiveKit Agent
```bash
# Development
npm run dev:agent # Start agent in dev mode
# Production
npm run build:agent # Build TypeScript
npm run start:agent # Run built agent
```
### Web App ### Web App
```bash ```bash
@@ -61,12 +95,20 @@ npm run start:mcp # Run built server
## Environment Variables ## Environment Variables
Create `.env.local` in `packages/web/`: Create `.env.local` in both `packages/web/` and `packages/agent/`:
```bash ```bash
# OpenAI
OPENAI_API_KEY=sk-your-api-key-here OPENAI_API_KEY=sk-your-api-key-here
AUTH_PASSWORD=your-secure-password-here
MCP_SERVER_URL=https://your-mcp-server-url # LiveKit Cloud
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
# Optional
AUTH_PASSWORD=your-secure-password-here # For web auth
MCP_SERVER_URL=https://your-mcp-server-url # For MCP integration
``` ```
See `packages/web/.env.local.example` for template. See `packages/web/.env.local.example` for template.
@@ -76,6 +118,13 @@ See `packages/web/.env.local.example` for template.
``` ```
voice-dev/ voice-dev/
├── packages/ ├── packages/
│ ├── agent/ # LiveKit Agent Package
│ │ ├── src/
│ │ │ └── agent.ts # Agent implementation
│ │ ├── dist/ # Compiled output
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── mcp-server/ # MCP Server Package │ ├── mcp-server/ # MCP Server Package
│ │ ├── src/ │ │ ├── src/
│ │ │ ├── index.ts # CLI entry point │ │ │ ├── index.ts # CLI entry point
@@ -87,9 +136,9 @@ voice-dev/
│ │ │ │
│ └── web/ # Web App Package │ └── web/ # Web App Package
│ ├── app/ │ ├── app/
│ │ ├── api/ # API routes │ │ ├── api/ # API routes (token generation)
│ │ ├── components/ # React components │ │ ├── components/ # React components
│ │ ├── hooks/ # Custom hooks │ │ ├── hooks/ # Custom hooks (LiveKit)
│ │ ├── types/ # TypeScript types │ │ ├── types/ # TypeScript types
│ │ └── voice-client.tsx # Main voice UI │ │ └── voice-client.tsx # Main voice UI
│ ├── package.json │ ├── package.json
@@ -97,20 +146,31 @@ voice-dev/
│ │
├── package.json # Root workspace config ├── package.json # Root workspace config
├── tsconfig.base.json # Shared TypeScript config ├── tsconfig.base.json # Shared TypeScript config
└── README.md # This file ├── README.md # This file
├── QUICKSTART.md # Setup guide
└── MIGRATION.md # LiveKit migration details
``` ```
## Features ## Features
### LiveKit Agent
- OpenAI Realtime API integration
- Voice-to-voice conversation
- Low-latency audio processing
- Automatic room management
- System prompt customization
- Tool/function calling support
### Web App ### Web App
- Real-time voice interaction with OpenAI - Real-time voice interaction via LiveKit
- WebRTC-based low-latency audio streaming - LiveKit React SDK components
- Live audio level visualization - Live audio level visualization
- Device selection
- Mute/unmute controls - Mute/unmute controls
- Password authentication - Password authentication
- Agent activity transparency (debug log panel) - Agent activity transparency (debug log panel)
- Real-time agent status display - Real-time agent status display
- Tool call visibility - Automatic reconnection
### MCP Server ### MCP Server
- Full terminal session control - Full terminal session control
@@ -155,12 +215,15 @@ npm run start:mcp -- --http --password your-password
| Script | Description | | Script | Description |
|--------|-------------| |--------|-------------|
| `npm run dev` | Start web app dev server | | `npm run dev` | Start web app dev server |
| `npm run dev:agent` | Start LiveKit agent dev server |
| `npm run dev:mcp` | Start MCP server dev server | | `npm run dev:mcp` | Start MCP server dev server |
| `npm run build` | Build all packages | | `npm run build` | Build all packages |
| `npm run build:agent` | Build agent only |
| `npm run build:mcp` | Build MCP server only | | `npm run build:mcp` | Build MCP server only |
| `npm run build:web` | Build web app only | | `npm run build:web` | Build web app only |
| `npm run typecheck` | Type check all packages | | `npm run typecheck` | Type check all packages |
| `npm run start` | Start web app production server | | `npm run start` | Start web app production server |
| `npm run start:agent` | Start agent production |
| `npm run start:mcp` | Start MCP server production | | `npm run start:mcp` | Start MCP server production |
### Package Level ### Package Level
@@ -178,11 +241,27 @@ npm run <script> --workspace=mcp-server
## Tech Stack ## Tech Stack
- **Architecture**: npm workspaces - **Architecture**: npm workspaces
- **Web**: Next.js 15, React 19, TypeScript, Tailwind CSS - **Web**: Next.js 15, React 19, TypeScript, Tailwind CSS, LiveKit React SDK
- **Agent**: LiveKit Agents Framework, OpenAI Realtime API, TypeScript
- **MCP Server**: TypeScript, Express, MCP SDK - **MCP Server**: TypeScript, Express, MCP SDK
- **Voice**: OpenAI Realtime API, WebRTC - **Voice**: LiveKit Cloud, OpenAI Realtime API, WebRTC
- **Build**: TypeScript compiler, Next.js build - **Build**: TypeScript compiler, Next.js build
## Architecture
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Browser │◄───────►│ LiveKit Cloud│◄───────►│ Agent │
│ (React UI) │ WebRTC │ (Server) │ WebRTC │ (Node.js) │
└─────────────┘ └──────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ OpenAI │
│ Realtime API│
└─────────────┘
```
## License ## License
MIT MIT
+3426 -353
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -7,12 +7,15 @@
], ],
"scripts": { "scripts": {
"dev": "npm run dev --workspace=@voice-dev/web", "dev": "npm run dev --workspace=@voice-dev/web",
"dev:agent": "npm run dev --workspace=@voice-dev/agent",
"dev:mcp": "npm run dev --workspace=@voice-dev/mcp-server", "dev:mcp": "npm run dev --workspace=@voice-dev/mcp-server",
"build": "npm run build --workspaces --if-present", "build": "npm run build --workspaces --if-present",
"build:agent": "npm run build --workspace=@voice-dev/agent",
"build:mcp": "npm run build --workspace=@voice-dev/mcp-server", "build:mcp": "npm run build --workspace=@voice-dev/mcp-server",
"build:web": "npm run build --workspace=@voice-dev/web", "build:web": "npm run build --workspace=@voice-dev/web",
"typecheck": "npm run typecheck --workspaces --if-present", "typecheck": "npm run typecheck --workspaces --if-present",
"start": "npm run start --workspace=@voice-dev/web", "start": "npm run start --workspace=@voice-dev/web",
"start:agent": "npm run start --workspace=@voice-dev/agent",
"start:mcp": "npm run start --workspace=@voice-dev/mcp-server" "start:mcp": "npm run start --workspace=@voice-dev/mcp-server"
}, },
"devDependencies": { "devDependencies": {
+10
View File
@@ -0,0 +1,10 @@
# LiveKit Configuration
LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=your-livekit-api-key
LIVEKIT_API_SECRET=your-livekit-api-secret
# MCP Server (Optional)
MCP_SERVER_URL=https://your-mcp-server.example.com
# Note: With LiveKit Inference, you don't need individual provider API keys
# LiveKit handles authentication to OpenAI, Cartesia, AssemblyAI, etc.
@@ -1,4 +1,29 @@
export const SYSTEM_PROMPT = `# Virtual Assistant Instructions """
LiveKit Voice Agent with MCP Support (Python)
Migrated from Node.js version with added MCP integration
"""
import asyncio
import os
from typing import Annotated
from dotenv import load_dotenv
from livekit import rtc
from livekit.agents import (
AutoSubscribe,
JobContext,
JobProcess,
WorkerOptions,
cli,
llm,
voice,
)
from livekit.agents.llm.mcp import MCPServerHTTP
# Load environment variables
load_dotenv()
SYSTEM_PROMPT = """# Virtual Assistant Instructions
## Your Role ## Your Role
@@ -481,4 +506,100 @@ You: Run gh pr create directly via send-text with return_output
### Blank.page - A minimal text editor in your browser ### Blank.page - A minimal text editor in your browser
- Location: ~/dev/blank.page/editor - Location: ~/dev/blank.page/editor
`; """
SYSTEM_PROMPT = """# Virtual Assistant Instructions
## Your Role
You are a virtual assistant with direct access to the user's terminal environment on their laptop. Your primary purpose is to help them code and manage their development workflow, especially through Claude Code running in terminal sessions.
## Connection Setup
- **Environment**: You connect remotely to the user's laptop terminal environment
- **User's device**: They typically code from their **phone**
- **Key implication**: Be mindful of voice-to-text issues, autocorrect problems, and typos
- **Projects location**: All projects are in ~/dev
- **GitHub CLI**: gh command is available and already authenticated - use it for GitHub operations
## Important Behavioral Guidelines
### Response Pattern: ALWAYS Talk First, Then Act
**CRITICAL**: For voice interactions, ALWAYS provide verbal acknowledgment BEFORE executing tool calls.
**Pattern to follow:**
1. **Acknowledge** what you heard: "Got it, I'll [action]"
2. **Briefly explain** what you're about to do (1-2 sentences max)
3. **Then execute** the tool calls
4. **Report back** what happened after the tools complete
### Communication Style
- **Confirm commands** before executing, especially destructive operations
- **Be patient** with spelling errors and voice-related mistakes
- **Clarify ambiguous requests** rather than guessing
- **Acknowledge typos naturally** without making a big deal of it
- **Use clear, concise language** - mobile screens are small
### Mobile-Friendly Responses
- Keep responses scannable and well-structured
- Use bullet points and headers effectively
- Avoid overwhelming walls of text
- Highlight important information with bold
## Remember
- **Mobile user** - Be concise and confirm actions
- **Voice input** - Forgive typos, clarify when needed
- **Be helpful** - You're here to make coding from a phone easier!
"""
async def entrypoint(ctx: JobContext):
"""Main entry point for the voice agent."""
# Get MCP server URL from environment
mcp_server_url = os.getenv("MCP_SERVER_URL")
# Prepare MCP servers list
mcp_servers = []
if mcp_server_url:
print(f"✓ MCP Server configured: {mcp_server_url}")
server = MCPServerHTTP(
url=mcp_server_url,
timeout=10
)
mcp_servers.append(server)
else:
print("⚠ No MCP_SERVER_URL found in environment")
# Connect to the room
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
# Create the voice agent with MCP tools
agent = voice.Agent(
instructions=SYSTEM_PROMPT,
mcp_servers=mcp_servers, # Native MCP support!
)
# Create the agent session with LiveKit Inference
# Using same configuration as Node.js version
session = voice.AgentSession(
stt="assemblyai/universal-streaming:en",
llm="openai/gpt-4.1-mini",
tts="cartesia/sonic-2:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
)
# Start the session
await session.start(agent=agent, room=ctx.room)
print(f"✓ Agent started successfully in room: {ctx.room.name}")
print(f"✓ MCP servers: {len(mcp_servers)} configured")
if __name__ == "__main__":
# Run the agent worker
cli.run_app(
WorkerOptions(
entrypoint_fnc=entrypoint,
)
)
+14
View File
@@ -0,0 +1,14 @@
[project]
name = "voice-dev-agent"
version = "1.0.0"
description = "LiveKit voice agent with MCP support"
requires-python = ">=3.10"
dependencies = [
"livekit-agents[mcp]>=1.0.0",
"python-dotenv>=1.0.0",
]
[project.optional-dependencies]
dev = [
"ruff>=0.1.0",
]
+2342
View File
File diff suppressed because it is too large Load Diff
+47 -42
View File
@@ -1,62 +1,67 @@
import { NextResponse } from 'next/server'; import { NextResponse } from "next/server";
import { SYSTEM_PROMPT } from './system-prompt'; import { AccessToken, type VideoGrant } from "livekit-server-sdk";
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY;
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET;
const LIVEKIT_URL = process.env.LIVEKIT_URL;
export async function POST() { export async function POST() {
const apiKey = process.env.OPENAI_API_KEY; if (!LIVEKIT_API_KEY) {
const mcpServerUrl = process.env.MCP_SERVER_URL;
if (!apiKey) {
return NextResponse.json( return NextResponse.json(
{ error: 'OpenAI API key not configured' }, { error: "LiveKit API key not configured" },
{ status: 500 } { status: 500 }
); );
} }
if (!mcpServerUrl) { if (!LIVEKIT_API_SECRET) {
return NextResponse.json( return NextResponse.json(
{ error: 'MCP server URL not configured' }, { error: "LiveKit API secret not configured" },
{ status: 500 }
);
}
if (!LIVEKIT_URL) {
return NextResponse.json(
{ error: "LiveKit URL not configured" },
{ status: 500 } { status: 500 }
); );
} }
try { try {
const response = await fetch( console.log("Creating LiveKit token...");
'https://api.openai.com/v1/realtime/sessions',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-realtime',
voice: 'shimmer',
instructions: SYSTEM_PROMPT,
input_audio_transcription: {
model: 'gpt-4o-transcribe',
},
tools: [
{
type: 'mcp',
server_label: 'voice-dev-mcp',
server_url: mcpServerUrl,
require_approval: 'never',
},
],
}),
}
);
if (!response.ok) { const participantIdentity = `voice_user_${Math.floor(Math.random() * 10_000)}`;
throw new Error(`OpenAI API error: ${response.status}`); const participantName = "user";
} const roomName = `voice_room_${Math.floor(Math.random() * 10_000)}`;
const data = await response.json(); const token = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
return NextResponse.json(data); identity: participantIdentity,
name: participantName,
ttl: "15m",
});
const grant: VideoGrant = {
room: roomName,
roomJoin: true,
canPublish: true,
canPublishData: true,
canSubscribe: true,
};
token.addGrant(grant);
const jwt = await token.toJwt();
return NextResponse.json({
serverUrl: LIVEKIT_URL,
roomName,
participantToken: jwt,
participantName,
});
} catch (error) { } catch (error) {
console.error('Session creation error:', error); console.error("Token creation error:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to create session' }, { error: "Failed to create token" },
{ status: 500 } { status: 500 }
); );
} }
+214
View File
@@ -0,0 +1,214 @@
'use client';
import { useState, useCallback, useRef, useEffect } from 'react';
import { Room, RoomEvent, Track } from 'livekit-client';
import type { AgentStatus } from '../types/realtime-events';
interface UseLiveKitVoiceReturn {
isConnected: boolean;
isConnecting: boolean;
error: string | null;
stream: MediaStream | null;
agentStatus: AgentStatus;
connect: (deviceId?: string) => Promise<void>;
disconnect: () => void;
}
interface UseLiveKitVoiceOptions {
onEvent?: (event: any) => void;
onStatusChange?: (status: AgentStatus) => void;
}
interface ConnectionDetails {
serverUrl: string;
roomName: string;
participantToken: string;
participantName: string;
}
export function useLiveKitVoice(options: UseLiveKitVoiceOptions = {}): UseLiveKitVoiceReturn {
const { onEvent, onStatusChange } = options;
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [stream, setStream] = useState<MediaStream | null>(null);
const [agentStatus, setAgentStatus] = useState<AgentStatus>('disconnected');
const roomRef = useRef<Room | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const updateStatus = useCallback((status: AgentStatus) => {
setAgentStatus(status);
onStatusChange?.(status);
}, [onStatusChange]);
useEffect(() => {
const room = new Room();
roomRef.current = room;
room.on(RoomEvent.Connected, () => {
console.log('Connected to LiveKit room');
setIsConnected(true);
setIsConnecting(false);
updateStatus('connected');
});
room.on(RoomEvent.Disconnected, () => {
console.log('Disconnected from LiveKit room');
setIsConnected(false);
setIsConnecting(false);
updateStatus('disconnected');
});
room.on(RoomEvent.Reconnecting, () => {
console.log('Reconnecting to LiveKit room');
setIsConnecting(true);
updateStatus('connecting');
});
room.on(RoomEvent.Reconnected, () => {
console.log('Reconnected to LiveKit room');
setIsConnected(true);
setIsConnecting(false);
updateStatus('connected');
});
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
console.log('Track subscribed:', {
kind: track.kind,
participant: participant.identity,
});
if (track.kind === Track.Kind.Audio) {
const audioElement = track.attach();
audioElement.play().catch(err => {
console.error('Error playing audio:', err);
});
}
});
room.on(RoomEvent.ParticipantConnected, (participant) => {
console.log('Participant connected:', participant.identity);
if (participant.isAgent) {
console.log('Agent joined the room');
updateStatus('connected');
}
});
room.on(RoomEvent.ParticipantDisconnected, (participant) => {
console.log('Participant disconnected:', participant.identity);
if (participant.isAgent) {
console.log('Agent left the room');
}
});
room.on(RoomEvent.DataReceived, (payload, participant) => {
try {
const decoder = new TextDecoder();
const text = decoder.decode(payload);
const data = JSON.parse(text);
console.log('Data received from', participant?.identity, ':', data);
onEvent?.(data);
} catch (err) {
console.error('Failed to parse data:', err);
}
});
return () => {
room.disconnect();
roomRef.current = null;
};
}, [updateStatus, onEvent]);
const connect = useCallback(async (deviceId?: string) => {
if (isConnecting || isConnected) return;
setIsConnecting(true);
setError(null);
updateStatus('connecting');
try {
if (!window.isSecureContext) {
throw new Error('HTTPS required. Please access this app over HTTPS or localhost.');
}
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Media devices not supported in this browser.');
}
const audioConstraints: MediaTrackConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
};
if (deviceId) {
audioConstraints.deviceId = { exact: deviceId };
}
const micStream = await navigator.mediaDevices.getUserMedia({
audio: audioConstraints
});
streamRef.current = micStream;
setStream(micStream);
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
const response = await fetch(`${basePath}/api/session`, { method: 'POST' });
if (!response.ok) {
throw new Error('Failed to get connection details');
}
const connectionDetails: ConnectionDetails = await response.json();
const room = roomRef.current;
if (!room) {
throw new Error('Room not initialized');
}
await room.connect(connectionDetails.serverUrl, connectionDetails.participantToken);
await room.localParticipant.setMicrophoneEnabled(true);
} catch (err) {
console.error('Connection error:', err);
setError(err instanceof Error ? err.message : 'Failed to connect');
setIsConnecting(false);
setIsConnected(false);
updateStatus('disconnected');
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
setStream(null);
}
}
}, [isConnecting, isConnected, updateStatus]);
const disconnect = useCallback(() => {
const room = roomRef.current;
if (room) {
room.disconnect();
}
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
setStream(null);
}
setIsConnected(false);
setIsConnecting(false);
setError(null);
updateStatus('disconnected');
}, [updateStatus]);
return {
isConnected,
isConnecting,
error,
stream,
agentStatus,
connect,
disconnect,
};
}
+33 -4
View File
@@ -1,8 +1,12 @@
// Re-export types from openai-realtime-api for type safety // LiveKit event types (simplified for our use case)
import type { RealtimeServerEvents, RealtimeClientEvents } from 'openai-realtime-api'; export interface LiveKitEvent {
type: string;
[key: string]: any;
}
export type RealtimeServerEvent = RealtimeServerEvents.EventMap[RealtimeServerEvents.EventType]; // Keep OpenAI types for backward compatibility during transition
export type RealtimeClientEvent = RealtimeClientEvents.EventMap[RealtimeClientEvents.EventType]; export type RealtimeServerEvent = LiveKitEvent;
export type RealtimeClientEvent = LiveKitEvent;
// Agent status enum // Agent status enum
export type AgentStatus = export type AgentStatus =
@@ -40,6 +44,13 @@ export interface LogEntry {
export function categorizeEvent(event: RealtimeServerEvent): EventCategory { export function categorizeEvent(event: RealtimeServerEvent): EventCategory {
const type = event.type; const type = event.type;
// LiveKit events
if (type.includes('connected') || type.includes('disconnected')) return 'connection';
if (type.includes('track')) return 'audio';
if (type.includes('participant')) return 'connection';
if (type.includes('data')) return 'other';
// OpenAI events (for backward compatibility)
if (type.startsWith('session.')) return 'connection'; if (type.startsWith('session.')) return 'connection';
if (type.startsWith('input_audio_buffer.')) return 'speech'; if (type.startsWith('input_audio_buffer.')) return 'speech';
if (type.includes('transcription')) return 'transcription'; if (type.includes('transcription')) return 'transcription';
@@ -57,6 +68,17 @@ export function categorizeEvent(event: RealtimeServerEvent): EventCategory {
export function getEventSummary(event: RealtimeServerEvent): string { export function getEventSummary(event: RealtimeServerEvent): string {
const type = event.type; const type = event.type;
// LiveKit events
if (type === 'connected') return 'Connected to LiveKit';
if (type === 'disconnected') return 'Disconnected from LiveKit';
if (type === 'reconnecting') return 'Reconnecting...';
if (type === 'reconnected') return 'Reconnected';
if (type === 'track_subscribed') return 'Audio track subscribed';
if (type === 'participant_connected') return 'Participant connected';
if (type === 'participant_disconnected') return 'Participant disconnected';
if (type === 'data_received') return 'Data received';
// OpenAI events (for backward compatibility)
switch (type) { switch (type) {
case 'session.created': case 'session.created':
return 'Session established'; return 'Session established';
@@ -118,6 +140,13 @@ export function getEventSummary(event: RealtimeServerEvent): string {
export function getStatusFromEvent(event: RealtimeServerEvent): AgentStatus | null { export function getStatusFromEvent(event: RealtimeServerEvent): AgentStatus | null {
const type = event.type; const type = event.type;
// LiveKit events
if (type === 'connected') return 'connected';
if (type === 'disconnected') return 'disconnected';
if (type === 'reconnecting') return 'connecting';
if (type === 'reconnected') return 'connected';
// OpenAI events (for backward compatibility)
switch (type) { switch (type) {
case 'session.created': case 'session.created':
return 'connected'; return 'connected';
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { useWebRTCVoice } from './hooks/use-webrtc-voice'; import { useLiveKitVoice } from './hooks/use-livekit-voice';
import { useAudioLevel } from './hooks/use-audio-level'; import { useAudioLevel } from './hooks/use-audio-level';
import { useAudioDevices } from './hooks/use-audio-devices'; import { useAudioDevices } from './hooks/use-audio-devices';
import { useEventLog } from './hooks/use-event-log'; import { useEventLog } from './hooks/use-event-log';
@@ -24,7 +24,7 @@ export default function VoiceClient() {
stream, stream,
connect, connect,
disconnect disconnect
} = useWebRTCVoice({ } = useLiveKitVoice({
onEvent: addLog onEvent: addLog
}); });
+3
View File
@@ -19,12 +19,15 @@
"license": "MIT", "license": "MIT",
"description": "Voice development assistant web interface", "description": "Voice development assistant web interface",
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.9.15",
"@tailwindcss/oxide": "^4.1.14", "@tailwindcss/oxide": "^4.1.14",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"@types/node": "^24.7.2", "@types/node": "^24.7.2",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"livekit-client": "^2.15.9",
"livekit-server-sdk": "^2.14.0",
"next": "^15.5.4", "next": "^15.5.4",
"openai-realtime-api": "^1.0.8", "openai-realtime-api": "^1.0.8",
"postcss": "^8.5.6", "postcss": "^8.5.6",