feat: add Hermes WebUI provisioning and English Dragon HD Omni voices
Rebuild & Deploy / rebuild (push) Successful in 2m17s
Rebuild & Deploy / rebuild (push) Successful in 2m17s
This commit is contained in:
1 parent
47cc9c88a0
commit
6b8d55fc92
59 files changed
+1175
-70
No files matched your search
+68
-19
@@ -8,20 +8,11 @@ const { Room, RoomEvent } = LivekitClient;
|
||||
const LIVEKIT_URL = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/livekit`;
|
||||
const AGENT_NAME = "voice-assistant";
|
||||
|
||||
const VOICES = [
|
||||
{ id: "en-US-Ava:DragonHDOmniLatestNeural", name: "Ava", tag: "Dragon HD Omni" },
|
||||
{ id: "en-US-JennyNeural", name: "Jenny" },
|
||||
{ id: "en-US-GuyNeural", name: "Guy" },
|
||||
{ id: "en-US-AndrewNeural", name: "Andrew" },
|
||||
{ id: "en-US-AriaNeural", name: "Aria" },
|
||||
{ id: "en-US-EmmaNeural", name: "Emma" },
|
||||
{ id: "en-US-EricNeural", name: "Eric" },
|
||||
{ id: "en-US-BrianNeural", name: "Brian" },
|
||||
{ id: "en-US-AshleyNeural", name: "Ashley" },
|
||||
{ id: "en-US-RichardNeural", name: "Richard" },
|
||||
{ id: "en-US-TinaNeural", name: "Tina" },
|
||||
{ id: "en-US-SteffanNeural", name: "Steffan" },
|
||||
];
|
||||
const OMNI_DEFAULT = "en-US-Ava:DragonHDOmniLatestNeural";
|
||||
let defaultVoice = OMNI_DEFAULT;
|
||||
let VOICES = [{ id: OMNI_DEFAULT, name: "Ava", locale: "en-US", gender: "Female", tag: "Dragon HD Omni" }];
|
||||
let voiceCatalogRequest = null;
|
||||
let voiceCatalogGeneration = 0;
|
||||
|
||||
let historyKey = null;
|
||||
const HISTORY_MAX = 50;
|
||||
@@ -574,6 +565,7 @@ function handleDataPacket(payload, participant, topic) {
|
||||
if (!HopeAuth.user || !participant || !isAgent(participant)) return;
|
||||
if (msg.type === "agent_ready" && topic === "conversation-control") {
|
||||
agentReady = true;
|
||||
sendVoiceMessage(currentVoiceId());
|
||||
} else if (msg.type === "transcript" && topic === "transcript") {
|
||||
if (!["agent", "user"].includes(msg.role) || typeof msg.text !== "string") return;
|
||||
addMessage(msg.role, msg.text);
|
||||
@@ -745,21 +737,71 @@ modelSettingsForm.addEventListener("submit", async event => {
|
||||
|
||||
// ── Voice settings sheet ────────────────────────────────────────────────────
|
||||
function currentVoiceId() {
|
||||
const saved = localStorage.getItem("preferred_voice");
|
||||
return VOICES.some((v) => v.id === saved) ? saved : VOICES[0].id;
|
||||
let saved = localStorage.getItem("preferred_voice");
|
||||
// Migrate older Dragon HD selections to the equivalent Omni persona.
|
||||
if (saved?.endsWith(":DragonHDLatestNeural")) {
|
||||
saved = saved.replace(":DragonHDLatestNeural", ":DragonHDOmniLatestNeural");
|
||||
localStorage.setItem("preferred_voice", saved);
|
||||
}
|
||||
return VOICES.some((v) => v.id === saved) ? saved : defaultVoice;
|
||||
}
|
||||
|
||||
const voiceSearchEl = document.getElementById("voiceSearch");
|
||||
const voiceStatusEl = document.getElementById("voiceCatalogStatus");
|
||||
const retryVoicesBtn = document.getElementById("retryVoices");
|
||||
voiceSearchEl.addEventListener("input", renderVoiceList);
|
||||
retryVoicesBtn.addEventListener("click", loadVoiceCatalog);
|
||||
|
||||
async function loadVoiceCatalog() {
|
||||
if (!HopeAuth.user) return;
|
||||
if (voiceCatalogRequest) return voiceCatalogRequest;
|
||||
const generation = voiceCatalogGeneration;
|
||||
voiceStatusEl.textContent = "Loading English voices from Azure…";
|
||||
retryVoicesBtn.hidden = true;
|
||||
voiceCatalogRequest = (async () => {
|
||||
try {
|
||||
const data = await HopeAuth.request("/voices");
|
||||
if (generation !== voiceCatalogGeneration) return;
|
||||
const voices = Array.isArray(data.voices) ? data.voices.filter(v =>
|
||||
v && typeof v.id === "string" && /^[A-Za-z0-9:_-]{1,120}$/.test(v.id) && typeof v.name === "string") : [];
|
||||
if (!voices.length) throw new Error("Empty voice catalog");
|
||||
VOICES = voices;
|
||||
defaultVoice = VOICES.some(v => v.id === data.default_voice) ? data.default_voice : OMNI_DEFAULT;
|
||||
voiceStatusEl.textContent = data.catalog_available
|
||||
? `${VOICES.length} English voices · Dragon HD Omni first. Ava Omni is the recommended female default.`
|
||||
: (data.message || "Azure’s full catalog is unavailable. Ava Omni remains available; retry to load all English voices.");
|
||||
retryVoicesBtn.hidden = data.catalog_available === true;
|
||||
renderVoiceList();
|
||||
} catch (_) {
|
||||
if (generation !== voiceCatalogGeneration) return;
|
||||
voiceStatusEl.textContent = "Couldn’t load Azure’s full catalog. Your current choices remain available. Retry to load all English voices.";
|
||||
retryVoicesBtn.hidden = false;
|
||||
} finally {
|
||||
if (generation === voiceCatalogGeneration) voiceCatalogRequest = null;
|
||||
}
|
||||
})();
|
||||
return voiceCatalogRequest;
|
||||
}
|
||||
|
||||
function renderVoiceList() {
|
||||
voiceListEl.innerHTML = "";
|
||||
voiceListEl.replaceChildren();
|
||||
const active = currentVoiceId();
|
||||
for (const v of VOICES) {
|
||||
const query = voiceSearchEl.value.trim().toLowerCase();
|
||||
const visible = VOICES.filter(v => `${v.name} ${v.locale || ""} ${v.gender || ""} ${v.tag || ""}`.toLowerCase().includes(query));
|
||||
if (!visible.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "model-settings-note";
|
||||
empty.textContent = "No matching voices. Try a name, accent locale, gender, or model.";
|
||||
voiceListEl.appendChild(empty);
|
||||
}
|
||||
for (const v of visible) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "voice-card" + (v.id === active ? " active" : "");
|
||||
card.dataset.voice = v.id;
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "voice-name";
|
||||
name.textContent = v.name + (v.tag ? ` (${v.tag})` : "");
|
||||
name.textContent = `${v.name} · ${v.locale || "English"} · ${v.gender || ""}` + (v.tag ? ` (${v.tag})` : "");
|
||||
|
||||
const selected = v.id === active;
|
||||
card.setAttribute("role", "radio");
|
||||
@@ -790,6 +832,7 @@ function selectVoice(voiceId) {
|
||||
function openSheet() {
|
||||
if (!HopeAuth.user) return;
|
||||
renderVoiceList();
|
||||
loadVoiceCatalog();
|
||||
settingsSheet.hidden = false; settingsSheet.inert = false;
|
||||
document.getElementById("authenticatedApp").inert = true;
|
||||
sheetBackdrop.hidden = false;
|
||||
@@ -1130,6 +1173,12 @@ HopeAuth.onChange((user, changed) => {
|
||||
if (changed || !user || user.status !== "ready") stopConversation();
|
||||
if (changed || settingsAccount !== (user ? String(user.id) : null)) resetModelSettings(user);
|
||||
if (changed) {
|
||||
voiceCatalogGeneration++;
|
||||
voiceCatalogRequest = null;
|
||||
VOICES = [{ id: OMNI_DEFAULT, name: "Ava", locale: "en-US", gender: "Female", tag: "Dragon HD Omni" }];
|
||||
defaultVoice = OMNI_DEFAULT;
|
||||
voiceSearchEl.value = "";
|
||||
if (user) loadVoiceCatalog();
|
||||
historyKey = user ? `hope_history:${location.origin}:${encodeURIComponent(user.id)}` : null;
|
||||
history = loadHistory(); renderHistory(); progress.clear(); tasksList.replaceChildren(); tasksBadge.hidden = true;
|
||||
chatInput.value = ""; sendChatBtn.disabled = true;
|
||||
|
||||
Reference in new issue
Block a user