From 2c3867dbdb2d49e72e2fd29b3dd88411853c1112 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 30 Jul 2026 06:08:48 -0400 Subject: [PATCH] [voice_assistant] Check for an already loaded model before building one A WakeWordModel claims a preference backend that is never released, so a model built only to be rejected by add_runtime_model() costs internal RAM that never comes back. The duplicate case is expected here, since a config change can re-queue a download that is already in flight. Move the check above the construction. The id was free a moment earlier and this runs on the main loop, so a failed add is now always a genuine failure. --- .../voice_assistant/voice_assistant.cpp | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d383178f39..7fa8574b9e 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -1562,6 +1562,16 @@ void VoiceAssistant::model_load_task(void *params) { this_va->erase_pending_wake_word_(id); return; } + if (this_va->micro_wake_word_->get_model_by_id(id) != nullptr) { + // A duplicate download slipped through (an earlier pass already added the model, e.g. a config + // change re-queued it while it was in flight). Benign: leave the existing model enabled. Check + // before building the model, because a WakeWordModel claims a preference backend that is never + // freed, so a model built only to be rejected costs internal RAM permanently. + ESP_LOGD(TAG, "Discarding downloaded model %s: already loaded", id.c_str()); + this_va->erase_pending_wake_word_(id); + return; + } + // A set_configuration while the download was in flight may have withdrawn the activation request. const bool still_wanted = this_va->is_wake_word_pending_(id); @@ -1569,14 +1579,9 @@ void VoiceAssistant::model_load_task(void *params) { trained_languages, arena); auto *raw = model.get(); if (!this_va->micro_wake_word_->add_runtime_model(std::move(model))) { - if (this_va->micro_wake_word_->get_model_by_id(id) != nullptr) { - // A duplicate download slipped through (an earlier pass already added the model, e.g. a config - // change re-queued it while it was in flight). Benign: leave the existing model enabled. - this_va->erase_pending_wake_word_(id); - } else { - // Genuine failure (e.g. the pause handshake timed out). Don't retry it on every boot. - this_va->mark_model_load_failed_(id); - } + // The id was free a moment ago and this is the main loop, so a duplicate is no longer possible: + // this is a genuine failure (e.g. the pause handshake timed out). Don't retry it on every boot. + this_va->mark_model_load_failed_(id); return; } if (still_wanted) {