Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion custom_components/speaker_recognition/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from __future__ import annotations

from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback

from .const import (
CONF_BACKEND_URL,
Expand All @@ -18,6 +19,31 @@

type SpeakerRecognitionConfigEntry = ConfigEntry[SpeakerRecognition]

TRAINING_NOTIFICATION_ID = "speaker_recognition_training"


@callback
def _async_update_training_notification(
hass: HomeAssistant, recognition: SpeakerRecognition
) -> None:
"""Notify the user if the last training attempt failed; clear it otherwise.

Training happens in the background, so without this a failure (backend
unreachable, audio files not found, ...) would only appear in the logs.
"""
if recognition.last_train_error:
persistent_notification.async_create(
hass,
(
"Speaker Recognition could not train the configured voices:\n\n"
f"{recognition.last_train_error}"
),
title="Speaker Recognition",
notification_id=TRAINING_NOTIFICATION_ID,
)
else:
persistent_notification.async_dismiss(hass, TRAINING_NOTIFICATION_ID)


def _get_main_entry(hass: HomeAssistant) -> ConfigEntry | None:
"""Get the main config entry."""
Expand Down Expand Up @@ -50,6 +76,7 @@ async def async_setup_main_entry(

if voice_samples:
await recognition.async_train()
_async_update_training_notification(hass, recognition)

entry.runtime_data = recognition
entry.async_on_unload(entry.add_update_listener(async_update_main_listener))
Expand Down
11 changes: 11 additions & 0 deletions custom_components/speaker_recognition/recognition.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ def __init__(
self.hass = hass
self.voice_samples = voice_samples
self._trained = False
# Human-readable reason the last training attempt failed, or None if the
# last attempt succeeded (or there was nothing to train). Surfaced to the
# user as a notification by __init__.py.
self.last_train_error: str | None = None
self._client = SpeakerRecognitionClient(base_url=base_url, timeout=300.0)

async def async_train(self) -> None:
"""Train the speaker recognition model with configured voice samples."""
self.last_train_error = None
_LOGGER.debug(
"Training speaker recognition with %d voice samples",
len(self.voice_samples),
Expand Down Expand Up @@ -91,6 +96,11 @@ async def async_train(self) -> None:
if not voice_sample_models:
_LOGGER.warning("No valid training samples prepared")
self._trained = False
self.last_train_error = (
"No valid audio files were found for the configured voice "
"samples. Make sure the selected files exist under Home "
"Assistant's media folder."
)
return

request = TrainingRequest(voice_samples=voice_sample_models)
Expand All @@ -99,6 +109,7 @@ async def async_train(self) -> None:
except (OSError, ValueError, TypeError) as error:
_LOGGER.error("Error during training: %s", error)
self._trained = False
self.last_train_error = str(error)
else:
self._trained = True
_LOGGER.info(
Expand Down