diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..19d3ee3 --- /dev/null +++ b/__init__.py @@ -0,0 +1,145 @@ +"""百度语音集成.""" +import logging +from typing import Any, Dict, Optional + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from aip import AipSpeech + +from .const import ( + DOMAIN, + CONF_APP_ID, + CONF_API_KEY, + CONF_SECRET_KEY, + SERVICE_TTS, + SERVICE_STT, + ATTR_MESSAGE, + ATTR_LANGUAGE, + ATTR_VOLUME, + ATTR_SPEED, + ATTR_PITCH, + ATTR_PERSON, + ATTR_AUDIO, + ATTR_FORMAT, + ATTR_SAMPLE_RATE, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.deprecated(DOMAIN) + +async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: + """设置百度语音集成.""" + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """设置百度语音配置项.""" + # 存储配置数据 + hass.data[DOMAIN] = { + CONF_APP_ID: entry.data[CONF_APP_ID], + CONF_API_KEY: entry.data[CONF_API_KEY], + CONF_SECRET_KEY: entry.data[CONF_SECRET_KEY], + } + + # 创建语音识别客户端 + client = AipSpeech( + entry.data[CONF_APP_ID], + entry.data[CONF_API_KEY], + entry.data[CONF_SECRET_KEY], + ) + + # 加载TTS平台 + await async_load_platform(hass, "tts", DOMAIN, {}, entry.data) + + # 加载STT平台 + await async_load_platform(hass, "stt", DOMAIN, {}, entry.data) + + # 注册服务 + async def async_tts_service(service): + """处理TTS服务调用.""" + message = service.data.get(ATTR_MESSAGE) + language = service.data.get(ATTR_LANGUAGE, "zh") + volume = service.data.get(ATTR_VOLUME, 5) + speed = service.data.get(ATTR_SPEED, 5) + pitch = service.data.get(ATTR_PITCH, 5) + person = service.data.get(ATTR_PERSON, 0) + + try: + result = await hass.async_add_executor_job( + client.synthesis, + message, + language, + 1, # 1表示mp3格式 + { + "vol": volume, + "per": person, + "spd": speed, + "pit": pitch, + } + ) + + if isinstance(result, dict): + _LOGGER.error("TTS失败: %s", result.get('err_msg')) + return + + # 保存音频文件 + with open('tts_output.mp3', 'wb') as f: + f.write(result) + + _LOGGER.info("TTS成功") + + except Exception as ex: + _LOGGER.error("TTS异常: %s", ex) + + async def async_stt_service(service): + """处理STT服务调用.""" + audio = service.data.get(ATTR_AUDIO) + format = service.data.get(ATTR_FORMAT, "pcm") + sample_rate = service.data.get(ATTR_SAMPLE_RATE, 16000) + + try: + result = await hass.async_add_executor_job( + client.asr, + audio, + format, + sample_rate, + { + "dev_pid": 1537, # 普通话(支持简单的英文识别) + } + ) + + if result.get("err_no") != 0: + _LOGGER.error("STT失败: %s", result.get('err_msg')) + return + + # 返回识别结果 + recognized_text = result.get("result", [""])[0] + _LOGGER.info("STT识别结果: %s", recognized_text) + + except Exception as ex: + _LOGGER.error("STT异常: %s", ex) + + # 注册服务 + hass.services.async_register( + DOMAIN, + SERVICE_TTS, + async_tts_service, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_STT, + async_stt_service, + ) + + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """卸载百度语音配置项.""" + hass.services.async_remove(DOMAIN, SERVICE_TTS) + hass.services.async_remove(DOMAIN, SERVICE_STT) + hass.data.pop(DOMAIN) + + return True \ No newline at end of file diff --git a/config_flow.py b/config_flow.py new file mode 100644 index 0000000..0f1a3db --- /dev/null +++ b/config_flow.py @@ -0,0 +1,178 @@ +"""百度语音配置流程.""" +import logging +import os +import asyncio +import voluptuous as vol +from typing import Any, Dict, Optional + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from aip import AipSpeech + +from .const import ( + DOMAIN, + CONF_APP_ID, + CONF_API_KEY, + CONF_SECRET_KEY, +) + +_LOGGER = logging.getLogger(__name__) + +# 测试音频文件路径 +TEST_AUDIO_PATH = os.path.join(os.path.dirname(__file__), "text2audio.pcm") + +async def async_read_file(file_path: str) -> bytes: + """异步读取文件内容.""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, lambda: open(file_path, "rb").read()) + +async def async_validate_api( + app_id: str, + api_key: str, + secret_key: str, +) -> bool: + """验证百度API配置.""" + try: + # 创建语音识别客户端 + client = AipSpeech(app_id, api_key, secret_key) + + # 测试TTS + result = await asyncio.get_event_loop().run_in_executor( + None, + client.synthesis, + "测试语音合成", + "zh", + 1, # 1表示mp3格式 + { + "vol": 5, # 音量,取值0-15,默认为5中音量 + "per": 0, # 发音人选择,0为女声,1为男声,3为情感合成-度逍遥,4为情感合成-度丫丫 + "spd": 5, # 语速,取值0-9,默认为5中语速 + "pit": 5, # 音调,取值0-9,默认为5中语调 + } + ) + + if isinstance(result, dict): + _LOGGER.error("TTS测试失败: %s", result.get('err_msg')) + return False + + # 测试STT + try: + audio_data = await async_read_file(TEST_AUDIO_PATH) + except Exception as ex: + _LOGGER.error("无法读取测试音频文件: %s", ex) + return False + + result = await asyncio.get_event_loop().run_in_executor( + None, + client.asr, + audio_data, + "pcm", # PCM格式 + 16000, # 采样率 + { + "dev_pid": 1537, # 普通话(支持简单的英文识别) + } + ) + + if result.get("err_no") != 0: + _LOGGER.error("STT测试失败: %s", result.get('err_msg')) + return False + + return True + + except Exception as ex: + _LOGGER.error("API验证异常: %s", ex) + return False + +class BaiduVoiceConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """处理百度语音配置流程.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def async_step_user( + self, user_input: Optional[Dict[str, Any]] = None + ) -> FlowResult: + """处理用户初始步骤.""" + errors: Dict[str, str] = {} + + if user_input is not None: + # 验证API配置 + if await async_validate_api( + user_input[CONF_APP_ID], + user_input[CONF_API_KEY], + user_input[CONF_SECRET_KEY], + ): + return self.async_create_entry( + title="百度语音", + data=user_input, + ) + errors["base"] = "invalid_auth" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_APP_ID): str, + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_SECRET_KEY): str, + } + ), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """创建选项流.""" + return BaiduVoiceOptionsFlow(config_entry) + +class BaiduVoiceOptionsFlow(config_entries.OptionsFlow): + """处理百度语音选项.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """初始化选项流.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: Optional[Dict[str, Any]] = None + ) -> FlowResult: + """管理百度语音选项.""" + errors: Dict[str, str] = {} + + if user_input is not None: + # 验证API配置 + if await async_validate_api( + user_input[CONF_APP_ID], + user_input[CONF_API_KEY], + user_input[CONF_SECRET_KEY], + ): + return self.async_create_entry( + title="", + data=user_input, + ) + errors["base"] = "invalid_auth" + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_APP_ID, + default=self._config_entry.data.get(CONF_APP_ID), + ): str, + vol.Required( + CONF_API_KEY, + default=self._config_entry.data.get(CONF_API_KEY), + ): str, + vol.Required( + CONF_SECRET_KEY, + default=self._config_entry.data.get(CONF_SECRET_KEY), + ): str, + } + ), + errors=errors, + ) \ No newline at end of file diff --git a/const.py b/const.py new file mode 100644 index 0000000..ab9b558 --- /dev/null +++ b/const.py @@ -0,0 +1,45 @@ +"""百度语音常量.""" +from typing import Final + +DOMAIN: Final = "baidu_voice" + +# 配置项 +CONF_APP_ID: Final = "app_id" +CONF_API_KEY: Final = "api_key" +CONF_SECRET_KEY: Final = "secret_key" + +# 服务 +SERVICE_TTS: Final = "tts" +SERVICE_STT: Final = "stt" + +# 服务属性 +ATTR_MESSAGE: Final = "message" +ATTR_LANGUAGE: Final = "language" +ATTR_VOLUME: Final = "volume" +ATTR_SPEED: Final = "speed" +ATTR_PITCH: Final = "pitch" +ATTR_PERSON: Final = "person" +ATTR_AUDIO: Final = "audio" +ATTR_FORMAT: Final = "format" +ATTR_SAMPLE_RATE: Final = "sample_rate" + +# 语音参数 +DEFAULT_SPEED: Final = 5 +DEFAULT_PITCH: Final = 5 +DEFAULT_VOLUME: Final = 5 +DEFAULT_PERSON: Final = 0 # 0: 女声, 1: 男声, 3: 情感男声, 4: 情感女声 + +# 音频格式 +SUPPORTED_AUDIO_FORMATS: Final = ["mp3", "wav", "pcm"] +SUPPORTED_SAMPLE_RATES: Final = [8000, 16000] +SUPPORTED_CHANNELS: Final = [1] # 单声道 +SUPPORTED_CODECS: Final = ["pcm", "mp3", "wav"] + +# 语言支持 +SUPPORTED_LANGUAGES: Final = ["zh"] # 目前只支持中文 + +# 错误码 +ERROR_INVALID_AUTH: Final = "invalid_auth" +ERROR_UNKNOWN: Final = "unknown" +ERROR_NOT_SUPPORTED: Final = "not_supported" +ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable" \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..065a84b --- /dev/null +++ b/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "baidu_voice", + "name": "Baidu Voice", + "documentation": "https://github.com/howelljiang/baidu_voice", + "dependencies": [], + "codeowners": ["@howelljiang"], + "requirements": ["baidu-aip>=4.16.10"], + "version": "0.1.0", + "iot_class": "cloud_push", + "config_flow": true, + "platforms": ["tts", "stt"] +} \ No newline at end of file diff --git a/stt.py b/stt.py new file mode 100644 index 0000000..ea072ea --- /dev/null +++ b/stt.py @@ -0,0 +1,173 @@ +"""Baidu Speech-to-Text integration.""" +import logging +from typing import Any, Optional + +from aip import AipSpeech +from homeassistant.components.stt import ( + AudioBitRates, + AudioChannels, + AudioCodecs, + AudioFormats, + AudioSampleRates, + SpeechToTextEntity, + SpeechMetadata, + SpeechResult, + SpeechResultState, + Provider, + async_get_provider, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +class BaiduSTTBase: + """Base class for Baidu STT functionality.""" + + @property + def supported_languages(self) -> list[str]: + """Return a list of supported languages.""" + return ["zh-CN"] + + @property + def supported_formats(self) -> list[AudioFormats]: + """Return a list of supported formats.""" + return [AudioFormats.WAV, AudioFormats.OGG] + + @property + def supported_codecs(self) -> list[AudioCodecs]: + """Return a list of supported codecs.""" + return [AudioCodecs.PCM, AudioCodecs.OPUS] + + @property + def supported_bit_rates(self) -> list[AudioBitRates]: + """Return a list of supported bit rates.""" + return [AudioBitRates.BITRATE_16] + + @property + def supported_sample_rates(self) -> list[AudioSampleRates]: + """Return a list of supported sample rates.""" + return [AudioSampleRates.SAMPLERATE_16000] + + @property + def supported_channels(self) -> list[AudioChannels]: + """Return a list of supported channels.""" + return [AudioChannels.CHANNEL_MONO] + + async def _process_audio(self, audio_data: bytes, client: AipSpeech) -> SpeechResult: + """Process audio data using Baidu STT API.""" + try: + if not audio_data: + _LOGGER.warning("No audio data received") + return SpeechResult("", SpeechResultState.ERROR) + + _LOGGER.debug("Total audio data size: %d bytes", len(audio_data)) + + # 使用百度语音识别API + result = await self.hass.async_add_executor_job( + client.asr, + audio_data, + 'pcm', + 16000, + { + 'dev_pid': 1537, # 普通话(支持简单的英文识别) + } + ) + + _LOGGER.debug("Baidu STT response: %s", result) + + if result.get('err_no') == 0: + text = result.get('result', [""])[0] + return SpeechResult(text, SpeechResultState.SUCCESS) + else: + _LOGGER.error("Baidu STT error: %s", result.get('err_msg')) + return SpeechResult("", SpeechResultState.ERROR) + + except Exception as ex: + _LOGGER.error("Error during Baidu STT: %s", ex, exc_info=True) + return SpeechResult("", SpeechResultState.ERROR) + +async def async_get_engine( + hass: HomeAssistant, + config: dict, + discovery_info: Optional[dict] = None +) -> Provider: + """Set up Baidu STT component.""" + _LOGGER.debug("Setting up Baidu STT engine") + return BaiduSTTProvider(hass) + +class BaiduSTTProvider(BaiduSTTBase, Provider): + """Baidu STT api provider.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize Baidu STT provider.""" + self.hass = hass + self._app_id = hass.data[DOMAIN]["app_id"] + self._api_key = hass.data[DOMAIN]["api_key"] + self._secret_key = hass.data[DOMAIN]["secret_key"] + self._client = AipSpeech(self._app_id, self._api_key, self._secret_key) + self.name = "Baidu STT" + self._language = "zh-CN" + _LOGGER.debug("Initialized Baidu STT provider with language: %s", self._language) + + @property + def default_language(self) -> str: + """Return the default language.""" + return self._language + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: Any + ) -> SpeechResult: + """Process an audio stream to STT service.""" + _LOGGER.debug("Processing audio stream with metadata: %s", metadata) + + # 读取音频数据 + audio_data = b"" + async for chunk in stream: + audio_data += chunk + + return await self._process_audio(audio_data, self._client) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Baidu STT from a config entry.""" + provider = await async_get_provider(hass, DOMAIN) + if provider: + async_add_entities([BaiduSTTEntity(provider, config_entry)]) + +class BaiduSTTEntity(BaiduSTTBase, SpeechToTextEntity): + """Baidu Speech-to-Text entity.""" + + def __init__(self, provider: Provider, config_entry: ConfigEntry) -> None: + """Initialize Baidu STT entity.""" + self._provider = provider + self._config_entry = config_entry + self._attr_name = "Baidu Speech-to-Text" + self._attr_unique_id = f"{DOMAIN}_stt" + self.hass = provider.hass + + # 初始化百度语音识别客户端 + self._client = AipSpeech( + config_entry.data["app_id"], + config_entry.data["api_key"], + config_entry.data["secret_key"] + ) + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: Any + ) -> SpeechResult: + """Process an audio stream to STT service.""" + _LOGGER.debug("Processing audio stream with metadata: %s", metadata) + + # 读取音频数据 + audio_data = b"" + async for chunk in stream: + audio_data += chunk + + return await self._process_audio(audio_data, self._client) \ No newline at end of file diff --git a/test2audio.pcm b/test2audio.pcm new file mode 100644 index 0000000..15e6e7b Binary files /dev/null and b/test2audio.pcm differ diff --git a/test_baidu_api.py b/test_baidu_api.py new file mode 100644 index 0000000..06992e2 --- /dev/null +++ b/test_baidu_api.py @@ -0,0 +1,111 @@ +"""测试百度语音API.""" +import asyncio +import logging +from aip import AipSpeech + +# 配置日志 +logging.basicConfig(level=logging.DEBUG) +_LOGGER = logging.getLogger(__name__) + +# 百度API配置 +APP_ID = "118461597" +API_KEY = "BZk7p3B0DurJuRLrN1cx9iHF" +SECRET_KEY = "sn0i6jtJNPcGI2V13P3MKiGCQYoz9xzf" + +# 测试音频文件路径 +TEST_AUDIO_PATH = "text2audio.pcm" # 修正文件名 + +async def test_tts(): + """测试语音合成.""" + try: + client = AipSpeech(APP_ID, API_KEY, SECRET_KEY) + + # 测试TTS + result = await asyncio.get_event_loop().run_in_executor( + None, + client.synthesis, + "测试语音合成", + "zh", + 1, # 1表示mp3格式 + { + "vol": 5, # 音量,取值0-15,默认为5中音量 + "per": 0, # 发音人选择,0为女声,1为男声,3为情感合成-度逍遥,4为情感合成-度丫丫 + "spd": 5, # 语速,取值0-9,默认为5中语速 + "pit": 5, # 音调,取值0-9,默认为5中语调 + } + ) + + if isinstance(result, dict): + _LOGGER.error("TTS测试失败: %s", result.get('err_msg')) + return False + + # 保存音频文件 + with open('tts_test.mp3', 'wb') as f: + f.write(result) + + _LOGGER.info("TTS测试成功") + return True + + except Exception as ex: + _LOGGER.error("TTS测试异常: %s", ex) + return False + +async def test_stt(): + """测试语音识别.""" + try: + client = AipSpeech(APP_ID, API_KEY, SECRET_KEY) + + # 读取测试音频 + with open(TEST_AUDIO_PATH, 'rb') as f: + audio_data = f.read() + + # 测试STT + result = await asyncio.get_event_loop().run_in_executor( + None, + client.asr, + audio_data, + "pcm", # PCM格式 + 16000, # 采样率 + { + "dev_pid": 1537, # 普通话(支持简单的英文识别) + } + ) + + if result.get("err_no") != 0: + _LOGGER.error("STT测试失败: %s", result.get('err_msg')) + return False + + # 验证识别结果 + recognized_text = result.get("result", [""])[0] + _LOGGER.info("STT识别结果: %s", recognized_text) + + if not recognized_text or "测试" not in recognized_text: + _LOGGER.error("STT识别结果不准确: %s", recognized_text) + return False + + _LOGGER.info("STT测试成功") + return True + + except Exception as ex: + _LOGGER.error("STT测试异常: %s", ex) + return False + +async def main(): + """主测试函数.""" + _LOGGER.info("开始测试百度语音API...") + + # 测试TTS + tts_result = await test_tts() + _LOGGER.info("TTS测试结果: %s", "成功" if tts_result else "失败") + + # 等待1秒 + await asyncio.sleep(1) + + # 测试STT + stt_result = await test_stt() + _LOGGER.info("STT测试结果: %s", "成功" if stt_result else "失败") + + _LOGGER.info("测试完成") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/text2audio.pcm b/text2audio.pcm new file mode 100644 index 0000000..15e6e7b Binary files /dev/null and b/text2audio.pcm differ diff --git a/tts.py b/tts.py new file mode 100644 index 0000000..342428e --- /dev/null +++ b/tts.py @@ -0,0 +1,91 @@ +"""Support for Baidu TTS.""" +import logging +import asyncio +from aip import AipSpeech +import voluptuous as vol + +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +from .const import ( + DOMAIN, + CONF_APP_ID, + CONF_API_KEY, + CONF_SECRET_KEY, + DEFAULT_SPEED, + DEFAULT_PITCH, + DEFAULT_VOLUME, + DEFAULT_PERSON, +) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_LANGUAGES = ["zh", "en"] +DEFAULT_LANG = "zh" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + } +) + +async def async_get_engine(hass, config, discovery_info=None): + """Set up Baidu TTS component.""" + return BaiduTTSProvider(hass) + +class BaiduTTSProvider(Provider): + """Baidu TTS api provider.""" + + def __init__(self, hass): + """Initialize Baidu TTS provider.""" + self.hass = hass + self._app_id = hass.data[DOMAIN][CONF_APP_ID] + self._api_key = hass.data[DOMAIN][CONF_API_KEY] + self._secret_key = hass.data[DOMAIN][CONF_SECRET_KEY] + self._client = AipSpeech(self._app_id, self._api_key, self._secret_key) + self.name = "Baidu TTS" + + @property + def default_language(self): + """Return the default language.""" + return DEFAULT_LANG + + @property + def supported_languages(self): + """Return list of supported languages.""" + return SUPPORT_LANGUAGES + + async def async_get_tts_audio(self, message, language, options=None): + """Load TTS from Baidu.""" + options = options or {} + speed = options.get("speed", DEFAULT_SPEED) + pitch = options.get("pitch", DEFAULT_PITCH) + volume = options.get("volume", DEFAULT_VOLUME) + person = options.get("person", DEFAULT_PERSON) + + def _get_tts(): + result = self._client.synthesis( + message, + language, + 1, # 1: mp3 + { + "spd": speed, + "pit": pitch, + "vol": volume, + "per": person, + }, + ) + if isinstance(result, dict): + _LOGGER.error("Baidu TTS error: %s", result) + return None, None + return "mp3", result + + try: + audio_type, audio_data = await self.hass.async_add_executor_job(_get_tts) + return audio_type, audio_data + except Exception as ex: + _LOGGER.error("Error during Baidu TTS: %s", ex) + return None, None \ No newline at end of file diff --git a/tts_test.mp3 b/tts_test.mp3 new file mode 100644 index 0000000..fb66419 Binary files /dev/null and b/tts_test.mp3 differ diff --git a/wake_word.py b/wake_word.py new file mode 100644 index 0000000..abbc9ab --- /dev/null +++ b/wake_word.py @@ -0,0 +1,134 @@ +"""Support for Baidu Wake Word Detection.""" +import logging +import asyncio +from aip import AipSpeech +import voluptuous as vol + +from homeassistant.components.wake_word import ( + WakeWordDetectionResult, + WakeWordDetectionResultState, + WakeWordProvider, +) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +from .const import ( + DOMAIN, + CONF_APP_ID, + CONF_API_KEY, + CONF_SECRET_KEY, + CONF_WAKE_WORD, + DEFAULT_WAKE_WORD, +) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_LANGUAGES = ["zh", "en"] +DEFAULT_LANG = "zh" + +async def async_get_engine(hass, config, discovery_info=None): + """Set up Baidu Wake Word component.""" + _LOGGER.debug("Setting up Baidu Wake Word engine") + return BaiduWakeWordProvider(hass) + +class BaiduWakeWordProvider(WakeWordProvider): + """Baidu Wake Word api provider.""" + + def __init__(self, hass): + """Initialize Baidu Wake Word provider.""" + self.hass = hass + self._app_id = hass.data[DOMAIN][CONF_APP_ID] + self._api_key = hass.data[DOMAIN][CONF_API_KEY] + self._secret_key = hass.data[DOMAIN][CONF_SECRET_KEY] + self._wake_word = hass.data[DOMAIN][CONF_WAKE_WORD] + self._client = AipSpeech(self._app_id, self._api_key, self._secret_key) + self.name = "Baidu Wake Word" + self._language = hass.config.language or DEFAULT_LANG + _LOGGER.debug("Initialized Baidu Wake Word provider with language: %s", self._language) + + @property + def supported_languages(self): + """Return list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_formats(self): + """Return list of supported formats.""" + return ["wav", "pcm"] + + @property + def supported_sample_rates(self): + """Return list of supported sample rates.""" + return [16000] + + @property + def supported_channels(self): + """Return list of supported channels.""" + return [1] + + async def async_process_audio_stream(self, stream, options=None): + """Process an audio stream to wake word service.""" + _LOGGER.debug("Processing audio stream for wake word detection") + options = options or {} + language = options.get("language", self._language) + _LOGGER.debug("Using language: %s", language) + + try: + # 读取音频流 + audio_data = b"" + async for chunk in stream: + if chunk: + audio_data += chunk + _LOGGER.debug("Received audio chunk of size: %d", len(chunk)) + + if not audio_data: + _LOGGER.warning("No audio data received") + return WakeWordDetectionResult( + state=WakeWordDetectionResultState.ERROR, + wake_word=None, + ) + + _LOGGER.debug("Total audio data size: %d bytes", len(audio_data)) + + def _process_audio(): + try: + # 使用百度语音识别API进行唤醒词检测 + result = self._client.asr(audio_data, 'pcm', 16000, { + 'dev_pid': 1536 if language == 'zh' else 1737, # 1536: 普通话, 1737: 英语 + }) + + _LOGGER.debug("ASR result: %s", result) + + if result.get('err_no') == 0: + text = result.get('result')[0] + _LOGGER.debug("Recognized text: %s", text) + # 检查是否包含唤醒词 + if self._wake_word in text: + _LOGGER.debug("Wake word detected: %s", self._wake_word) + return self._wake_word + _LOGGER.debug("No wake word detected") + return None + _LOGGER.warning("ASR error: %s", result) + return None + except Exception as ex: + _LOGGER.error("Error during Baidu Wake Word processing: %s", ex, exc_info=True) + return None + + wake_word = await self.hass.async_add_executor_job(_process_audio) + if wake_word is None: + _LOGGER.debug("No wake word detected") + return WakeWordDetectionResult( + state=WakeWordDetectionResultState.ERROR, + wake_word=None, + ) + _LOGGER.debug("Successfully detected wake word: %s", wake_word) + return WakeWordDetectionResult( + state=WakeWordDetectionResultState.SUCCESS, + wake_word=wake_word, + ) + except Exception as ex: + _LOGGER.error("Error during Baidu Wake Word: %s", ex, exc_info=True) + return WakeWordDetectionResult( + state=WakeWordDetectionResultState.ERROR, + wake_word=None, + ) \ No newline at end of file