diff --git a/README.md b/README.md index 992ccc8..bb14c2b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,26 @@ -需要在百度云里面创建应用。开通语音服务里面的 短语音识别和短文本在线合成两个服务 。 +# 百度语音服务集成 -在集成里面添加baidu_voice服务即可。 +这是一个 Home Assistant 集成,用于接入百度语音服务,提供文本转语音(TTS)和语音转文本(STT)功能。 -不会python,cursor帮忙写了一个初始版本,有空再来完善。 +## 配置要求 + +1. 需要在百度云创建应用 +2. 开通以下服务: + - 短语音识别服务 + - 短文本在线合成服务 + +## 配置步骤 + +1. 在百度云控制台创建应用并获取 API Key 和 Secret Key +2. 在 Home Assistant 中添加 baidu_voice 集成 +3. 输入您的 API Key 和 Secret Key + +## 配置截图 + +![百度tts语音服务配置](baidu1.png) +![百度stt语音服务配置](baidu2.png) + +## 注意事项 + +- 本集成需要互联网连接 +- 使用百度语音服务可能会产生费用,请参考百度云官方计费标准 diff --git a/__init__.py b/__init__.py index 19d3ee3..09f3dc6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,11 +1,10 @@ """百度语音集成.""" import logging -from typing import Any, Dict, Optional +from typing import Any, Dict 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 ( @@ -13,23 +12,14 @@ from .const import ( 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) +PLATFORMS = ["tts", "stt"] + async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: """设置百度语音集成.""" return True @@ -37,109 +27,23 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """设置百度语音配置项.""" # 存储配置数据 + hass.data.setdefault(DOMAIN, {}) 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, - ) - + # 加载平台 + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 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) + # 卸载平台 + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - return True \ No newline at end of file + if unload_ok: + hass.data.pop(DOMAIN) + + return unload_ok \ No newline at end of file diff --git a/baidu1.png b/baidu1.png new file mode 100644 index 0000000..1a2a353 Binary files /dev/null and b/baidu1.png differ diff --git a/baidu2.png b/baidu2.png new file mode 100644 index 0000000..5588e3f Binary files /dev/null and b/baidu2.png differ diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..794a77e Binary files /dev/null and b/icon.png differ diff --git a/manifest.json b/manifest.json index 065a84b..495e785 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "domain": "baidu_voice", "name": "Baidu Voice", - "documentation": "https://github.com/howelljiang/baidu_voice", + "documentation": "https://github.com/howelljiang/baidu-voice", "dependencies": [], "codeowners": ["@howelljiang"], - "requirements": ["baidu-aip>=4.16.10"], + "requirements": ["baidu-aip>=4.16.10", "chardet>=5.2.0"], "version": "0.1.0", "iot_class": "cloud_push", "config_flow": true, diff --git a/stt.py b/stt.py index ea072ea..3166f63 100644 --- a/stt.py +++ b/stt.py @@ -1,6 +1,6 @@ """Baidu Speech-to-Text integration.""" import logging -from typing import Any, Optional +from typing import Any from aip import AipSpeech from homeassistant.components.stt import ( @@ -13,8 +13,6 @@ from homeassistant.components.stt import ( SpeechMetadata, SpeechResult, SpeechResultState, - Provider, - async_get_provider, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -24,8 +22,31 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class BaiduSTTBase: - """Base class for Baidu STT functionality.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> bool: + """Set up Baidu STT from a config entry.""" + async_add_entities([BaiduSTTEntity(hass, config_entry)]) + return True + +class BaiduSTTEntity(SpeechToTextEntity): + """Baidu Speech-to-Text entity.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize Baidu STT entity.""" + self.hass = hass + self._config_entry = config_entry + self._attr_name = "Baidu STT" + self._attr_unique_id = f"{DOMAIN}_stt" + + # 初始化百度语音识别客户端 + self._client = AipSpeech( + config_entry.data["app_id"], + config_entry.data["api_key"], + config_entry.data["secret_key"] + ) @property def supported_languages(self) -> list[str]: @@ -57,18 +78,26 @@ class BaiduSTTBase: """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.""" + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: Any + ) -> SpeechResult: + """Process an audio stream to STT service.""" try: + # 读取音频数据 + audio_data = b"" + async for chunk in stream: + audio_data += chunk + if not audio_data: _LOGGER.warning("No audio data received") return SpeechResult("", SpeechResultState.ERROR) + _LOGGER.debug("Processing audio stream with metadata: %s", metadata) _LOGGER.debug("Total audio data size: %d bytes", len(audio_data)) # 使用百度语音识别API result = await self.hass.async_add_executor_job( - client.asr, + self._client.asr, audio_data, 'pcm', 16000, @@ -88,86 +117,4 @@ class BaiduSTTBase: 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 + return SpeechResult("", SpeechResultState.ERROR) \ No newline at end of file diff --git a/tts.py b/tts.py index 342428e..119e24a 100644 --- a/tts.py +++ b/tts.py @@ -1,18 +1,18 @@ """Support for Baidu TTS.""" import logging -import asyncio -from aip import AipSpeech -import voluptuous as vol +from typing import Any -from homeassistant.components.tts import Provider, PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv +from aip import AipSpeech +from homeassistant.components.tts import ( + TextToSpeechEntity, + PLATFORM_SCHEMA, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DOMAIN, - CONF_APP_ID, - CONF_API_KEY, - CONF_SECRET_KEY, DEFAULT_SPEED, DEFAULT_PITCH, DEFAULT_VOLUME, @@ -24,41 +24,43 @@ _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_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> bool: + """Set up Baidu TTS from a config entry.""" + async_add_entities([BaiduTTSEntity(hass, config_entry)]) + return True -async def async_get_engine(hass, config, discovery_info=None): - """Set up Baidu TTS component.""" - return BaiduTTSProvider(hass) +class BaiduTTSEntity(TextToSpeechEntity): + """Baidu TTS Entity.""" -class BaiduTTSProvider(Provider): - """Baidu TTS api provider.""" - - def __init__(self, hass): - """Initialize Baidu TTS provider.""" + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize Baidu TTS entity.""" 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" + self._config_entry = config_entry + self._attr_name = "Baidu TTS" + self._attr_unique_id = f"{DOMAIN}_tts" + + # 初始化百度语音合成客户端 + self._client = AipSpeech( + config_entry.data["app_id"], + config_entry.data["api_key"], + config_entry.data["secret_key"] + ) @property - def default_language(self): + def default_language(self) -> str: """Return the default language.""" return DEFAULT_LANG @property - def supported_languages(self): + def supported_languages(self) -> list[str]: """Return list of supported languages.""" return SUPPORT_LANGUAGES - async def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message: str, language: str, options: dict = None) -> tuple[str, bytes]: """Load TTS from Baidu.""" options = options or {} speed = options.get("speed", DEFAULT_SPEED) @@ -66,8 +68,9 @@ class BaiduTTSProvider(Provider): volume = options.get("volume", DEFAULT_VOLUME) person = options.get("person", DEFAULT_PERSON) - def _get_tts(): - result = self._client.synthesis( + try: + result = await self.hass.async_add_executor_job( + self._client.synthesis, message, language, 1, # 1: mp3 @@ -76,16 +79,15 @@ class BaiduTTSProvider(Provider): "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/wake_word.py b/wake_word.py deleted file mode 100644 index abbc9ab..0000000 --- a/wake_word.py +++ /dev/null @@ -1,134 +0,0 @@ -"""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