mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-06-12 20:10:19 -06:00
Add live wake word URL card
This commit is contained in:
@@ -286,6 +286,86 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordCard {
|
||||||
|
border-color: rgba(57,212,160,0.22);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(57,212,160,0.09), rgba(255,138,42,0.08)),
|
||||||
|
var(--panel2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordBadge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 68px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(57,212,160,0.34);
|
||||||
|
background: rgba(57,212,160,0.1);
|
||||||
|
color: var(--ok);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordTitle h3 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordTitle p {
|
||||||
|
max-width: 760px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordLinks {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordItem {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordItem strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordUrl {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtimeWakeWordItem button {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.firmwareLayout {
|
.firmwareLayout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1109,6 +1189,17 @@
|
|||||||
.firmwareActions button {
|
.firmwareActions button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.runtimeWakeWordHeader,
|
||||||
|
.runtimeWakeWordTitle {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.runtimeWakeWordItem {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.runtimeWakeWordItem button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.studioPanelHeader,
|
.studioPanelHeader,
|
||||||
.capturedControlPanel {
|
.capturedControlPanel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1441,6 +1532,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card runtimeWakeWordCard">
|
||||||
|
<div class="runtimeWakeWordHeader">
|
||||||
|
<div class="runtimeWakeWordTitle">
|
||||||
|
<span class="runtimeWakeWordBadge">3.0.3+</span>
|
||||||
|
<div>
|
||||||
|
<div class="firmwareKicker">Live Model Switching</div>
|
||||||
|
<h3>Tater firmware 3.0.3 or higher can swap wake words live</h3>
|
||||||
|
<p>No reflash is needed after training. Copy a trained wake word JSON URL below and paste it into your satellite's Home Assistant <code>microWakeWord Model URL</code> entity.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="pill ok">No reflash needed</span>
|
||||||
|
</div>
|
||||||
|
<div id="runtimeWakeWordLinks" class="runtimeWakeWordLinks">
|
||||||
|
<div class="emptyState">Train a wake word first, then its live model URL will appear here.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="firmwareLayout">
|
<div class="firmwareLayout">
|
||||||
<section class="card firmwarePanel stack">
|
<section class="card firmwarePanel stack">
|
||||||
<div class="firmwarePanelHeader">
|
<div class="firmwarePanelHeader">
|
||||||
@@ -1601,7 +1709,7 @@
|
|||||||
selectedFiles: [],
|
selectedFiles: [],
|
||||||
captured: { items: [], captured_count: 0, negative_count: 0, personal_count: 0 },
|
captured: { items: [], captured_count: 0, negative_count: 0, personal_count: 0 },
|
||||||
samples: { personal: [], negative: [], personal_count: 0, negative_count: 0, activeBucket: "personal", pages: { personal: 0, negative: 0 } },
|
samples: { personal: [], negative: [], personal_count: 0, negative_count: 0, activeBucket: "personal", pages: { personal: 0, negative: 0 } },
|
||||||
firmware: { devices: [], templates: [], flashing: null, logLines: [], activeTemplateKey: "" },
|
firmware: { devices: [], templates: [], wakeWords: [], flashing: null, logLines: [], activeTemplateKey: "" },
|
||||||
uploadBusy: false,
|
uploadBusy: false,
|
||||||
reviewBusy: false,
|
reviewBusy: false,
|
||||||
firmwareBusy: false,
|
firmwareBusy: false,
|
||||||
@@ -2388,6 +2496,8 @@
|
|||||||
const templates = Array.isArray(payload?.templates) ? payload.templates : [];
|
const templates = Array.isArray(payload?.templates) ? payload.templates : [];
|
||||||
const previousTemplateKey = $("firmwareTemplate").value || uiState.firmware.activeTemplateKey || "";
|
const previousTemplateKey = $("firmwareTemplate").value || uiState.firmware.activeTemplateKey || "";
|
||||||
uiState.firmware.templates = templates;
|
uiState.firmware.templates = templates;
|
||||||
|
uiState.firmware.wakeWords = Array.isArray(payload?.wake_words) ? payload.wake_words : [];
|
||||||
|
renderRuntimeWakeWordLinks();
|
||||||
$("firmwareTemplate").innerHTML = templates.length
|
$("firmwareTemplate").innerHTML = templates.length
|
||||||
? templates.map((item) => `<option value="${escapeAttr(item.value)}">${escapeHtml(item.label || item.value)}</option>`).join("")
|
? templates.map((item) => `<option value="${escapeAttr(item.value)}">${escapeHtml(item.label || item.value)}</option>`).join("")
|
||||||
: `<option value="">No firmware templates found</option>`;
|
: `<option value="">No firmware templates found</option>`;
|
||||||
@@ -2401,6 +2511,52 @@
|
|||||||
applyFirmwareTemplateTarget();
|
applyFirmwareTemplateTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRuntimeWakeWordLinks() {
|
||||||
|
const container = $("runtimeWakeWordLinks");
|
||||||
|
if (!container) return;
|
||||||
|
const wakeWords = Array.isArray(uiState.firmware.wakeWords) ? uiState.firmware.wakeWords : [];
|
||||||
|
const rows = wakeWords
|
||||||
|
.map((item) => ({
|
||||||
|
label: String(item.label || item.wake_word || item.wake_word_name || item.key || "Trained wake word"),
|
||||||
|
url: String(item.json_url || "").trim(),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.url);
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
container.innerHTML = `<div class="emptyState">Train a wake word first, then its live model URL will appear here.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = rows.map((item) => `
|
||||||
|
<div class="runtimeWakeWordItem">
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(item.label)}</strong>
|
||||||
|
<a class="runtimeWakeWordUrl" href="${escapeAttr(item.url)}" target="_blank" rel="noreferrer">${escapeHtml(item.url)}</a>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-runtime-wake-url="${escapeAttr(item.url)}">Copy URL</button>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTextToClipboard(text) {
|
||||||
|
const value = String(text || "");
|
||||||
|
if (!value) return false;
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = value;
|
||||||
|
textarea.setAttribute("readonly", "");
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
const copied = document.execCommand("copy");
|
||||||
|
textarea.remove();
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
|
||||||
function renderFirmwareFields() {
|
function renderFirmwareFields() {
|
||||||
resetWakeSoundPreview();
|
resetWakeSoundPreview();
|
||||||
const template = selectedFirmwareTemplate();
|
const template = selectedFirmwareTemplate();
|
||||||
@@ -3198,6 +3354,25 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("runtimeWakeWordLinks").addEventListener("click", async (event) => {
|
||||||
|
const target = event.target instanceof Element ? event.target : null;
|
||||||
|
const button = target?.closest("[data-runtime-wake-url]");
|
||||||
|
if (!(button instanceof HTMLButtonElement)) return;
|
||||||
|
try {
|
||||||
|
const copied = await copyTextToClipboard(button.dataset.runtimeWakeUrl || "");
|
||||||
|
if (!copied) throw new Error("Clipboard unavailable");
|
||||||
|
const previousText = button.textContent || "Copy URL";
|
||||||
|
button.textContent = "Copied";
|
||||||
|
setPill($("firmwareStatus"), "Model URL copied", "ok");
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = previousText;
|
||||||
|
}, 1200);
|
||||||
|
} catch (error) {
|
||||||
|
setPill($("firmwareStatus"), "Copy failed", "warn");
|
||||||
|
alert("Copy failed: " + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$("sampleFiles").addEventListener("change", () => {
|
$("sampleFiles").addEventListener("change", () => {
|
||||||
uiState.selectedFiles = Array.from($("sampleFiles").files || []);
|
uiState.selectedFiles = Array.from($("sampleFiles").files || []);
|
||||||
renderSelectedFiles();
|
renderSelectedFiles();
|
||||||
|
|||||||
Reference in New Issue
Block a user