Files

201 lines
6.6 KiB
Python

"""Speech to text support for OpenAI."""
from collections.abc import AsyncIterable
import io
import logging
from typing import TYPE_CHECKING, override
import wave
from openai import OpenAIError
from homeassistant.components import stt
from homeassistant.const import CONF_PROMPT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_CHAT_MODEL, DEFAULT_STT_PROMPT, RECOMMENDED_STT_MODEL
from .entity import OpenAIBaseLLMEntity
if TYPE_CHECKING:
from . import OpenAIConfigEntry
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: OpenAIConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up STT entities."""
for subentry in config_entry.subentries.values():
if subentry.subentry_type != "stt":
continue
async_add_entities(
[OpenAISTTEntity(config_entry, subentry)],
config_subentry_id=subentry.subentry_id,
)
class OpenAISTTEntity(stt.SpeechToTextEntity, OpenAIBaseLLMEntity):
"""OpenAI Speech to text entity."""
@property
@override
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
# https://developers.openai.com/api/docs/guides/speech-to-text#supported-languages
# The model may also transcribe the audio in other
# languages but with lower quality
return [
"af-ZA", # Afrikaans
"ar-SA", # Arabic
"hy-AM", # Armenian
"az-AZ", # Azerbaijani
"be-BY", # Belarusian
"bs-BA", # Bosnian
"bg-BG", # Bulgarian
"ca-ES", # Catalan
"zh-CN", # Chinese (Mandarin)
"hr-HR", # Croatian
"cs-CZ", # Czech
"da-DK", # Danish
"nl-NL", # Dutch
"en-US", # English
"et-EE", # Estonian
"fi-FI", # Finnish
"fr-FR", # French
"gl-ES", # Galician
"de-DE", # German
"el-GR", # Greek
"he-IL", # Hebrew
"hi-IN", # Hindi
"hu-HU", # Hungarian
"is-IS", # Icelandic
"id-ID", # Indonesian
"it-IT", # Italian
"ja-JP", # Japanese
"kn-IN", # Kannada
"kk-KZ", # Kazakh
"ko-KR", # Korean
"lv-LV", # Latvian
"lt-LT", # Lithuanian
"mk-MK", # Macedonian
"ms-MY", # Malay
"mr-IN", # Marathi
"mi-NZ", # Maori
"ne-NP", # Nepali
"no-NO", # Norwegian
"fa-IR", # Persian
"pl-PL", # Polish
"pt-PT", # Portuguese
"ro-RO", # Romanian
"ru-RU", # Russian
"sr-RS", # Serbian
"sk-SK", # Slovak
"sl-SI", # Slovenian
"es-ES", # Spanish
"sw-KE", # Swahili
"sv-SE", # Swedish
"fil-PH", # Tagalog (Filipino)
"ta-IN", # Tamil
"th-TH", # Thai
"tr-TR", # Turkish
"uk-UA", # Ukrainian
"ur-PK", # Urdu
"vi-VN", # Vietnamese
"cy-GB", # Welsh
]
@property
@override
def supported_formats(self) -> list[stt.AudioFormats]:
"""Return a list of supported formats."""
# https://developers.openai.com/api/docs/guides/speech-to-text#transcriptions
return [stt.AudioFormats.WAV, stt.AudioFormats.OGG]
@property
@override
def supported_codecs(self) -> list[stt.AudioCodecs]:
"""Return a list of supported codecs."""
return [stt.AudioCodecs.PCM, stt.AudioCodecs.OPUS]
@property
@override
def supported_bit_rates(self) -> list[stt.AudioBitRates]:
"""Return a list of supported bit rates."""
return [
stt.AudioBitRates.BITRATE_8,
stt.AudioBitRates.BITRATE_16,
stt.AudioBitRates.BITRATE_24,
stt.AudioBitRates.BITRATE_32,
]
@property
@override
def supported_sample_rates(self) -> list[stt.AudioSampleRates]:
"""Return a list of supported sample rates."""
return [
stt.AudioSampleRates.SAMPLERATE_8000,
stt.AudioSampleRates.SAMPLERATE_11000,
stt.AudioSampleRates.SAMPLERATE_16000,
stt.AudioSampleRates.SAMPLERATE_18900,
stt.AudioSampleRates.SAMPLERATE_22000,
stt.AudioSampleRates.SAMPLERATE_32000,
stt.AudioSampleRates.SAMPLERATE_37800,
stt.AudioSampleRates.SAMPLERATE_44100,
stt.AudioSampleRates.SAMPLERATE_48000,
]
@property
@override
def supported_channels(self) -> list[stt.AudioChannels]:
"""Return a list of supported channels."""
return [stt.AudioChannels.CHANNEL_MONO, stt.AudioChannels.CHANNEL_STEREO]
@override
async def async_process_audio_stream(
self, metadata: stt.SpeechMetadata, stream: AsyncIterable[bytes]
) -> stt.SpeechResult:
"""Process an audio stream to STT service."""
audio_bytes = bytearray()
async for chunk in stream:
audio_bytes.extend(chunk)
audio_data = bytes(audio_bytes)
if metadata.format == stt.AudioFormats.WAV:
# Add missing wav header
wav_buffer = io.BytesIO()
with wave.open(wav_buffer, "wb") as wf:
wf.setnchannels(metadata.channel.value)
wf.setsampwidth(metadata.bit_rate.value // 8)
wf.setframerate(metadata.sample_rate.value)
wf.writeframes(audio_data)
audio_data = wav_buffer.getvalue()
options = self.subentry.data
client = self.entry.runtime_data
try:
response = await client.audio.transcriptions.create(
model=options.get(CONF_CHAT_MODEL, RECOMMENDED_STT_MODEL),
file=(f"a.{metadata.format.value}", audio_data),
response_format="json",
language=metadata.language.split("-")[0],
prompt=options.get(CONF_PROMPT, DEFAULT_STT_PROMPT),
)
except OpenAIError:
_LOGGER.exception("Error during STT")
else:
if response.text:
return stt.SpeechResult(
response.text,
stt.SpeechResultState.SUCCESS,
)
return stt.SpeechResult(None, stt.SpeechResultState.ERROR)