Add VAD sensitivity to ESPHome (#95283)
* Change to "finished speaking detection" * Add select entity to ESPHome for finished speaking detection * Fix entity name * Use vad select in stt stream --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
c6775920f5
commit
0f08e6699c
6 changed files with 59 additions and 8 deletions
|
@ -13,7 +13,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vad_sensitivity": {
|
"vad_sensitivity": {
|
||||||
"name": "Silence sensitivity",
|
"name": "Finished speaking detection",
|
||||||
"state": {
|
"state": {
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"aggressive": "Aggressive",
|
"aggressive": "Aggressive",
|
||||||
|
|
|
@ -3,7 +3,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
from aioesphomeapi import EntityInfo, SelectInfo, SelectState
|
from aioesphomeapi import EntityInfo, SelectInfo, SelectState
|
||||||
|
|
||||||
from homeassistant.components.assist_pipeline.select import AssistPipelineSelect
|
from homeassistant.components.assist_pipeline.select import (
|
||||||
|
AssistPipelineSelect,
|
||||||
|
VadSensitivitySelect,
|
||||||
|
)
|
||||||
from homeassistant.components.select import SelectEntity
|
from homeassistant.components.select import SelectEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
@ -37,7 +40,12 @@ async def async_setup_entry(
|
||||||
entry_data = DomainData.get(hass).get_entry_data(entry)
|
entry_data = DomainData.get(hass).get_entry_data(entry)
|
||||||
assert entry_data.device_info is not None
|
assert entry_data.device_info is not None
|
||||||
if entry_data.device_info.voice_assistant_version:
|
if entry_data.device_info.voice_assistant_version:
|
||||||
async_add_entities([EsphomeAssistPipelineSelect(hass, entry_data)])
|
async_add_entities(
|
||||||
|
[
|
||||||
|
EsphomeAssistPipelineSelect(hass, entry_data),
|
||||||
|
EsphomeVadSensitivitySelect(hass, entry_data),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity):
|
class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity):
|
||||||
|
@ -68,3 +76,12 @@ class EsphomeAssistPipelineSelect(EsphomeAssistEntity, AssistPipelineSelect):
|
||||||
"""Initialize a pipeline selector."""
|
"""Initialize a pipeline selector."""
|
||||||
EsphomeAssistEntity.__init__(self, entry_data)
|
EsphomeAssistEntity.__init__(self, entry_data)
|
||||||
AssistPipelineSelect.__init__(self, hass, self._device_info.mac_address)
|
AssistPipelineSelect.__init__(self, hass, self._device_info.mac_address)
|
||||||
|
|
||||||
|
|
||||||
|
class EsphomeVadSensitivitySelect(EsphomeAssistEntity, VadSensitivitySelect):
|
||||||
|
"""VAD sensitivity selector for VoIP devices."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
|
||||||
|
"""Initialize a VAD sensitivity selector."""
|
||||||
|
EsphomeAssistEntity.__init__(self, entry_data)
|
||||||
|
VadSensitivitySelect.__init__(self, hass, self._device_info.mac_address)
|
||||||
|
|
|
@ -67,6 +67,14 @@
|
||||||
"state": {
|
"state": {
|
||||||
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
|
"preferred": "[%key:component::assist_pipeline::entity::select::pipeline::state::preferred%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"vad_sensitivity": {
|
||||||
|
"name": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::name%]",
|
||||||
|
"state": {
|
||||||
|
"default": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::default%]",
|
||||||
|
"aggressive": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::aggressive%]",
|
||||||
|
"relaxed": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::relaxed%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,10 @@ from homeassistant.components.assist_pipeline import (
|
||||||
async_pipeline_from_audio_stream,
|
async_pipeline_from_audio_stream,
|
||||||
select as pipeline_select,
|
select as pipeline_select,
|
||||||
)
|
)
|
||||||
from homeassistant.components.assist_pipeline.vad import VoiceCommandSegmenter
|
from homeassistant.components.assist_pipeline.vad import (
|
||||||
|
VadSensitivity,
|
||||||
|
VoiceCommandSegmenter,
|
||||||
|
)
|
||||||
from homeassistant.components.media_player import async_process_play_media_url
|
from homeassistant.components.media_player import async_process_play_media_url
|
||||||
from homeassistant.core import Context, HomeAssistant, callback
|
from homeassistant.core import Context, HomeAssistant, callback
|
||||||
|
|
||||||
|
@ -251,9 +254,9 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||||
chunk = await self.queue.get()
|
chunk = await self.queue.get()
|
||||||
|
|
||||||
async def _iterate_packets_with_vad(
|
async def _iterate_packets_with_vad(
|
||||||
self, pipeline_timeout: float
|
self, pipeline_timeout: float, silence_seconds: float
|
||||||
) -> Callable[[], AsyncIterable[bytes]] | None:
|
) -> Callable[[], AsyncIterable[bytes]] | None:
|
||||||
segmenter = VoiceCommandSegmenter()
|
segmenter = VoiceCommandSegmenter(silence_seconds=silence_seconds)
|
||||||
chunk_buffer: deque[bytes] = deque(maxlen=100)
|
chunk_buffer: deque[bytes] = deque(maxlen=100)
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(pipeline_timeout):
|
async with async_timeout.timeout(pipeline_timeout):
|
||||||
|
@ -305,7 +308,16 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||||
)
|
)
|
||||||
|
|
||||||
if use_vad:
|
if use_vad:
|
||||||
stt_stream = await self._iterate_packets_with_vad(pipeline_timeout)
|
stt_stream = await self._iterate_packets_with_vad(
|
||||||
|
pipeline_timeout,
|
||||||
|
silence_seconds=VadSensitivity.to_seconds(
|
||||||
|
pipeline_select.get_vad_sensitivity(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
self.device_info.mac_address,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
# Error or timeout occurred and was handled already
|
# Error or timeout occurred and was handled already
|
||||||
if stt_stream is None:
|
if stt_stream is None:
|
||||||
return
|
return
|
||||||
|
|
|
@ -25,6 +25,20 @@ async def test_pipeline_selector(
|
||||||
assert state.state == "preferred"
|
assert state.state == "preferred"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_vad_sensitivity_select(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_voice_assistant_v1_entry,
|
||||||
|
) -> None:
|
||||||
|
"""Test VAD sensitivity select.
|
||||||
|
|
||||||
|
Functionality is tested in assist_pipeline/test_select.py.
|
||||||
|
This test is only to ensure it is set up.
|
||||||
|
"""
|
||||||
|
state = hass.states.get("select.test_finished_speaking_detection")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "default"
|
||||||
|
|
||||||
|
|
||||||
async def test_select_generic_entity(
|
async def test_select_generic_entity(
|
||||||
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
|
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -29,6 +29,6 @@ async def test_vad_sensitivity_select(
|
||||||
Functionality is tested in assist_pipeline/test_select.py.
|
Functionality is tested in assist_pipeline/test_select.py.
|
||||||
This test is only to ensure it is set up.
|
This test is only to ensure it is set up.
|
||||||
"""
|
"""
|
||||||
state = hass.states.get("select.192_168_1_210_silence_sensitivity")
|
state = hass.states.get("select.192_168_1_210_finished_speaking_detection")
|
||||||
assert state is not None
|
assert state is not None
|
||||||
assert state.state == "default"
|
assert state.state == "default"
|
||||||
|
|
Loading…
Add table
Reference in a new issue