aut4rk's picture
Deploy static voice agent
009fd18 verified
Raw
History Blame
6.4 kB
import "./styles.css";
import { ConversationController } from "./app/controller";
import { AppStore } from "./app/store";
import {
CALIBRATION_INSTRUCTIONS,
CALIBRATION_PROMPT,
createInitialState,
} from "./app/types";
import { TransformersAsrAdapter } from "./adapters/asr";
import { WebLLMChatAdapter } from "./adapters/llm";
import { PocketTTSAdapter } from "./adapters/tts";
import { BrowserAudioCaptureService } from "./services/audio-capture";
import { BrowserCapabilitiesService } from "./services/capabilities";
import { LocalPersistenceService } from "./services/persistence";
import { BrowserPlaybackService } from "./services/playback";
const store = new AppStore(createInitialState());
const controller = new ConversationController(store, {
capabilities: new BrowserCapabilitiesService(),
audioCapture: new BrowserAudioCaptureService(),
asr: new TransformersAsrAdapter(store.getState().config.asrModelId),
llm: new WebLLMChatAdapter(store.getState().config.llmModelId),
tts: new PocketTTSAdapter(store.getState().config.ttsModelId),
playback: new BrowserPlaybackService(),
persistence: new LocalPersistenceService(),
});
type ViewName = "calibration" | "assistant";
let currentView: ViewName = "calibration";
const calibrationView = document.querySelector<HTMLElement>("#calibration-view");
const assistantView = document.querySelector<HTMLElement>("#assistant-view");
const calibrationButton =
document.querySelector<HTMLButtonElement>("#calibration-button");
const calibrationCopy =
document.querySelector<HTMLParagraphElement>("#calibration-copy");
const calibrationPrompt =
document.querySelector<HTMLParagraphElement>("#calibration-prompt");
const calibrationFill =
document.querySelector<HTMLDivElement>("#calibration-fill");
const calibrationTimer =
document.querySelector<HTMLParagraphElement>("#calibration-timer");
const benchmarkSummary =
document.querySelector<HTMLParagraphElement>("#benchmark-summary");
const calibrationError =
document.querySelector<HTMLDivElement>("#calibration-error");
const transcriptLog = document.querySelector<HTMLDivElement>("#transcript-log");
const transcriptEmpty =
document.querySelector<HTMLDivElement>("#transcript-empty");
const recordButton = document.querySelector<HTMLButtonElement>("#record-button");
const assistantError =
document.querySelector<HTMLDivElement>("#assistant-error");
if (
!calibrationView ||
!assistantView ||
!calibrationButton ||
!calibrationCopy ||
!calibrationPrompt ||
!calibrationFill ||
!calibrationTimer ||
!benchmarkSummary ||
!calibrationError ||
!transcriptLog ||
!transcriptEmpty ||
!recordButton ||
!assistantError
) {
throw new Error("The demo shell is missing required DOM nodes.");
}
const isAssistantActivePhase = (phase: ReturnType<typeof store.getState>["phase"]) =>
phase === "arming" ||
phase === "listening" ||
phase === "thinking" ||
phase === "speaking";
const render = () => {
const state = store.getState();
calibrationView.hidden = currentView !== "calibration";
assistantView.hidden = currentView !== "assistant";
calibrationCopy.textContent = state.runtimeReady
? "Calibration is complete. Continue when you're ready to open the assistant."
: state.phase === "benchmarking"
? "Calibration is running now. Keep reading the prompt clearly until the timer ends."
: CALIBRATION_INSTRUCTIONS;
calibrationPrompt.textContent = CALIBRATION_PROMPT;
calibrationFill.style.width = `${Math.round(state.calibrationProgress * 100)}%`;
calibrationTimer.textContent = state.phase === "benchmarking"
? state.calibrationRecording
? `${state.calibrationSecondsRemaining ?? 0}s remaining`
: "Finalizing calibration..."
: state.runtimeReady
? "Calibration complete."
: "Calibration required.";
benchmarkSummary.hidden = !state.benchmarkSummary;
benchmarkSummary.textContent = state.benchmarkSummary ?? "";
calibrationButton.disabled = state.phase === "benchmarking";
calibrationButton.textContent = state.phase === "benchmarking"
? "Calibrating..."
: state.runtimeReady
? "Continue"
: "Calibrate";
transcriptLog.replaceChildren();
if (state.turns.length === 0) {
transcriptEmpty.hidden = false;
transcriptEmpty.textContent = state.runtimeReady
? 'Press Record and say "Hello, my name is _____."'
: "Complete calibration first.";
transcriptLog.append(transcriptEmpty);
} else {
transcriptEmpty.hidden = true;
for (const turn of state.turns) {
const element = document.createElement("article");
element.className = "turn";
element.dataset.role = turn.role;
const header = document.createElement("div");
header.className = "turn-header";
header.innerHTML = `<span>${turn.role}</span><span>${turn.audioStatus}</span>`;
const body = document.createElement("div");
body.className = "turn-body";
body.textContent = turn.transcript || "…";
element.append(header, body);
transcriptLog.append(element);
}
}
recordButton.disabled = !state.runtimeReady || state.phase === "benchmarking";
recordButton.textContent =
state.phase === "listening" || state.phase === "arming"
? "Stop Recording"
: state.phase === "thinking" || state.phase === "speaking"
? "Stop"
: "Record";
calibrationError.hidden =
currentView !== "calibration" || !state.errorMessage;
calibrationError.textContent =
currentView === "calibration" ? state.errorMessage ?? "" : "";
assistantError.hidden = currentView !== "assistant" || !state.errorMessage;
assistantError.textContent =
currentView === "assistant" ? state.errorMessage ?? "" : "";
};
calibrationButton.addEventListener("click", () => {
controller.setConsentAccepted(true);
if (store.getState().runtimeReady) {
currentView = "assistant";
render();
return;
}
void controller.runBenchmark();
});
recordButton.addEventListener("click", () => {
controller.setConsentAccepted(true);
void controller.start();
});
window.addEventListener("beforeunload", () => {
void controller.dispose();
});
store.subscribe(() => {
render();
});
void controller.bootstrap().then(() => {
render();
});
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
void controller.dispose();
});
}