Assist zamienia mowę na tekst, ale nie wie, kto mówi. Satelity głosowe (Home Assistant Voice PE, ESPHome) wysyłają polecenie bez użytkownika, więc dla Home Assistant każde zdanie jest anonimowe. Kłopot pojawia się przy pytaniach w pierwszej osobie:
Whisper i inne modele zamiany mowy na tekst nie rozpoznają mówcy. Diaryzacja (np. WhisperX) odpowiada tylko na pytanie „kto kiedy mówił” z anonimowymi etykietami, a nie „który to domownik”. Assist Speaker ID dokłada brakujący element: zamienia głos na „odcisk” (wektor liczb), porównuje go z zapisanymi wzorcami domowników i przekazuje wynik dalej – agentowi konwersacji, modelowi LLM i automatyzacjom.
Projekt składa się z dwóch części w jednym repozytorium:
| Część | Co robi | Instalacja |
|---|---|---|
| Usługa wyoming-speaker-id | Pośrednik Wyoming przed Twoim serwerem zamiany mowy na tekst (np. Whisper). Przekazuje dźwięk dalej bez zmian, a równolegle liczy odcisk głosu i dopisuje mówcę do wyniku. | dodatek HA albo kontener Docker na innym komputerze |
| Integracja Assist Speaker ID | Encja zamiany mowy na tekst „Mowa na tekst z mówcą”, agent-opakowanie „Agent z mówcą”, sensor „Ostatni mówca”, akcje do nagrywania wzorców, kalibracji i ogłoszeń. | HACS (repozytorium niestandardowe, typ „Integracja”) |
Najwięcej daje w parze z Assist Routerem: on używa mówcy do pytań w pierwszej osobie, pokoju mówcy, określeń rodzinnych („mój brat”), odpowiedzi na „Kto to mówi?” i ogłoszeń. Assist Speaker ID działa jednak z każdym agentem.
speaker (id osoby albo unknown), speaker_name,
speaker_score, speaker_scores (wyniki wszystkich osób),
speech_seconds.get_speaker(), a także automatyzacje przez sensor i zdarzenie.Dlaczego pośrednik, a nie osobny model obok? Klient Wyoming w HA czyta z wyniku tylko tekst, a encja zamiany mowy na tekst nie wie, z której satelity przyszło nagranie. Dlatego usługa dopisuje mówcę do wyniku transkrypcji, a integracja ma własnego klienta, który te pola odczytuje. Zwykły klient Wyoming dodatkowe pola pomija, więc usługę można bez ryzyka wpiąć w miejsce Whispera: w najgorszym razie działa jak sam Whisper.
Przed wyborem porównaliśmy kilka modeli dostępnych w sherpa-onnx. Zestawy testowe: 6 polskich głosów Pipera × 12 zdań oraz nagrania 3 osób z przykładów sherpa-onnx. Dokładność liczona metodą „zostaw jedno nagranie”. EER (equal error rate) to punkt, w którym fałszywe akceptacje równają się fałszywym odrzuceniom – im niżej, tym lepiej.
| Model | Wymiar | 3 s mowy | Piper: dokładność / EER | Ludzie: dokładność / EER |
|---|---|---|---|---|
| 3D-Speaker CAM++ zh_en „advanced” (domyślny) | 192 | 91 ms | 100% / 0,2% | 100% / 0% |
| NeMo TitaNet-small | 192 | 59 ms | 100% / 0,9% | 100% / 0% |
| 3D-Speaker ERes2Net (VoxCeleb) | 192 | 218 ms | 100% / 1,9% | 100% / 0% |
| WeSpeaker ResNet34-LM | 256 | 257 ms | 98,6% / 12,4% | 92,9% / 15,4% |
| 3D-Speaker CAM++ (VoxCeleb) | 512 | 90 ms | 68,1% / 40,8% | 71,4% / 37,7% |
Przez cały łańcuch (Piper → wyoming-speaker-id → Whisper) wszystkie wypowiedzi z co najmniej
1 s mowy zostały przypisane właściwej osobie, z wynikami 0,76–0,92. Inny model można wskazać
opcją model (nazwa modelu sherpa-onnx albo ścieżka do pliku .onnx);
po zmianie modelu wzorce trzeba nagrać od nowa.
Sprawdzone w praktyce: u autora usługa działa na co dzień na NVIDIA Jetson AGX Orin 64 GB, jako kontener obok faster-whisper (Whisper na GPU, model głosu na CPU), dla całej rodziny i 7 satelit. Działa bardzo dobrze: rozpoznanie trwa ok. 90 ms i liczy się w tle transkrypcji, więc polecenia nie są wolniejsze niż z samym Whisperem.
Najprostsza droga, gdy Home Assistant działa jako HA OS albo Supervised.
https://github.com/witold-gren/home-assistant-assist-speaker-id.tcp://core-whisper:10300 dla dodatku Whisper albo adres własnego serwera, np.
tcp://192.168.1.10:10300.| Opcja dodatku | Domyślnie | Znaczenie |
|---|---|---|
upstream | tcp://core-whisper:10300 | serwer Wyoming zamiany mowy na tekst |
threshold / margin | 0,5 / 0,05 | wspólny próg i przewaga nad drugą osobą (wartości startowe) |
min_speech / max_speech | 1,0 / 8,0 s | krótsza mowa daje „nieznany”; ile mowy liczyć |
threads | 2 | wątki CPU modelu |
model | CAM++ zh_en | inny model sherpa-onnx (nazwa albo ścieżka) |
api_token | losowy | własny token API (zwykle niepotrzebny) |
debug | wyłączone | szczegółowy dziennik |
Gdy Whisper działa na osobnej maszynie, najlepiej postawić usługę obok niego. Dźwięk nie przechodzi wtedy dwa razy przez sieć.
git clone https://github.com/witold-gren/home-assistant-assist-speaker-id
docker build -t wyoming-speaker-id home-assistant-assist-speaker-id/speaker_id/
docker run -d --name speaker-id --restart unless-stopped \
-p 10350:10350 -p 10351:10351 -v speaker-id-data:/data \
-e SPEAKER_ID_UPSTREAM=tcp://192.168.1.10:10300 \
-e SPEAKER_ID_API_TOKEN=dlugi-losowy-token \
wyoming-speaker-id
Ten sam kontener w docker-compose.yaml, obok Whispera:
services:
whisper:
image: rhasspy/wyoming-whisper
# … Twoja dotychczasowa konfiguracja
speaker-id:
build: ./home-assistant-assist-speaker-id/speaker_id
restart: unless-stopped
ports: ["10350:10350", "10351:10351"]
volumes: ["./speaker-id/data:/data"]
environment:
SPEAKER_ID_UPSTREAM: tcp://whisper:10300
SPEAKER_ID_API_TOKEN: dlugi-losowy-token
| Zmienna | Domyślnie | Znaczenie |
|---|---|---|
SPEAKER_ID_UPSTREAM | (wymagana) | serwer Wyoming zamiany mowy na tekst |
SPEAKER_ID_THRESHOLD / SPEAKER_ID_MARGIN | 0.5 / 0.05 | wspólny próg i margines |
SPEAKER_ID_MIN_SPEECH / SPEAKER_ID_MAX_SPEECH | 1.0 / 8.0 | minimum mowy i ile mowy liczyć (sekundy) |
SPEAKER_ID_MODEL | CAM++ zh_en | nazwa modelu sherpa-onnx albo ścieżka |
SPEAKER_ID_API_TOKEN | brak | token API HTTP; bez niego trzymaj port 10351 wyłącznie w sieci domowej |
SPEAKER_ID_THREADS, SPEAKER_ID_DEBUG | 2, 0 | wątki modelu, szczegółowy dziennik |
Port 10350 to Wyoming (zamiana mowy na tekst z mówcą), port 10351 to API HTTP (wzorce, kalibracja, ostatnie nagranie).
Integracja tworzy urządzenie „Assist Speaker ID” z encją zamiany mowy na tekst „Mowa na tekst z mówcą”, agentem „Agent z mówcą” i sensorem „Ostatni mówca”.
Opcje integracji (Konfiguruj) to menu z dwiema pozycjami:
person.*),
agent docelowy dla „Agenta z mówcą”, linia promptu (domyślnie Pyta: {name}.) i
czy wykonywać polecenia jako użytkownik HA mówcy (context.user_id).Ustawienia → Asystenci głosowi → wybierz pipeline (asystenta) każdej satelity:
Dlaczego wyłączyć obsługę lokalną? Gdy jest włączona, HA najpierw próbuje własnych intencji, zanim zapyta agenta. Wtedy wbudowana intencja rozgłaszania złapie „Ogłoszenie, …” i przeczyta tekst syntezą zamiast Twojego nagrania, a podczas nagrywania wzorców agent nie zdąży odpowiedzieć krótkim „Nagrano 3 z 10”. Assist Router i „Agent z mówcą” i tak przekazują HA wszystko, co HA rozumie, więc zwykłe polecenia działają dalej.
Przełączaj satelity po jednej. Gdyby coś nie działało, wystarczy w pipeline'ie wrócić do zwykłego Whispera.
Sprawdzenie: Ustawienia → Asystenci głosowi → pipeline → ⋮ → Debugowanie. Po poleceniu ze
satelity sensor „Ostatni mówca” pokazuje unknown (nie ma jeszcze wzorców), a atrybut
speech_seconds długość mowy. To znak, że dźwięk przechodzi przez usługę.
Każdy domownik nagrywa 10–15 zwykłych zdań. Najwygodniej robi to nagrywanie prowadzone: satelita sama prosi o kolejne zdania, bez słowa wybudzającego.
speaker_id.start_enrollment:
action: speaker_id.start_enrollment
data:
speaker: anna # id osoby: małe litery, bez spacji
name: Anna # nazwa wypowiadana i wyświetlana
count: 10
satellite: assist_satellite.salon # satelita, przy której stoi AnnaBez pola satellite nagrywanie też działa, ale każde zdanie zaczyna się od słowa
wybudzającego. Postęp widać w atrybucie enrollment sensora „Ostatni mówca”. Przerwanie:
speaker_id.stop_enrollment.
Nieudane nagrywanie? Dograj próbki tą samą akcją (nowe dochodzą do starych)
albo zacznij od zera: speaker_id.delete_speaker i nagrywanie jeszcze raz. Pojedyncze
zdanie dodasz po fakcie akcją speaker_id.add_last_sample: ostatnia wypowiedź (najwyżej
sprzed 10 min) staje się próbką wskazanej osoby.
Dobre próbki: nagrywaj przez tę satelitę i z tej odległości, z której domownik zwykle mówi – mikrofon i pomieszczenie zmieniają głos bardziej niż słowa. Używaj zwykłych poleceń po kilka słów, zwykłym tonem (krzyk, szept i śmiech zaniżają wyniki). Nagraj wszystkich domowników, zanim zaczniesz kalibrować. Głosy dzieci zmieniają się z wiekiem, więc co kilka miesięcy dograj im nowe próbki.
Każda wypowiedź jest porównywana z wzorcem każdej osoby. Wynik to podobieństwo, zwykle od 0 do 1 (w odpowiedzi na „Kto to mówi?” podawane w procentach):
| Podobieństwo | Co zwykle oznacza (CAM++, mikrofon satelity) |
|---|---|
| 0,75–0,95 | ta sama osoba, dobre warunki |
| 0,55–0,75 | ta sama osoba, ale krótko, z daleka, w hałasie albo innym tonem |
| 0,35–0,55 | podobny głos, często rodzeństwo albo rodzic i dziecko tej samej płci |
| 0–0,35 | inna osoba |
To nie jest „unikatowość” głosu. Wysoki wynik oznacza tylko, że ta wypowiedź jest bardzo podobna do tego wzorca.
Najlepiej pasująca osoba zostaje rozpoznana tylko wtedy, gdy spełnia oba warunki:
Inaczej mówca jest „nieznany” (unknown), a Assist działa tak, jakby rozpoznawania
nie było. Pomyłka jest gorsza niż brak rozpoznania, więc domyślnie system jest ostrożny.
| Parametr | Domyślnie | Gdzie zmienić |
|---|---|---|
| Próg wspólny | 0,50 | opcje integracji → Progi rozpoznawania |
| Próg osoby | brak = wspólny | opcje integracji, akcja speaker_id.set_speaker, kalibracja z apply: true |
| Margines | 0,05 | opcje integracji |
| Minimum mowy | 1,0 s | opcje integracji; krótsza wypowiedź = zawsze „nieznany” |
Wartości z opcji integracji są zapisane w usłudze (speakers.json) i mają
pierwszeństwo przed opcjami dodatku i zmiennymi środowiska. Te są tylko wartościami startowymi.
Akcja speaker_id.calibrate porównuje próbki każdej osoby z jej wzorcem i z próbkami
pozostałych osób, a potem proponuje próg. Z apply: true od razu zapisuje progi
osób.
| Pole raportu | Znaczenie |
|---|---|
samples | liczba próbek |
own_min | najgorsza własna próbka: podobieństwo do wzorca z pozostałych próbek tej osoby |
own_p10 | 10. percentyl: 9 na 10 własnych próbek jest co najmniej tak podobnych |
own_mean | średnie podobieństwo własnych próbek |
others_max | najbardziej podobna cudza próbka |
separable | true, gdy own_min > others_max: żadna cudza próbka nie zbliżyła się do najgorszej własnej |
suggested_threshold | środek między own_p10 a others_max (w zakresie 0,2–0,9) |
Przykład (Tomek, dorosły):
samples: 10
own_min: 0.709 # najgorsza własna próbka: 71% podobieństwa do wzorca
own_p10: 0.778 # 9 z 10 własnych próbek ma co najmniej 78%
own_mean: 0.803 # średnio 80%
others_max: 0.305 # najbardziej podobna cudza próbka: tylko 31%
separable: true # 0.709 > 0.305 - głosy wyraźnie rozdzielone
suggested_threshold: 0.541 # (0.778 + 0.305) / 2
Między najgorszą własną próbką (71%) a najbliższą cudzą (31%) jest 40 punktów przerwy. Każdy
próg w przedziale ok. 0,35–0,70 zadziała, a proponowane 0,54 leży pośrodku z zapasem w obie strony.
U dziecka może wyjść np. own_p10: 0.74 i others_max: 0.68 (rodzeństwo):
próg 0,71 i tylko 6 punktów przerwy. Wtedy warto też podnieść margines.
others_max liczy się tylko z osób, które
mają wzorce.separable: false często winna jest jedna nieudana próbka (ktoś się wtrącił,
hałas). Dograj próbki albo nagraj tę osobę od nowa.others_max a own_p10: bliżej
own_p10 to mniej pomyłek, bliżej others_max – mniej „nieznanych”.Kalibracja z apply: true nadpisuje progi osób. Ręczne poprawki rób
po niej.
Z Assist Routerem zapytaj „Kto to mówi?”, „Kim jestem?”, „Czy mnie rozpoznajesz?” albo „Czyj to głos?”. Odpowiedź podaje osobę i podobieństwo, np. „Mówi Anna, podobieństwo 82%. Następna osoba: Kasia, 41%.”. To najszybszy test progów i jakości wzorców.
Ustaw go w pipeline'ie, a w opcjach integracji wybierz agenta docelowego (np. Ollama, OpenAI, wbudowany agent HA). Przed przekazaniem polecenia „Agent z mówcą”:
context.user_id na użytkownika HA mówcy, więc w historii
widać, kto przełączył światło;get_speaker()try:
from custom_components.speaker_id.api import get_speaker
except ImportError:
get_speaker = None
speaker = get_speaker(hass) if get_speaker else None
if speaker and speaker.speaker:
... # speaker.speaker (id), .name, .person, .user_id, .score
Agent, który sam obsługuje nagrywanie, wywołuje na początku enrollment_reply(hass):
zwraca krótką odpowiedź („Nagrano 3 z 10. Kolejne zdanie.”) i informację, czy słuchać dalej, albo
None dla zwykłej wypowiedzi. Tak robi Assist Router: pytanie w pierwszej osobie bez
imienia („jakie mam zajęcia”) dotyczy danych mówcy, „tu” to pokój mówcy, a „mój
brat” to konkretna osoba z rodziny.
Sensor „Ostatni mówca” (stan: imię albo unknown; atrybuty:
speaker, name, person, score,
scores, speech_seconds, enrollment) i zdarzenie
speaker_id_identified. Treść wypowiedzi celowo nie trafia do historii.
Automatyzacje z wyzwalaczem zdaniowym HA uruchamia przed agentem, więc nie dostają mówcy wprost.
Sensor zmienia się jednak w tym samym poleceniu, tuż przed nimi – wynik sprzed kilku sekund to
mówca tego zdania. Wygodne makro w /config/custom_templates/mowca.jinja:
{% macro mowca(max_s=20) -%}
{%- set s = states.sensor.assist_speaker_id_ostatni_mowca -%}
{%- if s and (now() - s.last_updated).total_seconds() <= max_s -%}
{{- s.attributes.speaker or '' -}}
{%- endif -%}
{%- endmacro %}
Po utworzeniu pliku uruchom akcję homeassistant.reload_custom_templates. Przykład:
„Gdzie jest mój telefon?” dzwoni na telefon osoby, która pyta:
triggers:
- trigger: conversation
command: "gdzie jest mój telefon"
actions:
- variables:
kto: "{% from 'mowca.jinja' import mowca %}{{ mowca() }}"
- if: "{{ kto in ['anna', 'tomek'] }}"
then:
- action: script.zadzwon_na_telefon # Twój skrypt, np. dzwonek przez aplikację HA
data: { osoba: "{{ kto }}" }
- set_conversation_response: "Dzwonię na Twój telefon."
else:
- set_conversation_response: "Nie wiem, kto pyta. Czyj telefon mam znaleźć?"
Nazwa sensora zależy od języka HA; po angielsku to
sensor.assist_speaker_id_last_speaker.
„OK Nabu, ogłoszenie, dzieci, proszę przyjść na obiad.” Pozostałe głośniki odtwarzają Twoje nagranie – dokładnie to, co powiedziałeś po słowie wybudzającym – zamiast czytać tekst syntezą mowy. Słychać, kto woła i jakim tonem.
/api/speaker_id/announcement/<token>, ważnym 10 minut. Nic nie trafia na dysk.speaker_id.announce_last w automatyzacji:| Pole | Domyślnie | Znaczenie |
|---|---|---|
satellites | wszystkie satelity z ogłoszeniami | gdzie zagrać |
exclude | brak | pominięta satelita, zwykle ta, która nagrała |
media_players | brak | dodatkowe odtwarzacze, np. głośniki przez Music Assistant (play_media z announce: true) |
preannounce | true | krótki sygnał przed nagraniem |
max_age | 60 s | starsza wypowiedź nie jest ogłaszana |
Agent konwersacji może wywołać to samo funkcją
async_announce_last(hass, exclude=…, media_players=…) z
custom_components.speaker_id.api. Pamiętaj o wyłączeniu obsługi lokalnej w pipeline'ie
(rozdział 6).
| Akcja | Co robi |
|---|---|
speaker_id.start_enrollment | nagrywanie wzorca: speaker, name, count, opcjonalnie satellite (nagrywanie prowadzone) |
speaker_id.stop_enrollment | przerywa nagrywanie |
speaker_id.add_last_sample | ostatnia wypowiedź (najwyżej sprzed 10 min) jako próbka osoby |
speaker_id.calibrate | raport kalibracji i propozycje progów; apply: true zapisuje progi |
speaker_id.set_speaker | zmiana nazwy i własnego progu osoby |
speaker_id.delete_speaker | usuwa wzorzec osoby |
speaker_id.list_speakers | lista osób z liczbą próbek i progami |
speaker_id.announce_last | odtwarza ostatnią wypowiedź jako ogłoszenie |
Usługa ma też API HTTP (port 10351, nagłówek Authorization: Bearer <token>),
przydatne do skryptów i testów:
| Zapytanie | Co robi |
|---|---|
GET /api/info, GET/PUT /api/settings | wersja i model; progi, margines, minimum mowy |
GET /api/speakers, PUT/DELETE /api/speakers/{id} | lista osób; zmiana albo usunięcie osoby |
POST /api/speakers/{id}/samples | próbka z pliku WAV |
POST /api/speakers/{id}/samples/last, DELETE …/samples/{n} | ostatnia wypowiedź jako próbka; usunięcie próbki |
POST/DELETE /api/enrollment | start i koniec nagrywania wzorca |
GET /api/last, GET /api/last.wav | wynik i nagranie ostatniej wypowiedzi (z RAM) |
POST /api/identify | rozpoznanie mówcy z przesłanego pliku WAV |
POST /api/calibrate | raport kalibracji |
To wygoda, nie zabezpieczenie. Głos można nagrać i odtworzyć, a rodzeństwo bywa do siebie bardzo podobne. Nie używaj mówcy do otwierania zamków, wyłączania alarmu ani niczego, co wymaga uwierzytelnienia.
Wzorzec głosu to dane biometryczne (RODO, art. 9). Dlatego:
/data/speakers.json), bez nagrań
(wektor opisuje barwę głosu, ale nie jest nagraniem);add_last_sample i ogłoszeń);delete_speaker usuwa wzorzec osoby w całości;