Files
jeremy 266ea11447 feat: FRIDAY Voice stack on the GPU machine; VoiceCoach history
FRIDAY Voice (speaches GPU STT/TTS + hermes agent + push-to-talk console) on
.150 Docker Desktop, fronted by coach.hobbs.farm. Also captures the VoiceCoach
compose/agent trees and deploy notes it replaced, which had never been committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:19:04 -05:00

159 lines
5.3 KiB
JavaScript

const LIVEKIT_URL = "wss://voice.hobbs.farm";
let room = null;
function log(msg, level) {
const el = document.getElementById("logOutput");
const time = new Date().toLocaleTimeString();
const cls = level === "error" ? "log-error" : level === "warn" ? "log-warn" : "log-info";
el.innerHTML += `<div class="${cls}"><span class="log-time">${time}</span> ${msg}</div>`;
el.scrollTop = el.scrollHeight;
}
function clearLog() {
document.getElementById("logOutput").innerHTML = "";
}
async function connect() {
setStatus("connecting", "Connecting...");
log("Requesting token from server...");
try {
const response = await fetch("/api/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ identity: "jeremy-" + Date.now() }),
});
if (!response.ok) {
const text = await response.text();
throw new Error("Token request failed: " + response.status + " " + text);
}
const data = await response.json();
log("Token received. LiveKit URL: " + data.url);
room = new LivekitClient.Room({
audioCaptureDefaults: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
room.on(LivekitClient.RoomEvent.Connected, () => {
setStatus("connected", "Connected");
document.getElementById("connectBtn").style.display = "none";
document.getElementById("disconnectBtn").style.display = "block";
log("Connected to LiveKit room");
log("Participants in room: " + room.remoteParticipants.size);
if (room.remoteParticipants.size === 0) {
log("No agent participant yet - waiting for voicecoach-agent to join...", "warn");
}
});
room.on(LivekitClient.RoomEvent.Disconnected, (reason) => {
setStatus("disconnected", "Disconnected");
document.getElementById("connectBtn").style.display = "block";
document.getElementById("disconnectBtn").style.display = "none";
log("Disconnected from LiveKit" + (reason ? ": " + reason : ""), "warn");
});
room.on(LivekitClient.RoomEvent.ParticipantConnected, (participant) => {
log("Participant joined: " + participant.identity);
});
room.on(LivekitClient.RoomEvent.ParticipantDisconnected, (participant) => {
log("Participant left: " + participant.identity, "warn");
});
room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => {
log("Track subscribed: " + track.kind + " from " + participant.identity);
if (track.kind === LivekitClient.Track.Kind.Audio) {
const element = track.attach();
document.body.appendChild(element);
element.play().then(() => {
log("Audio playback started");
}).catch((err) => {
log("Audio play failed: " + err.message, "error");
});
}
});
room.on(LivekitClient.RoomEvent.TrackPublished, (publication, participant) => {
log("Track published by " + participant.identity + ": " + publication.kind);
});
room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
log("Track unsubscribed: " + track.kind + " from " + participant.identity, "warn");
});
room.on(LivekitClient.RoomEvent.DataReceived, (payload, participant) => {
try {
const msg = new TextDecoder().decode(payload);
log("Data from " + (participant ? participant.identity : "server") + ": " + msg);
} catch (e) {
log("Data received (binary, " + payload.byteLength + " bytes)");
}
});
room.on(LivekitClient.RoomEvent.ConnectionQualityChanged, (quality, participant) => {
if (participant.isLocal) {
log("Connection quality: " + quality);
}
});
room.on(LivekitClient.RoomEvent.MediaDevicesError, (err) => {
log("Media device error: " + err.message, "error");
});
room.on(LivekitClient.RoomEvent.RoomMetadataChanged, (metadata) => {
log("Room metadata changed: " + metadata);
});
log("Connecting to " + LIVEKIT_URL + "...");
await room.connect(data.url || LIVEKIT_URL, data.token);
log("Enabling microphone...");
await room.localParticipant.setMicrophoneEnabled(true);
log("Microphone enabled. Speak now.");
// Log local tracks
room.localParticipant.audioTrackPublications.forEach((pub) => {
log("Local audio track: " + pub.trackSid + " muted=" + pub.isMuted);
});
} catch (err) {
setStatus("disconnected", "Error: " + err.message);
log("Connection error: " + err.message, "error");
console.error(err);
}
}
async function disconnect() {
if (room) {
log("Disconnecting...");
await room.disconnect();
room = null;
}
}
function setMode(mode) {
document.getElementById("modeDisplay").textContent = "Mode: " + mode;
log("Mode changed to: " + mode);
if (room && room.localParticipant) {
const encoder = new TextEncoder();
room.localParticipant.publishData(
encoder.encode(JSON.stringify({ type: "mode_change", mode })),
{ reliable: true }
);
}
}
function setStatus(state, text) {
const el = document.getElementById("status");
el.className = "status " + state;
el.textContent = text;
}
document.getElementById("connectBtn").addEventListener("click", connect);
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
log("VoiceCoach client loaded. Ready to connect.");