mirror of
https://github.com/howelljiang/baidu-voice.git
synced 2026-07-21 22:53:57 +08:00
init
This commit is contained in:
+145
@@ -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
|
||||||
+178
@@ -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,
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
Binary file not shown.
@@ -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())
|
||||||
Binary file not shown.
@@ -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
|
||||||
Binary file not shown.
+134
@@ -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,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user