├── .github └── workflows │ └── release.yml ├── .gitignore ├── README.md ├── appcast.json ├── release.py └── src ├── config.js ├── icon.png ├── info.json ├── main.js └── utils.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Bob Plugin 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | sparse-checkout: | 21 | .github 22 | src 23 | release.py 24 | appcast.json 25 | sparse-checkout-cone-mode: false 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: '3.x' 31 | 32 | - name: Get latest tag 33 | id: get_latest_tag 34 | run: | 35 | git fetch --tags 36 | latest_tag=$(git tag | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) 37 | if [ -z "$latest_tag" ]; then 38 | echo "version=v0.0.1" >> $GITHUB_OUTPUT 39 | else 40 | current_version=${latest_tag#v} 41 | IFS='.' read -r major minor patch <<< "$current_version" 42 | new_version="v$major.$minor.$((patch + 1))" 43 | echo "version=$new_version" >> $GITHUB_OUTPUT 44 | fi 45 | 46 | - name: Create Release Package 47 | run: | 48 | version="${{ steps.get_latest_tag.outputs.version }}" 49 | version_number=${version#v} 50 | python release.py $version_number 51 | 52 | - name: Create Release 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | tag_name: ${{ steps.get_latest_tag.outputs.version }} 56 | name: Release ${{ steps.get_latest_tag.outputs.version }} 57 | files: | 58 | bob-plugin-siliconflow-tts-*.bobplugin 59 | appcast.json 60 | generate_release_notes: true 61 | 62 | - name: Commit and Push appcast.json 63 | run: | 64 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 65 | git config --local user.name "github-actions[bot]" 66 | git add appcast.json 67 | git add src/info.json 68 | git commit -m "chore: update version files for ${{ steps.get_latest_tag.outputs.version }}" 69 | git push 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bob Plugin for Siliconflow TTS 2 | 3 | This is a [Bob](https://bobtranslate.com/) plugin that provides text-to-speech functionality using [Siliconflow](https://cloud.siliconflow.cn/) API. 4 | 5 | -------------------------------------------------------------------------------- /appcast.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.whymeta.bob-plugin-siliconflow-tts", 3 | "versions": [ 4 | { 5 | "version": "0.0.4", 6 | "desc": "auto", 7 | "sha256": "108f50da37cb33c582eefb84fd9b8c2328a2ca6f12cf7d9c53dcd28efb2d595a", 8 | "url": "https://github.com/whymeta/bob-plugin-siliconflow-tts/releases/download/v0.0.4/bob-plugin-siliconflow-tts-0.0.4.bobplugin", 9 | "minBobVersion": "0.5.0" 10 | }, 11 | { 12 | "version": "0.0.3", 13 | "desc": "auto", 14 | "sha256": "a2b4a19c85f5a111737e4370a2084fdae6c5da46ab13ec032d100a6fddcf44f7", 15 | "url": "https://github.com/whymeta/bob-plugin-siliconflow-tts/releases/download/v0.0.3/bob-plugin-siliconflow-tts-0.0.3.bobplugin", 16 | "minBobVersion": "0.5.0" 17 | }, 18 | { 19 | "version": "0.0.2", 20 | "desc": "auto", 21 | "sha256": "dfa98c8532f8f4e69a60a73299fe62722ea5846fdb619dcca275057c52a19308", 22 | "url": "https://github.com/whymeta/bob-plugin-siliconflow-tts/releases/download/v0.0.2/bob-plugin-siliconflow-tts-0.0.2.bobplugin", 23 | "minBobVersion": "0.5.0" 24 | }, 25 | { 26 | "version": "0.0.1", 27 | "desc": "auto", 28 | "sha256": "2a516bbbfaf37e0d75a8b657bac9866bd60620833ef463be887b141435d98adf", 29 | "url": "https://github.com/whymeta/bob-plugin-siliconflow-tts/releases/download/v0.0.1/bob-plugin-siliconflow-tts-0.0.1.bobplugin", 30 | "minBobVersion": "0.5.0" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import json 4 | import hashlib 5 | import shutil 6 | from pathlib import Path 7 | import argparse 8 | from datetime import datetime 9 | 10 | def calculate_sha256(filename): 11 | # Read file in chunks and calculate SHA256 12 | with open(filename, "rb") as f: 13 | return hashlib.file_digest(f, "sha256").hexdigest() 14 | 15 | def create_plugin_package(version): 16 | # Update version in info.json first 17 | base_dir = Path(__file__).parent 18 | info_json_path = base_dir / "src" / "info.json" 19 | 20 | if not info_json_path.exists(): 21 | raise FileNotFoundError(f"Could not find info.json at {info_json_path}") 22 | 23 | with open(info_json_path, 'r', encoding='utf-8') as f: 24 | info = json.load(f) 25 | 26 | info['version'] = version 27 | 28 | with open(info_json_path, 'w', encoding='utf-8') as f: 29 | json.dump(info, f, indent=2, ensure_ascii=False) 30 | 31 | # Create zip file from src directory 32 | src_dir = base_dir / "src" 33 | output_name = f"bob-plugin-siliconflow-tts-{version}" 34 | 35 | # Create zip archive 36 | shutil.make_archive(output_name, 'zip', src_dir) 37 | 38 | # Rename to .bobplugin 39 | plugin_file = f"{output_name}.bobplugin" 40 | os.rename(f"{output_name}.zip", plugin_file) 41 | 42 | return plugin_file 43 | 44 | def update_appcast(version, plugin_file): 45 | base_dir = Path(__file__).parent 46 | appcast_file = base_dir / "appcast.json" 47 | 48 | # Calculate SHA256 49 | sha256 = calculate_sha256(plugin_file) 50 | 51 | # Read existing appcast.json 52 | if os.path.exists(appcast_file): 53 | with open(appcast_file, 'r') as f: 54 | appcast = json.load(f) 55 | else: 56 | appcast = { 57 | "identifier": "com.whymeta.bob-plugin-siliconflow-tts", 58 | "versions": [] 59 | } 60 | 61 | # Create new version entry 62 | new_version = { 63 | "version": version, 64 | "desc": "auto", 65 | "sha256": sha256, 66 | "url": f"https://github.com/whymeta/bob-plugin-siliconflow-tts/releases/download/v{version}/{plugin_file}", 67 | "minBobVersion": "0.5.0" 68 | } 69 | 70 | # Add or update version 71 | versions = appcast["versions"] 72 | for i, v in enumerate(versions): 73 | if v["version"] == version: 74 | versions[i] = new_version 75 | break 76 | else: 77 | versions.insert(0, new_version) 78 | 79 | # Write updated appcast.json 80 | with open(appcast_file, 'w') as f: 81 | json.dump(appcast, f, indent=2) 82 | 83 | def main(): 84 | parser = argparse.ArgumentParser(description='Create Bob plugin package and update appcast.json') 85 | parser.add_argument('version', help='Version number (e.g., 0.0.1)') 86 | args = parser.parse_args() 87 | 88 | plugin_file = create_plugin_package(args.version) 89 | update_appcast(args.version, plugin_file) 90 | print(f"Created plugin package: {plugin_file}") 91 | print("Updated appcast.json") 92 | 93 | if __name__ == "__main__": 94 | main() -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const supportedLanguages = [ 2 | ["auto", "auto"], 3 | ["zh-Hans", "zh-CN"], 4 | ["zh-Hant", "zh-TW"], 5 | ["en", "en"], 6 | ["yue", "粤语"], 7 | ["wyw", "古文"], 8 | ["ja", "ja"], 9 | ["ko", "ko"], 10 | ["fr", "fr"], 11 | ["de", "de"], 12 | ["es", "es"], 13 | ["it", "it"], 14 | ["ru", "ru"], 15 | ["pt", "pt"], 16 | ["nl", "nl"], 17 | ["pl", "pl"], 18 | ["ar", "ar"], 19 | ["af", "af"], 20 | ["am", "am"], 21 | ["az", "az"], 22 | ["be", "be"], 23 | ["bg", "bg"], 24 | ["bn", "bn"], 25 | ["bs", "bs"], 26 | ["ca", "ca"], 27 | ["ceb", "ceb"], 28 | ["co", "co"], 29 | ["cs", "cs"], 30 | ["cy", "cy"], 31 | ["da", "da"], 32 | ["el", "el"], 33 | ["eo", "eo"], 34 | ["et", "et"], 35 | ["eu", "eu"], 36 | ["fa", "fa"], 37 | ["fi", "fi"], 38 | ["fj", "fj"], 39 | ["fy", "fy"], 40 | ["ga", "ga"], 41 | ["gd", "gd"], 42 | ["gl", "gl"], 43 | ["gu", "gu"], 44 | ["ha", "ha"], 45 | ["haw", "haw"], 46 | ["he", "he"], 47 | ["hi", "hi"], 48 | ["hmn", "hmn"], 49 | ["hr", "hr"], 50 | ["ht", "ht"], 51 | ["hu", "hu"], 52 | ["hy", "hy"], 53 | ["id", "id"], 54 | ["ig", "ig"], 55 | ["is", "is"], 56 | ["jw", "jw"], 57 | ["ka", "ka"], 58 | ["kk", "kk"], 59 | ["km", "km"], 60 | ["kn", "kn"], 61 | ["ku", "ku"], 62 | ["ky", "ky"], 63 | ["la", "lo"], 64 | ["lb", "lb"], 65 | ["lo", "lo"], 66 | ["lt", "lt"], 67 | ["lv", "lv"], 68 | ["mg", "mg"], 69 | ["mi", "mi"], 70 | ["mk", "mk"], 71 | ["ml", "ml"], 72 | ["mn", "mn"], 73 | ["mr", "mr"], 74 | ["ms", "ms"], 75 | ["mt", "mt"], 76 | ["my", "my"], 77 | ["ne", "ne"], 78 | ["no", "no"], 79 | ["ny", "ny"], 80 | ["or", "or"], 81 | ["pa", "pa"], 82 | ["ps", "ps"], 83 | ["ro", "ro"], 84 | ["rw", "rw"], 85 | ["si", "si"], 86 | ["sk", "sk"], 87 | ["sl", "sl"], 88 | ["sm", "sm"], 89 | ["sn", "sn"], 90 | ["so", "so"], 91 | ["sq", "sq"], 92 | ["sr", "sr"], 93 | ["sr-Cyrl", "sr"], 94 | ["sr-Latn", "sr"], 95 | ["st", "st"], 96 | ["su", "su"], 97 | ["sv", "sv"], 98 | ["sw", "sw"], 99 | ["ta", "ta"], 100 | ["te", "te"], 101 | ["tg", "tg"], 102 | ["th", "th"], 103 | ["tk", "tk"], 104 | ["tl", "tl"], 105 | ["tr", "tr"], 106 | ["tt", "tt"], 107 | ["ug", "ug"], 108 | ["uk", "uk"], 109 | ["ur", "ur"], 110 | ["uz", "uz"], 111 | ["vi", "vi"], 112 | ["xh", "xh"], 113 | ["yi", "yi"], 114 | ["yo", "yo"], 115 | ["zu", "zu"], 116 | ]; 117 | 118 | const langMap = new Map(supportedLanguages); 119 | 120 | exports.supportedLanguages = supportedLanguages; 121 | exports.langMap = langMap; -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WhyMeta/bob-plugin-siliconflow-tts/77e9d161fb546f0e9bf9f8fee278d3ed021ae5df/src/icon.png -------------------------------------------------------------------------------- /src/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.whymeta.bob-plugin-siliconflow-tts", 3 | "version": "0.0.4", 4 | "category": "tts", 5 | "name": "Siliconflow 文本转语音插件", 6 | "summary": "Siliconflow 文本转语音插件", 7 | "icon": "137", 8 | "author": "WhyMeta ", 9 | "homepage": "https://github.com/whymeta/bob-plugin-siliconflow-tts", 10 | "appcast": "https://raw.githubusercontent.com/whymeta/bob-plugin-siliconflow-tts/main/appcast.json", 11 | "minBobVersion": "1.6.0", 12 | "apiUrl": "https://api.siliconflow.cn", 13 | "options": [ 14 | { 15 | "identifier": "apiKey", 16 | "type": "text", 17 | "title": "输入 API Key" 18 | }, 19 | { 20 | "identifier": "voice", 21 | "type": "menu", 22 | "title": "音色", 23 | "defaultValue": "anna", 24 | "menuValues": [ 25 | { 26 | "title": "沉稳男声 - alex", 27 | "value": "alex" 28 | }, 29 | { 30 | "title": "低沉男声 - benjamin", 31 | "value": "benjamin" 32 | }, 33 | { 34 | "title": "磁性男声 - charles", 35 | "value": "charles" 36 | }, 37 | { 38 | "title": "欢快男声 - david", 39 | "value": "david" 40 | }, 41 | { 42 | "title": "沉稳女声 - anna", 43 | "value": "anna" 44 | }, 45 | { 46 | "title": "激情女声 - bella", 47 | "value": "bella" 48 | }, 49 | { 50 | "title": "温柔女声 - claire", 51 | "value": "claire" 52 | }, 53 | { 54 | "title": "欢快女声 - diana", 55 | "value": "diana" 56 | } 57 | ] 58 | }, 59 | { 60 | "identifier": "speed", 61 | "type": "menu", 62 | "title": "音频速度", 63 | "defaultValue": "1.0", 64 | "menuValues": [ 65 | { 66 | "title": "0.25x", 67 | "value": "0.25" 68 | }, 69 | { 70 | "title": "0.5x", 71 | "value": "0.5" 72 | }, 73 | { 74 | "title": "0.75x", 75 | "value": "0.75" 76 | }, 77 | { 78 | "title": "1.0x", 79 | "value": "1.0" 80 | }, 81 | { 82 | "title": "1.25x", 83 | "value": "1.25" 84 | }, 85 | { 86 | "title": "1.5x", 87 | "value": "1.5" 88 | }, 89 | { 90 | "title": "1.75x", 91 | "value": "1.75" 92 | }, 93 | { 94 | "title": "2.0x", 95 | "value": "2.0" 96 | }, 97 | { 98 | "title": "2.25x", 99 | "value": "2.25" 100 | }, 101 | { 102 | "title": "2.5x", 103 | "value": "2.5" 104 | }, 105 | { 106 | "title": "2.75x", 107 | "value": "2.75" 108 | }, 109 | { 110 | "title": "3.0x", 111 | "value": "3.0" 112 | }, 113 | { 114 | "title": "3.25x", 115 | "value": "3.25" 116 | }, 117 | { 118 | "title": "3.5x", 119 | "value": "3.5" 120 | }, 121 | { 122 | "title": "3.75x", 123 | "value": "3.75" 124 | }, 125 | { 126 | "title": "4.0x", 127 | "value": "4.0" 128 | } 129 | ] 130 | }, 131 | { 132 | "identifier": "gain", 133 | "type": "menu", 134 | "title": "音频增益", 135 | "defaultValue": "0.0", 136 | "menuValues": [ 137 | { 138 | "title": "-10.0dB", 139 | "value": "-10.0" 140 | }, 141 | { 142 | "title": "-9.0dB", 143 | "value": "-9.0" 144 | }, 145 | { 146 | "title": "-8.0dB", 147 | "value": "-8.0" 148 | }, 149 | { 150 | "title": "-7.0dB", 151 | "value": "-7.0" 152 | }, 153 | { 154 | "title": "-6.0dB", 155 | "value": "-6.0" 156 | }, 157 | { 158 | "title": "-5.0dB", 159 | "value": "-5.0" 160 | }, 161 | { 162 | "title": "-4.0dB", 163 | "value": "-4.0" 164 | }, 165 | { 166 | "title": "-3.0dB", 167 | "value": "-3.0" 168 | }, 169 | { 170 | "title": "-2.0dB", 171 | "value": "-2.0" 172 | }, 173 | { 174 | "title": "-1.0dB", 175 | "value": "-1.0" 176 | }, 177 | { 178 | "title": "0.0dB", 179 | "value": "0.0" 180 | }, 181 | { 182 | "title": "1.0dB", 183 | "value": "1.0" 184 | }, 185 | { 186 | "title": "2.0dB", 187 | "value": "2.0" 188 | }, 189 | { 190 | "title": "3.0dB", 191 | "value": "3.0" 192 | }, 193 | { 194 | "title": "4.0dB", 195 | "value": "4.0" 196 | }, 197 | { 198 | "title": "5.0dB", 199 | "value": "5.0" 200 | }, 201 | { 202 | "title": "6.0dB", 203 | "value": "6.0" 204 | }, 205 | { 206 | "title": "7.0dB", 207 | "value": "7.0" 208 | }, 209 | { 210 | "title": "8.0dB", 211 | "value": "8.0" 212 | }, 213 | { 214 | "title": "9.0dB", 215 | "value": "9.0" 216 | }, 217 | { 218 | "title": "10.0dB", 219 | "value": "10.0" 220 | } 221 | ] 222 | }, 223 | { 224 | "identifier": "timeout", 225 | "type": "text", 226 | "title": "语音合成的超时时间(秒)", 227 | "defaultValue": "60", 228 | "textConfig": { 229 | "type": "visible", 230 | "placeholderText": "60" 231 | } 232 | } 233 | ] 234 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | var config = require('./config.js'); 2 | var utils = require('./utils.js'); 3 | 4 | function supportLanguages() { 5 | return config.supportedLanguages.map(([standardLang]) => standardLang); 6 | } 7 | 8 | function pluginValidate(completion) { 9 | (async () => { 10 | try { 11 | if (!$option.apiKey) { 12 | completion({ 13 | result: false, 14 | error: { 15 | type: "secretKey", 16 | message: "请输入您的 Siliconflow API Key", 17 | troubleshootingLink: "https://bobtranslate.com/faq/" 18 | } 19 | }); 20 | return; 21 | } 22 | 23 | const resp = await $http.request({ 24 | method: "GET", 25 | url: `${$info.apiUrl}/v1/audio/voice/list`, 26 | header: { 27 | 'Authorization': `Bearer ${$option.apiKey}` 28 | } 29 | }); 30 | 31 | if (resp.response.statusCode === 200) { 32 | completion({ result: true }); 33 | } else { 34 | completion({ 35 | result: false, 36 | error: { 37 | type: "secretKey", 38 | message: "Invalid API key", 39 | troubleshootingLink: "https://bobtranslate.com/faq/" 40 | } 41 | }); 42 | } 43 | } catch (err) { 44 | completion({ 45 | result: false, 46 | error: { 47 | type: "network", 48 | message: "Failed to validate API key: " + (err.message || "Unknown error"), 49 | troubleshootingLink: "https://bobtranslate.com/faq/" 50 | } 51 | }); 52 | } 53 | })(); 54 | } 55 | 56 | function tts(query, completion) { 57 | const targetLanguage = utils.langMap.get(query.lang); 58 | if (!targetLanguage) { 59 | const err = new Error(`不支持 ${query.lang} 语种`); 60 | throw err; 61 | } 62 | const originText = query.text; 63 | 64 | try { 65 | $http.request({ 66 | method: 'POST', 67 | url: `${$info.apiUrl}/v1/audio/speech`, 68 | header: { 69 | 'Authorization': `Bearer ${$option.apiKey}`, 70 | 'Content-Type': 'application/json' 71 | }, 72 | body: { 73 | model: 'FunAudioLLM/CosyVoice2-0.5B', 74 | input: originText, 75 | voice: `fishaudio/fish-speech-1.4:${$option.voice}`, 76 | speed: parseFloat($option.speed), 77 | gain: parseFloat($option.gain), 78 | response_format: "mp3", 79 | stream: true 80 | }, 81 | handler: function (resp) { 82 | if (resp.error) { 83 | $log.error(`TTS请求失败: ${resp.error}`); 84 | completion({ 85 | error: { 86 | type: "network", 87 | message: `TTS请求失败: ${resp.error.message || "未知错误"}` 88 | } 89 | }); 90 | return; 91 | } 92 | 93 | if (!resp.rawData) { 94 | completion({ 95 | error: { 96 | type: "data", 97 | message: "未收到音频数据" 98 | } 99 | }); 100 | return; 101 | } 102 | 103 | // let audioData = $data.fromData(resp.rawData); 104 | completion({ 105 | result: { 106 | type: 'base64', 107 | value: resp.rawData.toBase64(), 108 | raw: {} 109 | } 110 | }); 111 | } 112 | }); 113 | } catch (e) { 114 | $log.error(`TTS处理异常: ${e}`); 115 | completion({ 116 | error: { 117 | type: "exception", 118 | message: `TTS处理异常: ${e.message || "未知错误"}` 119 | } 120 | }); 121 | } 122 | } 123 | 124 | function pluginTimeoutInterval() { 125 | return parseInt($option.timeout) || 60; 126 | } 127 | 128 | exports.supportLanguages = supportLanguages; 129 | exports.tts = tts; 130 | exports.pluginValidate = pluginValidate; 131 | exports.pluginTimeoutInterval = pluginTimeoutInterval; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var config = require('./config.js'); 2 | 3 | const langMap = new Map(config.supportedLanguages); 4 | const langMapReverse = new Map(config.supportedLanguages.map(([standardLang, lang]) => [lang, standardLang])); 5 | 6 | exports.langMap = langMap; 7 | exports.langMapReverse = langMapReverse; --------------------------------------------------------------------------------