feat: vendor openai_conversation from core 2026.9.2, rename domain
Verbatim copy from home-assistant/core at tag 2026.9.2, with only the domain constant and manifest changed. Apache-2.0, attribution in NOTICE. Also fixed self-references the domain rename left dangling: the config_entry selector's `integration:` field in services.yaml, the `[%key:component::openai_conversation::...%]` translation cross-references in strings.json, the deprecated-action descriptions naming the old domain, and config_flow.py's instructions_url (now pointing at this repo instead of the core docs page). Local pytest run skipped per owner decision (see prior commit); files validated with py_compile / json.load / yaml.safe_load instead. CI (added in the prior commit) is the real gate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01739Seac5eMAfNwFeiwV5r4
This commit is contained in:
1 parent
178d948b1b
commit
9d8c36f793
15 files changed
+3498
No files matched your search
@@ -0,0 +1,200 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user