From c5542fc1567ffebb93303f4457dd232dfd9ed735 Mon Sep 17 00:00:00 2001 From: Bijit Mondal Date: Thu, 19 Feb 2026 18:42:06 +0530 Subject: [PATCH] feat(example): video streaming --- .../frame_00000_2026-02-19T13-10-00-344Z.webp | Bin 0 -> 10790 bytes .../frame_00001_2026-02-19T13-10-04-584Z.webp | Bin 0 -> 10870 bytes .../frame_00002_2026-02-19T13-10-06-446Z.webp | Bin 0 -> 11190 bytes .../frame_00003_2026-02-19T13-10-14-543Z.webp | Bin 0 -> 10680 bytes .../frame_00004_2026-02-19T13-10-24-540Z.webp | Bin 0 -> 10826 bytes example/serve-client.js | 6 +- example/video-client.html | 998 ++++++++++++++++++ example/ws-server-video.ts | 161 +++ package.json | 3 +- src/VideoAgent.ts | 60 +- 10 files changed, 1214 insertions(+), 14 deletions(-) create mode 100644 example/frames/frame_00000_2026-02-19T13-10-00-344Z.webp create mode 100644 example/frames/frame_00001_2026-02-19T13-10-04-584Z.webp create mode 100644 example/frames/frame_00002_2026-02-19T13-10-06-446Z.webp create mode 100644 example/frames/frame_00003_2026-02-19T13-10-14-543Z.webp create mode 100644 example/frames/frame_00004_2026-02-19T13-10-24-540Z.webp create mode 100644 example/video-client.html create mode 100644 example/ws-server-video.ts diff --git a/example/frames/frame_00000_2026-02-19T13-10-00-344Z.webp b/example/frames/frame_00000_2026-02-19T13-10-00-344Z.webp new file mode 100644 index 0000000000000000000000000000000000000000..d05b820213149f5fdafb8c47789d9057cc969f05 GIT binary patch literal 10790 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 literal 0 HcmV?d00001 diff --git a/example/frames/frame_00001_2026-02-19T13-10-04-584Z.webp b/example/frames/frame_00001_2026-02-19T13-10-04-584Z.webp new file mode 100644 index 0000000000000000000000000000000000000000..a0acc5ae0c09899ba72cacddfeedebb2fcb17735 GIT binary patch literal 10870 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 MQ<%D*ynnM**DyDu4pu0X}&=m`J3pC@LnEx#6G> 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 UgeAk?0!#GzW8FR2j}lLS0NcsA^8f$< literal 0 HcmV?d00001 diff --git a/example/frames/frame_00003_2026-02-19T13-10-14-543Z.webp b/example/frames/frame_00003_2026-02-19T13-10-14-543Z.webp new file mode 100644 index 0000000000000000000000000000000000000000..095e541c058a902fe89e8b902c6fd87f401ac123 GIT binary patch literal 10680 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 Uo{3ZbPYByCZK0`8PU1?y0M|kTcmMzZ literal 0 HcmV?d00001 diff --git a/example/serve-client.js b/example/serve-client.js index c8b1651..19b6082 100644 --- a/example/serve-client.js +++ b/example/serve-client.js @@ -2,19 +2,19 @@ const http = require('http'); const fs = require('fs'); const path = require('path'); -const PORT = 3000; +const PORT = 3102; // Create a simple HTTP server to serve the voice client HTML const server = http.createServer((req, res) => { if (req.url === '/' || req.url === '/index.html') { - const htmlPath = path.join(__dirname, 'voice-client.html'); + const htmlPath = path.join(__dirname, 'video-client.html'); fs.readFile(htmlPath, (err, data) => { if (err) { res.writeHead(500); res.end('Error loading voice-client.html'); return; } - res.writeHead(200, {'Content-Type': 'text/html'}); + res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(data); }); } else { diff --git a/example/video-client.html b/example/video-client.html new file mode 100644 index 0000000..65304f8 --- /dev/null +++ b/example/video-client.html @@ -0,0 +1,998 @@ + + + + + + + Video + Voice Agent Client + + + + + +

📹 Video + Voice Agent

+

Webcam + microphone → multimodal AI (vision + speech)

+ +
+ + + +
+ + + +
+ + +
+ + + +
+ + +
+ +
+
+
+ 0.000 +
+ +
+ + + + +
+ +
+ + + + + +
+ +
+ Disconnected +
+
+ +

👤 You said

+
+ +

🤖 Assistant

+
+ + + + + +

📜 Log

