mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-06-12 20:10:19 -06:00
wake sound
This commit is contained in:
@@ -363,6 +363,20 @@
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wakeSoundPreviewRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-self: end;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wakeSoundPreviewStatus {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.readOnlyValue {
|
.readOnlyValue {
|
||||||
min-height: 43px;
|
min-height: 43px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1419,6 +1433,8 @@
|
|||||||
};
|
};
|
||||||
let firmwareProfileSaveTimer = null;
|
let firmwareProfileSaveTimer = null;
|
||||||
let firmwareProfileReloadTimer = null;
|
let firmwareProfileReloadTimer = null;
|
||||||
|
let wakeSoundPreviewAudio = null;
|
||||||
|
let wakeSoundPreviewButton = null;
|
||||||
|
|
||||||
function setPill(el, text, cls) {
|
function setPill(el, text, cls) {
|
||||||
el.className = "pill " + (cls || "");
|
el.className = "pill " + (cls || "");
|
||||||
@@ -1907,6 +1923,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderFirmwareFields() {
|
function renderFirmwareFields() {
|
||||||
|
resetWakeSoundPreview();
|
||||||
const template = selectedFirmwareTemplate();
|
const template = selectedFirmwareTemplate();
|
||||||
const fields = Array.isArray(template?.fields) ? template.fields : [];
|
const fields = Array.isArray(template?.fields) ? template.fields : [];
|
||||||
if (!fields.length) {
|
if (!fields.length) {
|
||||||
@@ -1928,10 +1945,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="firmwareSettingsGrid">
|
<div class="firmwareSettingsGrid">
|
||||||
${rows.map((field) => renderFirmwareField(field)).join("")}
|
${rows.map((field) => renderFirmwareField(field)).join("")}
|
||||||
|
${section === "Wake Sound" ? renderWakeSoundPreviewControl() : ""}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`).join("");
|
`).join("");
|
||||||
syncRenderedWakeWordSelection();
|
syncRenderedWakeWordSelection();
|
||||||
|
syncRenderedWakeSoundSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFirmwareField(field) {
|
function renderFirmwareField(field) {
|
||||||
@@ -1976,6 +1995,28 @@
|
|||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
if (type === "wake_sound_select") {
|
||||||
|
const optionHtml = options.length
|
||||||
|
? options.map((option) => {
|
||||||
|
const optionValue = String(option.value || "");
|
||||||
|
const selected = optionValue && optionValue === String(value || "") ? " selected" : "";
|
||||||
|
return `
|
||||||
|
<option value="${escapeAttr(optionValue)}"${selected}>
|
||||||
|
${escapeHtml(option.label || optionValue)}
|
||||||
|
</option>
|
||||||
|
`;
|
||||||
|
}).join("")
|
||||||
|
: "";
|
||||||
|
return `
|
||||||
|
<label class="field">
|
||||||
|
<strong>${escapeHtml(label)}</strong>
|
||||||
|
<select data-firmware-field="${escapeAttr(key)}" data-wake-sound-select ${options.length ? "" : "disabled"}>
|
||||||
|
${optionHtml || `<option value="__custom__">Custom URL</option>`}
|
||||||
|
</select>
|
||||||
|
${description}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
if (type === "checkbox") {
|
if (type === "checkbox") {
|
||||||
return `
|
return `
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -1997,6 +2038,15 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderWakeSoundPreviewControl() {
|
||||||
|
return `
|
||||||
|
<div class="wakeSoundPreviewRow">
|
||||||
|
<button type="button" data-wake-sound-preview>Play Selected Sound</button>
|
||||||
|
<span class="wakeSoundPreviewStatus" data-wake-sound-preview-status></span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function syncRenderedWakeWordSelection() {
|
function syncRenderedWakeWordSelection() {
|
||||||
const select = document.querySelector("select[data-wake-word-select]");
|
const select = document.querySelector("select[data-wake-word-select]");
|
||||||
if (select && select.value) {
|
if (select && select.value) {
|
||||||
@@ -2024,6 +2074,114 @@
|
|||||||
syncButtons();
|
syncButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncRenderedWakeSoundSelection({ fromPicker = false } = {}) {
|
||||||
|
const select = document.querySelector("select[data-wake-sound-select]");
|
||||||
|
const urlInput = document.querySelector('[data-firmware-field="wake_word_triggered_sound_file"]');
|
||||||
|
if (!(select instanceof HTMLSelectElement) || !(urlInput instanceof HTMLInputElement)) return;
|
||||||
|
if (fromPicker) {
|
||||||
|
const selectedUrl = String(select.value || "").trim();
|
||||||
|
if (selectedUrl && selectedUrl !== "__custom__") {
|
||||||
|
urlInput.value = selectedUrl;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentUrl = String(urlInput.value || "").trim();
|
||||||
|
const optionValues = Array.from(select.options).map((option) => String(option.value || "").trim());
|
||||||
|
if (currentUrl && optionValues.includes(currentUrl)) {
|
||||||
|
select.value = currentUrl;
|
||||||
|
} else if (optionValues.includes("__custom__")) {
|
||||||
|
select.value = "__custom__";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWakeSoundSelection(select, options = {}) {
|
||||||
|
syncRenderedWakeSoundSelection({ fromPicker: true });
|
||||||
|
if (!options.silent) {
|
||||||
|
setPill($("firmwareStatus"), "Wake sound selected", "ok");
|
||||||
|
scheduleFirmwareProfileSave();
|
||||||
|
}
|
||||||
|
syncButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetWakeSoundPreview(message = "") {
|
||||||
|
if (wakeSoundPreviewAudio instanceof HTMLAudioElement) {
|
||||||
|
try {
|
||||||
|
wakeSoundPreviewAudio.pause();
|
||||||
|
wakeSoundPreviewAudio.currentTime = 0;
|
||||||
|
} catch (_error) {
|
||||||
|
// Browser cleanup can fail if the media element is already gone.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wakeSoundPreviewButton instanceof HTMLButtonElement && document.body.contains(wakeSoundPreviewButton)) {
|
||||||
|
wakeSoundPreviewButton.disabled = false;
|
||||||
|
wakeSoundPreviewButton.textContent = "Play Selected Sound";
|
||||||
|
const section = wakeSoundPreviewButton.closest(".firmwareSettingsSection");
|
||||||
|
const status = section?.querySelector?.("[data-wake-sound-preview-status]");
|
||||||
|
if (status instanceof HTMLElement) {
|
||||||
|
status.textContent = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wakeSoundPreviewAudio = null;
|
||||||
|
wakeSoundPreviewButton = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWakeSoundPreviewUrl() {
|
||||||
|
const urlInput = document.querySelector('[data-firmware-field="wake_word_triggered_sound_file"]');
|
||||||
|
const picker = document.querySelector("select[data-wake-sound-select]");
|
||||||
|
const urlValue = String(urlInput instanceof HTMLInputElement ? urlInput.value || "" : "").trim();
|
||||||
|
if (urlValue) return urlValue;
|
||||||
|
const pickerValue = String(picker instanceof HTMLSelectElement ? picker.value || "" : "").trim();
|
||||||
|
return pickerValue && pickerValue !== "__custom__" ? pickerValue : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWakeSoundPreview(button) {
|
||||||
|
const status = button.closest(".firmwareSettingsSection")?.querySelector?.("[data-wake-sound-preview-status]");
|
||||||
|
if (wakeSoundPreviewAudio instanceof HTMLAudioElement && wakeSoundPreviewButton === button && !wakeSoundPreviewAudio.paused) {
|
||||||
|
resetWakeSoundPreview("Stopped.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = getWakeSoundPreviewUrl();
|
||||||
|
if (!url) {
|
||||||
|
if (status instanceof HTMLElement) status.textContent = "Choose a wake sound or enter a URL first.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetWakeSoundPreview();
|
||||||
|
const audio = new Audio(url);
|
||||||
|
audio.preload = "auto";
|
||||||
|
wakeSoundPreviewAudio = audio;
|
||||||
|
wakeSoundPreviewButton = button;
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = "Loading...";
|
||||||
|
if (status instanceof HTMLElement) status.textContent = "Loading preview...";
|
||||||
|
|
||||||
|
const finish = (message) => {
|
||||||
|
if (wakeSoundPreviewAudio !== audio) return;
|
||||||
|
if (button instanceof HTMLButtonElement && document.body.contains(button)) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "Play Selected Sound";
|
||||||
|
}
|
||||||
|
if (status instanceof HTMLElement) status.textContent = message;
|
||||||
|
wakeSoundPreviewAudio = null;
|
||||||
|
wakeSoundPreviewButton = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener("ended", () => finish("Finished."));
|
||||||
|
audio.addEventListener("error", () => finish("Could not play this wake sound."));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await audio.play();
|
||||||
|
if (wakeSoundPreviewAudio === audio) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "Stop";
|
||||||
|
if (status instanceof HTMLElement) status.textContent = "Playing...";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
finish("Playback blocked or unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function firmwareTemplateQuery() {
|
function firmwareTemplateQuery() {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const host = ($("firmwareHost").value || "").trim();
|
const host = ($("firmwareHost").value || "").trim();
|
||||||
@@ -2564,16 +2722,36 @@
|
|||||||
applyFirmwareTemplateTarget();
|
applyFirmwareTemplateTarget();
|
||||||
syncButtons();
|
syncButtons();
|
||||||
});
|
});
|
||||||
$("firmwareFields").addEventListener("input", () => {
|
$("firmwareFields").addEventListener("input", (event) => {
|
||||||
|
if (event.target?.matches?.('[data-firmware-field="wake_word_triggered_sound_file"]')) {
|
||||||
|
resetWakeSoundPreview();
|
||||||
|
syncRenderedWakeSoundSelection();
|
||||||
|
}
|
||||||
syncButtons();
|
syncButtons();
|
||||||
scheduleFirmwareProfileSave();
|
scheduleFirmwareProfileSave();
|
||||||
});
|
});
|
||||||
|
$("firmwareFields").addEventListener("click", (event) => {
|
||||||
|
const button = event.target.closest("button[data-wake-sound-preview]");
|
||||||
|
if (!button) return;
|
||||||
|
handleWakeSoundPreview(button).catch(() => {
|
||||||
|
const status = button.closest(".firmwareSettingsSection")?.querySelector?.("[data-wake-sound-preview-status]");
|
||||||
|
if (status instanceof HTMLElement) {
|
||||||
|
status.textContent = "Preview failed.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
$("firmwareFields").addEventListener("change", (event) => {
|
$("firmwareFields").addEventListener("change", (event) => {
|
||||||
const select = event.target.closest("select[data-wake-word-select]");
|
const select = event.target.closest("select[data-wake-word-select]");
|
||||||
if (select) {
|
if (select) {
|
||||||
applyWakeWordSelection(select);
|
applyWakeWordSelection(select);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const wakeSoundSelect = event.target.closest("select[data-wake-sound-select]");
|
||||||
|
if (wakeSoundSelect) {
|
||||||
|
resetWakeSoundPreview();
|
||||||
|
applyWakeSoundSelection(wakeSoundSelect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
syncButtons();
|
syncButtons();
|
||||||
scheduleFirmwareProfileSave();
|
scheduleFirmwareProfileSave();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,12 +85,16 @@ FIRMWARE_MAX_LOG_LINES = int(os.environ.get("FIRMWARE_MAX_LOG_LINES", "500"))
|
|||||||
FIRMWARE_GITHUB_OWNER = os.environ.get("FIRMWARE_GITHUB_OWNER", "TaterTotterson")
|
FIRMWARE_GITHUB_OWNER = os.environ.get("FIRMWARE_GITHUB_OWNER", "TaterTotterson")
|
||||||
FIRMWARE_GITHUB_REPO = os.environ.get("FIRMWARE_GITHUB_REPO", "microWakeWords")
|
FIRMWARE_GITHUB_REPO = os.environ.get("FIRMWARE_GITHUB_REPO", "microWakeWords")
|
||||||
FIRMWARE_GITHUB_REF = os.environ.get("FIRMWARE_GITHUB_REF", "main")
|
FIRMWARE_GITHUB_REF = os.environ.get("FIRMWARE_GITHUB_REF", "main")
|
||||||
|
WAKE_SOUND_CATALOG_CACHE_TTL_SECONDS = int(os.environ.get("WAKE_SOUND_CATALOG_CACHE_TTL_SECONDS", "600"))
|
||||||
FIRMWARE_PLATFORMIO_DIR = FIRMWARE_CACHE_DIR / "platformio"
|
FIRMWARE_PLATFORMIO_DIR = FIRMWARE_CACHE_DIR / "platformio"
|
||||||
FIRMWARE_HOME_DIR = FIRMWARE_CACHE_DIR / "home"
|
FIRMWARE_HOME_DIR = FIRMWARE_CACHE_DIR / "home"
|
||||||
FIRMWARE_XDG_CACHE_DIR = FIRMWARE_CACHE_DIR / "cache"
|
FIRMWARE_XDG_CACHE_DIR = FIRMWARE_CACHE_DIR / "cache"
|
||||||
FIRMWARE_PROFILE_FILE = Path(
|
FIRMWARE_PROFILE_FILE = Path(
|
||||||
os.environ.get("FIRMWARE_PROFILE_FILE", str(FIRMWARE_CACHE_DIR / "profiles.json"))
|
os.environ.get("FIRMWARE_PROFILE_FILE", str(FIRMWARE_CACHE_DIR / "profiles.json"))
|
||||||
).resolve()
|
).resolve()
|
||||||
|
WAKE_SOUND_MANIFEST_PATHS = ("wake_sound_manifest.json", "wake-sound-manifest.json")
|
||||||
|
WAKE_SOUND_CATALOG_CACHE: Dict[str, Any] = {"ts": 0.0, "payload": {}}
|
||||||
|
WAKE_SOUND_CATALOG_LOCK = threading.Lock()
|
||||||
TRAIN_LOG_TAIL_LINES = int(os.environ.get("REC_TRAIN_LOG_TAIL_LINES", "400"))
|
TRAIN_LOG_TAIL_LINES = int(os.environ.get("REC_TRAIN_LOG_TAIL_LINES", "400"))
|
||||||
TRAIN_LOG_MAX_BYTES = int(os.environ.get("REC_TRAIN_LOG_MAX_BYTES", str(512 * 1024)))
|
TRAIN_LOG_MAX_BYTES = int(os.environ.get("REC_TRAIN_LOG_MAX_BYTES", str(512 * 1024)))
|
||||||
|
|
||||||
@@ -1449,6 +1453,93 @@ def _load_firmware_template_text(spec: Dict[str, Any]) -> tuple[str, str]:
|
|||||||
raise RuntimeError(f"Could not download firmware template from {url}: {exc}") from exc
|
raise RuntimeError(f"Could not download firmware template from {url}: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _wake_sound_label_from_slug(slug: str) -> str:
|
||||||
|
text = str(slug or "").strip()
|
||||||
|
if not text:
|
||||||
|
return "Wake Sound"
|
||||||
|
return re.sub(r"[_\-.]+", " ", text).strip().title() or "Wake Sound"
|
||||||
|
|
||||||
|
|
||||||
|
def _wake_sound_entries_from_manifest(payload: Any) -> List[Dict[str, str]]:
|
||||||
|
rows: List[Any] = []
|
||||||
|
if isinstance(payload, list):
|
||||||
|
rows = list(payload)
|
||||||
|
elif isinstance(payload, dict):
|
||||||
|
for key in ("entries", "wake_sounds", "sounds", "audio", "items"):
|
||||||
|
candidate = payload.get(key)
|
||||||
|
if isinstance(candidate, list):
|
||||||
|
rows = list(candidate)
|
||||||
|
break
|
||||||
|
|
||||||
|
entries: List[Dict[str, str]] = []
|
||||||
|
seen = set()
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
url = str(
|
||||||
|
row.get("url")
|
||||||
|
or row.get("download_url")
|
||||||
|
or row.get("audio_url")
|
||||||
|
or row.get("sound_url")
|
||||||
|
or row.get("wake_sound_url")
|
||||||
|
or row.get("wake_word_triggered_sound_file")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
path = str(row.get("path") or "").strip()
|
||||||
|
if not url and path:
|
||||||
|
url = _firmware_raw_url(path)
|
||||||
|
if not url or url in seen:
|
||||||
|
continue
|
||||||
|
seen.add(url)
|
||||||
|
slug = str(row.get("slug") or row.get("name") or row.get("key") or Path(path or url).stem).strip()
|
||||||
|
entries.append(
|
||||||
|
{
|
||||||
|
"value": url,
|
||||||
|
"label": str(row.get("label") or row.get("title") or _wake_sound_label_from_slug(slug)).strip(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return sorted(entries, key=lambda item: (item["label"].lower(), item["value"]))
|
||||||
|
|
||||||
|
|
||||||
|
def _load_wake_sound_catalog() -> Dict[str, Any]:
|
||||||
|
now = time.time()
|
||||||
|
with WAKE_SOUND_CATALOG_LOCK:
|
||||||
|
cached_ts = float(WAKE_SOUND_CATALOG_CACHE.get("ts") or 0.0)
|
||||||
|
cached_payload = WAKE_SOUND_CATALOG_CACHE.get("payload")
|
||||||
|
if isinstance(cached_payload, dict) and (now - cached_ts) < WAKE_SOUND_CATALOG_CACHE_TTL_SECONDS:
|
||||||
|
return copy.deepcopy(cached_payload)
|
||||||
|
|
||||||
|
warnings: List[str] = []
|
||||||
|
for manifest_path in WAKE_SOUND_MANIFEST_PATHS:
|
||||||
|
manifest_url = _firmware_raw_url(manifest_path)
|
||||||
|
try:
|
||||||
|
payload = json.loads(_fetch_text_url(manifest_url, timeout=20))
|
||||||
|
entries = _wake_sound_entries_from_manifest(payload)
|
||||||
|
if entries:
|
||||||
|
catalog = {"entries": entries, "warning": "", "source_label": manifest_url}
|
||||||
|
with WAKE_SOUND_CATALOG_LOCK:
|
||||||
|
WAKE_SOUND_CATALOG_CACHE["ts"] = now
|
||||||
|
WAKE_SOUND_CATALOG_CACHE["payload"] = copy.deepcopy(catalog)
|
||||||
|
return catalog
|
||||||
|
except Exception as exc:
|
||||||
|
warnings.append(f"{manifest_path}: {exc}")
|
||||||
|
|
||||||
|
catalog = {
|
||||||
|
"entries": [],
|
||||||
|
"warning": warnings[0] if warnings else "Wake sound catalog unavailable.",
|
||||||
|
"source_label": "",
|
||||||
|
}
|
||||||
|
with WAKE_SOUND_CATALOG_LOCK:
|
||||||
|
WAKE_SOUND_CATALOG_CACHE["ts"] = now
|
||||||
|
WAKE_SOUND_CATALOG_CACHE["payload"] = copy.deepcopy(catalog)
|
||||||
|
return catalog
|
||||||
|
|
||||||
|
|
||||||
|
def _wake_sound_picker_options(catalog: Dict[str, Any]) -> List[Dict[str, str]]:
|
||||||
|
entries = catalog.get("entries") if isinstance(catalog.get("entries"), list) else []
|
||||||
|
return [{"value": "__custom__", "label": "Custom URL"}, *[dict(row) for row in entries if isinstance(row, dict)]]
|
||||||
|
|
||||||
|
|
||||||
def _extract_substitution_sections(raw_text: str) -> Dict[str, str]:
|
def _extract_substitution_sections(raw_text: str) -> Dict[str, str]:
|
||||||
section_map: Dict[str, str] = {}
|
section_map: Dict[str, str] = {}
|
||||||
in_substitutions = False
|
in_substitutions = False
|
||||||
@@ -1582,6 +1673,11 @@ def _normalize_firmware_profile_update(template_key: str, values: Dict[str, Any]
|
|||||||
wake_word_choice = str(values.get("wake_word_choice") or "").strip()
|
wake_word_choice = str(values.get("wake_word_choice") or "").strip()
|
||||||
if wake_word_choice:
|
if wake_word_choice:
|
||||||
normalized["wake_word_choice"] = wake_word_choice
|
normalized["wake_word_choice"] = wake_word_choice
|
||||||
|
wake_sound_choice = str(values.get("wake_sound_catalog") or "").strip()
|
||||||
|
if wake_sound_choice:
|
||||||
|
normalized["wake_sound_catalog"] = wake_sound_choice
|
||||||
|
if wake_sound_choice != "__custom__" and "wake_word_triggered_sound_file" in substitutions:
|
||||||
|
normalized["wake_word_triggered_sound_file"] = wake_sound_choice
|
||||||
|
|
||||||
target_host = str(values.get("__target_host") or "").strip()
|
target_host = str(values.get("__target_host") or "").strip()
|
||||||
target_port = str(values.get("__target_port") or "").strip()
|
target_port = str(values.get("__target_port") or "").strip()
|
||||||
@@ -1657,9 +1753,11 @@ def _firmware_template_fields(template_key: str, base_url: str = "", profile_key
|
|||||||
fixed_keys.add(identity_key)
|
fixed_keys.add(identity_key)
|
||||||
hidden_keys = {"ha_voice_ip"} | set(spec.get("auto_keys") or set())
|
hidden_keys = {"ha_voice_ip"} | set(spec.get("auto_keys") or set())
|
||||||
trained_wake_words = _list_trained_wake_words(base_url)
|
trained_wake_words = _list_trained_wake_words(base_url)
|
||||||
|
wake_sound_catalog = _load_wake_sound_catalog()
|
||||||
selected_wake_word_row = _selected_trained_wake_word(trained_wake_words, profile, ctx["substitutions"])
|
selected_wake_word_row = _selected_trained_wake_word(trained_wake_words, profile, ctx["substitutions"])
|
||||||
selected_wake_word = str(selected_wake_word_row.get("key") or "") if selected_wake_word_row else ""
|
selected_wake_word = str(selected_wake_word_row.get("key") or "") if selected_wake_word_row else ""
|
||||||
wake_picker_added = False
|
wake_picker_added = False
|
||||||
|
wake_sound_picker_added = False
|
||||||
|
|
||||||
for key, raw_value in ctx["substitutions"].items():
|
for key, raw_value in ctx["substitutions"].items():
|
||||||
key_text = str(key or "").strip()
|
key_text = str(key or "").strip()
|
||||||
@@ -1685,6 +1783,35 @@ def _firmware_template_fields(template_key: str, base_url: str = "", profile_key
|
|||||||
)
|
)
|
||||||
wake_picker_added = True
|
wake_picker_added = True
|
||||||
|
|
||||||
|
if key_text == "wake_word_triggered_sound_file" and not wake_sound_picker_added:
|
||||||
|
wake_sound_entries = wake_sound_catalog.get("entries") if isinstance(wake_sound_catalog.get("entries"), list) else []
|
||||||
|
current_sound_url = str(profile.get(key_text) or _template_default_string(raw_value) or "").strip()
|
||||||
|
saved_sound_choice = str(profile.get("wake_sound_catalog") or "").strip()
|
||||||
|
available_sound_urls = {str(row.get("value") or "") for row in wake_sound_entries if isinstance(row, dict)}
|
||||||
|
if saved_sound_choice in available_sound_urls or saved_sound_choice == "__custom__":
|
||||||
|
picker_value = saved_sound_choice
|
||||||
|
else:
|
||||||
|
picker_value = current_sound_url if current_sound_url in available_sound_urls else "__custom__"
|
||||||
|
description = (
|
||||||
|
f"Choose from {len(wake_sound_entries)} prebuilt wake sounds, or leave this on Custom URL and paste your own audio URL below."
|
||||||
|
if wake_sound_entries
|
||||||
|
else "Prebuilt wake-sound catalog is unavailable right now. You can still paste any custom audio URL below."
|
||||||
|
)
|
||||||
|
if wake_sound_catalog.get("warning") and not wake_sound_entries:
|
||||||
|
description = f"{description} {wake_sound_catalog['warning']}".strip()
|
||||||
|
fields.append(
|
||||||
|
{
|
||||||
|
"key": "wake_sound_catalog",
|
||||||
|
"label": "Prebuilt Wake Sound",
|
||||||
|
"type": "wake_sound_select",
|
||||||
|
"value": picker_value,
|
||||||
|
"options": _wake_sound_picker_options(wake_sound_catalog),
|
||||||
|
"description": description,
|
||||||
|
"section": "Wake Sound",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wake_sound_picker_added = True
|
||||||
|
|
||||||
default = _template_default_string(raw_value)
|
default = _template_default_string(raw_value)
|
||||||
saved = str(profile.get(key_text) or "")
|
saved = str(profile.get(key_text) or "")
|
||||||
field_type = "text"
|
field_type = "text"
|
||||||
@@ -1716,8 +1843,13 @@ def _firmware_template_fields(template_key: str, base_url: str = "", profile_key
|
|||||||
if selected_wake_word_row:
|
if selected_wake_word_row:
|
||||||
value = str(selected_wake_word_row.get("wake_word_name") or selected_wake_word_row.get("key") or "")
|
value = str(selected_wake_word_row.get("wake_word_name") or selected_wake_word_row.get("key") or "")
|
||||||
placeholder = "hey_tater"
|
placeholder = "hey_tater"
|
||||||
|
elif key_text == "wake_word_triggered_sound_file":
|
||||||
|
placeholder = "https://.../wake-sound.mp3"
|
||||||
|
description = "Pick a prebuilt wake sound above or paste any custom audio URL."
|
||||||
section = ctx["sections"].get(key_text) or "Firmware"
|
section = ctx["sections"].get(key_text) or "Firmware"
|
||||||
if key_text in {"wake_word_name", "wake_word_model_url"}:
|
if key_text == "wake_word_triggered_sound_file":
|
||||||
|
section = "Wake Sound"
|
||||||
|
elif key_text in {"wake_word_name", "wake_word_model_url"}:
|
||||||
section = "Micro Wake Word"
|
section = "Micro Wake Word"
|
||||||
elif key_text.endswith("_sound_file"):
|
elif key_text.endswith("_sound_file"):
|
||||||
section = "Sounds"
|
section = "Sounds"
|
||||||
@@ -1788,6 +1920,9 @@ def _render_firmware_config(
|
|||||||
normalized[key_text] = str(raw_value if raw_value is not None else "").strip() or _template_default_string(
|
normalized[key_text] = str(raw_value if raw_value is not None else "").strip() or _template_default_string(
|
||||||
substitutions.get(key_text)
|
substitutions.get(key_text)
|
||||||
)
|
)
|
||||||
|
wake_sound_choice = str(values.get("wake_sound_catalog") or "").strip()
|
||||||
|
if wake_sound_choice and wake_sound_choice != "__custom__" and "wake_word_triggered_sound_file" in substitutions:
|
||||||
|
normalized["wake_word_triggered_sound_file"] = wake_sound_choice
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
if not normalized.get("wifi_ssid"):
|
if not normalized.get("wifi_ssid"):
|
||||||
|
|||||||
Reference in New Issue
Block a user