mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 10:16:02 -05:00
211 lines
6.9 KiB
Python
211 lines
6.9 KiB
Python
"""Support for the cloud for speech to text service."""
|
|
|
|
from collections.abc import AsyncIterable
|
|
import logging
|
|
from typing import override
|
|
|
|
from hass_nabucasa import Cloud, SpeechToTextV2Error
|
|
from hass_nabucasa.voice import STT_LANGUAGES, STTResponse, VoiceError
|
|
|
|
from homeassistant.components import labs
|
|
from homeassistant.components.stt import (
|
|
DEFAULT_AUDIO_PROCESSING,
|
|
AudioBitRates,
|
|
AudioChannels,
|
|
AudioCodecs,
|
|
AudioFormats,
|
|
AudioSampleRates,
|
|
SpeechAudioProcessing,
|
|
SpeechMetadata,
|
|
SpeechResult,
|
|
SpeechResultState,
|
|
SpeechToTextEntity,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.setup import async_when_setup
|
|
|
|
from .assist_pipeline import async_migrate_cloud_pipeline_engine
|
|
from .client import CloudClient
|
|
from .const import (
|
|
DATA_CLOUD,
|
|
DATA_PLATFORMS_SETUP,
|
|
DOMAIN,
|
|
PREVIEW_FEATURE_STT_V2,
|
|
STT_ENTITY_UNIQUE_ID,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# STT v2 detects the end of speech itself and works best on untouched audio.
|
|
STT_V2_AUDIO_PROCESSING = SpeechAudioProcessing(
|
|
requires_external_vad=True,
|
|
prefers_auto_gain_enabled=False,
|
|
prefers_noise_reduction_enabled=False,
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up Home Assistant Cloud speech platform via config entry."""
|
|
stt_platform_loaded = hass.data[DATA_PLATFORMS_SETUP][Platform.STT]
|
|
stt_platform_loaded.set()
|
|
cloud = hass.data[DATA_CLOUD]
|
|
async_add_entities([CloudProviderEntity(cloud)])
|
|
|
|
|
|
class CloudProviderEntity(SpeechToTextEntity):
|
|
"""Home Assistant Cloud speech API provider."""
|
|
|
|
_attr_name = "Home Assistant Cloud"
|
|
_attr_unique_id = STT_ENTITY_UNIQUE_ID
|
|
|
|
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
|
"""Initialize cloud Speech to text entity."""
|
|
self.cloud = cloud
|
|
|
|
@property
|
|
def _stt_v2_enabled(self) -> bool:
|
|
"""Return if the v2 speech to text service is enabled."""
|
|
return labs.async_is_preview_feature_enabled(
|
|
self.hass, DOMAIN, PREVIEW_FEATURE_STT_V2
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def supported_languages(self) -> list[str]:
|
|
"""Return a list of supported languages."""
|
|
return STT_LANGUAGES
|
|
|
|
@property
|
|
@override
|
|
def supported_formats(self) -> list[AudioFormats]:
|
|
"""Return a list of supported formats."""
|
|
return [AudioFormats.WAV, AudioFormats.OGG]
|
|
|
|
@property
|
|
@override
|
|
def supported_codecs(self) -> list[AudioCodecs]:
|
|
"""Return a list of supported codecs."""
|
|
return [AudioCodecs.PCM, AudioCodecs.OPUS]
|
|
|
|
@property
|
|
@override
|
|
def supported_bit_rates(self) -> list[AudioBitRates]:
|
|
"""Return a list of supported bitrates."""
|
|
return [AudioBitRates.BITRATE_16]
|
|
|
|
@property
|
|
@override
|
|
def supported_sample_rates(self) -> list[AudioSampleRates]:
|
|
"""Return a list of supported samplerates."""
|
|
return [AudioSampleRates.SAMPLERATE_16000]
|
|
|
|
@property
|
|
@override
|
|
def supported_channels(self) -> list[AudioChannels]:
|
|
"""Return a list of supported channels."""
|
|
return [AudioChannels.CHANNEL_MONO]
|
|
|
|
@property
|
|
@override
|
|
def audio_processing(self) -> SpeechAudioProcessing:
|
|
"""Return required/preferred input audio processing settings."""
|
|
if self._stt_v2_enabled:
|
|
return STT_V2_AUDIO_PROCESSING
|
|
return DEFAULT_AUDIO_PROCESSING
|
|
|
|
@override
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Run when entity is about to be added to hass."""
|
|
|
|
async def pipeline_setup(hass: HomeAssistant, _comp: str) -> None:
|
|
"""When assist_pipeline is set up."""
|
|
assert self.platform.config_entry
|
|
self.platform.config_entry.async_create_task(
|
|
hass,
|
|
async_migrate_cloud_pipeline_engine(
|
|
self.hass, platform=Platform.STT, engine_id=self.entity_id
|
|
),
|
|
)
|
|
|
|
async_when_setup(self.hass, "assist_pipeline", pipeline_setup)
|
|
|
|
self.async_on_remove(
|
|
labs.async_subscribe_preview_feature(
|
|
self.hass,
|
|
DOMAIN,
|
|
PREVIEW_FEATURE_STT_V2,
|
|
self._async_handle_labs_update,
|
|
)
|
|
)
|
|
|
|
@override
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
"""Close the connection when the entity is removed."""
|
|
await self.cloud.stt_v2.disconnect()
|
|
|
|
async def _async_handle_labs_update(
|
|
self, event_data: labs.EventLabsUpdatedData
|
|
) -> None:
|
|
"""Close the connection to the v2 service when it is turned off."""
|
|
if not event_data["enabled"]:
|
|
await self.cloud.stt_v2.disconnect()
|
|
|
|
@override
|
|
async def async_process_audio_stream(
|
|
self, metadata: SpeechMetadata, stream: AsyncIterable[bytes]
|
|
) -> SpeechResult:
|
|
"""Process an audio stream to STT service."""
|
|
# STT v2 covers fewer languages, so fall back for the rest.
|
|
use_stt_v2 = self._stt_v2_enabled and bool(
|
|
self.cloud.stt_v2.resolve_language(metadata.language)
|
|
)
|
|
|
|
try:
|
|
if use_stt_v2:
|
|
result = await self._async_process_stt_v2(metadata, stream)
|
|
else:
|
|
result = await self._async_process_azure_stt(metadata, stream)
|
|
except (SpeechToTextV2Error, VoiceError) as err:
|
|
_LOGGER.error("Voice error: %s", err)
|
|
return SpeechResult(None, SpeechResultState.ERROR)
|
|
|
|
return SpeechResult(
|
|
result.text,
|
|
SpeechResultState.SUCCESS if result.success else SpeechResultState.ERROR,
|
|
)
|
|
|
|
async def _async_process_stt_v2(
|
|
self, metadata: SpeechMetadata, stream: AsyncIterable[bytes]
|
|
) -> STTResponse:
|
|
"""Process an audio stream with the v2 speech to text service."""
|
|
return await self.cloud.stt_v2.process_stt(
|
|
stream=stream,
|
|
language=metadata.language,
|
|
audio_format=metadata.format.value,
|
|
codec=metadata.codec.value,
|
|
bit_rate=metadata.bit_rate.value,
|
|
sample_rate=metadata.sample_rate.value,
|
|
channel=metadata.channel.value,
|
|
)
|
|
|
|
async def _async_process_azure_stt(
|
|
self, metadata: SpeechMetadata, stream: AsyncIterable[bytes]
|
|
) -> STTResponse:
|
|
"""Process an audio stream with the Azure speech to text service."""
|
|
content_type = (
|
|
f"audio/{metadata.format!s}; codecs=audio/{metadata.codec!s};"
|
|
" samplerate=16000"
|
|
)
|
|
return await self.cloud.voice.process_stt(
|
|
stream=stream,
|
|
content_type=content_type,
|
|
language=metadata.language,
|
|
)
|