按照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)
## 注意事项
- 本集成需要互联网连接
- 使用百度语音服务可能会产生费用,请参考百度云官方计费标准
+12 -108
View File
@@ -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
if unload_ok:
hass.data.pop(DOMAIN)
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",
"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,
+38 -91
View File
@@ -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)
return SpeechResult("", SpeechResultState.ERROR)
+41 -39
View File
@@ -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
-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,
)