├── wrangler.toml ├── LICENSE ├── README.md └── index.js /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "tts-voice-magic" 2 | main = "index.js" 3 | compatibility_date = "2024-01-15" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | [env.production] 7 | name = "tts-voice-magic" 8 | 9 | [env.staging] 10 | name = "tts-voice-magic-staging" 11 | 12 | # 变量配置(如果需要的话) 13 | [vars] 14 | # 可以在这里添加环境变量 15 | 16 | # 如果需要 KV 存储(可选) 17 | # [[kv_namespaces]] 18 | # binding = "MY_KV_NAMESPACE" 19 | # id = "your-kv-namespace-id" 20 | # preview_id = "your-preview-kv-namespace-id" 21 | 22 | # 如果需要 D1 数据库(可选) 23 | # [[d1_databases]] 24 | # binding = "DB" 25 | # database_name = "my-database" 26 | # database_id = "your-database-id" 27 | 28 | # 路由配置 29 | [triggers] 30 | crons = [] 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 一只会飞的旺旺 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎙️ VoiceCraft - AI驱动的语音处理平台 2 | 3 | 一个功能强大的AI语音处理平台,集成了文字转语音(TTS)和语音转文字(STT)双向功能。基于Microsoft Edge TTS和硅基流动API,支持20+种语音选项,为用户提供完整的语音处理解决方案。 4 | > 5 | > 直接使用: https://tts.wangwangit.com 6 | 7 | 8 | 9 | 10 | ## ✨ 特性 11 | 12 | ### 🎯 核心功能 13 | - 🗣️ **文字转语音(TTS)** - 基于Microsoft Edge TTS,支持20+种中文语音 14 | - 🎧 **语音转文字(STT)** - 集成硅基流动API,高精度语音识别 15 | - 🔄 **双向处理** - 智能模式切换,语音与文字无缝转换 16 | - 🌍 **多语言支持** - 支持中文、英文、日文、韩文、西班牙文、法文、德文、俄文 17 | 18 | ### 🎨 用户体验 19 | - ⚡ **秒速生成** - 快速生成高质量语音文件和转录文本 20 | - 🆓 **完全免费** - 无需注册,无使用限制 21 | - 📱 **响应式设计** - 完美适配桌面端和移动端 22 | - 🎛️ **丰富参数** - 支持语速、音调、语音风格等多种调节 23 | - 📥 **支持下载** - 生成的音频可直接下载为 MP3 格式 24 | - 📋 **便捷操作** - 转录结果可复制、编辑,支持转为语音功能 25 | 26 | ### 🔧 技术特性 27 | - 🔗 **API 兼容** - 兼容 OpenAI TTS API 格式 28 | - 🎵 **多音频格式** - 支持mp3、wav、m4a、flac、aac等9种音频格式 29 | - 🔐 **灵活配置** - 支持默认Token和自定义Token配置 30 | - 🎨 **现代化UI** - 优雅的卡片式设计,直观的模式切换 31 | 32 | ## 🚀 一键部署 33 | 34 | ### 点击按钮,一键部署到 CloudFlare Workers, 35 | 36 | [](https://deploy.workers.cloudflare.com/?url=https://github.com/wangwangit/tts) 37 | 38 | 39 | 40 | ## 🎯 使用方法 41 | 42 | ### 🌐 网页界面使用 43 | 44 | #### 文字转语音模式 45 | 1. 访问部署后的 Worker 域名 46 | 2. 确保当前为"文字转语音"模式(默认模式) 47 | 3. 选择输入方式:手动输入或上传txt文件 48 | 4. 在文本框中输入要转换的文字,或上传txt文件 49 | 5. 选择喜欢的语音、语速、音调、语音风格等参数 50 | 6. 点击"开始生成语音"按钮 51 | 7. 播放生成的音频或下载 MP3 文件 52 | 53 | #### 语音转文字模式 54 | 1. 点击页面顶部的"语音转文字"按钮切换模式 55 | 2. 上传音频文件(支持mp3、wav、m4a等9种格式,最大10MB) 56 | 3. 选择Token配置:使用默认Token或输入自定义硅基流动Token 57 | 4. 点击"开始语音转录"按钮 58 | 5. 查看转录结果,支持复制、编辑或直接转为语音 59 | 60 | #### 🌍 多语言切换 61 | - 点击右上角的语言切换器 62 | - 支持中文、English、日本語、한국어、Español、Français、Deutsch、Русский 63 | - 自动记住用户的语言偏好 64 | 65 | ### 🔌 API 调用 66 | 67 | #### 文字转语音 API 68 | 69 | ```javascript 70 | // JavaScript 调用示例 71 | const response = await fetch('https://your-worker.workers.dev/v1/audio/speech', { 72 | method: 'POST', 73 | headers: { 74 | 'Content-Type': 'application/json', 75 | }, 76 | body: JSON.stringify({ 77 | input: "你好,这是一个测试", 78 | voice: "zh-CN-XiaoxiaoNeural", 79 | speed: 1.0, 80 | pitch: "0", 81 | style: "general" 82 | }) 83 | }); 84 | 85 | const audioBlob = await response.blob(); 86 | ``` 87 | 88 | ```bash 89 | # cURL 调用示例 90 | curl -X POST "https://your-worker.workers.dev/v1/audio/speech" \ 91 | -H "Content-Type: application/json" \ 92 | -d '{ 93 | "input": "你好,这是一个测试", 94 | "voice": "zh-CN-XiaoxiaoNeural", 95 | "speed": 1.0, 96 | "pitch": "0", 97 | "style": "general" 98 | }' \ 99 | --output speech.mp3 100 | ``` 101 | 102 | #### 语音转文字 API 103 | 104 | ```javascript 105 | // JavaScript 调用示例 106 | const formData = new FormData(); 107 | formData.append('file', audioFile); // 音频文件 108 | formData.append('token', 'your-siliconflow-token'); // 可选,不提供则使用默认token 109 | 110 | const response = await fetch('https://your-worker.workers.dev/v1/audio/transcriptions', { 111 | method: 'POST', 112 | body: formData 113 | }); 114 | 115 | const result = await response.json(); 116 | console.log(result.text); // 转录结果 117 | ``` 118 | 119 | ```bash 120 | # cURL 调用示例 121 | curl -X POST "https://your-worker.workers.dev/v1/audio/transcriptions" \ 122 | -F "file=@audio.mp3" \ 123 | -F "token=your-siliconflow-token" 124 | ``` 125 | 126 | ## 🎨 支持的语音 127 | 128 | ### 女声 129 | - `zh-CN-XiaoxiaoNeural` - 晓晓 (温柔) 130 | - `zh-CN-XiaoyiNeural` - 晓伊 (甜美) 131 | - `zh-CN-XiaochenNeural` - 晓辰 (知性) 132 | - `zh-CN-XiaohanNeural` - 晓涵 (优雅) 133 | - `zh-CN-XiaomengNeural` - 晓梦 (梦幻) 134 | - `zh-CN-XiaomoNeural` - 晓墨 (文艺) 135 | - `zh-CN-XiaoqiuNeural` - 晓秋 (成熟) 136 | - `zh-CN-XiaoruiNeural` - 晓睿 (智慧) 137 | - `zh-CN-XiaoshuangNeural` - 晓双 (活泼) 138 | - `zh-CN-XiaoxuanNeural` - 晓萱 (清新) 139 | - `zh-CN-XiaoyanNeural` - 晓颜 (柔美) 140 | - `zh-CN-XiaoyouNeural` - 晓悠 (悠扬) 141 | - `zh-CN-XiaozhenNeural` - 晓甄 (端庄) 142 | 143 | ### 男声 144 | - `zh-CN-YunxiNeural` - 云希 (清朗) 145 | - `zh-CN-YunyangNeural` - 云扬 (阳光) 146 | - `zh-CN-YunjianNeural` - 云健 (稳重) 147 | - `zh-CN-YunfengNeural` - 云枫 (磁性) 148 | - `zh-CN-YunhaoNeural` - 云皓 (豪迈) 149 | - `zh-CN-YunxiaNeural` - 云夏 (热情) 150 | - `zh-CN-YunyeNeural` - 云野 (野性) 151 | - `zh-CN-YunzeNeural` - 云泽 (深沉) 152 | 153 | ## ⚙️ API 参数 154 | 155 | ### 🗣️ 文字转语音 API 参数 156 | 157 | | 参数 | 类型 | 默认值 | 说明 | 158 | |------|------|--------|------| 159 | | `input` | string | - | 要转换的文本内容(必填) | 160 | | `voice` | string | `zh-CN-XiaoxiaoNeural` | 语音选择 | 161 | | `speed` | number | `1.0` | 语速 (0.5-2.0) | 162 | | `pitch` | string | `"0"` | 音调 (-50 到 50) | 163 | | `style` | string | `"general"` | 语音风格 | 164 | | `volume` | string | `"0"` | 音量调节 | 165 | 166 | ### 🎧 语音转文字 API 参数 167 | 168 | | 参数 | 类型 | 默认值 | 说明 | 169 | |------|------|--------|------| 170 | | `file` | File | - | 音频文件(必填,支持多种格式) | 171 | | `token` | string | 默认内置 | 硅基流动API Token(可选) | 172 | 173 | #### 支持的音频格式 174 | - **文件格式**: mp3, wav, m4a, flac, aac, ogg, webm, amr, 3gp 175 | - **文件大小**: 最大 10MB 176 | - **模型**: FunAudioLLM/SenseVoiceSmall(自动使用) 177 | 178 | ### 支持的语音风格 179 | 180 | - `general` - 通用风格 181 | - `assistant` - 智能助手 182 | - `chat` - 聊天对话 183 | - `customerservice` - 客服专业 184 | - `newscast` - 新闻播报 185 | - `affectionate` - 亲切温暖 186 | - `calm` - 平静舒缓 187 | - `cheerful` - 愉快欢乐 188 | - `gentle` - 温和柔美 189 | - `lyrical` - 抒情诗意 190 | - `serious` - 严肃正式 191 | 192 | ## 🛠️ 技术架构 193 | 194 | ### 🔧 核心技术 195 | - **前端**: 现代化 HTML5 + CSS3 + 原生JavaScript 196 | - **后端**: Cloudflare Workers(边缘计算) 197 | - **TTS引擎**: Microsoft Edge TTS(20+种中文语音) 198 | - **STT引擎**: 硅基流动 FunAudioLLM/SenseVoiceSmall 199 | - **国际化**: 内置8种语言支持,自动检测浏览器语言 200 | 201 | ### 🎨 设计架构 202 | - **设计系统**: CSS变量 + 响应式布局 203 | - **UI框架**: 无依赖,纯原生实现 204 | - **交互设计**: 双向模式切换,直观的用户体验 205 | - **API设计**: RESTful API,兼容OpenAI格式 206 | 207 | ### 🔒 安全与性能 208 | - **无服务器**: 基于Cloudflare Workers,全球边缘部署 209 | - **数据安全**: 所有处理在边缘完成,无数据存储 210 | - **高可用**: 全球CDN加速,99.9%可用性 211 | - **零配置**: 开箱即用,无需额外配置 212 | 213 | ## 🎨 设计特色 214 | 215 | ### 🎯 用户界面 216 | - **双模式设计**: 水平布局的模式切换器,消除界面空白 217 | - **现代化 UI**: 采用简洁的卡片式设计 218 | - **国际化界面**: 8种语言无缝切换,右上角语言选择器 219 | - **响应式布局**: 完美适配各种设备尺寸 220 | - **微交互**: 丰富的悬停效果和动画 221 | 222 | ### 🎨 视觉体验 223 | - **统一风格**: 新旧功能视觉一致,无违和感 224 | - **智能切换**: 根据模式动态显示相关界面元素 225 | - **无渐变设计**: 使用纯色设计,更加专业 226 | - **可访问性**: 支持键盘导航和屏幕阅读器 227 | 228 | ## 📱 移动端优化 229 | 230 | ### 🔄 适配策略 231 | - **模式切换**: 移动端垂直布局,桌面端水平布局 232 | - **触摸友好**: 按钮尺寸针对移动端优化 233 | - **文件上传**: 支持拖拽和点击两种方式 234 | - **性能优化**: 针对移动设备的网络和性能优化 235 | 236 | ### 📋 功能适配 237 | - **音频上传**: 移动端优化的文件选择界面 238 | - **结果展示**: 移动端友好的转录结果展示 239 | - **语言切换**: 移动端下拉菜单适配 240 | 241 | ## 🔧 开发 242 | 243 | ### 本地开发 244 | 245 | ```bash 246 | # 克隆项目 247 | git clone 248 | 249 | # 安装 Wrangler CLI 250 | npm install -g wrangler 251 | 252 | # 本地开发 253 | wrangler dev 254 | ``` 255 | 256 | ### 项目结构 257 | 258 | ``` 259 | ├── index.js # 主要代码文件 260 | ├── README.md # 项目文档 261 | └── wrangler.toml # Cloudflare Workers 配置 262 | ``` 263 | 264 | ## 🤝 贡献 265 | 266 | 欢迎提交 Issue 和 Pull Request! 267 | 268 | ## 📄 许可证 269 | 270 | MIT License 271 | 272 | ## 🌟 更新日志 273 | 274 | ### v2.0.0 - 重大更新 275 | - 🎉 **新增语音转文字功能** - 集成硅基流动API,支持高精度语音识别 276 | - 🌍 **多语言国际化** - 支持8种语言,自动检测浏览器语言 277 | - 🎨 **品牌升级** - 更名为VoiceCraft,全新的AI语音处理平台 278 | - 🔄 **双向处理** - 智能模式切换,语音与文字无缝转换 279 | - 📱 **界面优化** - 水平布局的模式切换器,更好的空间利用 280 | - 🔧 **错误修复** - 修复文本包含特殊字符时的XML转义问题 281 | 282 | ### v1.x.x - 历史版本 283 | - 基础的文字转语音功能 284 | - Microsoft Edge TTS集成 285 | - 响应式设计 286 | 287 | ## 🙏 致谢 288 | 289 | - **Microsoft Edge TTS** - 提供高质量的语音合成服务 290 | - **硅基流动** - 提供先进的语音识别API 291 | - **Cloudflare Workers** - 提供无服务器计算平台 292 | - **开源社区** - 感谢所有贡献者和用户的支持 293 | 294 | ## 📞 联系我们 295 | 296 | 关注公众号「一只会飞的旺旺」获取更多 AI 工具和技术分享: 297 | 298 | - 🔥 最新 AI 工具推荐和使用教程 299 | - 🚀 前沿技术解析和实战案例 300 | - 💎 独家资源和工具源码分享 301 | - 💬 技术问题答疑和交流社群 302 | 303 | --- 304 | 305 | **🎙️ VoiceCraft - 让语音处理更智能,让创意更有声音!** 306 | 307 | *从文字到语音,从语音到文字,AI驱动的完整语音处理解决方案。* 308 | 309 | 310 | 311 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const TOKEN_REFRESH_BEFORE_EXPIRY = 3 * 60; 2 | let tokenInfo = { 3 | endpoint: null, 4 | token: null, 5 | expiredAt: null 6 | }; 7 | 8 | // HTML 页面模板 9 | const HTML_PAGE = ` 10 | 11 | 12 | 13 | 14 | 15 | VoiceCraft - AI-Powered Voice Processing Platform 16 | 17 | 18 | 895 | 896 | 897 | 898 | 899 | 900 | 🌐 901 | English 902 | 903 | 904 | 905 | 906 | 907 | 908 | 🇺🇸 909 | English 910 | 911 | 912 | 🇨🇳 913 | 中文 914 | 915 | 916 | 🇯🇵 917 | 日本語 918 | 919 | 920 | 🇰🇷 921 | 한국어 922 | 923 | 924 | 🇪🇸 925 | Español 926 | 927 | 928 | 🇫🇷 929 | Français 930 | 931 | 932 | 🇩🇪 933 | Deutsch 934 | 935 | 936 | 🇷🇺 937 | Русский 938 | 939 | 940 | 941 | 942 | 943 | 944 | VoiceCraft 945 | AI-Powered Voice Processing Platform 946 | 947 | 948 | ✨ 949 | 20+ Voice Options 950 | 951 | 952 | ⚡ 953 | Lightning Fast 954 | 955 | 956 | 🆓 957 | Completely Free 958 | 959 | 960 | 📱 961 | Download Support 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | Text to Speech 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | Speech to Text 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 选择输入方式 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 手动输入 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 上传文件 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 输入文本 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 上传txt文件 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 拖拽txt文件到此处,或点击选择文件 1037 | 支持txt格式,最大500KB 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | ✕ 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 语音选择 1054 | 1055 | 晓晓 (女声·温柔) 1056 | 云希 (男声·清朗) 1057 | 云扬 (男声·阳光) 1058 | 晓伊 (女声·甜美) 1059 | 云健 (男声·稳重) 1060 | 晓辰 (女声·知性) 1061 | 晓涵 (女声·优雅) 1062 | 晓梦 (女声·梦幻) 1063 | 晓墨 (女声·文艺) 1064 | 晓秋 (女声·成熟) 1065 | 晓睿 (女声·智慧) 1066 | 晓双 (女声·活泼) 1067 | 晓萱 (女声·清新) 1068 | 晓颜 (女声·柔美) 1069 | 晓悠 (女声·悠扬) 1070 | 晓甄 (女声·端庄) 1071 | 云枫 (男声·磁性) 1072 | 云皓 (男声·豪迈) 1073 | 云夏 (男声·热情) 1074 | 云野 (男声·野性) 1075 | 云泽 (男声·深沉) 1076 | 1077 | 1078 | 1079 | 1080 | 语速调节 1081 | 1082 | 🐌 很慢 1083 | 🚶 慢速 1084 | ⚡ 正常 1085 | 🏃 快速 1086 | 🚀 很快 1087 | 💨 极速 1088 | 1089 | 1090 | 1091 | 1092 | 音调高低 1093 | 1094 | 📉 很低沉 1095 | 📊 低沉 1096 | 🎵 标准 1097 | 📈 高亢 1098 | 🎶 很高亢 1099 | 1100 | 1101 | 1102 | 1103 | 语音风格 1104 | 1105 | 🎭 通用风格 1106 | 🤖 智能助手 1107 | 💬 聊天对话 1108 | 📞 客服专业 1109 | 📺 新闻播报 1110 | 💕 亲切温暖 1111 | 😌 平静舒缓 1112 | 😊 愉快欢乐 1113 | 🌸 温和柔美 1114 | 🎼 抒情诗意 1115 | 🎯 严肃正式 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 🎙️ 1122 | 开始生成语音 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 正在生成语音,请稍候... 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 📥 1137 | 下载音频文件 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 上传音频文件 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 拖拽音频文件到此处,或点击选择文件 1163 | 支持mp3、wav、m4a、flac、aac、ogg、webm、amr、3gp格式,最大10MB 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 | ✕ 1173 | 1174 | 1175 | 1176 | 1177 | API Token配置 1178 | 1179 | 1180 | 1181 | 1182 | 使用默认Token 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 使用硅基流动自定义Token 1189 | 1190 | 1191 | 1192 | 1194 | 1195 | 1196 | 1197 | 🎧 1198 | 开始语音转录 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 正在转录音频,请稍候... 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 转录结果 1212 | 1214 | 1215 | 1216 | 📋 1217 | 复制文本 1218 | 1219 | 1220 | ✏️ 1221 | 编辑文本 1222 | 1223 | 1224 | 🎙️ 1225 | 转为语音 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 🎉 生成成功!喜欢这个工具吗? 1240 | 关注我们获取更多AI工具和技术分享 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 关注「一只会飞的旺旺」公众号 1248 | 获取更多实用的AI工具、技术教程和独家资源分享 1249 | 1250 | 最新AI工具推荐和使用教程 1251 | 前沿技术解析和实战案例 1252 | 独家资源和工具源码分享 1253 | 技术问题答疑和交流社群 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 2100 | 2101 | 2102 | `; 2103 | 2104 | export default { 2105 | async fetch(request, env, ctx) { 2106 | return handleRequest(request); 2107 | } 2108 | }; 2109 | 2110 | async function handleRequest(request) { 2111 | if (request.method === "OPTIONS") { 2112 | return handleOptions(request); 2113 | } 2114 | 2115 | 2116 | 2117 | 2118 | const requestUrl = new URL(request.url); 2119 | const path = requestUrl.pathname; 2120 | 2121 | // 返回前端页面 2122 | if (path === "/" || path === "/index.html") { 2123 | return new Response(HTML_PAGE, { 2124 | headers: { 2125 | "Content-Type": "text/html; charset=utf-8", 2126 | ...makeCORSHeaders() 2127 | } 2128 | }); 2129 | } 2130 | 2131 | if (path === "/v1/audio/transcriptions") { 2132 | try { 2133 | return await handleAudioTranscription(request); 2134 | } catch (error) { 2135 | console.error("Audio transcription error:", error); 2136 | return new Response(JSON.stringify({ 2137 | error: { 2138 | message: error.message, 2139 | type: "api_error", 2140 | param: null, 2141 | code: "transcription_error" 2142 | } 2143 | }), { 2144 | status: 500, 2145 | headers: { 2146 | "Content-Type": "application/json", 2147 | ...makeCORSHeaders() 2148 | } 2149 | }); 2150 | } 2151 | } 2152 | 2153 | if (path === "/v1/audio/speech") { 2154 | try { 2155 | const contentType = request.headers.get("content-type") || ""; 2156 | 2157 | // 处理文件上传 2158 | if (contentType.includes("multipart/form-data")) { 2159 | return await handleFileUpload(request); 2160 | } 2161 | 2162 | // 处理JSON请求(原有功能) 2163 | const requestBody = await request.json(); 2164 | const { 2165 | input, 2166 | voice = "zh-CN-XiaoxiaoNeural", 2167 | speed = '1.0', 2168 | volume = '0', 2169 | pitch = '0', 2170 | style = "general" 2171 | } = requestBody; 2172 | 2173 | let rate = parseInt(String((parseFloat(speed) - 1.0) * 100)); 2174 | let numVolume = parseInt(String(parseFloat(volume) * 100)); 2175 | let numPitch = parseInt(pitch); 2176 | const response = await getVoice( 2177 | input, 2178 | voice, 2179 | rate >= 0 ? `+${rate}%` : `${rate}%`, 2180 | numPitch >= 0 ? `+${numPitch}Hz` : `${numPitch}Hz`, 2181 | numVolume >= 0 ? `+${numVolume}%` : `${numVolume}%`, 2182 | style, 2183 | "audio-24khz-48kbitrate-mono-mp3" 2184 | ); 2185 | 2186 | return response; 2187 | 2188 | } catch (error) { 2189 | console.error("Error:", error); 2190 | return new Response(JSON.stringify({ 2191 | error: { 2192 | message: error.message, 2193 | type: "api_error", 2194 | param: null, 2195 | code: "edge_tts_error" 2196 | } 2197 | }), { 2198 | status: 500, 2199 | headers: { 2200 | "Content-Type": "application/json", 2201 | ...makeCORSHeaders() 2202 | } 2203 | }); 2204 | } 2205 | } 2206 | 2207 | // 默认返回 404 2208 | return new Response("Not Found", { status: 404 }); 2209 | } 2210 | 2211 | async function handleOptions(request) { 2212 | return new Response(null, { 2213 | status: 204, 2214 | headers: { 2215 | ...makeCORSHeaders(), 2216 | "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", 2217 | "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers") || "Authorization" 2218 | } 2219 | }); 2220 | } 2221 | 2222 | // 添加延迟函数 2223 | function delay(ms) { 2224 | return new Promise(resolve => setTimeout(resolve, ms)); 2225 | } 2226 | 2227 | // 优化文本分块函数 2228 | function optimizedTextSplit(text, maxChunkSize = 1500) { 2229 | const chunks = []; 2230 | const sentences = text.split(/[。!?\n]/); 2231 | let currentChunk = ''; 2232 | 2233 | for (const sentence of sentences) { 2234 | const trimmedSentence = sentence.trim(); 2235 | if (!trimmedSentence) continue; 2236 | 2237 | // 如果单个句子就超过最大长度,按字符分割 2238 | if (trimmedSentence.length > maxChunkSize) { 2239 | if (currentChunk) { 2240 | chunks.push(currentChunk.trim()); 2241 | currentChunk = ''; 2242 | } 2243 | 2244 | // 按字符分割长句子 2245 | for (let i = 0; i < trimmedSentence.length; i += maxChunkSize) { 2246 | chunks.push(trimmedSentence.slice(i, i + maxChunkSize)); 2247 | } 2248 | } else if ((currentChunk + trimmedSentence).length > maxChunkSize) { 2249 | // 当前块加上新句子会超过限制,先保存当前块 2250 | if (currentChunk) { 2251 | chunks.push(currentChunk.trim()); 2252 | } 2253 | currentChunk = trimmedSentence; 2254 | } else { 2255 | // 添加到当前块 2256 | currentChunk += (currentChunk ? '。' : '') + trimmedSentence; 2257 | } 2258 | } 2259 | 2260 | // 添加最后一个块 2261 | if (currentChunk.trim()) { 2262 | chunks.push(currentChunk.trim()); 2263 | } 2264 | 2265 | return chunks.filter(chunk => chunk.length > 0); 2266 | } 2267 | 2268 | // 批量处理音频块 2269 | async function processBatchedAudioChunks(chunks, voiceName, rate, pitch, volume, style, outputFormat, batchSize = 3, delayMs = 1000) { 2270 | const audioChunks = []; 2271 | 2272 | for (let i = 0; i < chunks.length; i += batchSize) { 2273 | const batch = chunks.slice(i, i + batchSize); 2274 | const batchPromises = batch.map(async (chunk, index) => { 2275 | try { 2276 | // 为每个请求添加小延迟,避免同时发送 2277 | if (index > 0) { 2278 | await delay(index * 200); 2279 | } 2280 | return await getAudioChunk(chunk, voiceName, rate, pitch, volume, style, outputFormat); 2281 | } catch (error) { 2282 | console.error(`处理音频块失败 (批次 ${Math.floor(i/batchSize) + 1}, 块 ${index + 1}):`, error); 2283 | throw error; 2284 | } 2285 | }); 2286 | 2287 | try { 2288 | const batchResults = await Promise.all(batchPromises); 2289 | audioChunks.push(...batchResults); 2290 | 2291 | // 批次间延迟 2292 | if (i + batchSize < chunks.length) { 2293 | await delay(delayMs); 2294 | } 2295 | } catch (error) { 2296 | console.error(`批次处理失败:`, error); 2297 | throw error; 2298 | } 2299 | } 2300 | 2301 | return audioChunks; 2302 | } 2303 | 2304 | async function getVoice(text, voiceName = "zh-CN-XiaoxiaoNeural", rate = '+0%', pitch = '+0Hz', volume = '+0%', style = "general", outputFormat = "audio-24khz-48kbitrate-mono-mp3") { 2305 | try { 2306 | // 文本预处理 2307 | const cleanText = text.trim(); 2308 | if (!cleanText) { 2309 | throw new Error("文本内容为空"); 2310 | } 2311 | 2312 | // 如果文本很短,直接处理 2313 | if (cleanText.length <= 1500) { 2314 | const audioBlob = await getAudioChunk(cleanText, voiceName, rate, pitch, volume, style, outputFormat); 2315 | return new Response(audioBlob, { 2316 | headers: { 2317 | "Content-Type": "audio/mpeg", 2318 | ...makeCORSHeaders() 2319 | } 2320 | }); 2321 | } 2322 | 2323 | // 优化的文本分块 2324 | const chunks = optimizedTextSplit(cleanText, 1500); 2325 | 2326 | // 检查分块数量,防止超过CloudFlare限制 2327 | if (chunks.length > 40) { 2328 | throw new Error(`文本过长,分块数量(${chunks.length})超过限制。请缩短文本或分批处理。`); 2329 | } 2330 | 2331 | console.log(`文本已分为 ${chunks.length} 个块进行处理`); 2332 | 2333 | // 批量处理音频块,控制并发数量和频率 2334 | const audioChunks = await processBatchedAudioChunks( 2335 | chunks, 2336 | voiceName, 2337 | rate, 2338 | pitch, 2339 | volume, 2340 | style, 2341 | outputFormat, 2342 | 3, // 每批处理3个 2343 | 800 // 批次间延迟800ms 2344 | ); 2345 | 2346 | // 将音频片段拼接起来 2347 | const concatenatedAudio = new Blob(audioChunks, { type: 'audio/mpeg' }); 2348 | return new Response(concatenatedAudio, { 2349 | headers: { 2350 | "Content-Type": "audio/mpeg", 2351 | ...makeCORSHeaders() 2352 | } 2353 | }); 2354 | 2355 | } catch (error) { 2356 | console.error("语音合成失败:", error); 2357 | return new Response(JSON.stringify({ 2358 | error: { 2359 | message: error.message || String(error), 2360 | type: "api_error", 2361 | param: `${voiceName}, ${rate}, ${pitch}, ${volume}, ${style}, ${outputFormat}`, 2362 | code: "edge_tts_error" 2363 | } 2364 | }), { 2365 | status: 500, 2366 | headers: { 2367 | "Content-Type": "application/json", 2368 | ...makeCORSHeaders() 2369 | } 2370 | }); 2371 | } 2372 | } 2373 | 2374 | 2375 | 2376 | //获取单个音频数据(增强错误处理和重试机制) 2377 | async function getAudioChunk(text, voiceName, rate, pitch, volume, style, outputFormat = 'audio-24khz-48kbitrate-mono-mp3', maxRetries = 3) { 2378 | const retryDelay = 500; // 重试延迟500ms 2379 | 2380 | for (let attempt = 0; attempt <= maxRetries; attempt++) { 2381 | try { 2382 | const endpoint = await getEndpoint(); 2383 | const url = `https://${endpoint.r}.tts.speech.microsoft.com/cognitiveservices/v1`; 2384 | 2385 | // 处理文本中的延迟标记 2386 | let m = text.match(/\[(\d+)\]\s*?$/); 2387 | let slien = 0; 2388 | if (m && m.length == 2) { 2389 | slien = parseInt(m[1]); 2390 | text = text.replace(m[0], ''); 2391 | } 2392 | 2393 | // 验证文本长度 2394 | if (!text.trim()) { 2395 | throw new Error("文本块为空"); 2396 | } 2397 | 2398 | if (text.length > 2000) { 2399 | throw new Error(`文本块过长: ${text.length} 字符,最大支持2000字符`); 2400 | } 2401 | 2402 | const response = await fetch(url, { 2403 | method: "POST", 2404 | headers: { 2405 | "Authorization": endpoint.t, 2406 | "Content-Type": "application/ssml+xml", 2407 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0", 2408 | "X-Microsoft-OutputFormat": outputFormat 2409 | }, 2410 | body: getSsml(text, voiceName, rate, pitch, volume, style, slien) 2411 | }); 2412 | 2413 | if (!response.ok) { 2414 | const errorText = await response.text(); 2415 | 2416 | // 根据错误类型决定是否重试 2417 | if (response.status === 429) { 2418 | // 频率限制,需要重试 2419 | if (attempt < maxRetries) { 2420 | console.log(`频率限制,第${attempt + 1}次重试,等待${retryDelay * (attempt + 1)}ms`); 2421 | await delay(retryDelay * (attempt + 1)); 2422 | continue; 2423 | } 2424 | throw new Error(`请求频率过高,已重试${maxRetries}次仍失败`); 2425 | } else if (response.status >= 500) { 2426 | // 服务器错误,可以重试 2427 | if (attempt < maxRetries) { 2428 | console.log(`服务器错误,第${attempt + 1}次重试,等待${retryDelay * (attempt + 1)}ms`); 2429 | await delay(retryDelay * (attempt + 1)); 2430 | continue; 2431 | } 2432 | throw new Error(`Edge TTS服务器错误: ${response.status} ${errorText}`); 2433 | } else { 2434 | // 客户端错误,不重试 2435 | throw new Error(`Edge TTS API错误: ${response.status} ${errorText}`); 2436 | } 2437 | } 2438 | 2439 | return await response.blob(); 2440 | 2441 | } catch (error) { 2442 | if (attempt === maxRetries) { 2443 | // 最后一次重试失败 2444 | throw new Error(`音频生成失败(已重试${maxRetries}次): ${error.message}`); 2445 | } 2446 | 2447 | // 如果是网络错误或其他可重试错误 2448 | if (error.message.includes('fetch') || error.message.includes('network')) { 2449 | console.log(`网络错误,第${attempt + 1}次重试,等待${retryDelay * (attempt + 1)}ms`); 2450 | await delay(retryDelay * (attempt + 1)); 2451 | continue; 2452 | } 2453 | 2454 | // 其他错误直接抛出 2455 | throw error; 2456 | } 2457 | } 2458 | } 2459 | 2460 | // XML文本转义函数 2461 | function escapeXmlText(text) { 2462 | return text 2463 | .replace(/&/g, '&') // 必须首先处理 & 2464 | .replace(//g, '>') // 处理 > 2466 | .replace(/"/g, '"') // 处理 " 2467 | .replace(/'/g, '''); // 处理 ' 2468 | } 2469 | 2470 | function getSsml(text, voiceName, rate, pitch, volume, style, slien = 0) { 2471 | // 对文本进行XML转义 2472 | const escapedText = escapeXmlText(text); 2473 | 2474 | let slien_str = ''; 2475 | if (slien > 0) { 2476 | slien_str = `` 2477 | } 2478 | return ` 2479 | 2480 | 2481 | ${escapedText} 2482 | 2483 | ${slien_str} 2484 | 2485 | `; 2486 | 2487 | } 2488 | 2489 | async function getEndpoint() { 2490 | const now = Date.now() / 1000; 2491 | 2492 | if (tokenInfo.token && tokenInfo.expiredAt && now < tokenInfo.expiredAt - TOKEN_REFRESH_BEFORE_EXPIRY) { 2493 | return tokenInfo.endpoint; 2494 | } 2495 | 2496 | // 获取新token 2497 | const endpointUrl = "https://dev.microsofttranslator.com/apps/endpoint?api-version=1.0"; 2498 | const clientId = crypto.randomUUID().replace(/-/g, ""); 2499 | 2500 | try { 2501 | const response = await fetch(endpointUrl, { 2502 | method: "POST", 2503 | headers: { 2504 | "Accept-Language": "zh-Hans", 2505 | "X-ClientVersion": "4.0.530a 5fe1dc6c", 2506 | "X-UserId": "0f04d16a175c411e", 2507 | "X-HomeGeographicRegion": "zh-Hans-CN", 2508 | "X-ClientTraceId": clientId, 2509 | "X-MT-Signature": await sign(endpointUrl), 2510 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0", 2511 | "Content-Type": "application/json; charset=utf-8", 2512 | "Content-Length": "0", 2513 | "Accept-Encoding": "gzip" 2514 | } 2515 | }); 2516 | 2517 | if (!response.ok) { 2518 | throw new Error(`获取endpoint失败: ${response.status}`); 2519 | } 2520 | 2521 | const data = await response.json(); 2522 | const jwt = data.t.split(".")[1]; 2523 | const decodedJwt = JSON.parse(atob(jwt)); 2524 | 2525 | tokenInfo = { 2526 | endpoint: data, 2527 | token: data.t, 2528 | expiredAt: decodedJwt.exp 2529 | }; 2530 | 2531 | return data; 2532 | 2533 | } catch (error) { 2534 | console.error("获取endpoint失败:", error); 2535 | // 如果有缓存的token,即使过期也尝试使用 2536 | if (tokenInfo.token) { 2537 | console.log("使用过期的缓存token"); 2538 | return tokenInfo.endpoint; 2539 | } 2540 | throw error; 2541 | } 2542 | } 2543 | 2544 | 2545 | 2546 | function makeCORSHeaders() { 2547 | return { 2548 | "Access-Control-Allow-Origin": "*", 2549 | "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", 2550 | "Access-Control-Allow-Headers": "Content-Type, x-api-key", 2551 | "Access-Control-Max-Age": "86400" 2552 | }; 2553 | } 2554 | 2555 | async function hmacSha256(key, data) { 2556 | const cryptoKey = await crypto.subtle.importKey( 2557 | "raw", 2558 | key, 2559 | { name: "HMAC", hash: { name: "SHA-256" } }, 2560 | false, 2561 | ["sign"] 2562 | ); 2563 | const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(data)); 2564 | return new Uint8Array(signature); 2565 | } 2566 | 2567 | async function base64ToBytes(base64) { 2568 | const binaryString = atob(base64); 2569 | const bytes = new Uint8Array(binaryString.length); 2570 | for (let i = 0; i < binaryString.length; i++) { 2571 | bytes[i] = binaryString.charCodeAt(i); 2572 | } 2573 | return bytes; 2574 | } 2575 | 2576 | async function bytesToBase64(bytes) { 2577 | return btoa(String.fromCharCode.apply(null, bytes)); 2578 | } 2579 | 2580 | function uuid() { 2581 | return crypto.randomUUID().replace(/-/g, ""); 2582 | } 2583 | 2584 | async function sign(urlStr) { 2585 | const url = urlStr.split("://")[1]; 2586 | const encodedUrl = encodeURIComponent(url); 2587 | const uuidStr = uuid(); 2588 | const formattedDate = dateFormat(); 2589 | const bytesToSign = `MSTranslatorAndroidApp${encodedUrl}${formattedDate}${uuidStr}`.toLowerCase(); 2590 | const decode = await base64ToBytes("oik6PdDdMnOXemTbwvMn9de/h9lFnfBaCWbGMMZqqoSaQaqUOqjVGm5NqsmjcBI1x+sS9ugjB55HEJWRiFXYFw=="); 2591 | const signData = await hmacSha256(decode, bytesToSign); 2592 | const signBase64 = await bytesToBase64(signData); 2593 | return `MSTranslatorAndroidApp::${signBase64}::${formattedDate}::${uuidStr}`; 2594 | } 2595 | 2596 | function dateFormat() { 2597 | const formattedDate = (new Date()).toUTCString().replace(/GMT/, "").trim() + " GMT"; 2598 | return formattedDate.toLowerCase(); 2599 | } 2600 | 2601 | // 处理文件上传的函数 2602 | async function handleFileUpload(request) { 2603 | try { 2604 | const formData = await request.formData(); 2605 | const file = formData.get('file'); 2606 | const voice = formData.get('voice') || 'zh-CN-XiaoxiaoNeural'; 2607 | const speed = formData.get('speed') || '1.0'; 2608 | const volume = formData.get('volume') || '0'; 2609 | const pitch = formData.get('pitch') || '0'; 2610 | const style = formData.get('style') || 'general'; 2611 | 2612 | // 验证文件 2613 | if (!file) { 2614 | return new Response(JSON.stringify({ 2615 | error: { 2616 | message: "未找到上传的文件", 2617 | type: "invalid_request_error", 2618 | param: "file", 2619 | code: "missing_file" 2620 | } 2621 | }), { 2622 | status: 400, 2623 | headers: { 2624 | "Content-Type": "application/json", 2625 | ...makeCORSHeaders() 2626 | } 2627 | }); 2628 | } 2629 | 2630 | // 验证文件类型 2631 | if (!file.type.includes('text/') && !file.name.toLowerCase().endsWith('.txt')) { 2632 | return new Response(JSON.stringify({ 2633 | error: { 2634 | message: "不支持的文件类型,请上传txt文件", 2635 | type: "invalid_request_error", 2636 | param: "file", 2637 | code: "invalid_file_type" 2638 | } 2639 | }), { 2640 | status: 400, 2641 | headers: { 2642 | "Content-Type": "application/json", 2643 | ...makeCORSHeaders() 2644 | } 2645 | }); 2646 | } 2647 | 2648 | // 验证文件大小(限制为500KB) 2649 | if (file.size > 500 * 1024) { 2650 | return new Response(JSON.stringify({ 2651 | error: { 2652 | message: "文件大小超过限制(最大500KB)", 2653 | type: "invalid_request_error", 2654 | param: "file", 2655 | code: "file_too_large" 2656 | } 2657 | }), { 2658 | status: 400, 2659 | headers: { 2660 | "Content-Type": "application/json", 2661 | ...makeCORSHeaders() 2662 | } 2663 | }); 2664 | } 2665 | 2666 | // 读取文件内容 2667 | const text = await file.text(); 2668 | 2669 | // 验证文本内容 2670 | if (!text.trim()) { 2671 | return new Response(JSON.stringify({ 2672 | error: { 2673 | message: "文件内容为空", 2674 | type: "invalid_request_error", 2675 | param: "file", 2676 | code: "empty_file" 2677 | } 2678 | }), { 2679 | status: 400, 2680 | headers: { 2681 | "Content-Type": "application/json", 2682 | ...makeCORSHeaders() 2683 | } 2684 | }); 2685 | } 2686 | 2687 | // 文本长度限制(10000字符) 2688 | if (text.length > 10000) { 2689 | return new Response(JSON.stringify({ 2690 | error: { 2691 | message: "文本内容过长(最大10000字符)", 2692 | type: "invalid_request_error", 2693 | param: "file", 2694 | code: "text_too_long" 2695 | } 2696 | }), { 2697 | status: 400, 2698 | headers: { 2699 | "Content-Type": "application/json", 2700 | ...makeCORSHeaders() 2701 | } 2702 | }); 2703 | } 2704 | 2705 | // 处理参数格式,与原有逻辑保持一致 2706 | let rate = parseInt(String((parseFloat(speed) - 1.0) * 100)); 2707 | let numVolume = parseInt(String(parseFloat(volume) * 100)); 2708 | let numPitch = parseInt(pitch); 2709 | 2710 | // 调用TTS服务 2711 | return await getVoice( 2712 | text, 2713 | voice, 2714 | rate >= 0 ? `+${rate}%` : `${rate}%`, 2715 | numPitch >= 0 ? `+${numPitch}Hz` : `${numPitch}Hz`, 2716 | numVolume >= 0 ? `+${numVolume}%` : `${numVolume}%`, 2717 | style, 2718 | "audio-24khz-48kbitrate-mono-mp3" 2719 | ); 2720 | 2721 | } catch (error) { 2722 | console.error("文件上传处理失败:", error); 2723 | return new Response(JSON.stringify({ 2724 | error: { 2725 | message: "文件处理失败", 2726 | type: "api_error", 2727 | param: null, 2728 | code: "file_processing_error" 2729 | } 2730 | }), { 2731 | status: 500, 2732 | headers: { 2733 | "Content-Type": "application/json", 2734 | ...makeCORSHeaders() 2735 | } 2736 | }); 2737 | } 2738 | } 2739 | 2740 | // 处理语音转录的函数 2741 | async function handleAudioTranscription(request) { 2742 | try { 2743 | // 验证请求方法 2744 | if (request.method !== 'POST') { 2745 | return new Response(JSON.stringify({ 2746 | error: { 2747 | message: "只支持POST方法", 2748 | type: "invalid_request_error", 2749 | param: "method", 2750 | code: "method_not_allowed" 2751 | } 2752 | }), { 2753 | status: 405, 2754 | headers: { 2755 | "Content-Type": "application/json", 2756 | ...makeCORSHeaders() 2757 | } 2758 | }); 2759 | } 2760 | 2761 | const contentType = request.headers.get("content-type") || ""; 2762 | 2763 | // 验证Content-Type 2764 | if (!contentType.includes("multipart/form-data")) { 2765 | return new Response(JSON.stringify({ 2766 | error: { 2767 | message: "请求必须使用multipart/form-data格式", 2768 | type: "invalid_request_error", 2769 | param: "content-type", 2770 | code: "invalid_content_type" 2771 | } 2772 | }), { 2773 | status: 400, 2774 | headers: { 2775 | "Content-Type": "application/json", 2776 | ...makeCORSHeaders() 2777 | } 2778 | }); 2779 | } 2780 | 2781 | // 解析FormData 2782 | const formData = await request.formData(); 2783 | const audioFile = formData.get('file'); 2784 | const customToken = formData.get('token'); 2785 | 2786 | // 验证音频文件 2787 | if (!audioFile) { 2788 | return new Response(JSON.stringify({ 2789 | error: { 2790 | message: "未找到音频文件", 2791 | type: "invalid_request_error", 2792 | param: "file", 2793 | code: "missing_file" 2794 | } 2795 | }), { 2796 | status: 400, 2797 | headers: { 2798 | "Content-Type": "application/json", 2799 | ...makeCORSHeaders() 2800 | } 2801 | }); 2802 | } 2803 | 2804 | // 验证文件大小(限制为10MB) 2805 | if (audioFile.size > 10 * 1024 * 1024) { 2806 | return new Response(JSON.stringify({ 2807 | error: { 2808 | message: "音频文件大小不能超过10MB", 2809 | type: "invalid_request_error", 2810 | param: "file", 2811 | code: "file_too_large" 2812 | } 2813 | }), { 2814 | status: 400, 2815 | headers: { 2816 | "Content-Type": "application/json", 2817 | ...makeCORSHeaders() 2818 | } 2819 | }); 2820 | } 2821 | 2822 | // 验证音频文件格式 2823 | const allowedTypes = [ 2824 | 'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/m4a', 'audio/flac', 'audio/aac', 2825 | 'audio/ogg', 'audio/webm', 'audio/amr', 'audio/3gpp' 2826 | ]; 2827 | 2828 | const isValidType = allowedTypes.some(type => 2829 | audioFile.type.includes(type) || 2830 | audioFile.name.toLowerCase().match(/\.(mp3|wav|m4a|flac|aac|ogg|webm|amr|3gp)$/i) 2831 | ); 2832 | 2833 | if (!isValidType) { 2834 | return new Response(JSON.stringify({ 2835 | error: { 2836 | message: "不支持的音频文件格式,请上传mp3、wav、m4a、flac、aac、ogg、webm、amr或3gp格式的文件", 2837 | type: "invalid_request_error", 2838 | param: "file", 2839 | code: "invalid_file_type" 2840 | } 2841 | }), { 2842 | status: 400, 2843 | headers: { 2844 | "Content-Type": "application/json", 2845 | ...makeCORSHeaders() 2846 | } 2847 | }); 2848 | } 2849 | 2850 | // 使用默认token或用户提供的token 2851 | const token = customToken || 'sk-wtldsvuprmwltxpbspbmawtolbacghzawnjhtlzlnujjkfhh'; 2852 | 2853 | // 构建发送到硅基流动API的FormData 2854 | const apiFormData = new FormData(); 2855 | apiFormData.append('file', audioFile); 2856 | apiFormData.append('model', 'FunAudioLLM/SenseVoiceSmall'); 2857 | 2858 | // 发送请求到硅基流动API 2859 | const apiResponse = await fetch('https://api.siliconflow.cn/v1/audio/transcriptions', { 2860 | method: 'POST', 2861 | headers: { 2862 | 'Authorization': `Bearer ${token}` 2863 | }, 2864 | body: apiFormData 2865 | }); 2866 | 2867 | if (!apiResponse.ok) { 2868 | const errorText = await apiResponse.text(); 2869 | console.error('硅基流动API错误:', apiResponse.status, errorText); 2870 | 2871 | let errorMessage = '语音转录服务暂时不可用'; 2872 | 2873 | if (apiResponse.status === 401) { 2874 | errorMessage = 'API Token无效,请检查您的配置'; 2875 | } else if (apiResponse.status === 429) { 2876 | errorMessage = '请求过于频繁,请稍后再试'; 2877 | } else if (apiResponse.status === 413) { 2878 | errorMessage = '音频文件太大,请选择较小的文件'; 2879 | } 2880 | 2881 | return new Response(JSON.stringify({ 2882 | error: { 2883 | message: errorMessage, 2884 | type: "api_error", 2885 | param: null, 2886 | code: "transcription_api_error" 2887 | } 2888 | }), { 2889 | status: apiResponse.status, 2890 | headers: { 2891 | "Content-Type": "application/json", 2892 | ...makeCORSHeaders() 2893 | } 2894 | }); 2895 | } 2896 | 2897 | // 获取转录结果 2898 | const transcriptionResult = await apiResponse.json(); 2899 | 2900 | // 返回转录结果 2901 | return new Response(JSON.stringify(transcriptionResult), { 2902 | headers: { 2903 | "Content-Type": "application/json", 2904 | ...makeCORSHeaders() 2905 | } 2906 | }); 2907 | 2908 | } catch (error) { 2909 | console.error("语音转录处理失败:", error); 2910 | return new Response(JSON.stringify({ 2911 | error: { 2912 | message: "语音转录处理失败", 2913 | type: "api_error", 2914 | param: null, 2915 | code: "transcription_processing_error" 2916 | } 2917 | }), { 2918 | status: 500, 2919 | headers: { 2920 | "Content-Type": "application/json", 2921 | ...makeCORSHeaders() 2922 | } 2923 | }); 2924 | } 2925 | } 2926 | 2927 | --------------------------------------------------------------------------------
AI-Powered Voice Processing Platform
拖拽txt文件到此处,或点击选择文件
支持txt格式,最大500KB
正在生成语音,请稍候...
拖拽音频文件到此处,或点击选择文件
支持mp3、wav、m4a、flac、aac、ogg、webm、amr、3gp格式,最大10MB
正在转录音频,请稍候...
关注我们获取更多AI工具和技术分享
获取更多实用的AI工具、技术教程和独家资源分享