Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Hansen
a09b2e4f5f Add X-Speech-Options header 2026-03-09 14:31:49 -05:00
Michael Hansen
25a02eb2c8 Add options dict 2026-03-09 14:14:50 -05:00
3 changed files with 77 additions and 0 deletions

View File

@@ -28,6 +28,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_suggest_report_issue
from homeassistant.util import dt as dt_util, language as language_util
from homeassistant.util.json import json_loads_object
from .const import (
DATA_COMPONENT,
@@ -269,6 +270,7 @@ class SpeechToTextView(HomeAssistantView):
# Get metadata
try:
metadata = _metadata_from_header(request)
metadata.options = _options_from_header(request)
except ValueError as err:
raise HTTPBadRequest(text=str(err)) from err
@@ -407,6 +409,22 @@ def _metadata_from_header(request: web.Request) -> SpeechMetadata:
raise ValueError(f"Wrong format of X-Speech-Content: {err}") from err
def _options_from_header(request: web.Request) -> dict[str, Any] | None:
"""Extract options from STT options header.
X-Speech-Options:
{"key":"value", ...}
"""
data = request.headers.get(istr("X-Speech-Options"))
if not data:
return None
try:
return json_loads_object(data)
except ValueError as err:
raise ValueError(f"Wrong format of X-Speech-Content: {err}") from err
@websocket_api.websocket_command(
{
"type": "stt/engine/list",

View File

@@ -1,6 +1,7 @@
"""Speech-to-text data models."""
from dataclasses import dataclass
from typing import Any
from .const import (
AudioBitRates,
@@ -22,6 +23,7 @@ class SpeechMetadata:
bit_rate: AudioBitRates
sample_rate: AudioSampleRates
channel: AudioChannels
options: dict[str, Any] | None = None
def __post_init__(self) -> None:
"""Finish initializing the metadata."""

View File

@@ -3,6 +3,7 @@
from collections.abc import Generator, Iterable
from contextlib import ExitStack
from http import HTTPStatus
import json
from pathlib import Path
from unittest.mock import AsyncMock
@@ -554,3 +555,59 @@ async def test_get_engine_entity(
await mock_config_entry_setup(hass, tmp_path, mock_provider_entity)
assert async_get_speech_to_text_engine(hass, "stt.test") is mock_provider_entity
@pytest.mark.parametrize(
"setup", ["mock_setup", "mock_config_entry_setup"], indirect=True
)
async def test_speech_options_header(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
setup: MockSTTProvider | MockSTTProviderEntity,
) -> None:
"""Test that X-Speech-Options header is passed to SpeechMetadata."""
client = await hass_client()
options = {"key1": "value1", "key2": 123, "nested": {"a": "b"}}
response = await client.post(
f"/api/stt/{setup.url_path}",
headers={
"X-Speech-Content": (
"format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1;"
" language=en"
),
"X-Speech-Options": json.dumps(
options,
separators=(",", ":"),
ensure_ascii=True,
),
},
)
assert response.status == HTTPStatus.OK
assert len(setup.calls) == 1
metadata, _ = setup.calls[0]
assert metadata.options == options
@pytest.mark.parametrize(
"setup", ["mock_setup", "mock_config_entry_setup"], indirect=True
)
async def test_speech_options_header_not_present(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
setup: MockSTTProvider | MockSTTProviderEntity,
) -> None:
"""Test that options are None when X-Speech-Options header is not present."""
client = await hass_client()
response = await client.post(
f"/api/stt/{setup.url_path}",
headers={
"X-Speech-Content": (
"format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1;"
" language=en"
),
},
)
assert response.status == HTTPStatus.OK
assert len(setup.calls) == 1
metadata, _ = setup.calls[0]
assert metadata.options is None