├── requirements.txt ├── Readme.md └── upload.py /requirements.txt: -------------------------------------------------------------------------------- 1 | gradio>=3.0.0 2 | requests>=2.25.1 3 | pyinstaller>=5.0.0 -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # 硅基流动参考音色上传工具 2 | 3 | ## 简介 4 | 5 | 这是一个用于上传参考音频创建自定义音色的工具,适用于硅基流动API。通过简单的界面操作,您可以轻松上传音频文件,创建专属于您的AI语音音色。 6 | [硅基流动文档](https://docs.siliconflow.cn/cn/userguide/capabilities/text-to-speech#5) 7 | 8 | 9 | ## 功能特点 10 | 11 | - 简洁直观的用户界面 12 | - 支持多种音频格式上传 13 | - 支持选择不同的模型 14 | - 自动验证API密钥 15 | - 提供详细的上传结果信息 16 | - 查询已上传的音频ID 17 | ## 安装方法 18 | 19 | #### 前提条件 20 | 21 | - Python 3.7+ 22 | 23 | #### 安装步骤 24 | 25 | 1. 克隆或下载本仓库 26 | ``` 27 | git clone https://github.com/flyhunterl/SiliconflowVoiceUpLoad.git 28 | cd SiliconflowVoiceUpLoad 29 | ``` 30 | 31 | 2. 安装Python依赖 32 | ``` 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | 3. 运行程序 37 | ``` 38 | python upload.py 39 | ``` 40 | 41 | ## 使用方法 42 | 43 | 1. 启动应用后,您将看到一个简洁的界面 44 | 2. 输入您的API Key(从[硅基流动控制台](https://cloud.siliconflow.cn/account/ak)获取) 45 | 3. 上传参考音频文件(支持mp3、wav等格式) 46 | 4. 选择模型名称 47 | 5. 为您的音色取一个名字 48 | 6. 输入音频中说的文字内容(尽量准确匹配音频内容) 49 | 7. 点击"提交上传"按钮 50 | 8. 上传成功后,您将获得一个音色ID,可用于后续请求 51 | 52 | ## 注意事项 53 | 54 | - **重要:使用自定义音色功能需要完成实名认证** 55 | - 音频文件应清晰无噪音,时长建议在5-30秒之间 56 | - 文字内容应与音频内容精确匹配,这将影响克隆音色的质量 57 | - 上传过程可能需要一些时间,请耐心等待 58 | - 非本地使用请把代码中的server_name="127.0.0.1",改成server_name="0.0.0.0", 59 | 60 | ## 常见问题 61 | 62 | ### Q: 为什么上传失败? 63 | A: 请检查以下几点: 64 | - API Key是否正确 65 | - 网络连接是否正常 66 | - 音频文件是否符合要求 67 | - 是否已完成实名认证 68 | 69 | ### Q: 如何获取API Key? 70 | A: 登录[硅基流动控制台](https://cloud.siliconflow.cn/account/ak),在API密钥管理页面获取。 71 | 72 | ### Q: 支持哪些音频格式? 73 | A: 支持常见的音频格式,如mp3、wav、ogg等。 74 | 75 | ## 贡献指南 76 | 77 | 欢迎贡献代码或提出建议!请遵循以下步骤: 78 | 79 | 1. Fork本仓库 80 | 2. 创建您的特性分支 (`git checkout -b feature/amazing-feature`) 81 | 3. 提交您的更改 (`git commit -m 'Add some amazing feature'`) 82 | 4. 推送到分支 (`git push origin feature/amazing-feature`) 83 | 5. 开启一个Pull Request 84 | 85 | ## 打赏 86 | 87 | 您的打赏能让我在下一顿的泡面里加上一根火腿肠。 88 | 89 |  90 | 91 | ## 许可证 92 | 93 | 本项目采用MIT许可证 - 详见[LICENSE](LICENSE)文件 94 | 95 | ## 联系方式 96 | 97 | - 作者博客:[https://llingfei.com](https://llingfei.com) 98 | - 问题反馈:请在[GitHub Issues](https://github.com/flyhunterl/SiliconflowVoiceUpLoad/issues)提交 99 | 100 | -------------------------------------------------------------------------------- /upload.py: -------------------------------------------------------------------------------- 1 | import gradio as gr 2 | import requests 3 | import os 4 | import tempfile 5 | import logging 6 | import sys 7 | import subprocess 8 | import threading 9 | import webbrowser 10 | import json # 添加这一行 11 | from pathlib import Path 12 | 13 | # 禁用代理设置 14 | os.environ['HTTP_PROXY'] = '' 15 | os.environ['HTTPS_PROXY'] = '' 16 | os.environ['http_proxy'] = '' 17 | os.environ['https_proxy'] = '' 18 | 19 | # 配置日志 20 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 21 | logger = logging.getLogger(__name__) 22 | 23 | def upload_voice(api_key, audio_file, model_name, voice_name, voice_text): 24 | """ 25 | 上传音频文件创建自定义参考音色 26 | 27 | 参数: 28 | api_key (str): 硅基流动API密钥 29 | audio_file (str): 音频文件路径 30 | model_name (str): 模型名称 31 | voice_name (str): 参考音频名称 32 | voice_text (str): 参考音频文字内容 33 | 34 | 返回: 35 | str: 上传结果信息 36 | """ 37 | # 参数验证 38 | if not api_key: 39 | return "错误:请输入API Key" 40 | if not audio_file: 41 | return "错误:请上传音频文件" 42 | if not voice_name: 43 | return "错误:请输入参考音频名称" 44 | if not voice_text: 45 | return "错误:请输入参考音频文字内容" 46 | 47 | try: 48 | # 准备API请求 49 | url = "https://api.siliconflow.cn/v1/uploads/audio/voice" 50 | headers = { 51 | "Authorization": f"Bearer {api_key.strip()}" # 去除可能的空格 52 | } 53 | 54 | # 检查文件是否存在 55 | if not os.path.exists(audio_file): 56 | return f"错误:文件不存在 - {audio_file}" 57 | 58 | # 检查文件大小 59 | file_size = os.path.getsize(audio_file) / (1024 * 1024) # 转换为MB 60 | if file_size > 50: # 假设最大限制为50MB 61 | return f"错误:文件过大 ({file_size:.2f}MB),请上传小于50MB的文件" 62 | 63 | logger.info(f"准备上传文件: {os.path.basename(audio_file)}, 大小: {file_size:.2f}MB") 64 | 65 | files = { 66 | "file": open(audio_file, "rb") 67 | } 68 | data = { 69 | "model": model_name.strip(), 70 | "customName": voice_name.strip(), 71 | "text": voice_text.strip() 72 | } 73 | 74 | # 发送请求 75 | logger.info("发送API请求...") 76 | response = requests.post(url, headers=headers, files=files, data=data, timeout=60) 77 | 78 | # 显示响应 79 | if response.status_code == 200: 80 | result = response.json() 81 | logger.info(f"上传成功: {result}") 82 | if "uri" in result: 83 | return f"上传成功!\n\n音色ID (uri): {result['uri']}\n\n您可以将此ID作为后续请求中的voice参数使用。" 84 | else: 85 | return f"上传成功,但未返回uri字段\n\n{result}" 86 | else: 87 | logger.error(f"上传失败: {response.status_code} - {response.text}") 88 | return f"上传失败,状态码: {response.status_code}\n\n{response.text}" 89 | except requests.exceptions.Timeout: 90 | logger.error("请求超时") 91 | return "错误:请求超时,请稍后重试" 92 | except requests.exceptions.ConnectionError: 93 | logger.error("连接错误") 94 | return "错误:无法连接到服务器,请检查网络连接" 95 | except Exception as e: 96 | logger.exception("上传过程中发生错误") 97 | return f"发生错误: {str(e)}" 98 | finally: 99 | # 确保文件被关闭 100 | if 'files' in locals() and 'file' in files: 101 | files['file'].close() 102 | 103 | def validate_api_key(api_key): 104 | """验证API密钥格式""" 105 | if not api_key: 106 | return "请输入API Key" 107 | if len(api_key.strip()) < 10: # 假设API Key至少有10个字符 108 | return "API Key格式可能不正确" 109 | return None 110 | 111 | def create_electron_files(): 112 | """创建Electron应用所需的文件""" 113 | # 创建package.json 114 | package_json = { 115 | "name": "voice-upload-tool", 116 | "version": "1.0.0", 117 | "description": "硅基流动参考音色上传工具", 118 | "main": "main.js", 119 | "scripts": { 120 | "start": "electron ." 121 | }, 122 | "dependencies": { 123 | "electron": "^28.0.0" 124 | } 125 | } 126 | 127 | # 创建main.js,添加隐藏菜单栏的代码 128 | main_js = """ 129 | const { app, BrowserWindow, Menu } = require('electron'); 130 | const path = require('path'); 131 | const url = require('url'); 132 | 133 | let mainWindow; 134 | 135 | function createWindow() { 136 | // 创建浏览器窗口 137 | mainWindow = new BrowserWindow({ 138 | width: 1000, 139 | height: 800, 140 | webPreferences: { 141 | nodeIntegration: true 142 | }, 143 | title: '硅基流动参考音色上传工具' 144 | }); 145 | 146 | // 隐藏菜单栏 147 | Menu.setApplicationMenu(null); 148 | 149 | // 加载应用 150 | mainWindow.loadURL('http://127.0.0.1:7860/'); 151 | 152 | // 打开开发者工具 153 | // mainWindow.webContents.openDevTools(); 154 | 155 | // 当窗口关闭时触发 156 | mainWindow.on('closed', function () { 157 | mainWindow = null; 158 | // 关闭Python服务器 159 | process.exit(); 160 | }); 161 | } 162 | 163 | // 当Electron完成初始化并准备创建浏览器窗口时调用此方法 164 | app.on('ready', createWindow); 165 | 166 | // 当所有窗口关闭时退出应用 167 | app.on('window-all-closed', function () { 168 | if (process.platform !== 'darwin') { 169 | app.quit(); 170 | } 171 | }); 172 | 173 | app.on('activate', function () { 174 | if (mainWindow === null) { 175 | createWindow(); 176 | } 177 | }); 178 | """ 179 | 180 | # 创建electron目录 181 | electron_dir = Path("electron_app") 182 | electron_dir.mkdir(exist_ok=True) 183 | 184 | # 写入文件 185 | with open(electron_dir / "package.json", "w", encoding="utf-8") as f: 186 | import json 187 | json.dump(package_json, f, indent=2) 188 | 189 | with open(electron_dir / "main.js", "w", encoding="utf-8") as f: 190 | f.write(main_js) 191 | 192 | logger.info("Electron文件创建完成") 193 | return electron_dir 194 | 195 | def find_npm(): 196 | """尝试在常见的安装位置查找npm""" 197 | # 检查是否已在PATH中 198 | try: 199 | subprocess.run(["npm", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 200 | return "npm" # npm在PATH中可用 201 | except (subprocess.CalledProcessError, FileNotFoundError): 202 | pass 203 | 204 | # 常见的Node.js安装路径 205 | common_paths = [ 206 | r"C:\Program Files\nodejs\npm.cmd", 207 | r"C:\Program Files (x86)\nodejs\npm.cmd", 208 | r"C:\ProgramData\nodejs\npm.cmd", 209 | os.path.expanduser("~\\AppData\\Roaming\\npm\\npm.cmd"), 210 | r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Node.js\npm.cmd" 211 | ] 212 | 213 | # 检查Node.js安装目录 214 | nodejs_dir = os.environ.get("ProgramFiles") + "\\nodejs" 215 | if os.path.exists(nodejs_dir): 216 | common_paths.append(os.path.join(nodejs_dir, "npm.cmd")) 217 | 218 | # 尝试在注册表中查找Node.js安装路径 219 | try: 220 | import winreg 221 | key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Node.js") 222 | value, _ = winreg.QueryValueEx(key, "InstallPath") 223 | if value: 224 | common_paths.append(os.path.join(value, "npm.cmd")) 225 | except: 226 | pass 227 | 228 | # 检查每个可能的路径 229 | for path in common_paths: 230 | if os.path.exists(path): 231 | logger.info(f"找到npm: {path}") 232 | return path 233 | 234 | return None 235 | 236 | def launch_electron_app(electron_dir): 237 | """启动Electron应用""" 238 | try: 239 | # 查找npm 240 | npm_cmd = find_npm() 241 | if not npm_cmd: 242 | raise FileNotFoundError("未找到npm命令") 243 | 244 | # 检查electron_app目录中是否已安装依赖 245 | node_modules_path = electron_dir / "node_modules" 246 | if not node_modules_path.exists(): 247 | # 安装依赖 248 | logger.info("安装Electron依赖...") 249 | if npm_cmd == "npm": 250 | # npm在PATH中可用 251 | subprocess.run(["npm", "install"], cwd=electron_dir, check=True) 252 | else: 253 | # 使用完整路径 254 | subprocess.run([npm_cmd, "install"], cwd=electron_dir, check=True) 255 | 256 | # 启动Electron应用 257 | logger.info("启动Electron应用...") 258 | if npm_cmd == "npm": 259 | subprocess.Popen(["npm", "start"], cwd=electron_dir) 260 | else: 261 | subprocess.Popen([npm_cmd, "start"], cwd=electron_dir) 262 | return True 263 | 264 | except (subprocess.CalledProcessError, FileNotFoundError) as e: 265 | logger.error(f"未安装npm或安装依赖失败: {e}") 266 | logger.error("将使用浏览器打开应用") 267 | webbrowser.open("http://127.0.0.1:7860/") 268 | return False 269 | except Exception as e: 270 | logger.exception(f"启动Electron应用失败: {e}") 271 | webbrowser.open("http://127.0.0.1:7860/") 272 | return False 273 | 274 | # 创建Gradio界面 275 | def get_voice_list(api_key): 276 | """获取音频列表""" 277 | if not api_key: 278 | return "错误:请输入API Key" 279 | 280 | try: 281 | url = "https://api.siliconflow.cn/v1/audio/voice/list" 282 | headers = { 283 | "Authorization": f"Bearer {api_key.strip()}" 284 | } 285 | 286 | logger.info("发送API请求...") 287 | response = requests.get(url, headers=headers) 288 | 289 | if response.status_code == 200: 290 | result = response.json() 291 | formatted_result = json.dumps(result, indent=2, ensure_ascii=False) 292 | logger.info("获取列表成功") 293 | return formatted_result 294 | else: 295 | logger.error(f"获取列表失败: {response.status_code} - {response.text}") 296 | return f"获取失败,状态码: {response.status_code}\n\n{response.text}" 297 | 298 | except requests.exceptions.Timeout: 299 | logger.error("请求超时") 300 | return "错误:请求超时,请稍后重试" 301 | except requests.exceptions.ConnectionError: 302 | logger.error("连接错误") 303 | return "错误:无法连接到服务器,请检查网络连接" 304 | except Exception as e: 305 | logger.exception("获取列表过程中发生错误") 306 | return f"发生错误: {str(e)}" 307 | 308 | def create_gradio_interface(): 309 | """ 310 | 获取音频列表 311 | 312 | 参数: 313 | api_key (str): 硅基流动API密钥 314 | 315 | 返回: 316 | str: 音频列表信息 317 | """ 318 | if not api_key: 319 | return "错误:请输入API Key" 320 | 321 | try: 322 | url = "https://api.siliconflow.cn/v1/audio/voice/list" 323 | headers = { 324 | "Authorization": f"Bearer {api_key.strip()}" 325 | } 326 | 327 | logger.info("发送API请求...") 328 | response = requests.get(url, headers=headers) 329 | 330 | if response.status_code == 200: 331 | result = response.json() 332 | formatted_result = json.dumps(result, indent=2, ensure_ascii=False) 333 | logger.info("获取列表成功") 334 | return formatted_result 335 | else: 336 | logger.error(f"获取列表失败: {response.status_code} - {response.text}") 337 | return f"获取失败,状态码: {response.status_code}\n\n{response.text}" 338 | 339 | except requests.exceptions.Timeout: 340 | logger.error("请求超时") 341 | return "错误:请求超时,请稍后重试" 342 | except requests.exceptions.ConnectionError: 343 | logger.error("连接错误") 344 | return "错误:无法连接到服务器,请检查网络连接" 345 | except Exception as e: 346 | logger.exception("获取列表过程中发生错误") 347 | return f"发生错误: {str(e)}" 348 | 349 | def create_gradio_interface(): 350 | # 定义颜色变量 351 | primary_color = "#6366f1" # 紫色 352 | light_bg_color = "#f3f4f6" # 浅灰色背景 353 | 354 | # 自定义主题 355 | theme = gr.themes.Soft( 356 | primary_hue="indigo", 357 | secondary_hue="blue", 358 | neutral_hue="gray" 359 | ).set( 360 | button_primary_background_fill=primary_color, 361 | button_primary_background_fill_hover="#4f46e5", # 深一点的紫色 362 | block_title_background_fill=light_bg_color, 363 | block_label_background_fill=light_bg_color, 364 | input_background_fill="white", 365 | background_fill_primary="white" 366 | ) 367 | 368 | with gr.Blocks(title="硅基流动自定义参考音色上传工具", theme=theme) as demo: 369 | # 标题栏 - 使用与按钮相同的背景色 370 | with gr.Row(): 371 | gr.Markdown(f'