From 1321396da1570af30a060cca3a8399745d61518f Mon Sep 17 00:00:00 2001 From: Sohit Kumar Date: Tue, 27 Jan 2026 02:42:56 +0530 Subject: [PATCH] Add SimpleTest page and WebSocket integration for audio transcription --- client/src/app.tsx | 5 + client/src/pages/SimpleTest/SimpleTest.tsx | 365 +++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 client/src/pages/SimpleTest/SimpleTest.tsx diff --git a/client/src/app.tsx b/client/src/app.tsx index 49d6d3b..38bb656 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -5,10 +5,15 @@ import { } from "react-router-dom"; import "./index.css"; import { Queue } from "./pages/Queue/Queue"; +import { SimpleTest } from "./pages/SimpleTest/SimpleTest"; const router = createBrowserRouter([ { path: "/", + element: , + }, + { + path: "/full", element: , }, ]); diff --git a/client/src/pages/SimpleTest/SimpleTest.tsx b/client/src/pages/SimpleTest/SimpleTest.tsx new file mode 100644 index 0000000..07c0039 --- /dev/null +++ b/client/src/pages/SimpleTest/SimpleTest.tsx @@ -0,0 +1,365 @@ +import { useState, useCallback, useRef, useEffect } from "react"; +import { WSMessage } from "../../protocol/types"; +import { encodeMessage, decodeMessage } from "../../protocol/encoder"; + +const VOICE_OPTIONS = [ + "NATF0.pt", "NATF1.pt", "NATF2.pt", "NATF3.pt", + "NATM0.pt", "NATM1.pt", "NATM2.pt", "NATM3.pt", + "VARF0.pt", "VARF1.pt", "VARF2.pt", "VARF3.pt", "VARF4.pt", + "VARM0.pt", "VARM1.pt", "VARM2.pt", "VARM3.pt", "VARM4.pt", +]; + +const DEFAULT_TEXT_PROMPT = "You are a wise and friendly teacher. Answer questions or provide advice in a clear and engaging way."; + +type ConnectionStatus = "disconnected" | "connecting" | "connected"; + +export const SimpleTest = () => { + const [connectionStatus, setConnectionStatus] = useState("disconnected"); + const [textPrompt, setTextPrompt] = useState(DEFAULT_TEXT_PROMPT); + const [voicePrompt, setVoicePrompt] = useState("NATF2.pt"); + const [transcribedText, setTranscribedText] = useState([]); + const [error, setError] = useState(null); + const [isRecording, setIsRecording] = useState(false); + + const wsRef = useRef(null); + const audioContextRef = useRef(null); + const mediaStreamRef = useRef(null); + const audioChunksRef = useRef([]); + const mediaRecorderRef = useRef(null); + + const buildWebSocketURL = useCallback(() => { + // Default to localhost:8998 if running locally, otherwise use current host + const hostname = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" + ? window.location.hostname + : window.location.hostname; + const port = window.location.port || "8998"; + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + + const url = new URL(`${protocol}//${hostname}:${port}/api/chat`); + + // Add query parameters + url.searchParams.append("text_temperature", "0.7"); + url.searchParams.append("text_topk", "50"); + url.searchParams.append("audio_temperature", "0.7"); + url.searchParams.append("audio_topk", "50"); + url.searchParams.append("pad_mult", "1.0"); + url.searchParams.append("text_seed", Math.round(1000000 * Math.random()).toString()); + url.searchParams.append("audio_seed", Math.round(1000000 * Math.random()).toString()); + url.searchParams.append("repetition_penalty_context", "1.0"); + url.searchParams.append("repetition_penalty", "1.0"); + url.searchParams.append("text_prompt", textPrompt); + url.searchParams.append("voice_prompt", voicePrompt); + + return url.toString(); + }, [textPrompt, voicePrompt]); + + const connect = useCallback(async () => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + return; + } + + setConnectionStatus("connecting"); + setError(null); + + try { + const url = buildWebSocketURL(); + console.log("Connecting to:", url); + + const ws = new WebSocket(url); + ws.binaryType = "arraybuffer"; + + ws.onopen = () => { + console.log("WebSocket connected"); + setConnectionStatus("connecting"); + }; + + ws.onmessage = (event) => { + const data = new Uint8Array(event.data); + const message = decodeMessage(data); + + console.log("Received message:", message.type); + + if (message.type === "handshake") { + console.log("Handshake received!"); + setConnectionStatus("connected"); + } else if (message.type === "text") { + console.log("Text received:", message.data); + setTranscribedText(prev => [...prev, message.data]); + } else if (message.type === "audio") { + // Handle audio data - for now just log it + console.log("Audio received:", message.data.length, "bytes"); + } else if (message.type === "error") { + setError(message.data); + console.error("Error from server:", message.data); + } + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + setError("Connection error occurred"); + setConnectionStatus("disconnected"); + }; + + ws.onclose = () => { + console.log("WebSocket closed"); + setConnectionStatus("disconnected"); + wsRef.current = null; + }; + + wsRef.current = ws; + } catch (err) { + console.error("Failed to connect:", err); + setError(err instanceof Error ? err.message : "Failed to connect"); + setConnectionStatus("disconnected"); + } + }, [buildWebSocketURL]); + + const disconnect = useCallback(() => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + setConnectionStatus("disconnected"); + stopRecording(); + }, []); + + const startRecording = useCallback(async () => { + if (isRecording || connectionStatus !== "connected") { + return; + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaStreamRef.current = stream; + + if (!audioContextRef.current) { + audioContextRef.current = new AudioContext(); + } + + const mediaRecorder = new MediaRecorder(stream, { + mimeType: "audio/webm;codecs=opus", + audioBitsPerSecond: 128000, + }); + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + console.log("Recording stopped"); + }; + + mediaRecorder.start(100); // Collect data every 100ms + mediaRecorderRef.current = mediaRecorder; + setIsRecording(true); + + // Send audio chunks to server + const sendAudioInterval = setInterval(() => { + if (!isRecording || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + clearInterval(sendAudioInterval); + return; + } + + // For simplicity, we'll send audio data periodically + // In a real implementation, you'd process the audio chunks properly + }, 100); + + } catch (err) { + console.error("Failed to start recording:", err); + setError("Failed to access microphone"); + } + }, [isRecording, connectionStatus]); + + const stopRecording = useCallback(() => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + mediaRecorderRef.current = null; + } + + if (mediaStreamRef.current) { + mediaStreamRef.current.getTracks().forEach(track => track.stop()); + mediaStreamRef.current = null; + } + + setIsRecording(false); + audioChunksRef.current = []; + }, [isRecording]); + + const sendControlMessage = useCallback((action: "start" | "endTurn" | "pause" | "restart") => { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + setError("Not connected"); + return; + } + + const message: WSMessage = { + type: "control", + action, + }; + + wsRef.current.send(encodeMessage(message)); + console.log("Sent control message:", action); + }, []); + + useEffect(() => { + return () => { + disconnect(); + }; + }, [disconnect]); + + const statusColor = { + disconnected: "bg-red-500", + connecting: "bg-yellow-500", + connected: "bg-green-500", + }[connectionStatus]; + + return ( +
+
+

PersonaPlex Test

+

Simple frontend to test the PersonaPlex stack

+ + {/* Connection Status */} +
+
+
+
+ + Status: {connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)} + +
+
+ {connectionStatus === "disconnected" ? ( + + ) : ( + + )} +
+
+ {error && ( +
+ {error} +
+ )} +
+ + {/* Configuration */} +
+

Configuration

+ +
+ +