按照ha推荐的插件初始化方式进行重构

This commit is contained in:
Howell Jiang
2025-04-13 19:34:14 +08:00
parent 6e080408b6
commit cd430ff498
9 changed files with 117 additions and 377 deletions
+24 -3
View File
@@ -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)
## 注意事项
- 本集成需要互联网连接
- 使用百度语音服务可能会产生费用,请参考百度云官方计费标准
+11 -107
View File
@@ -1,11 +1,10 @@
"""百度语音集成.""" """百度语音集成."""
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from aip import AipSpeech from aip import AipSpeech
from .const import ( from .const import (
@@ -13,23 +12,14 @@ from .const import (
CONF_APP_ID, CONF_APP_ID,
CONF_API_KEY, CONF_API_KEY,
CONF_SECRET_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__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.deprecated(DOMAIN) CONFIG_SCHEMA = cv.deprecated(DOMAIN)
PLATFORMS = ["tts", "stt"]
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
"""设置百度语音集成.""" """设置百度语音集成."""
return True 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""设置百度语音配置项.""" """设置百度语音配置项."""
# 存储配置数据 # 存储配置数据
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN] = { hass.data[DOMAIN] = {
CONF_APP_ID: entry.data[CONF_APP_ID], CONF_APP_ID: entry.data[CONF_APP_ID],
CONF_API_KEY: entry.data[CONF_API_KEY], CONF_API_KEY: entry.data[CONF_API_KEY],
CONF_SECRET_KEY: entry.data[CONF_SECRET_KEY], CONF_SECRET_KEY: entry.data[CONF_SECRET_KEY],
} }
# 创建语音识别客户端 # 加载平台
client = AipSpeech( await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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 return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""卸载百度语音配置项.""" """卸载百度语音配置项."""
hass.services.async_remove(DOMAIN, SERVICE_TTS) # 卸载平台
hass.services.async_remove(DOMAIN, SERVICE_STT) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data.pop(DOMAIN) hass.data.pop(DOMAIN)
return True return unload_ok
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"domain": "baidu_voice", "domain": "baidu_voice",
"name": "Baidu Voice", "name": "Baidu Voice",
"documentation": "https://github.com/howelljiang/baidu_voice", "documentation": "https://github.com/howelljiang/baidu-voice",
"dependencies": [], "dependencies": [],
"codeowners": ["@howelljiang"], "codeowners": ["@howelljiang"],
"requirements": ["baidu-aip>=4.16.10"], "requirements": ["baidu-aip>=4.16.10", "chardet>=5.2.0"],
"version": "0.1.0", "version": "0.1.0",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"config_flow": true, "config_flow": true,
+37 -90
View File
@@ -1,6 +1,6 @@
"""Baidu Speech-to-Text integration.""" """Baidu Speech-to-Text integration."""
import logging import logging
from typing import Any, Optional from typing import Any
from aip import AipSpeech from aip import AipSpeech
from homeassistant.components.stt import ( from homeassistant.components.stt import (
@@ -13,8 +13,6 @@ from homeassistant.components.stt import (
SpeechMetadata, SpeechMetadata,
SpeechResult, SpeechResult,
SpeechResultState, SpeechResultState,
Provider,
async_get_provider,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -24,8 +22,31 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class BaiduSTTBase: async def async_setup_entry(
"""Base class for Baidu STT functionality.""" 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 @property
def supported_languages(self) -> list[str]: def supported_languages(self) -> list[str]:
@@ -57,18 +78,26 @@ class BaiduSTTBase:
"""Return a list of supported channels.""" """Return a list of supported channels."""
return [AudioChannels.CHANNEL_MONO] return [AudioChannels.CHANNEL_MONO]
async def _process_audio(self, audio_data: bytes, client: AipSpeech) -> SpeechResult: async def async_process_audio_stream(
"""Process audio data using Baidu STT API.""" self, metadata: SpeechMetadata, stream: Any
) -> SpeechResult:
"""Process an audio stream to STT service."""
try: try:
# 读取音频数据
audio_data = b""
async for chunk in stream:
audio_data += chunk
if not audio_data: if not audio_data:
_LOGGER.warning("No audio data received") _LOGGER.warning("No audio data received")
return SpeechResult("", SpeechResultState.ERROR) return SpeechResult("", SpeechResultState.ERROR)
_LOGGER.debug("Processing audio stream with metadata: %s", metadata)
_LOGGER.debug("Total audio data size: %d bytes", len(audio_data)) _LOGGER.debug("Total audio data size: %d bytes", len(audio_data))
# 使用百度语音识别API # 使用百度语音识别API
result = await self.hass.async_add_executor_job( result = await self.hass.async_add_executor_job(
client.asr, self._client.asr,
audio_data, audio_data,
'pcm', 'pcm',
16000, 16000,
@@ -89,85 +118,3 @@ class BaiduSTTBase:
except Exception as ex: except Exception as ex:
_LOGGER.error("Error during Baidu STT: %s", ex, exc_info=True) _LOGGER.error("Error during Baidu STT: %s", ex, exc_info=True)
return SpeechResult("", SpeechResultState.ERROR) 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)
+40 -38
View File
@@ -1,18 +1,18 @@
"""Support for Baidu TTS.""" """Support for Baidu TTS."""
import logging import logging
import asyncio from typing import Any
from aip import AipSpeech
import voluptuous as vol
from homeassistant.components.tts import Provider, PLATFORM_SCHEMA from aip import AipSpeech
from homeassistant.const import CONF_API_KEY from homeassistant.components.tts import (
import homeassistant.helpers.config_validation as cv TextToSpeechEntity,
PLATFORM_SCHEMA,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DOMAIN, DOMAIN,
CONF_APP_ID,
CONF_API_KEY,
CONF_SECRET_KEY,
DEFAULT_SPEED, DEFAULT_SPEED,
DEFAULT_PITCH, DEFAULT_PITCH,
DEFAULT_VOLUME, DEFAULT_VOLUME,
@@ -24,41 +24,43 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_LANGUAGES = ["zh", "en"] SUPPORT_LANGUAGES = ["zh", "en"]
DEFAULT_LANG = "zh" DEFAULT_LANG = "zh"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_entry(
{ hass: HomeAssistant,
vol.Required(CONF_APP_ID): cv.string, config_entry: ConfigEntry,
vol.Required(CONF_API_KEY): cv.string, async_add_entities: AddEntitiesCallback,
vol.Required(CONF_SECRET_KEY): cv.string, ) -> 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): class BaiduTTSEntity(TextToSpeechEntity):
"""Set up Baidu TTS component.""" """Baidu TTS Entity."""
return BaiduTTSProvider(hass)
class BaiduTTSProvider(Provider): def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Baidu TTS api provider.""" """Initialize Baidu TTS entity."""
def __init__(self, hass):
"""Initialize Baidu TTS provider."""
self.hass = hass self.hass = hass
self._app_id = hass.data[DOMAIN][CONF_APP_ID] self._config_entry = config_entry
self._api_key = hass.data[DOMAIN][CONF_API_KEY] self._attr_name = "Baidu TTS"
self._secret_key = hass.data[DOMAIN][CONF_SECRET_KEY] self._attr_unique_id = f"{DOMAIN}_tts"
self._client = AipSpeech(self._app_id, self._api_key, self._secret_key)
self.name = "Baidu TTS" # 初始化百度语音合成客户端
self._client = AipSpeech(
config_entry.data["app_id"],
config_entry.data["api_key"],
config_entry.data["secret_key"]
)
@property @property
def default_language(self): def default_language(self) -> str:
"""Return the default language.""" """Return the default language."""
return DEFAULT_LANG return DEFAULT_LANG
@property @property
def supported_languages(self): def supported_languages(self) -> list[str]:
"""Return list of supported languages.""" """Return list of supported languages."""
return SUPPORT_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.""" """Load TTS from Baidu."""
options = options or {} options = options or {}
speed = options.get("speed", DEFAULT_SPEED) speed = options.get("speed", DEFAULT_SPEED)
@@ -66,8 +68,9 @@ class BaiduTTSProvider(Provider):
volume = options.get("volume", DEFAULT_VOLUME) volume = options.get("volume", DEFAULT_VOLUME)
person = options.get("person", DEFAULT_PERSON) person = options.get("person", DEFAULT_PERSON)
def _get_tts(): try:
result = self._client.synthesis( result = await self.hass.async_add_executor_job(
self._client.synthesis,
message, message,
language, language,
1, # 1: mp3 1, # 1: mp3
@@ -76,16 +79,15 @@ class BaiduTTSProvider(Provider):
"pit": pitch, "pit": pitch,
"vol": volume, "vol": volume,
"per": person, "per": person,
}, }
) )
if isinstance(result, dict): if isinstance(result, dict):
_LOGGER.error("Baidu TTS error: %s", result) _LOGGER.error("Baidu TTS error: %s", result)
return None, None return None, None
return "mp3", result 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: except Exception as ex:
_LOGGER.error("Error during Baidu TTS: %s", ex) _LOGGER.error("Error during Baidu TTS: %s", ex)
return None, None return None, None
-134
View File
@@ -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,
)