From 01e7f3c26400af4dc10ced81cf1e551ac8234a84 Mon Sep 17 00:00:00 2001 From: Ethan930717 <51702667+Ethan930717@users.noreply.github.com> Date: Fri, 1 Aug 2025 23:02:17 +0800 Subject: [PATCH] First upload --- .github/workflows/validate.yml | 20 ++ README.md | 250 +++++++++++++++++++ README_OPENSOURCE.md | 306 ++++++++++++++++++++++++ SETUP_GUIDE.md | 95 ++++++++ __init__.py | 48 ++++ __pycache__/__init__.cpython-313.pyc | Bin 0 -> 2511 bytes __pycache__/config_flow.cpython-313.pyc | Bin 0 -> 10299 bytes __pycache__/const.cpython-313.pyc | Bin 0 -> 2150 bytes __pycache__/tts.cpython-313.pyc | Bin 0 -> 12161 bytes config_flow.py | 189 +++++++++++++++ const.py | 68 ++++++ hacs.json | 9 + info.md | 24 ++ manifest.json | 13 + strings.json | 36 +++ tts.py | 246 +++++++++++++++++++ 16 files changed, 1304 insertions(+) create mode 100644 .github/workflows/validate.yml create mode 100644 README.md create mode 100644 README_OPENSOURCE.md create mode 100644 SETUP_GUIDE.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-313.pyc create mode 100644 __pycache__/config_flow.cpython-313.pyc create mode 100644 __pycache__/const.cpython-313.pyc create mode 100644 __pycache__/tts.cpython-313.pyc create mode 100644 config_flow.py create mode 100644 const.py create mode 100644 hacs.json create mode 100644 info.md create mode 100644 manifest.json create mode 100644 strings.json create mode 100644 tts.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..922fbfb --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,20 @@ +name: Validate + +on: + push: + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + validate: + runs-on: "ubuntu-latest" + name: Validate + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: HACS validation + uses: hacs/action@main + with: + category: "integration" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcd3134 --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +# SiliconFlow TTS 组件 + +这是一个基于硅基流动API的Home Assistant TTS组件,支持CosyVoice2-0.5B模型。 + +## 功能特性 + +- 🎭 **动态音色配置** - 在 `configuration.yaml` 中自定义音色,无需修改代码 +- 🎵 内置音色:阿狸、露露、索拉卡、扎克、吴楠 +- 🎚️ **前端可视化配置** - 在 Home Assistant 界面中实时调整参数 +- 🎧 支持多种音频格式:mp3, wav, opus, pcm +- ⚡ 支持语速调节(0.25-4.0倍速) +- 🔊 支持音量增益调节(-10到+10 dB) +- 😊 支持情感表达 +- 📊 支持多种采样率(最高44.1kHz) +- 🔄 **无需重启** - 配置更改立即生效 +- 🛠️ **兼容性强** - 支持媒体界面和服务调用 + +## 安装 + +1. 将 `siliconflow_tts` 文件夹复制到你的 `custom_components` 目录 +2. 重启 Home Assistant +3. 在配置文件中添加以下配置: + +```yaml +# configuration.yaml +siliconflow_tts: + api_key: "your_api_key_here" + # 可选:自定义音色选项 + custom_voices: + "speech:Dahu-HA-MyVoice:qc10fwc040:customid123": "我的专属音色" + "speech:Dahu-HA-BossVoice:qc10fwc040:customid456": "老板音色" + "speech:your-custom-voice-id": "自定义名称" +``` + +## 配置选项 + +| 参数 | 类型 | 必需 | 描述 | +|------|------|------|------| +| `api_key` | string | 是 | 硅基流动API密钥 | +| `custom_voices` | dict | 否 | 自定义音色选项(音色ID: 显示名称) | + +## 🎭 自定义音色配置 + +您可以在 `configuration.yaml` 中添加自定义音色,这样就不需要修改代码: + +### 基础配置 +```yaml +siliconflow_tts: + custom_voices: + "speech:Dahu-HA-MyCustom:qc10fwc040:uniqueid123": "我的音色" + "speech:Dahu-HA-Family:qc10fwc040:uniqueid456": "家人音色" +``` + +### 完整配置示例 +```yaml +siliconflow_tts: + custom_voices: + # 专业音色 + "speech:Dahu-HA-Professional:qc10fwc040:prof001": "专业播音" + "speech:Dahu-HA-News:qc10fwc040:news001": "新闻主播" + + # 个人音色 + "speech:Dahu-HA-Dad:qc10fwc040:family001": "爸爸的声音" + "speech:Dahu-HA-Mom:qc10fwc040:family002": "妈妈的声音" + + # 角色音色 + "speech:Dahu-HA-Robot:qc10fwc040:robot001": "机器人音色" + "speech:Dahu-HA-Child:qc10fwc040:child001": "儿童音色" +``` + +### 使用自定义音色 +配置后,您可以在前端配置界面中选择这些音色,也可以在服务调用中使用: + +```yaml +service: tts.speak +data: + entity_id: tts.siliconflow_tts + message: "你好,我是专业播音音色" + voice: "speech:Dahu-HA-Professional:qc10fwc040:prof001" +``` + +## 使用方法 + +### 在自动化中使用 + +```yaml +automation: + - alias: "TTS测试" + trigger: + platform: state + entity_id: input_boolean.tts_test + action: + - service: tts.siliconflow_say + data: + message: "你好,这是TTS测试" + voice: "anna" + speed: 1.2 + gain: 2 +``` + +### 在脚本中使用 + +```yaml +script: + tts_demo: + sequence: + - service: tts.siliconflow_say + data: + message: "今天天气很好" + voice: "bella" + emotion: "happy" + speed: 1.0 + gain: 0 +``` + +### 使用自定义音色 + +```yaml +# 使用预设语音 +script: + preset_voice_demo: + sequence: + - service: tts.siliconflow_say + data: + message: "你能用高兴的情感说吗?<|endofprompt|>今天真是太开心了!" + voice: "FunAudioLLM/CosyVoice2-0.5B:anna" + speed: 1.0 + gain: 0 + +# 使用自定义音色URI +script: + custom_voice_demo: + sequence: + - service: tts.siliconflow_say + data: + message: "你能用高兴的情感说吗?<|endofprompt|>这是使用自定义音色的测试!" + reference_audio: "speech:Dahu-HA-Ari:qc10fwc040:jxlvonagnnzdvbotrmrr" + speed: 1.0 + gain: 0 +``` + +## 支持的参数 + +### voice(预设语音) +- `FunAudioLLM/CosyVoice2-0.5B:alex` - Alex (男声) +- `FunAudioLLM/CosyVoice2-0.5B:anna` - Anna (女声) +- `FunAudioLLM/CosyVoice2-0.5B:bella` - Bella (女声) +- `FunAudioLLM/CosyVoice2-0.5B:benjamin` - Benjamin (男声) +- `FunAudioLLM/CosyVoice2-0.5B:charles` - Charles (男声) +- `FunAudioLLM/CosyVoice2-0.5B:claire` - Claire (女声) +- `FunAudioLLM/CosyVoice2-0.5B:david` - David (男声) +- `FunAudioLLM/CosyVoice2-0.5B:diana` - Diana (女声) + +### reference_audio(自定义音色) +- 类型:string(URI格式) +- 描述:使用上传后获得的URI作为自定义音色 +- 格式:speech:custom-name:model-id:unique-id +- 注意:与voice参数互斥,同时使用时会优先使用reference_audio +- 推荐:使用upload_voice.py脚本先上传音频文件获取URI + +### response_format(音频格式) +- `mp3` - MP3格式(默认) +- `wav` - WAV格式 +- `opus` - Opus格式 +- `pcm` - PCM格式 + +### speed(语速) +- 范围:0.25 - 4.0 +- 默认:1.0 + +### gain(音量增益) +- 范围:-10 - +10 +- 默认:0 + +### sample_rate(采样率) +- mp3: 32000, 44100 Hz +- wav/pcm: 8000, 16000, 24000, 32000, 44100 Hz +- opus: 48000 Hz + +### emotion(情感) +- 支持各种情感描述,如:happy, sad, angry, excited等 +- 会自动添加API所需的特殊标记格式 + +## 自定义音色使用指南 + +### 方法一:使用本地音频文件 + +#### 音频文件要求 +1. **格式**:支持mp3, wav, opus, pcm格式 +2. **时长**:建议5-30秒,太短可能影响音色提取效果 +3. **质量**:建议使用清晰、无噪音的音频 +4. **内容**:建议包含完整的句子,避免背景音乐 + +#### 文件路径 +- 可以使用绝对路径:`/config/custom_components/siliconflow_tts/Ari.mp3` +- 也可以使用相对路径(相对于Home Assistant配置目录) + +### 方法二:使用上传的URI参考音频 + +#### 上传步骤 +1. 使用提供的`upload_voice.py`脚本上传音频文件 +2. 脚本会返回一个URI格式的字符串 +3. 在TTS配置中直接使用这个URI + +#### 使用示例 +```bash +# 运行上传脚本 +python3 upload_voice.py + +# 脚本会输出类似这样的URI: +# speech:Dahu-HA-Ari:qc10fwc040:jxlvonagnnzdvbotrmrr +``` + +### 使用示例 + +```yaml +# 使用预设语音 +- service: tts.siliconflow_say + data: + message: "你能用高兴的情感说吗?<|endofprompt|>使用预设语音测试!" + voice: "FunAudioLLM/CosyVoice2-0.5B:anna" + +# 使用自定义音色URI +- service: tts.siliconflow_say + data: + message: "你能用高兴的情感说吗?<|endofprompt|>使用自定义音色测试!" + reference_audio: "speech:Dahu-HA-Ari:qc10fwc040:jxlvonagnnzdvbotrmrr" +``` + +## 故障排除 + +1. **API密钥错误**:确保API密钥正确且有效 +2. **网络连接问题**:检查网络连接是否正常 +3. **参数范围错误**:确保speed和gain参数在有效范围内 +4. **音频格式不支持**:确保选择的音频格式和采样率组合有效 +5. **自定义音色文件不存在**:检查音频文件路径是否正确 +6. **自定义音色编码失败**:检查音频文件是否损坏或格式不支持 + +## 日志 + +组件会记录详细的日志信息,可以在Home Assistant的开发者工具中查看: + +```yaml +logger: + custom_components.siliconflow_tts: debug +``` + +## 许可证 + +MIT License \ No newline at end of file diff --git a/README_OPENSOURCE.md b/README_OPENSOURCE.md new file mode 100644 index 0000000..30aaa6b --- /dev/null +++ b/README_OPENSOURCE.md @@ -0,0 +1,306 @@ +# SiliconFlow TTS for Home Assistant + +🎙️ 一个功能强大的 Home Assistant 文本转语音集成,基于 [SiliconFlow](https://siliconflow.cn) 的 CosyVoice2-0.5B 模型。 + +[![GitHub Release](https://img.shields.io/github/release/your-username/siliconflow-tts-ha)](https://github.com/your-username/siliconflow-tts-ha/releases) +[![License](https://img.shields.io/github/license/your-username/siliconflow-tts-ha)](LICENSE) +[![Home Assistant](https://img.shields.io/badge/Home%20Assistant-compatible-brightgreen)](https://www.home-assistant.io/) +[![中文文档](https://img.shields.io/badge/文档-中文-blue)](README_OPENSOURCE.md) + +## ✨ 功能特性 + +- 🎭 **动态音色配置** - 在 `configuration.yaml` 中自定义音色,无需修改代码 +- 🎵 **内置精品音色** - 阿狸、露露、索拉卡、扎克等精选音色 +- 🎚️ **可视化配置界面** - 在 Home Assistant 前端实时调整所有参数 +- 🎧 **多种音频格式** - 支持 mp3, wav, opus, pcm +- ⚡ **语速调节** - 0.25-4.0倍速,满足不同场景需求 +- 🔊 **音量增益控制** - -10到+10 dB精确调节 +- 😊 **情感表达支持** - 让语音更生动自然 +- 📊 **高音质采样** - 最高支持 44.1kHz 采样率 +- 🔄 **即时生效** - 配置更改无需重启,立即生效 +- 🛠️ **完美兼容** - 支持媒体界面和所有服务调用方式 +- 🎤 **自定义音色** - 支持上传个人音色文件 + +## 📦 安装方式 + +### 方法一:HACS 安装(推荐) + +1. 确保已安装 [HACS](https://hacs.xyz/) +2. 进入 HACS → 集成 +3. 点击右上角 ⋮ → 自定义存储库 +4. 添加此仓库 URL:`https://github.com/your-username/siliconflow-tts-ha` +5. 类别选择"集成" +6. 搜索 "SiliconFlow TTS" 并安装 +7. 重启 Home Assistant + +### 方法二:手动安装 + +1. 下载最新版本的压缩包 +2. 解压到 `custom_components/siliconflow_tts/` +3. 重启 Home Assistant + +## 🚀 快速开始 + +### 1. 获取 API 密钥 + +前往 [SiliconFlow 官网](https://cloud.siliconflow.cn/) 注册账号并获取 API 密钥。 + +### 2. 基础配置 + +在 Home Assistant 中: +1. 进入 **设置** → **设备与服务** +2. 点击 **添加集成** +3. 搜索 **SiliconFlow TTS** +4. 输入您的 API 密钥 + +### 3. 使用测试 + +```yaml +# 在开发者工具中测试 +service: tts.speak +data: + entity_id: tts.siliconflow_tts + message: "你好,欢迎使用 SiliconFlow TTS" +``` + +## ⚙️ 高级配置 + +### 🎭 自定义音色配置 + +在 `configuration.yaml` 中添加自定义音色: + +```yaml +siliconflow_tts: + custom_voices: + # 专业音色 + "speech:Dahu-HA-Professional:qc10fwc040:prof001": "专业播音" + "speech:Dahu-HA-News:qc10fwc040:news001": "新闻主播" + + # 个人音色 + "speech:Dahu-HA-Dad:qc10fwc040:family001": "爸爸的声音" + "speech:Dahu-HA-Mom:qc10fwc040:family002": "妈妈的声音" + + # 角色音色 + "speech:Dahu-HA-Robot:qc10fwc040:robot001": "机器人音色" + "speech:Dahu-HA-Child:qc10fwc040:child001": "儿童音色" +``` + +配置后重启 Home Assistant,即可在前端配置界面中选择这些音色。 + +### 🎚️ 前端参数调整 + +1. 进入 **设置** → **设备与服务** +2. 找到 **SiliconFlow TTS** 集成 +3. 点击 **配置** 按钮 +4. 在界面中调整: + - 🎭 选择音色 + - 🚀 调整语速 (0.25-4.0) + - 🎵 选择音频格式 + - 📊 设置采样率 + - 🔊 调整音量增益 + +**💡 提示**:所有配置更改立即生效,无需重启! + +## 🎤 上传自定义音色教程 + +### 准备工作 + +1. **获取 API 密钥**:确保已从 [SiliconFlow](https://cloud.siliconflow.cn/) 获取有效的 API 密钥 +2. **准备音频文件**: + - 格式:wav, mp3, m4a 等常见格式 + - 时长:建议 10-30 秒 + - 内容:清晰的语音,最好包含多种音调 + - 质量:无背景噪音,语音清晰 + +### 方法一:使用 cURL 命令(推荐) + +打开终端,执行以下命令: + +```bash +curl --request POST \ + --url https://api.siliconflow.cn/v1/uploads/audio/voice \ + --header 'Authorization: Bearer YOUR_API_KEY' \ + --header 'Content-Type: multipart/form-data' \ + --form model=FunAudioLLM/CosyVoice2-0.5B \ + --form customName=my-custom-voice \ + --form 'text=在一无所知中,梦里的一天结束了,一个新的轮回便会开始' \ + --form file=@/path/to/your/audio.wav +``` + +**参数说明**: +- `YOUR_API_KEY`:替换为您的 SiliconFlow API 密钥 +- `customName`:自定义音色名称(英文,无空格) +- `text`:音频对应的文本内容 +- `file=@/path/to/your/audio.wav`:音频文件路径 + +### 方法二:使用 Python 脚本 + +```python +import requests + +def upload_voice(api_key, audio_file_path, custom_name, text): + url = "https://api.siliconflow.cn/v1/uploads/audio/voice" + + headers = { + 'Authorization': f'Bearer {api_key}' + } + + files = { + 'file': open(audio_file_path, 'rb') + } + + data = { + 'model': 'FunAudioLLM/CosyVoice2-0.5B', + 'customName': custom_name, + 'text': text + } + + response = requests.post(url, headers=headers, files=files, data=data) + + if response.status_code == 200: + result = response.json() + print(f"上传成功!音色 URI: {result['uri']}") + return result['uri'] + else: + print(f"上传失败:{response.status_code} - {response.text}") + return None + +# 使用示例 +api_key = "your_siliconflow_api_key" +audio_path = "/path/to/your/voice.wav" +voice_name = "my-voice" +voice_text = "这是我的自定义音色测试" + +uri = upload_voice(api_key, audio_path, voice_name, voice_text) +``` + +### 上传成功后的配置 + +上传成功后,您会收到类似这样的响应: + +```json +{ + "uri": "speech:my-custom-voice:qc10fwc040:abc123def456" +} +``` + +将这个 URI 添加到您的 `configuration.yaml`: + +```yaml +siliconflow_tts: + custom_voices: + "speech:my-custom-voice:qc10fwc040:abc123def456": "我的专属音色" +``` + +重启 Home Assistant 后,您就可以在配置界面中选择这个音色了! + +### 🔧 故障排除 + +**上传失败常见原因**: +1. **API 密钥错误**:检查密钥是否正确复制 +2. **音频格式不支持**:尝试转换为 wav 格式 +3. **文件过大**:建议音频文件小于 10MB +4. **网络问题**:检查网络连接 + +**音色使用问题**: +1. **音色不显示**:确保已重启 Home Assistant +2. **音色不生效**:检查 URI 是否正确复制 +3. **音质不佳**:尝试使用更高质量的源音频 + +## 📖 使用示例 + +### 在自动化中使用 + +```yaml +automation: + - alias: "门铃提醒" + trigger: + platform: state + entity_id: binary_sensor.doorbell + to: "on" + action: + - service: tts.speak + data: + entity_id: tts.siliconflow_tts + message: "有客人来访,请注意查看" + # 使用自定义音色 + voice: "speech:my-custom-voice:qc10fwc040:abc123def456" +``` + +### 在脚本中使用 + +```yaml +script: + welcome_home: + sequence: + - service: tts.speak + data: + entity_id: tts.siliconflow_tts + message: "欢迎回家,{{ states('sensor.family_member') }}!" + speed: 1.2 + gain: 2 +``` + +### 在 Node-RED 中使用 + +```json +{ + "id": "tts_node", + "type": "api-call-service", + "name": "TTS 播报", + "server": "home_assistant_server", + "service_domain": "tts", + "service": "speak", + "data": { + "entity_id": "tts.siliconflow_tts", + "message": "这是来自 Node-RED 的消息" + } +} +``` + +## 🔧 配置选项 + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `voice` | string | 阿狸 | 选择的音色 | +| `speed` | float | 1.0 | 语速 (0.25-4.0) | +| `response_format` | string | mp3 | 音频格式 | +| `sample_rate` | int | 44100 | 采样率 (Hz) | +| `gain` | int | -2 | 音量增益 (dB) | + +## 🐛 问题反馈 + +遇到问题?请通过以下方式反馈: + +1. [GitHub Issues](https://github.com/your-username/siliconflow-tts-ha/issues) +2. [Home Assistant 社区论坛](https://bbs.hassbian.com/) + +## 🤝 贡献代码 + +欢迎提交 Pull Request!请确保: + +1. 代码符合 Python PEP 8 规范 +2. 添加必要的测试用例 +3. 更新相关文档 + +## 📄 开源协议 + +本项目基于 [MIT License](LICENSE) 开源。 + +## 🙏 致谢 + +- [SiliconFlow](https://siliconflow.cn/) - 提供优秀的语音合成 API +- [Home Assistant](https://www.home-assistant.io/) - 开源智能家居平台 +- 所有贡献者和用户的支持 + +## 📚 相关资源 + +- [SiliconFlow API 文档](https://docs.siliconflow.cn/) +- [Home Assistant TTS 文档](https://www.home-assistant.io/integrations/tts/) +- [CosyVoice 项目](https://github.com/FunAudioLLM/CosyVoice) + +--- + +⭐ 如果这个项目对您有帮助,请给我们一个 Star! + +🔗 **项目链接**: https://github.com/your-username/siliconflow-tts-ha \ No newline at end of file diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..22edff4 --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,95 @@ +# SiliconFlow TTS/STT 组件安装指南 + +## 安装步骤 + +### 1. 重启Home Assistant +修改了组件文件后,需要重启Home Assistant以加载新的组件。 + +### 2. 添加集成 +重启后,按以下步骤添加集成: + +1. 进入 **设置** > **设备与服务** +2. 点击右下角的 **添加集成** 按钮 +3. 搜索 **SiliconFlow** +4. 你应该能看到两个集成: + - **SiliconFlow TTS** + - **SiliconFlow STT** + +### 3. 配置TTS +1. 选择 **SiliconFlow TTS** +2. 输入你的硅基流动API密钥 +3. 点击提交 + +### 4. 配置STT +1. 选择 **SiliconFlow STT** +2. 输入你的硅基流动API密钥 +3. 点击提交 + +### 5. 配置语音助手 +配置完成后: + +1. 进入 **设置** > **语音助手** +2. 在 **文本转语音** 中选择 **SiliconFlow TTS** +3. 在 **语音转文本** 中选择 **SiliconFlow STT** + +## 故障排除 + +### 找不到集成 +如果在添加集成页面找不到SiliconFlow: +1. 确保已重启Home Assistant +2. 检查日志是否有错误信息 +3. 确认文件结构正确 + +### API密钥验证失败 +如果API密钥验证失败: +1. 检查API密钥是否正确 +2. 确认网络连接正常 +3. 检查硅基流动服务状态 + +### 语音助手中找不到选项 +如果在语音助手设置中找不到对应选项: +1. 确保两个集成都已成功配置 +2. 重新加载集成 +3. 重启Home Assistant + +## 使用方法 + +配置完成后,你可以: + +### 在自动化中使用TTS +```yaml +action: + - service: tts.speak + target: + entity_id: tts.siliconflow_tts + data: + message: "你能用高兴的情感说吗?<|endofprompt|>Hello World!" +``` + +### 在脚本中使用 +```yaml +script: + test_tts: + sequence: + - service: tts.speak + target: + entity_id: tts.siliconflow_tts + data: + message: "测试消息" + options: + voice: "FunAudioLLM/CosyVoice2-0.5B:anna" +``` + +### 使用自定义音色 +```yaml +script: + custom_voice: + sequence: + - service: tts.speak + target: + entity_id: tts.siliconflow_tts + data: + message: "自定义音色测试" + options: + reference_audio: "speech:your-voice-uri" +``` \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4d5851f --- /dev/null +++ b/__init__.py @@ -0,0 +1,48 @@ +"""硅基流动TTS组件初始化文件""" +import logging +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, CONF_CUSTOM_VOICES, update_voice_options + +_LOGGER = logging.getLogger(__name__) + +# configuration.yaml 配置架构 +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_CUSTOM_VOICES, default={}): vol.Schema( + {cv.string: cv.string} + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +async def async_setup(hass: HomeAssistant, config: dict): + """设置组件""" + # 读取用户自定义音色配置 + domain_config = config.get(DOMAIN, {}) + custom_voices = domain_config.get(CONF_CUSTOM_VOICES, {}) + + if custom_voices: + _LOGGER.info(f"🎭 加载了 {len(custom_voices)} 个自定义音色: {list(custom_voices.values())}") + update_voice_options(custom_voices) + else: + _LOGGER.debug("未发现自定义音色配置,使用默认音色") + + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """从配置条目设置组件""" + await hass.config_entries.async_forward_entry_setups(entry, ["tts"]) + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """卸载配置条目""" + return await hass.config_entries.async_unload_platforms(entry, ["tts"]) \ No newline at end of file diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e2218b95c57867ac51e151792a6e124ca010ea1 GIT binary patch literal 2511 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 Kg+PGUrT+mqh_%xI literal 0 HcmV?d00001 diff --git a/config_flow.py b/config_flow.py new file mode 100644 index 0000000..87c3ba6 --- /dev/null +++ b/config_flow.py @@ -0,0 +1,189 @@ +"""SiliconFlow TTS配置流程""" +import asyncio +import logging +from typing import Any + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import ( + DOMAIN, + DEFAULT_VOICE, + DEFAULT_SPEED, + DEFAULT_SAMPLE_RATE, + DEFAULT_RESPONSE_FORMAT, + DEFAULT_GAIN, + MIN_SPEED, + MAX_SPEED, + MIN_GAIN, + MAX_GAIN, + SUPPORTED_FORMATS, + SUPPORTED_SAMPLE_RATES, + get_voice_options, + CONF_VOICE, + CONF_SPEED, + CONF_SAMPLE_RATE, + CONF_RESPONSE_FORMAT, + CONF_GAIN, +) + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("api_key"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + api_key = data["api_key"] + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + payload = { + "model": "FunAudioLLM/CosyVoice2-0.5B", + "input": "测试", + "voice": "FunAudioLLM/CosyVoice2-0.5B:anna", + "response_format": "mp3", + } + + try: + async with aiohttp.ClientSession() as session: + async with async_timeout.timeout(10): + async with session.post( + "https://api.siliconflow.cn/v1/audio/speech", + headers=headers, + json=payload + ) as response: + _LOGGER.debug(f"TTS API验证响应状态: {response.status}") + if response.status == 401: + _LOGGER.error("TTS API密钥无效") + raise InvalidAuth + elif response.status == 403: + _LOGGER.error("TTS API密钥权限不足") + raise InvalidAuth + elif response.status >= 500: + _LOGGER.error(f"TTS服务器错误: {response.status}") + raise CannotConnect + # 200, 400等其他状态码都表示API密钥有效 + _LOGGER.info("TTS API密钥验证成功") + except aiohttp.ClientError as e: + _LOGGER.error(f"TTS网络连接错误: {e}") + raise CannotConnect + except asyncio.TimeoutError: + _LOGGER.error("TTS API请求超时") + raise CannotConnect + except Exception as e: + _LOGGER.error(f"TTS验证过程中出现未知错误: {e}") + raise CannotConnect + + # Return info that you want to store in the config entry. + return {"title": "SiliconFlow TTS"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SiliconFlow TTS.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + _LOGGER.info(f"开始验证TTS配置,API密钥长度: {len(user_input.get('api_key', ''))}") + try: + info = await validate_input(self.hass, user_input) + _LOGGER.info("TTS配置验证成功") + except CannotConnect: + _LOGGER.error("TTS配置验证失败: 无法连接") + errors["base"] = "cannot_connect" + except InvalidAuth: + _LOGGER.error("TTS配置验证失败: API密钥无效") + errors["base"] = "invalid_auth" + except Exception as e: # pylint: disable=broad-except + _LOGGER.exception(f"TTS配置验证时出现未知异常: {e}") + errors["base"] = "unknown" + else: + _LOGGER.info(f"TTS集成配置成功: {info['title']}") + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options for SiliconFlow TTS.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + # 获取当前配置的值,如果没有则使用默认值 + current_voice = self._config_entry.options.get(CONF_VOICE, DEFAULT_VOICE) + current_speed = self._config_entry.options.get(CONF_SPEED, DEFAULT_SPEED) + current_sample_rate = self._config_entry.options.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATE) + current_response_format = self._config_entry.options.get(CONF_RESPONSE_FORMAT, DEFAULT_RESPONSE_FORMAT) + current_gain = self._config_entry.options.get(CONF_GAIN, DEFAULT_GAIN) + + # 根据选择的格式获取支持的采样率 + available_sample_rates = SUPPORTED_SAMPLE_RATES.get(current_response_format, [DEFAULT_SAMPLE_RATE]) + + # 获取当前可用的音色选项(默认 + 用户自定义) + voice_options = get_voice_options() + _LOGGER.debug(f"当前可用音色选项: {len(voice_options)} 个") + + options_schema = vol.Schema( + { + vol.Optional(CONF_VOICE, default=current_voice): vol.In(voice_options), + vol.Optional(CONF_SPEED, default=current_speed): vol.All( + vol.Coerce(float), vol.Range(min=MIN_SPEED, max=MAX_SPEED) + ), + vol.Optional(CONF_RESPONSE_FORMAT, default=current_response_format): vol.In(SUPPORTED_FORMATS), + vol.Optional(CONF_SAMPLE_RATE, default=current_sample_rate): vol.In(available_sample_rates), + vol.Optional(CONF_GAIN, default=current_gain): vol.All( + vol.Coerce(int), vol.Range(min=MIN_GAIN, max=MAX_GAIN) + ), + } + ) + + return self.async_show_form( + step_id="init", + data_schema=options_schema, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" \ No newline at end of file diff --git a/const.py b/const.py new file mode 100644 index 0000000..64d5807 --- /dev/null +++ b/const.py @@ -0,0 +1,68 @@ +"""硅基流动TTS组件常量定义""" + +DOMAIN = "siliconflow_tts" +DEFAULT_NAME = "SiliconFlow TTS" + +# API配置 +API_BASE_URL = "https://api.siliconflow.cn/v1" +API_ENDPOINT = f"{API_BASE_URL}/audio/speech" + +# 默认配置 +DEFAULT_MODEL = "FunAudioLLM/CosyVoice2-0.5B" +DEFAULT_VOICE = "speech:Dahu-HA-Ari:qc10fwc040:jxlvonagnnzdvbotrmrr" +DEFAULT_RESPONSE_FORMAT = "mp3" +DEFAULT_SAMPLE_RATE = 44100 # 更高的采样率,减少切断问题 +DEFAULT_SPEED = 1.0 # 正常语速,避免因语速过快导致切断 +DEFAULT_GAIN = -2 # 稍微降低增益,避免音频削峰导致的切断 +DEFAULT_STREAM = False # 不使用流式传输,确保最快响应 + +# 支持的音频格式 +SUPPORTED_FORMATS = ["mp3", "wav", "opus", "pcm"] + +# 支持的采样率 +SUPPORTED_SAMPLE_RATES = { + "opus": [48000], + "wav": [8000, 16000, 24000, 32000, 44100], + "pcm": [8000, 16000, 24000, 32000, 44100], + "mp3": [32000, 44100] +} + +# 速度范围 +MIN_SPEED = 0.25 +MAX_SPEED = 4.0 + +# 增益范围 +MIN_GAIN = -10 +MAX_GAIN = 10 + +# 默认音色选项(内置) +DEFAULT_VOICE_OPTIONS = { + "speech:Dahu-HA-Ari:qc10fwc040:jxlvonagnnzdvbotrmrr": "阿狸", + "speech:Dahu-HA-Lulu:qc10fwc040:ugmvnfqyesexvqjuneyf": "露露", + "speech:Dahu-HA-Soraka:qc10fwc040:ezxwqvmnazypoekujait": "索拉卡", + "speech:Dahu-HA-Zac:qc10fwc040:smplyqhoxcxwepjavhci": "扎克" +} + +# 全局变量存储合并后的音色选项(默认 + 用户自定义) +_VOICE_OPTIONS = DEFAULT_VOICE_OPTIONS.copy() + +def get_voice_options(): + """获取当前可用的所有音色选项(默认 + 用户自定义)""" + return _VOICE_OPTIONS.copy() + +def update_voice_options(custom_voices: dict): + """更新音色选项,将用户自定义音色与默认音色合并""" + global _VOICE_OPTIONS + _VOICE_OPTIONS = DEFAULT_VOICE_OPTIONS.copy() + if custom_voices: + _VOICE_OPTIONS.update(custom_voices) + +# configuration.yaml 配置键名 +CONF_CUSTOM_VOICES = "custom_voices" + +# 配置选项的键名 +CONF_VOICE = "voice" +CONF_SPEED = "speed" +CONF_SAMPLE_RATE = "sample_rate" +CONF_RESPONSE_FORMAT = "response_format" +CONF_GAIN = "gain" \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..97ab74d --- /dev/null +++ b/hacs.json @@ -0,0 +1,9 @@ +{ + "name": "SiliconFlow TTS", + "hacs": "1.6.0", + "domains": ["tts"], + "country": ["CN"], + "homeassistant": "2023.1.0", + "iot_class": "Cloud Polling", + "render_readme": true +} \ No newline at end of file diff --git a/info.md b/info.md new file mode 100644 index 0000000..a972c5b --- /dev/null +++ b/info.md @@ -0,0 +1,24 @@ +# SiliconFlow TTS + +基于硅基流动 CosyVoice2-0.5B 模型的高质量文本转语音集成。 + +## 功能特性 + +- 🎭 动态音色配置 - 支持自定义音色 +- 🎚️ 前端可视化配置界面 +- 🔄 配置实时生效,无需重启 +- 🎵 多种内置精品音色 +- ⚡ 语速、音量、格式全面可调 + +## 快速开始 + +1. 获取 [SiliconFlow](https://cloud.siliconflow.cn/) API 密钥 +2. 在集成配置中输入 API 密钥 +3. 在配置界面调整音色和参数 +4. 开始使用高质量语音合成 + +## 自定义音色 + +支持上传个人音色文件,实现个性化语音输出。 + +详细使用说明请查看 [GitHub 仓库](https://github.com/your-username/siliconflow-tts-ha)。 \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..9d82ae3 --- /dev/null +++ b/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "siliconflow_tts", + "name": "SiliconFlow TTS", + "version": "1.0.0", + "documentation": "https://github.com/your-username/siliconflow-tts-ha", + "issue_tracker": "https://github.com/your-username/siliconflow-tts-ha/issues", + "requirements": ["aiohttp>=3.8.0", "async_timeout>=4.0.0"], + "codeowners": ["@your-username"], + "config_flow": true, + "dependencies": [], + "after_dependencies": [], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..e6f7810 --- /dev/null +++ b/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "title": "设置SiliconFlow TTS", + "description": "请输入您的SiliconFlow API密钥", + "data": { + "api_key": "API密钥" + } + } + }, + "error": { + "cannot_connect": "无法连接到SiliconFlow服务,请检查网络连接", + "invalid_auth": "API密钥无效,请检查密钥是否正确", + "unknown": "发生未知错误,请重试" + }, + "abort": { + "already_configured": "该API密钥已经配置过了" + } + }, + "options": { + "step": { + "init": { + "title": "配置SiliconFlow TTS选项", + "description": "调整语音合成参数,更改后将立即生效,无需重启", + "data": { + "voice": "音色", + "speed": "语速 (0.25-4.0)", + "response_format": "音频格式", + "sample_rate": "采样率 (Hz)", + "gain": "音量增益 (-10到10 dB)" + } + } + } + } +} \ No newline at end of file diff --git a/tts.py b/tts.py new file mode 100644 index 0000000..80b37e1 --- /dev/null +++ b/tts.py @@ -0,0 +1,246 @@ +"""硅基流动TTS组件""" +import asyncio +import logging +import aiohttp +import async_timeout +import base64 +import os +from typing import Any + +from homeassistant.components.tts import ( + TextToSpeechEntity, + Voice, + TtsAudioType, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + API_ENDPOINT, + DEFAULT_MODEL, + DEFAULT_VOICE, + DEFAULT_RESPONSE_FORMAT, + DEFAULT_SAMPLE_RATE, + DEFAULT_SPEED, + DEFAULT_GAIN, + DEFAULT_STREAM, + SUPPORTED_FORMATS, + MIN_SPEED, + MAX_SPEED, + MIN_GAIN, + MAX_GAIN, + get_voice_options, + CONF_VOICE, + CONF_SPEED, + CONF_SAMPLE_RATE, + CONF_RESPONSE_FORMAT, + CONF_GAIN, +) + +_LOGGER = logging.getLogger(__name__) + +# 支持的语音列表(根据CosyVoice2-0.5B模型的实际语音) +SUPPORTED_VOICES = { + "zh": [ + Voice("speech:Dahu-HA-Lulu:qc10fwc040:ugmvnfqyesexvqjuneyf", "Lulu"), + ], + "en": [ + Voice("speech:Dahu-HA-Lulu:qc10fwc040:ugmvnfqyesexvqjuneyf", "Lulu"), + ] +} + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """设置TTS实体""" + async_add_entities([SiliconFlowTTSEntity(config_entry)]) + +class SiliconFlowTTSEntity(TextToSpeechEntity): + """硅基流动TTS实体""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """初始化TTS实体""" + self._config_entry = config_entry + self._attr_name = "SiliconFlow TTS" + self._attr_unique_id = f"siliconflow_tts_{config_entry.entry_id}" + + @property + def supported_languages(self) -> list[str]: + """返回支持的语言列表""" + return list(SUPPORTED_VOICES.keys()) + + def get_supported_voices(self, language: str) -> list[Voice] | None: + """获取指定语言支持的语音(动态加载所有可用音色)""" + # 从动态音色选项中生成Voice对象列表 + voice_options = get_voice_options() + voices = [] + for voice_id, voice_name in voice_options.items(): + voices.append(Voice(voice_id, voice_name)) + _LOGGER.debug(f"为语言 '{language}' 提供 {len(voices)} 个音色选项") + return voices + + @property + def default_language(self) -> str: + """返回默认语言""" + return "zh" + + @property + def supported_audio_codecs(self) -> list[str]: + """返回支持的音频编解码器""" + return SUPPORTED_FORMATS + + def _encode_audio_file(self, file_path: str) -> str | None: + """将音频文件编码为base64字符串""" + try: + if not os.path.exists(file_path): + _LOGGER.error(f"音频文件不存在: {file_path}") + return None + + with open(file_path, 'rb') as audio_file: + audio_data = audio_file.read() + + # 检查文件大小,如果太大则截取前100KB + if len(audio_data) > 100 * 1024: # 100KB + _LOGGER.warning(f"音频文件较大 ({len(audio_data)} bytes),截取前100KB") + audio_data = audio_data[:100 * 1024] + + encoded_audio = base64.b64encode(audio_data).decode('utf-8') + _LOGGER.debug(f"成功编码音频文件: {file_path}, 大小: {len(audio_data)} bytes") + return encoded_audio + except Exception as e: + _LOGGER.error(f"编码音频文件失败: {e}") + return None + + def _is_uri_reference(self, reference: str) -> bool: + """检查是否为URI格式的参考音频""" + return reference.startswith("speech:") + + async def async_get_tts_audio( + self, + message: str, + language: str, + options: dict[str, Any] | None = None, + ) -> TtsAudioType: + """获取TTS音频""" + + api_key = self._config_entry.data.get("api_key") + if not api_key: + _LOGGER.error("未找到API密钥") + return None + + try: + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + # 从配置条目的选项中获取参数,优先级:service call options > config entry options > defaults + config_options = self._config_entry.options + _LOGGER.debug(f"配置选项: {config_options}") + _LOGGER.debug(f"服务调用选项: {options}") + + # 优化参数获取逻辑,确保正确处理None值 + response_format = ( + options.get("response_format") if options and "response_format" in options + else config_options.get(CONF_RESPONSE_FORMAT, DEFAULT_RESPONSE_FORMAT) + ) + speed = ( + options.get("speed") if options and "speed" in options + else config_options.get(CONF_SPEED, DEFAULT_SPEED) + ) + sample_rate = ( + options.get("sample_rate") if options and "sample_rate" in options + else config_options.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATE) + ) + gain = ( + options.get("gain") if options and "gain" in options + else config_options.get(CONF_GAIN, DEFAULT_GAIN) + ) + + # 验证参数范围 + speed = max(MIN_SPEED, min(MAX_SPEED, speed)) + gain = max(MIN_GAIN, min(MAX_GAIN, gain)) + + # 构建输入文本,根据API文档添加特殊标记,并解决语音切断问题 + input_text = message.strip() + + # 解决语音切断问题:在文本末尾添加适当的标点符号和停顿 + if not input_text.endswith(('.', '。', '!', '!', '?', '?', ',', ',', '、')): + input_text += "。" # 添加句号确保语音完整 + + # 添加轻微的停顿来避免切断(使用标点符号) + input_text += " " # 两个空格提供自然的停顿 + + if options and "emotion" in options: + emotion = options["emotion"] + input_text = f"Can you say it with a {emotion} emotion? <|endofprompt|>{input_text}" + + _LOGGER.debug(f"处理后的文本: '{input_text}'") + + # 构建payload + payload = { + "model": DEFAULT_MODEL, + "input": input_text, + "response_format": response_format, + "speed": speed, + "sample_rate": sample_rate, + "gain": gain, + "stream": DEFAULT_STREAM, # 显式设置为false,确保最快响应 + } + + # 确定使用的语音,优先级:service call options > config entry options > defaults + voice = DEFAULT_VOICE + + # 检查是否在service call中指定了语音(包括预设语音和自定义音色) + if options and "voice" in options: + voice = options["voice"] + _LOGGER.info(f"使用service call指定的语音: {voice}") + elif options and "reference_audio" in options: + # 为了兼容性,仍然支持reference_audio参数 + reference_audio = options["reference_audio"] + if self._is_uri_reference(reference_audio): + voice = reference_audio + _LOGGER.info(f"使用reference_audio参数指定的URI音色: {reference_audio}") + else: + _LOGGER.warning(f"本地文件参考音频暂不支持,请先上传获取URI: {reference_audio}") + elif config_options.get(CONF_VOICE): + # 使用配置的音色 + voice = config_options.get(CONF_VOICE) + _LOGGER.info(f"使用配置的语音: {voice}") + else: + # 使用全局默认语音 + _LOGGER.info(f"🎭 使用全局默认语音: {voice}") + + # 验证选择的音色是否在可用音色列表中 + voice_options = get_voice_options() + if voice not in voice_options: + _LOGGER.warning(f"选择的音色 '{voice}' 不在可用列表中,使用默认音色") + voice = DEFAULT_VOICE + + payload["voice"] = voice + + _LOGGER.info(f"TTS配置 - 音色:{voice[-10:]}..., 格式:{response_format}, 采样率:{sample_rate}Hz, 语速:{speed}, 增益:{gain}dB") + _LOGGER.debug(f"完整音色ID: {voice}") + _LOGGER.debug(f"完整payload: {payload}") + + async with aiohttp.ClientSession() as session: + async with async_timeout.timeout(30): + async with session.post(API_ENDPOINT, headers=headers, json=payload) as response: + if response.status == 200: + audio_data = await response.read() + _LOGGER.debug(f"TTS成功,音频大小: {len(audio_data)} bytes") + return (response_format, audio_data) + else: + error_text = await response.text() + _LOGGER.error(f"TTS API错误 {response.status}: {error_text}") + return None + + except asyncio.TimeoutError: + _LOGGER.error("TTS请求超时") + return None + except Exception as e: + _LOGGER.error(f"TTS处理错误: {e}") + return None \ No newline at end of file