+
+ + + + + \ No newline at end of file diff --git a/example/ws-server-video.ts b/example/ws-server-video.ts new file mode 100644 index 0000000..e155194 --- /dev/null +++ b/example/ws-server-video.ts @@ -0,0 +1,161 @@ +// ws-server-video.ts +import "dotenv/config"; +import { WebSocketServer } from "ws"; +import { VideoAgent } from "../src/VideoAgent"; // adjust path +import { tool } from "ai"; +import { z } from "zod"; +import { openai } from "@ai-sdk/openai"; +import { mkdirSync, writeFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +// ── Frame saving ──────────────────────────────────────────────────────── +const __dirname = typeof import.meta.dirname === "string" + ? import.meta.dirname + : dirname(fileURLToPath(import.meta.url)); + +const FRAMES_DIR = join(__dirname, "frames"); +mkdirSync(FRAMES_DIR, { recursive: true }); +console.log(`[video-ws] Saving received frames to ${FRAMES_DIR}/`); + +let frameCounter = 0; + +function saveFrame(msg: { + sequence?: number; + timestamp?: number; + triggerReason?: string; + image: { data: string; format?: string; width?: number; height?: number }; +}) { + const idx = frameCounter++; + const ext = msg.image.format === "jpeg" ? "jpg" : (msg.image.format || "webp"); + const ts = new Date(msg.timestamp ?? Date.now()) + .toISOString() + .replace(/[:.]/g, "-"); + const filename = `frame_${String(idx).padStart(5, "0")}_${ts}.${ext}`; + const filepath = join(FRAMES_DIR, filename); + + const buf = Buffer.from(msg.image.data, "base64"); + writeFileSync(filepath, buf); + + console.log( + `[frames] Saved ${filename} (${(buf.length / 1024).toFixed(1)} kB` + + `${msg.image.width ? `, ${msg.image.width}×${msg.image.height}` : ""}` + + `, ${msg.triggerReason ?? "unknown"})` + ); +} + +const endpoint = process.env.VIDEO_WS_ENDPOINT || "ws://localhost:8081"; +const url = new URL(endpoint); +const port = Number(url.port || 8081); +const host = url.hostname || "localhost"; + + +// ── Tools (same as demo.ts) ──────────────────────────────────────────── +const weatherTool = tool({ + description: "Get the weather in a location", + inputSchema: z.object({ + location: z.string().describe("The location to get the weather for"), + }), + execute: async ({ location }) => ({ + location, + temperature: 72 + Math.floor(Math.random() * 21) - 10, + conditions: ["sunny", "cloudy", "rainy", "partly cloudy"][ + Math.floor(Math.random() * 4) + ], + }), +}); + +const timeTool = tool({ + description: "Get the current time", + inputSchema: z.object({}), + execute: async () => ({ + time: new Date().toLocaleTimeString(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }), +}); +const wss = new WebSocketServer({ port, host }); + +wss.on("listening", () => { + console.log(`[video-ws] listening on ${endpoint}`); + console.log(`[video-ws] Open video-client.html and connect → ${endpoint}`); +}); + +wss.on("connection", (socket) => { + console.log("[video-ws] ✓ client connected"); + + const agent = new VideoAgent({ + model: openai("gpt-4o"), // or gpt-4o-mini, claude-3.5-sonnet, gemini-1.5-flash… + transcriptionModel: openai.transcription("whisper-1"), + speechModel: openai.speech("gpt-4o-mini-tts"), + instructions: `You are a helpful video+voice assistant. +You can SEE what the user is showing via webcam. +Describe what you see when it helps answer the question. +Keep spoken answers concise and natural.`, + voice: "alloy", + streamingSpeech: { + minChunkSize: 25, + maxChunkSize: 140, + parallelGeneration: true, + maxParallelRequests: 3, + }, + tools: { getWeather: weatherTool, getTime: timeTool }, + // Tune these depending on your budget & latency goals + maxContextFrames: 6, // very important — each frame ≈ 100–400 tokens + maxFrameInputSize: 2_500_000, // ~2.5 MB + }); + + // Reuse most of the same event logging you have in ws-server.ts + agent.on("text", (data: { role: string; text: string }) => { + console.log(`[video] Text (${data.role}): ${data.text?.substring(0, 100)}...`); + }); + agent.on("chunk:text_delta", (data: { id: string; text: string }) => { + process.stdout.write(data.text || ""); + }); + agent.on("frame_received", ({ sequence, size, dimensions, triggerReason }) => { + console.log(`[video] Frame #${sequence} (${triggerReason}) ${size / 1024 | 0} kB ${dimensions.width}×${dimensions.height}`); + }); + agent.on("frame_requested", ({ reason }) => console.log(`[video] Requested frame: ${reason}`)); + + // Audio and transcription events + agent.on("audio_received", ({ size, format }) => { + console.log(`[video] Audio received: ${size} bytes, format: ${format}`); + }); + agent.on("transcription", ({ text, language }) => { + console.log(`[video] Transcription: "${text}" (${language || "unknown"})`); + }); + + // Speech events + agent.on("speech_start", () => console.log(`[video] Speech started`)); + agent.on("speech_complete", () => console.log(`[video] Speech complete`)); + agent.on("audio_chunk", ({ chunkId, text }) => { + console.log(`[video] Audio chunk #${chunkId}: "${text?.substring(0, 50)}..."`); + }); + + // Error handling + agent.on("error", (error: Error) => { + console.error(`[video] ERROR:`, error); + }); + agent.on("warning", (warning: string) => { + console.warn(`[video] WARNING:`, warning); + }); + + agent.on("disconnected", () => { + agent.destroy(); + console.log("[video-ws] ✗ client disconnected (agent destroyed)"); + }); + + // ── Intercept raw messages to save frames to disk ──────────────────── + socket.on("message", (raw) => { + try { + const msg = JSON.parse(raw.toString()); + if (msg.type === "video_frame" && msg.image?.data) { + saveFrame(msg); + } + } catch { + // not JSON — ignore, agent will handle binary etc. + } + }); + + // The crucial line — same as VoiceAgent + agent.handleSocket(socket); +}); \ No newline at end of file diff --git a/package.json b/package.json index e910961..07c7841 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "demo": "tsx example/demo.ts", "ws:server": "tsx example/ws-server.ts", "client": "node example/serve-client.js", + "ws:video": "tsx example/ws-server-video.ts", "prepublishOnly": "pnpm build" }, "keywords": [ @@ -56,4 +57,4 @@ "tsx": "^4.20.5", "typescript": "^5.9.3" } -} +} \ No newline at end of file diff --git a/src/VideoAgent.ts b/src/VideoAgent.ts index a8165fa..21697fd 100644 --- a/src/VideoAgent.ts +++ b/src/VideoAgent.ts @@ -84,6 +84,10 @@ const DEFAULT_VIDEO_AGENT_CONFIG: VideoAgentConfig = { }; export interface VideoAgentOptions { + /** + * AI SDK Model for chat. Must be a vision-enabled model (e.g., openai('gpt-4o'), + * anthropic('claude-3.5-sonnet'), google('gemini-1.5-pro')) to process video frames. + */ model: LanguageModel; // AI SDK Model for chat (e.g., openai('gpt-4o')) transcriptionModel?: TranscriptionModel; // AI SDK Transcription Model (e.g., openai.transcription('whisper-1')) speechModel?: SpeechModel; // AI SDK Speech Model (e.g., openai.speech('gpt-4o-mini-tts')) @@ -285,6 +289,7 @@ Use tools when needed to provide accurate information.`; // Handle raw audio data that needs transcription case "audio": if (typeof message.data !== "string" || !message.data) { + console.warn("Received empty or invalid audio message"); this.emit("warning", "Received empty or invalid audio message"); return; } @@ -293,9 +298,15 @@ Use tools when needed to provide accurate information.`; // Force capture current frame when user speaks this.requestFrameCapture("user_request"); console.log( - `Received audio data (${message.data.length / 1000}KB) for processing, format: ${message.format || "unknown"}` + `[audio handler] Received audio data (${(message.data.length / 1000).toFixed(1)}KB) for processing, format: ${message.format || "unknown"}` ); - await this.processAudioInput(message); + try { + await this.processAudioInput(message); + console.log(`[audio handler] processAudioInput completed`); + } catch (audioError) { + console.error(`[audio handler] Error in processAudioInput:`, audioError); + this.emit("error", audioError); + } break; // Handle video frame from client @@ -850,13 +861,20 @@ Use tools when needed to provide accurate information.`; /** * Process incoming audio data: transcribe and generate response */ - private async processAudioInput(audioMessage: AudioData): Promise { + private async processAudioInput(audioMessage: AudioData | { type: string; data: string; format?: string; sessionId?: string }): Promise { if (!this.transcriptionModel) { - this.emit("error", new Error("Transcription model not configured for audio input")); + const error = new Error("Transcription model not configured for audio input"); + console.error(error.message); + this.emit("error", error); + this.sendWebSocketMessage({ + type: "error", + error: error.message, + }); return; } try { + console.log(`[processAudioInput] Starting audio processing, data length: ${audioMessage.data?.length || 0}`); const audioBuffer = Buffer.from(audioMessage.data, "base64"); if (audioBuffer.length > this.maxAudioInputSize) { @@ -877,19 +895,23 @@ Use tools when needed to provide accurate information.`; this.emit("audio_received", { size: audioBuffer.length, format: audioMessage.format, - sessionId: audioMessage.sessionId, + sessionId: audioMessage.sessionId || this.sessionId, }); console.log( - `Processing audio input: ${audioBuffer.length} bytes, format: ${audioMessage.format || "unknown"}` + `[processAudioInput] Processing audio: ${audioBuffer.length} bytes, format: ${audioMessage.format || "unknown"}` ); + console.log(`[processAudioInput] Calling transcribeAudio...`); const transcribedText = await this.transcribeAudio(audioBuffer); - console.log(`Transcribed text: "${transcribedText}"`); + console.log(`[processAudioInput] Transcribed text: "${transcribedText}"`); if (transcribedText.trim()) { + console.log(`[processAudioInput] Enqueueing text input: "${transcribedText}"`); await this.enqueueTextInput(transcribedText); + console.log(`[processAudioInput] Text input processing complete`); } else { + console.warn(`[processAudioInput] Transcription returned empty text`); this.emit("warning", "Transcription returned empty text"); this.sendWebSocketMessage({ type: "transcription_error", @@ -897,7 +919,7 @@ Use tools when needed to provide accurate information.`; }); } } catch (error) { - console.error("Failed to process audio input:", error); + console.error("[processAudioInput] Failed to process audio input:", error); this.emit("error", error); this.sendWebSocketMessage({ type: "transcription_error", @@ -1049,28 +1071,38 @@ Use tools when needed to provide accurate information.`; * Drain the input queue, processing one request at a time */ private async drainInputQueue(): Promise { - if (this.processingQueue) return; + if (this.processingQueue) { + console.log(`[drainInputQueue] Already processing, skipping`); + return; + } this.processingQueue = true; + console.log(`[drainInputQueue] Starting to drain queue, ${this.inputQueue.length} items`); try { while (this.inputQueue.length > 0) { const item = this.inputQueue.shift()!; + console.log(`[drainInputQueue] Processing item: text="${item.text?.substring(0, 50)}...", hasFrame=${!!item.frame}`); try { let result: string; if (item.frame && item.text) { + console.log(`[drainInputQueue] Calling processMultimodalInput`); result = await this.processMultimodalInput(item.text, item.frame); } else if (item.text) { + console.log(`[drainInputQueue] Calling processUserInput`); result = await this.processUserInput(item.text); } else { result = ""; } + console.log(`[drainInputQueue] Got result: "${result?.substring(0, 100)}..."`); item.resolve(result); } catch (error) { + console.error(`[drainInputQueue] Error processing item:`, error); item.reject(error); } } } finally { this.processingQueue = false; + console.log(`[drainInputQueue] Done draining queue`); } } @@ -1173,6 +1205,7 @@ Use tools when needed to provide accurate information.`; * Process user input with streaming text generation */ private async processUserInput(text: string): Promise { + console.log(`[processUserInput] Starting with text: "${text}"`); this.isProcessing = true; this.currentStreamAbortController = new AbortController(); const streamAbortSignal = this.currentStreamAbortController.signal; @@ -1182,6 +1215,7 @@ Use tools when needed to provide accurate information.`; // Check if we have current frame data - if so, include it const hasVisualContext = !!this.currentFrameData; + console.log(`[processUserInput] hasVisualContext: ${hasVisualContext}`); let messages: ModelMessage[]; @@ -1207,6 +1241,10 @@ Use tools when needed to provide accurate information.`; this.trimHistory(); + console.log(`[processUserInput] Calling streamText with ${messages.length} messages`); + console.log(`[processUserInput] Model:`, this.model); + console.log(`[processUserInput] Tools:`, Object.keys(this.tools)); + const result = streamText({ model: this.model, system: this.instructions, @@ -1218,6 +1256,7 @@ Use tools when needed to provide accurate information.`; this.handleStreamChunk(chunk); }, onFinish: async (event) => { + console.log(`[processUserInput] onFinish called`); for (const step of event.steps) { for (const toolResult of step.toolResults) { this.emit("tool_result", { @@ -1229,11 +1268,12 @@ Use tools when needed to provide accurate information.`; } }, onError: ({ error }) => { - console.error("Stream error:", error); + console.error("[processUserInput] Stream error:", error); this.emit("error", error); }, }); + console.log(`[processUserInput] Calling processStreamResult`); return await this.processStreamResult(result); } catch (error) { this.pendingTextBuffer = "";