mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[voice_assistant] Make TTS playback start timeout configurable (#18707)
Co-authored-by: n-IA-hane <n-IA-hane@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from esphome import automation
|
||||
from esphome import automation, core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import media_player, micro_wake_word, microphone, speaker
|
||||
import esphome.config_validation as cv
|
||||
@@ -47,6 +47,9 @@ CONF_MICRO_WAKE_WORD = "micro_wake_word"
|
||||
CONF_WAKE_WORD = "wake_word"
|
||||
|
||||
CONF_CONVERSATION_TIMEOUT = "conversation_timeout"
|
||||
CONF_TTS_PLAYBACK_START_TIMEOUT = "tts_playback_start_timeout"
|
||||
# Keep in sync with VoiceAssistant::tts_playback_start_timeout_ in voice_assistant.h.
|
||||
TTS_PLAYBACK_START_TIMEOUT_DEFAULT = core.TimePeriod(milliseconds=2000)
|
||||
|
||||
CONF_ON_TIMER_STARTED = "on_timer_started"
|
||||
CONF_ON_TIMER_UPDATED = "on_timer_updated"
|
||||
@@ -110,6 +113,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_CONVERSATION_TIMEOUT, default="300s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_TTS_PLAYBACK_START_TIMEOUT,
|
||||
default=TTS_PLAYBACK_START_TIMEOUT_DEFAULT,
|
||||
): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=core.TimePeriod(milliseconds=1)),
|
||||
),
|
||||
cv.Optional(CONF_VOLUME_MULTIPLIER, default=1.0): cv.float_range(
|
||||
min=0.0, min_included=False
|
||||
),
|
||||
@@ -218,6 +228,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN]))
|
||||
cg.add(var.set_volume_multiplier(config[CONF_VOLUME_MULTIPLIER]))
|
||||
cg.add(var.set_conversation_timeout(config[CONF_CONVERSATION_TIMEOUT]))
|
||||
if (
|
||||
timeout := config[CONF_TTS_PLAYBACK_START_TIMEOUT]
|
||||
) != TTS_PLAYBACK_START_TIMEOUT_DEFAULT:
|
||||
cg.add(var.set_tts_playback_start_timeout(timeout))
|
||||
|
||||
if CONF_ON_LISTENING in config:
|
||||
await automation.build_automation(
|
||||
|
||||
@@ -760,7 +760,7 @@ void VoiceAssistant::signal_stop_() {
|
||||
}
|
||||
|
||||
void VoiceAssistant::start_playback_timeout_() {
|
||||
this->set_timeout(PLAYBACK_TIMEOUT_ID, 2000, [this]() {
|
||||
this->set_timeout(PLAYBACK_TIMEOUT_ID, this->tts_playback_start_timeout_, [this]() {
|
||||
this->cancel_timeout(SPEAKER_IDLE_TIMEOUT_ID);
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ class VoiceAssistant final : public Component {
|
||||
void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; }
|
||||
void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; }
|
||||
void set_conversation_timeout(uint32_t conversation_timeout) { this->conversation_timeout_ = conversation_timeout; }
|
||||
void set_tts_playback_start_timeout(uint32_t timeout) { this->tts_playback_start_timeout_ = timeout; }
|
||||
void reset_conversation_id();
|
||||
|
||||
Trigger<> *get_intent_end_trigger() { return &this->intent_end_trigger_; }
|
||||
@@ -318,6 +319,8 @@ class VoiceAssistant final : public Component {
|
||||
|
||||
float volume_multiplier_;
|
||||
uint32_t conversation_timeout_;
|
||||
// Keep in sync with TTS_PLAYBACK_START_TIMEOUT_DEFAULT in __init__.py.
|
||||
uint32_t tts_playback_start_timeout_{2000};
|
||||
|
||||
Configuration config_{};
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Test playback timeout C++ generation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("timeout", [None, "2s", "2000ms", "1ms", "10s"])
|
||||
def test_playback_timeout_codegen(
|
||||
timeout: str | None,
|
||||
tmp_path: Path,
|
||||
generate_main: Callable[[Path], str],
|
||||
) -> None:
|
||||
config = tmp_path / "voice_assistant.yaml"
|
||||
config.write_text(
|
||||
"""esphome:
|
||||
name: test-voice-assistant
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
wifi:
|
||||
ssid: test-network
|
||||
api:
|
||||
i2s_audio:
|
||||
i2s_lrclk_pin: GPIO25
|
||||
i2s_bclk_pin: GPIO26
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: test_mic
|
||||
adc_type: external
|
||||
i2s_din_pin: GPIO34
|
||||
voice_assistant:
|
||||
microphone: test_mic
|
||||
"""
|
||||
+ (f" tts_playback_start_timeout: {timeout}\n" if timeout else "")
|
||||
)
|
||||
main = generate_main(config)
|
||||
if timeout in (None, "2s", "2000ms"):
|
||||
assert "set_tts_playback_start_timeout(" not in main
|
||||
else:
|
||||
milliseconds = 1 if timeout == "1ms" else 10000
|
||||
assert f"set_tts_playback_start_timeout({milliseconds});" in main
|
||||
@@ -55,6 +55,7 @@ voice_assistant:
|
||||
speaker: va_speaker_id
|
||||
micro_wake_word: mww_id
|
||||
conversation_timeout: 60s
|
||||
tts_playback_start_timeout: 10s
|
||||
on_listening:
|
||||
- logger.log: "Voice assistant microphone listening"
|
||||
on_start:
|
||||
|
||||
@@ -36,6 +36,7 @@ voice_assistant:
|
||||
channels: 0
|
||||
speaker: va_speaker_id
|
||||
conversation_timeout: 60s
|
||||
tts_playback_start_timeout: 10s
|
||||
on_listening:
|
||||
- logger.log: "Voice assistant microphone listening"
|
||||
on_start:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Validate Voice Assistant playback timeout configuration."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import voice_assistant
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
["0s", "0ms", {"seconds": 0}, "-1s", "500us", "1ns", "500ns", {"nanoseconds": 1}],
|
||||
)
|
||||
def test_playback_timeout_rejects_invalid_duration(value: str | dict[str, int]) -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
with pytest.raises(cv.Invalid):
|
||||
voice_assistant.CONFIG_SCHEMA({"tts_playback_start_timeout": value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, milliseconds", [("1ms", 1), ("2s", 2000), ("10s", 10000)]
|
||||
)
|
||||
def test_playback_timeout_accepts_positive_milliseconds(
|
||||
value: str, milliseconds: int
|
||||
) -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
config = voice_assistant.CONFIG_SCHEMA({"tts_playback_start_timeout": value})
|
||||
assert config["tts_playback_start_timeout"].total_milliseconds == milliseconds
|
||||
|
||||
|
||||
def test_playback_timeout_default_is_two_seconds() -> None:
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: "esp32"}
|
||||
config = voice_assistant.CONFIG_SCHEMA({})
|
||||
assert config["tts_playback_start_timeout"].total_milliseconds == 2000
|
||||
Reference in New Issue
Block a user