├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── help.md ├── rustfmt.toml └── src ├── cmd └── mod.rs ├── error.rs ├── main.rs ├── ms_tts.rs ├── resource ├── api │ ├── ms-api-edge.json │ └── ms-api-subscribe.json ├── azure_voices_list.json ├── blank.mp3 └── edge_voices_list.json ├── tests ├── azure_api_test.rs ├── mod.rs └── other.rs ├── utils ├── azure_api.rs ├── log.rs └── mod.rs └── web ├── controller.rs ├── entity.rs ├── error.rs ├── middleware ├── error_handle.rs ├── mod.rs └── token_auth.rs ├── mod.rs ├── utils.rs ├── vo └── mod.rs └── web_entrance.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | preparation_projects: 8 | name: Preparation Projects 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - name: Git Submodule Update 14 | run: | 15 | git submodule sync --recursive 16 | git submodule update --init --recursive 17 | - name: Install Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '14' 21 | - name: Web Package Build 22 | run: npm --prefix web/ install && npm --prefix web/ run build 23 | - name: Create Web Artifact 24 | uses: DuckSoft/create-7z-action@v1.0 25 | with: 26 | pathSource: web/dist/ 27 | pathTarget: web.7z 28 | - name: Upload Web Artifact 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: web-html 32 | path: web.7z 33 | retention-days: 1 34 | 35 | build: 36 | name: build for ${{ matrix.target }} 37 | needs: preparation_projects 38 | runs-on: ${{ matrix.os }} 39 | strategy: 40 | matrix: 41 | include: 42 | - os: ubuntu-latest 43 | target: x86_64-unknown-linux-gnu 44 | file: tts-server 45 | args: --best --lzma 46 | use_upx: true 47 | - os: ubuntu-latest 48 | target: x86_64-unknown-linux-musl 49 | file: tts-server 50 | args: --best --lzma 51 | use_upx: true 52 | - os: ubuntu-latest 53 | target: aarch64-unknown-linux-musl 54 | file: tts-server 55 | args: --best --lzma 56 | use_upx: true 57 | - os: ubuntu-latest 58 | target: arm-unknown-linux-musleabi 59 | file: tts-server 60 | args: --best --lzma 61 | use_upx: true 62 | - os: ubuntu-latest 63 | target: arm-unknown-linux-musleabihf 64 | file: tts-server 65 | args: --best --lzma 66 | use_upx: true 67 | - os: ubuntu-latest 68 | target: armv7-unknown-linux-musleabihf 69 | file: tts-server 70 | args: --best --lzma 71 | use_upx: true 72 | - os: ubuntu-latest 73 | target: mips-unknown-linux-gnu 74 | file: tts-server 75 | args: --best --lzma 76 | use_upx: true 77 | - os: ubuntu-latest 78 | target: mips-unknown-linux-musl 79 | file: tts-server 80 | args: --best --lzma 81 | use_upx: true 82 | - os: ubuntu-latest 83 | target: mipsel-unknown-linux-gnu 84 | file: tts-server 85 | args: --best --lzma 86 | use_upx: true 87 | - os: ubuntu-latest 88 | target: mipsel-unknown-linux-musl 89 | file: tts-server 90 | args: --best --lzma 91 | use_upx: true 92 | - os: ubuntu-latest 93 | target: mips64-unknown-linux-gnuabi64 94 | file: tts-server 95 | args: --best --lzma 96 | use_upx: false 97 | - os: ubuntu-latest 98 | target: mips64el-unknown-linux-gnuabi64 99 | file: tts-server 100 | args: --best --lzma 101 | use_upx: false 102 | - os: macos-latest 103 | target: x86_64-apple-darwin 104 | file: tts-server 105 | args: --best 106 | use_upx: true 107 | 108 | - os: windows-latest 109 | target: x86_64-pc-windows-msvc 110 | file: tts-server.exe 111 | args: -9 112 | use_upx: true 113 | steps: 114 | - name: Checkout repository 115 | uses: actions/checkout@v2 116 | - name: Git Submodule Update 117 | run: | 118 | git submodule sync --recursive 119 | git submodule update --init --recursive 120 | - name: Download Web Artifact 121 | uses: actions/download-artifact@v3 122 | with: 123 | name: web-html 124 | - name: extract Web Artifact 125 | uses: DuckSoft/extract-7z-action@v1.0 126 | with: 127 | pathSource: web.7z 128 | pathTarget: ./ 129 | - name: Install Rust 130 | uses: actions-rs/toolchain@v1 131 | with: 132 | toolchain: stable 133 | profile: minimal 134 | override: true 135 | target: ${{ matrix.target }} 136 | - name: Install cross 137 | run: cargo install --version 0.1.16 cross 138 | - name: Install dependencies 139 | if: matrix.os == 'ubuntu-latest' 140 | run: | 141 | sudo apt-get install llvm -y 142 | - name: Build binary 143 | run: cross build --release --target ${{ matrix.target }} 144 | env: 145 | RUST_BACKTRACE: 1 146 | - name: LLVM Strip 147 | if: matrix.os == 'ubuntu-latest' 148 | continue-on-error: true 149 | run: llvm-strip target/${{ matrix.target }}/release/${{ matrix.file }} 150 | - name: MacOS Strip 151 | if: matrix.os == 'macos-latest' 152 | continue-on-error: true 153 | run: strip target/${{ matrix.target }}/release/${{ matrix.file }} 154 | - name: Compress binaries 155 | uses: svenstaro/upx-action@v2 156 | if: matrix.use_upx 157 | with: 158 | file: "target/${{ matrix.target }}/release/${{ matrix.file }}" 159 | args: ${{ matrix.args }} 160 | strip: false 161 | - name: Upload archive 162 | uses: actions/upload-artifact@v1 163 | with: 164 | name: tts-server-${{ matrix.target }} 165 | path: "target/${{ matrix.target }}/release/${{ matrix.file }}" 166 | - name: Zip Release 167 | uses: TheDoctor0/zip-release@0.6.1 168 | with: 169 | type: zip 170 | filename: tts-server-${{ matrix.target }}.zip 171 | directory: target/${{ matrix.target }}/release/ 172 | path: ${{ matrix.file }} 173 | - name: Publish 174 | uses: softprops/action-gh-release@v1 175 | if: startsWith(github.ref, 'refs/tags/') 176 | with: 177 | files: target/${{ matrix.target }}/release/tts-server-${{ matrix.target }}.zip 178 | generate_release_notes: true 179 | draft: true 180 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tmp 3 | /记录.md 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "web"] 2 | path = web 3 | url = https://github.com/ZYFDroid/tts-server-frontend.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tts-server" 3 | version = "0.3.3" 4 | authors = ["litcc"] 5 | edition = "2021" 6 | readme = "README.md" 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | description = """TTS Api Server 软件仅供学习交流,严禁用于商业用途,请于24小时内删除! 10 | 11 | 微软官方的 Azure TTS 服务目前拥有一定的免费额度,如果免费额度对你来说够用的话,请支持官方的服务。 12 | 13 | 如果只需要为固定的文本生成语音,可以使用有声内容创作。它提供了更丰富的功能可以生成更自然的声音。 14 | 15 | 本项目构建的二进制程序仅供学习交流和参考,严禁用于商业用途,请于下载后24小时内删除! 16 | 17 | 目前已实现接口有:[微软文本转语音] 后续看情况可能会再加入其他接口。 18 | 19 | 具体使用方法请看 --help 中参数介绍,以及项目中 help.md 文档 20 | """ 21 | 22 | 23 | [dependencies] 24 | clap = { version = "3", features = ["derive"] } # 命令行解析库 25 | itertools = "0.10" # 迭代器工具库 26 | fancy-regex = "0.11" # 正则库 27 | log = "0.4" # 日志库 28 | log4rs = "1" # 日志扩展库 29 | thiserror = "1" 30 | anyhow = "1" 31 | #env_logger = "0.9.0" # 日志扩展库 32 | num = "0.4" #计算库 33 | uuid = { version = "1", features = ["v4"] } # uuid库 34 | urlencoding = "2" # url编码解析库 35 | rand = "0.8" # 随机数库 36 | futures = "0.3" # 异步工具库 37 | reqwest = { version = "0.11", features = ["json", "native-tls"] } # http 请求库 38 | once_cell = "1" 39 | bytes = "1" 40 | serde = "1" 41 | serde_json = "1" 42 | bincode = "1" 43 | actix-web = "4" 44 | actix-http = "3" # web框架 45 | #actix-rt = "2.7.0" 46 | local_ipaddress = "0.1" # 获取本地ip 47 | tokio-native-tls = "0.3" 48 | ## tokio-tungstenite 暂时不能升级,可能是库有问题了,或者是我用法出问题了,暂时没有分析 49 | tokio-tungstenite = { version = "0.18", features = ["native-tls"] } 50 | tokio = { version = "1.28", features = ["full"] } # 协程库 51 | mime_guess = "2" # 文件类型检测 52 | #event_bus = { path = "/mnt/work/code/Me/Rust/EventBus" } 53 | event_bus = { git = "https://github.com/litcc/event_bus.git" } 54 | chrono = "0.4" # 时间库 55 | async-trait = "0.1" 56 | base64 = "0.21" 57 | rust-embed = { version = "6" } # 嵌入文件库 , optional = true 58 | backon = "0.4" # 重试库 59 | 60 | 61 | 62 | 63 | [profile.release] 64 | opt-level = 'z' 65 | #lto = true 66 | #codegen-units = 1 67 | panic = 'abort' 68 | 69 | 70 | [features] 71 | #default = [] 72 | 73 | 74 | # Build this crate in isolation, without using PyOxidizer. 75 | #web-entrance = ["rust-embed"] 76 | 77 | # Build this crate by executing a `pyoxidizer` executable to build 78 | # required artifacts. 79 | 80 | # Build this crate by reusing artifacts generated by `pyoxidizer` out-of-band. 81 | # In this mode, the PYOXIDIZER_ARTIFACT_DIR environment variable can refer 82 | # to the directory containing build artifacts produced by `pyoxidizer`. If not 83 | # set, OUT_DIR will be used. 84 | 85 | 86 | 87 | # This empty workspace table forces Cargo to treat this manifest as its 88 | # own workspace, even if a parent directory defines a workspace. If you want 89 | # this Rust project to exist as part of a larger workspace, simply delete this. 90 | [workspace] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 litcc 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 | ![MIT](https://img.shields.io/badge/license-MIT-green) 2 | [![CI](https://github.com/litcc/tts-server/actions/workflows/rust.yml/badge.svg)](https://github.com/litcc/tts-server/actions/workflows/rust.yml) 3 | ![GitHub release (latest by date)](https://img.shields.io/github/downloads/litcc/tts-server/latest/total) 4 | 5 | # tts-server 6 | 7 | - 本项目目前使用的是 Edge 浏览器“大声朗读”和 Azure TTS 演示页面的接口,以及官方订阅接口,除官方订阅接口外,不保证后续可用性和稳定性。还是强烈推荐使用订阅接口! 8 | 9 | - 项目使用保持连接的 websocket 极大提升了请求并发性以及请求速度,减少了频繁使用 http 到 websocket 升级协议握手的时间(如果国内服务器的话可能不太明显,国外服务器的情况下,重连很耗费时间) 10 | 11 | 12 | - 代码可能会有点乱,介意的话请移步下面的相关项目 13 | 14 | 15 | 16 | 使用介绍的话没工夫写,可以使用程序里面的 --help ,或者也可以看看 help.md(过期) 文件 17 | 18 | 19 | 如果有人愿意来贡献的话请直接提 PR 20 | 21 | 22 | 且行且珍惜 23 | 24 | 25 | ## 项目动态 26 | 27 | 2022-09-22: 添加多途径api调用,新增对官方订阅key的使用支持; 28 | 29 | 2022-08-00: 经反馈 ip 加速失效; 30 | 31 | 2022-06-16:Edge 浏览器提供的接口现在已经不能设置讲话风格了,简单点说就是 style 参数废了,传什么都默认了; 32 | 33 | 34 | 35 | ## 相关项目 36 | 排名不分前后 37 | - [ag2s20150909/TTS](https://github.com/ag2s20150909/TTS):安卓版,可代替系统自带的TTS。 38 | - [wxxxcxx/ms-ra-forwarder](https://github.com/wxxxcxx/ms-ra-forwarder):Nodejs 运行的版本,自带web页面。 39 | - [jing332/tts-server-go](https://github.com/jing332/tts-server-go):Go语言实现版本。 40 | - [jing332/tts-server-android](https://github.com/jing332/tts-server-android):tts-server-go 的 Android 实现版本。 41 | 42 | 43 | 44 | ## 免责声明 45 | 46 | 微软官方的 Azure TTS 服务目前拥有一定的免费额度,如果免费额度对你来说够用的话,请支持官方的服务。 47 | 48 | 如果只需要为固定的文本生成语音,可以使用有声内容创作。它提供了更丰富的功能可以生成更自然的声音。 49 | 50 | 本项目构建的二进制程序仅供学习交流和参考,严禁用于商业用途,请于下载后24小时内删除! -------------------------------------------------------------------------------- /help.md: -------------------------------------------------------------------------------- 1 | # Windows端: 2 | 3 | ##### 切换到 tts-server.exe所在路径 4 | 5 | ##### shell输入以下命令 6 | 7 | ` ./tts-server.exe --help` 8 | 9 | ##### cmd输入 10 | 11 | `tts-server.exe --help` 12 | 13 | ##### 会发现出来一堆文本 14 | 15 | ``` 16 | tts-server 0.1.2 17 | 18 | litcc 19 | 20 | TTS Api Server 软件仅供学习交流,严禁用于商业用途,请于24小时内删除! 21 | 目前已实现接口有:[微软文本转语音] 后续看情况可能会再加入其他接口。 22 | 23 | 微软文本转语音接口(edge渠道): /api/tts-ms-edge 24 | 微软文本转语音接口(官网预览渠道): /api/tts-ms-official-preview 25 | 微软文本转语音接口(官方订阅Key渠道): /api/tts-ms-subscribe-api 26 | 接口支持 get,post 请求, get请求时参数拼接在url上,使用post时,参数请使用json body传递。 27 | 目前支持参数有: 28 | text - 待转换内容 必填参数 29 | informant - 发音人 可选参数,大小写严格, 默认为 zh-CN-XiaoxiaoNeural 30 | 31 | 可通过命令行参数查看所有支持的列表,下列参数可能在部分渠道无法使用 32 | style - 发音风格 可选参数,默认为 general 33 | rate - 语速 可选参数 值范围 0-3 可保留两位小数, 默认为 1 34 | pitch - 音调 可选参数 值范围 0-2 可保留两位小数, 默认为 1 35 | quality - 音频格式 可选参数,默认为 audio-24khz-48kbitrate-mono-mp3 36 | 可通过命令行参数查看所有支持的列表 37 | 38 | 基本使用教程: 39 | 举例: 在开源软件[阅读]App中可以使用如下配置来使用该接口 40 | http://ip:port/api/tts-ms-edge,{ 41 | "method": "POST", 42 | "body": { 43 | "informant": "zh-CN-XiaoxiaoNeural", 44 | "style": "general", 45 | "rate": {{ speakSpeed / 15 }}, 46 | "text": "{{java.encodeURI(speakText).replace('+','%20')}}" 47 | } 48 | } 49 | 50 | ``` 51 | 52 | 53 | 54 | 55 | ##### 找到一般需要使用的 56 | ``` 57 | --listen-address
58 | 监听地址 59 | 60 | [default: 0.0.0.0] 61 | 62 | --listen-port 63 | 监听端口 64 | 65 | [default: 8080] 66 | ``` 67 | 68 | ###### 执行命令 69 | `./tts-server.exe --listen-address 192.168.0.101 --listen-port 20222` 70 | 71 | 192.168.0.101 是指本地IP 20222是监听端口 72 | 73 | 当然也可以直接双击tts-server.exe,就直接是默认的端口:8080 74 | 75 | shell/cmd 不能关闭,否则程序断开 76 | 77 | # Linux端 78 | 79 | ###### 与Windows端一致,不过可使用screen等放在后台,同样可直接 `./tts-server` ,IP会是本机IP,端口为8080 80 | 81 | ###### 输入 82 | 83 | `./tts-server.exe --help` 84 | 85 | `./tts-server.exe --listen-address 192.168.0.101 --listen-port 20222` 86 | 87 | 本机IP192.168.0.101需自行调整,监听端口20222看个人喜好 88 | 89 | # 阅读导入 90 | 91 | ``` 92 | http://192.168.0.101:20222/tts-ms,{ 93 | "method": "POST", 94 | "body": { 95 | "informant": "zh-CN-XiaoxiaoNeural", 96 | "style": "general", 97 | "rate": {{ speakSpeed / 6.5 }}, 98 | "quality":"audio-48khz-96kbitrate-mono-mp3", 99 | "text": "{{java.encodeURI(speakText).replace('+','%20')}}" 100 | } 101 | } 102 | ``` 103 | 104 | ###### 根据以上模板修改IP、端口 105 | 106 | 发音人 "informant" 、风格 "style" 、朗读语速 "rate" 与音频格式 "quality" 可根据自己喜好修改 107 | 以及`--help`所提及的音调 “pitch” 108 | 109 | 如果音频格式选择默认可删除这一行 110 | ` "quality":"audio-48khz-96kbitrate-mono-mp3",` 111 | 112 | 113 | 114 | # 扩展 115 | ## 音频格式 116 | 117 | #### 输入命令 118 | 119 | `./tts-server.exe --show-quality-list` 120 | 121 | ##### 会发现出来一堆参数,挑选出来排列一下便如下,按需填至"quality" 一栏即可 122 | 123 | ``` 124 | "audio-16khz-128kbitrate-mono-mp3", 125 | "audio-16khz-16bit-32kbps-mono-opus", 126 | "audio-16khz-16kbps-mono-siren", 127 | "audio-16khz-32kbitrate-mono-mp3", 128 | "audio-16khz-64kbitrate-mono-mp3", 129 | "audio-24khz-160kbitrate-mono-mp3", 130 | "audio-24khz-16bit-24kbps-mono-opus", 131 | "audio-24khz-16bit-48kbps-mono-opus", 132 | "audio-24khz-48kbitrate-mono-mp3", 133 | "audio-24khz-96kbitrate-mono-mp3", 134 | "audio-48khz-192kbitrate-mono-mp3", 135 | "audio-48khz-96kbitrate-mono-mp3", 136 | "ogg-16khz-16bit-mono-opus", 137 | "ogg-24khz-16bit-mono-opus", 138 | "ogg-48khz-16bit-mono-opus", 139 | "raw-16khz-16bit-mono-pcm", 140 | "raw-16khz-16bit-mono-truesilk", 141 | "raw-24khz-16bit-mono-pcm", 142 | "raw-24khz-16bit-mono-truesilk", 143 | "raw-48khz-16bit-mono-pcm", 144 | "raw-8khz-16bit-mono-pcm", 145 | "raw-8khz-8bit-mono-alaw", 146 | "raw-8khz-8bit-mono-mulaw", 147 | "riff-16khz-16bit-mono-pcm", 148 | "riff-24khz-16bit-mono-pcm", 149 | "riff-48khz-16bit-mono-pcm", 150 | "riff-8khz-16bit-mono-pcm", 151 | "riff-8khz-8bit-mono-alaw", 152 | "riff-8khz-8bit-mono-mulaw", 153 | "webm-16khz-16bit-mono-opus", 154 | "webm-24khz-16bit-24kbps-mono-opus", 155 | "webm-24khz-16bit-mono-opus" 156 | ``` 157 | 158 | ## 发音人 159 | 160 | ``` 161 | Xiaoxiao(Neura)-晓晓 162 | Yunyang(Neural)-云扬 163 | Xiaochen(Neural)-晓辰 164 | Xiaohan(Neural)-晓涵 165 | Xiaomo(Neural))-晓墨 166 | Xiaoqiu(Neural)-晓秋 167 | Xiaorui(Neura)-晓睿 168 | Xiaoshuang(Neural)-晓双 169 | Xiaoxuan(Neural)-晓萱 170 | Xiaoyan(Neura))-晓颜 171 | Xiaoyou(Neural)-晓悠 172 | Yunxi(Neural)-云希 173 | Yunye(Neural)-云野 174 | ``` 175 | 176 | ##### 具体可使用命令 177 | 178 | `./tts-server.exe --show-informant-list` 179 | 180 | 及阅读 ~~微软~~ 巨硬官方文档 181 | 182 | ###### 以下发音风格同理 183 | 184 | 185 | > https://azure.microsoft.com/zh-cn/services/cognitive-services/text-to-speech/#features 186 | 187 | > https://docs.microsoft.com/zh-cn/azure/cognitive-services/speech-service/speech-synthesis-markup?tabs=csharp#adjust-speaking-styles 188 | 189 | ## 发音风格 190 | 191 | ##### Style 说明 192 | ``` 193 | style="affectionate" 以较高的音调和音量表达温暖而亲切的语气。 说话者处于吸引听众注意力的状态。 说话者的个性往往是讨喜的。 194 | style="angry" 表达生气和厌恶的语气。 195 | style="assistant" 以热情而轻松的语气对数字助理讲话。 196 | style="calm" 以沉着冷静的态度说话。 语气、音调和韵律与其他语音类型相比要统一得多。 197 | style="chat" 表达轻松随意的语气。 198 | style="cheerful" 表达积极愉快的语气。 199 | style="customerservice" 以友好热情的语气为客户提供支持。 200 | style="depressed" 调低音调和音量来表达忧郁、沮丧的语气。 201 | style="disgruntled" 表达轻蔑和抱怨的语气。 这种情绪的语音表现出不悦和蔑视。 202 | style="embarrassed" 在说话者感到不舒适时表达不确定、犹豫的语气。 203 | style="empathetic" 表达关心和理解。 204 | style="envious" 当你渴望别人拥有的东西时,表达一种钦佩的语气。 205 | style="fearful" 以较高的音调、较高的音量和较快的语速来表达恐惧、紧张的语气。 说话人处于紧张和不安的状态。 206 | style="gentle" 以较低的音调和音量表达温和、礼貌和愉快的语气。 207 | style="lyrical" 以优美又带感伤的方式表达情感。 208 | style="narration-professional" 以专业、客观的语气朗读内容。 209 | style="narration-relaxed" 为内容阅读表达一种舒缓而悦耳的语气。 210 | style="newscast" 以正式专业的语气叙述新闻。 211 | style="newscast-casual" 以通用、随意的语气发布一般新闻。 212 | style="newscast-formal" 以正式、自信和权威的语气发布新闻。 213 | style="sad" 表达悲伤语气。 214 | style="serious" 表达严肃和命令的语气。 说话者的声音通常比较僵硬,节奏也不那么轻松。 215 | ``` 216 | 217 | ###### 大部分发音人无法使用全部风格 218 | 219 | 具体阅读并使用 ~~微软~~ 巨硬官方文档 220 | 221 | > https://azure.microsoft.com/zh-cn/services/cognitive-services/text-to-speech/#features 222 | 223 | 224 | 225 | 226 | 以上结束,本人菜鸡,瞎写的帮助文档,不过程序能够运行就是了👀️ 227 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | version="Two" 2 | # 固定换行符 LF 3 | newline_style="Unix" 4 | # 自动排序mod 5 | reorder_modules=true 6 | # 自动排序use 7 | reorder_imports=true 8 | # 自动优化导入 (未稳定) 9 | imports_granularity="Crate" 10 | # StdExternalCrate 导入模块分组规则 (未稳定) 11 | # 1. 导入来自 std、core 和 alloc 的模块需要置于前面一组。 12 | # 2. 导入来自 第三方库的模块 应该置于中间一组。 13 | # 3. 导入来自本地 self、super和crate前缀的模块,置于后面一组。 14 | group_imports="StdExternalCrate" -------------------------------------------------------------------------------- /src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgEnum, Parser}; 2 | use log::LevelFilter; 3 | use once_cell::sync::OnceCell; 4 | 5 | #[derive(Parser, Debug)] 6 | #[clap( 7 | name = env!("CARGO_PKG_NAME"), 8 | version, 9 | about = env!("CARGO_PKG_DESCRIPTION"), 10 | author = env!("CARGO_PKG_AUTHORS"), 11 | )] 12 | pub struct AppArgs { 13 | /// 指定连接渠道, 可加速 Edge 接口请求速度 14 | #[clap(long, arg_enum, value_name = "area", default_value_t = ServerArea::Default)] 15 | pub server_area: ServerArea, 16 | 17 | /// 监听地址 18 | #[clap(long, value_name = "address", default_value_t = String::from("0.0.0.0"))] 19 | pub listen_address: String, 20 | 21 | /// 监听端口 22 | #[clap(long, value_name = "port", default_value_t = String::from("8080"))] 23 | pub listen_port: String, 24 | 25 | /// 显示可用发音人列表 26 | #[clap(long, parse(from_flag))] 27 | pub show_informant_list: bool, 28 | 29 | /// 显示音频质量参数列表 30 | #[clap(long, parse(from_flag))] 31 | pub show_quality_list: bool, 32 | 33 | /// 禁用 edge 免费预览接口 34 | #[clap(long, parse(from_flag))] 35 | pub close_edge_free_api: bool, 36 | 37 | /// 禁用 官方网页免费预览接口 38 | #[clap(long, parse(from_flag))] 39 | pub close_official_preview_api: bool, 40 | 41 | /// 禁用 官方网页收费(有免费额度)版本接口 42 | #[clap(long, parse(from_flag))] 43 | pub close_official_subscribe_api: bool, 44 | 45 | /// 对订阅API添加独立认证token 46 | #[clap(long, value_name = "token")] 47 | pub subscribe_api_auth_token: Option, 48 | 49 | /// 指定不从官方更新最新发音人 (可以快速使用本地缓存启动程序) 50 | #[clap(long, parse(from_flag))] 51 | pub do_not_update_speakers_list: bool, 52 | 53 | /// 指定订阅API的官方订阅密钥以及地域, 可添加多个,遍历使用,格式:{subscribe_key},{region} 例: --subscribe-key 956d0b8cb34e4kb1b9cb8c614d313ae3,southeastasia 54 | #[clap(long)] 55 | pub subscribe_key: Vec, 56 | 57 | /// 是否启用 webUI 58 | #[clap(long, parse(from_flag))] 59 | pub web_ui: bool, 60 | 61 | /// 是否开启 debug 日志 可用参数有: Off, Error, Warn, Info, Debug, Trace 62 | #[clap(long, default_value_t = LevelFilter::Info)] 63 | pub log_level: LevelFilter, 64 | 65 | /// 将日志记录至文件 66 | #[clap(long, parse(from_flag))] 67 | pub log_to_file: bool, 68 | 69 | /// 日志文件路径 70 | #[clap(long, default_value_t = format ! ("{}/local_ocr/ocr.log", std::env::temp_dir().to_str().unwrap()))] 71 | pub log_path: String, 72 | } 73 | 74 | impl AppArgs { 75 | #[allow(dead_code)] 76 | pub fn parse_macro() -> &'static Self { 77 | static GLOBAL_ARGS: OnceCell = OnceCell::new(); 78 | GLOBAL_ARGS.get_or_init(|| { 79 | 80 | AppArgs::parse() 81 | }) 82 | } 83 | 84 | #[cfg(test)] 85 | #[allow(dead_code)] 86 | pub fn test_parse_macro(param: &[&str]) -> &'static Self { 87 | static TEST_GLOBAL_ARGS: OnceCell = OnceCell::new(); 88 | TEST_GLOBAL_ARGS.get_or_init(|| { 89 | let app_args = AppArgs::try_parse_from(param); 90 | app_args.unwrap() 91 | }) 92 | } 93 | 94 | /*#[allow(dead_code)] 95 | pub fn parse_config() -> Self { 96 | /*let arg_parse = Command::new(env!("CARGO_PKG_NAME")) 97 | .version(env!("CARGO_PKG_VERSION")) 98 | .author(env!("CARGO_PKG_AUTHORS")) 99 | .about(env!("CARGO_PKG_DESCRIPTION")) 100 | .disable_help_subcommand(true) 101 | .arg( 102 | Arg::new("version") 103 | .long("version") 104 | .short('v') 105 | .action(ArgAction::Version) 106 | .help("显示版本信息") 107 | .display_order(0), 108 | ) 109 | .arg( 110 | Arg::new("help") 111 | .long("help") 112 | .short('h') 113 | .action(ArgAction::Help) 114 | .help("显示帮助") 115 | .display_order(1), 116 | ) 117 | .arg( 118 | Arg::new("listen_address") 119 | .value_name("address") 120 | .required(false) 121 | .default_value("0.0.0.0") 122 | .long("listen-address") 123 | .short('a') 124 | .takes_value(true) 125 | .help("监听地址") 126 | .display_order(2), 127 | ) 128 | .arg( 129 | Arg::new("listen_port") 130 | .value_name("port") 131 | .required(false) 132 | .default_value("8080") 133 | .long("listen-port") 134 | .short('p') 135 | .takes_value(true) 136 | .help("监听端口") 137 | .display_order(3), 138 | ) 139 | .arg( 140 | Arg::new("server_area") 141 | .value_name("area") 142 | .long("server-area") 143 | .required(false) 144 | .takes_value(true) 145 | .help("微软 edge 接口指定连接渠道") 146 | .display_order(4), 147 | ) 148 | .arg( 149 | Arg::new("log_level") 150 | .value_name("level") 151 | .required(false) 152 | .long("log-level") 153 | .value_parser(EnumValueParser::::new()) 154 | .default_value(LevelFilter::Info.as_str()) 155 | .ignore_case(true) 156 | .takes_value(true) 157 | .help("日志等级") 158 | .display_order(5), 159 | ) 160 | .arg( 161 | Arg::new("log_to_file") 162 | .required(false) 163 | .long("log-to-file") 164 | .takes_value(true) 165 | .action(ArgAction::SetTrue) 166 | .help("日志写入文件") 167 | .display_order(6), 168 | ) 169 | .arg( 170 | Arg::new("log_path") 171 | .value_name("path") 172 | .required(false) 173 | .long("log-path") 174 | // .default_value() 175 | .takes_value(true) 176 | .help("日志路径") 177 | 178 | .display_order(7), 179 | ) 180 | .subcommands(vec![ 181 | Command::new("config") 182 | .about("Controls configuration functionality") 183 | .arg(Arg::new("config_file")), 184 | Command::new("debug").about("Controls debug functionality"), 185 | ]); 186 | let matchs = arg_parse.get_matches(); 187 | info!("{:#?}", matchs);*/ 188 | 189 | }*/ 190 | } 191 | 192 | /// Wrapping LevelFilter 193 | // 包装 LevelFilter 用以绕过孤儿规则 194 | /*#[derive(Clone, Debug)] 195 | struct LevelFilterPack(LevelFilter); 196 | 197 | impl ToString for LevelFilterPack { 198 | fn to_string(&self) -> String { 199 | self.0.to_string() 200 | } 201 | } 202 | 203 | impl PartialEq for LevelFilterPack { 204 | fn eq(&self, other: &Self) -> bool { 205 | self.0.eq(&other.0) 206 | } 207 | } 208 | 209 | impl ValueEnum for LevelFilterPack { 210 | fn value_variants<'a>() -> &'a [Self] { 211 | let er = LevelFilter::iter() 212 | .map(|i| LevelFilterPack(i)) 213 | .collect::>(); 214 | let list = er.leak(); 215 | list 216 | } 217 | 218 | fn from_str(input: &str, _ignore_case: bool) -> Result { 219 | Ok(LevelFilterPack( 220 | LevelFilter::from_str(input).unwrap_or(LevelFilter::Info), 221 | )) 222 | } 223 | 224 | fn to_possible_value<'a>(&self) -> Option> { 225 | Some(PossibleValue::new(self.0.as_str())) 226 | } 227 | }*/ 228 | // impl From for AppArgs { 229 | // fn from(m: ArgMatches) -> Self { 230 | // AppArgs { 231 | // // verbose: *m.get_one::("verbose").expect("defaulted_by_clap"), 232 | // // name: m.get_one::("name").cloned(), 233 | // server_area: m 234 | // .get_one::("server_area") 235 | // .cloned() 236 | // .unwrap_or(ServerArea::Default), 237 | // listen_address: m 238 | // .get_one::("listen_address") 239 | // .cloned() 240 | // .unwrap_or("0.0.0.0".to_owned()), 241 | // listen_port: m 242 | // .get_one::("listen_port") 243 | // .cloned() 244 | // .unwrap_or("8080".to_owned()), 245 | // show_informant_list: false, 246 | // show_quality_list: false, 247 | // do_not_update_speakers_list: false, 248 | // log_level: m 249 | // .get_one::("log_level") 250 | // .cloned() 251 | // .map(|i| i.0), 252 | // log_to_file: m 253 | // .get_one::("log_to_file").copied(), 254 | // log_path: None, 255 | // } 256 | // } 257 | // } 258 | 259 | /// Edge 免费接口连接地域 260 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum)] 261 | pub enum ServerArea { 262 | Default, 263 | China, 264 | ChinaHK, 265 | ChinaTW, 266 | } 267 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error as AnyError, Result as AnyResult}; 2 | use thiserror::Error; 3 | 4 | pub type Result = AnyResult; 5 | 6 | pub type Error = AnyError; 7 | /// 8 | /// 该程序所有错误 9 | #[derive(Error, Debug)] 10 | pub enum TTSServerError { 11 | /// 12 | /// 程序意外错误 13 | #[error("Program unexpected error! {0}")] 14 | ProgramError(String), 15 | /// 16 | /// 第三方api调用出错 17 | #[error("3rd party api call failed! {0}")] 18 | ThirdPartyApiCallFailed(String), 19 | } 20 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod ms_tts; 2 | 3 | pub(crate) mod cmd; 4 | pub(crate) mod error; 5 | pub(crate) mod utils; 6 | pub(crate) mod web; 7 | 8 | #[cfg(test)] 9 | pub(crate) mod tests; 10 | 11 | use std::sync::{ 12 | atomic::{AtomicUsize, Ordering}, 13 | Arc, 14 | }; 15 | 16 | use anyhow::Result; 17 | use event_bus::{core::EventBus, message::VertxMessage}; 18 | pub use log::*; 19 | use once_cell::sync::{Lazy, OnceCell}; 20 | use tokio::runtime::Runtime; 21 | use utils::log::init_log; 22 | 23 | use crate::{ 24 | cmd::AppArgs, 25 | utils::{azure_api::MS_TTS_QUALITY_LIST, random_string}, 26 | }; 27 | 28 | pub(crate) static GLOBAL_EB: Lazy>> = Lazy::new(|| { 29 | let eb = EventBus::::new(Default::default()); 30 | Arc::new(eb) 31 | }); 32 | 33 | // #[tokio::main] 34 | // async 35 | async fn main_async() -> Result<()> { 36 | let args = AppArgs::parse_macro(); 37 | debug!("程序参数: {:#?}", args); 38 | if args.show_quality_list { 39 | println!( 40 | "当前可使用的音频参数有: (注意:Edge免费接口可能个别音频参数无法使用,是正常情况,是因为微软不允许滥用!) \n{:?}", 41 | MS_TTS_QUALITY_LIST 42 | ); 43 | std::process::exit(0); 44 | } 45 | if args.show_informant_list { 46 | info!( 47 | "由于提供多种接口,且多种接口发音人可能会有不相同的支持,所以建议通过官方手段获取发音人列表!这里就不做展示了!", 48 | ); 49 | std::process::exit(0); 50 | } 51 | // 52 | info!("准备启动,程序参数: {:?}", args); 53 | GLOBAL_EB.start().await; 54 | ms_tts::register_service().await; 55 | web::register_service().await; 56 | info!("谢谢使用,希望能收到您对软件的看法和建议!"); 57 | Ok(()) 58 | } 59 | 60 | static GLOBAL_RT: OnceCell = OnceCell::new(); 61 | 62 | fn main() -> Result<()> { 63 | let args = AppArgs::parse_macro(); 64 | init_log( 65 | args.log_level, 66 | Some(args.log_to_file), 67 | Some(&args.log_path), 68 | None, 69 | ); 70 | 71 | GLOBAL_RT 72 | .get_or_init(|| { 73 | tokio::runtime::Builder::new_multi_thread() 74 | .worker_threads(2) 75 | .thread_name_fn(|| { 76 | static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); 77 | let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); 78 | format!("tts-server-global-{}", id) 79 | }) 80 | .thread_stack_size(3 * 1024 * 1024) 81 | .enable_all() 82 | .build() 83 | .unwrap() 84 | }) 85 | .block_on(main_async()) 86 | } 87 | -------------------------------------------------------------------------------- /src/ms_tts.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{ 4 | atomic::{AtomicBool, Ordering}, 5 | Arc, 6 | }, 7 | time::Duration, 8 | }; 9 | 10 | use bytes::{BufMut, Bytes, BytesMut}; 11 | use event_bus::message::IMessage; 12 | use futures::{ 13 | stream::{SplitSink, SplitStream}, 14 | SinkExt, StreamExt, 15 | }; 16 | use log::{debug, error, trace}; 17 | use once_cell::sync::Lazy; 18 | use serde::{Deserialize, Serialize}; 19 | use tokio::{ 20 | net::TcpStream, 21 | sync::{Mutex, OnceCell}, 22 | time::sleep, 23 | }; 24 | use tokio_native_tls::TlsStream; 25 | use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; 26 | 27 | use crate::{ 28 | utils::{ 29 | azure_api::{ 30 | AzureApiEdgeFree, AzureApiGenerateXMML, AzureApiNewWebsocket, AzureApiRegionIdentifier, 31 | AzureApiSpeakerList, AzureApiSubscribeToken, AzureSubscribeKey, MsTtsMsgRequest, 32 | VoicesList, 33 | }, 34 | binary_search, 35 | }, 36 | AppArgs, 37 | }; 38 | 39 | // "Path:audio\r\n" 40 | pub(crate) static TAG_BODY_SPLIT: [u8; 12] = [80, 97, 116, 104, 58, 97, 117, 100, 105, 111, 13, 10]; 41 | // gX-R 42 | pub(crate) static TAG_NONE_DATA_START: [u8; 2] = [0, 103]; 43 | 44 | impl MsTtsMsgRequest { 45 | #[inline] 46 | pub fn to_bytes(&self) -> Bytes { 47 | Bytes::from(bincode::serialize(self).unwrap()) 48 | } 49 | 50 | #[inline] 51 | pub fn from_bytes(bytes: Bytes) -> Self { 52 | let data: Self = bincode::deserialize(&bytes[..]).unwrap(); 53 | data 54 | } 55 | } 56 | 57 | impl From for Vec { 58 | #[inline] 59 | fn from(val: MsTtsMsgRequest) -> Self { 60 | val.to_bytes().to_vec() 61 | } 62 | } 63 | 64 | impl From for event_bus::message::Body { 65 | #[inline] 66 | fn from(val: MsTtsMsgRequest) -> Self { 67 | event_bus::message::Body::ByteArray(val.into()) 68 | } 69 | } 70 | 71 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 72 | pub struct MsTtsMsgResponse { 73 | pub request_id: String, 74 | pub data: Vec, 75 | pub file_type: String, 76 | } 77 | 78 | impl MsTtsMsgResponse { 79 | #[inline] 80 | pub fn to_bytes(&self) -> Bytes { 81 | Bytes::from(bincode::serialize(self).unwrap()) 82 | } 83 | 84 | #[inline] 85 | pub fn from_bytes(bytes: Bytes) -> Self { 86 | let data: Self = bincode::deserialize(&bytes[..]).unwrap(); 87 | data 88 | } 89 | 90 | #[inline] 91 | pub fn to_vec(&self) -> Vec { 92 | Bytes::from(bincode::serialize(self).unwrap()).to_vec() 93 | } 94 | 95 | #[inline] 96 | pub fn from_vec(bytes: Vec) -> Self { 97 | let data: Self = bincode::deserialize(&bytes[..]).unwrap(); 98 | data 99 | } 100 | } 101 | 102 | type WebsocketRt = SplitSink>, Message>; 103 | 104 | pub struct MsTtsCache { 105 | pub data: BytesMut, 106 | pub reply: IMessage, 107 | pub file_type: Option, 108 | } 109 | 110 | #[derive(Debug)] 111 | pub struct MsSocketInfo 112 | where 113 | T: AzureApiSpeakerList + AzureApiNewWebsocket + AzureApiGenerateXMML, 114 | { 115 | azure_api: Arc, 116 | tx: Arc>>, 117 | new: AtomicBool, 118 | } 119 | 120 | /// 121 | /// 微软 文本转语音接口注册服务 122 | #[allow(dead_code)] 123 | pub(crate) async fn register_service() { 124 | debug!("register_service"); 125 | 126 | let args = AppArgs::parse_macro(); 127 | if args.close_edge_free_api 128 | && args.close_official_preview_api 129 | && args.close_official_subscribe_api 130 | { 131 | error!("请最起码启用一个api"); 132 | std::process::exit(1); 133 | } 134 | 135 | // 注册 edge 免费接口的服务 136 | if !args.close_edge_free_api { 137 | /// edge 免费接口 socket 连接 138 | static SOCKET_TX_EDGE_FREE: OnceCell>>> = 139 | OnceCell::const_new(); 140 | 141 | /// edge 免费接口 数据缓存 142 | static MS_TTS_DATA_CACHE_EDGE_FREE: Lazy< 143 | Arc>>>>, 144 | > = Lazy::new(|| { 145 | let kk = HashMap::new(); 146 | Arc::new(Mutex::new(kk)) 147 | }); 148 | /// edge 免费接口 新请求限制措施 149 | static MS_TTS_GET_NEW_EDGE_FREE: Lazy = Lazy::new(|| AtomicBool::new(false)); 150 | 151 | AzureApiEdgeFree::new().get_vices_list().await.unwrap(); 152 | 153 | crate::GLOBAL_EB 154 | .consumer("tts_ms_edge_free", |fn_msg| async move { 155 | let eb_msg = fn_msg.msg.clone(); 156 | let eb = Arc::clone(&fn_msg.eb); 157 | let ll = Bytes::from( 158 | eb_msg 159 | .body() 160 | .await 161 | .as_bytes() 162 | .expect("event_bus[ms-tts]: body is not bytes") 163 | .to_vec(), 164 | ); 165 | let request = MsTtsMsgRequest::from_bytes(ll); 166 | let tx_socket = Arc::clone( 167 | SOCKET_TX_EDGE_FREE 168 | .get_or_init(|| async { Arc::new(Mutex::new(None)) }) 169 | .await, 170 | ); 171 | 172 | if !MS_TTS_GET_NEW_EDGE_FREE.load(Ordering::Relaxed) 173 | && !tx_socket.clone().lock().await.is_some() 174 | { 175 | MS_TTS_GET_NEW_EDGE_FREE.store(true, Ordering::Release); 176 | debug!("websocket is not connected"); 177 | let mut result = AzureApiEdgeFree::new().get_connection().await; 178 | 'outer: loop { 179 | // 'outer: 180 | trace!("进入循环,防止websocket连接失败"); 181 | let result_bool = result.is_ok(); 182 | 183 | if result_bool { 184 | trace!("websocket连接成功"); 185 | let (tx_tmp, rx_tmp) = result.unwrap().split(); 186 | *tx_socket.clone().lock().await = Some(tx_tmp); 187 | let tx_tmp1 = Arc::clone(&tx_socket); 188 | trace!("启动消息处理线程"); 189 | eb.runtime.spawn(async move { 190 | process_response_body( 191 | rx_tmp, 192 | tx_tmp1, 193 | MS_TTS_DATA_CACHE_EDGE_FREE.clone(), 194 | ) 195 | .await; 196 | }); 197 | trace!("准备跳出循环"); 198 | break 'outer; 199 | } else { 200 | trace!("reconnection websocket"); 201 | sleep(Duration::from_secs(1)).await; 202 | result = AzureApiEdgeFree::new().get_connection().await; 203 | } 204 | } 205 | trace!("循环已跳出"); 206 | MS_TTS_GET_NEW_EDGE_FREE.store(false, Ordering::Release) 207 | } else { 208 | while MS_TTS_GET_NEW_EDGE_FREE.load(Ordering::Relaxed) 209 | || !tx_socket.clone().lock().await.is_some() 210 | { 211 | tokio::time::sleep(Duration::from_millis(200)).await; 212 | } 213 | } 214 | trace!("存在websocket连接,继续处理"); 215 | let request_id = request.request_id.clone(); 216 | debug!("发送请求: {} | {:?}", request.request_id, request); 217 | 218 | let xmml = AzureApiEdgeFree::new() 219 | .generate_xmml(request) 220 | .await 221 | .expect("generate_xmml 错误"); 222 | 223 | // 向 websocket 发送消息 224 | MS_TTS_DATA_CACHE_EDGE_FREE.clone().lock().await.insert( 225 | request_id, 226 | Arc::new(Mutex::new(MsTtsCache { 227 | data: BytesMut::new(), 228 | reply: eb_msg.clone(), 229 | file_type: None, 230 | })), 231 | ); 232 | 233 | let mut gg = tx_socket.lock().await; 234 | let socket = gg.as_mut(); 235 | if let Some(s) = socket { 236 | for i in xmml { 237 | debug!("\n >>>>>>>>>> xmml data\n{}\n <<<<<<<<<<\n", &i); 238 | s.send(Message::Text(i)).await.unwrap(); 239 | } 240 | drop(gg) 241 | } 242 | }) 243 | .await; 244 | } 245 | 246 | // 注册 官网ApiKey 调用服务 247 | if !args.close_official_subscribe_api { 248 | static OFFICIAL_SUBSCRIBE_API_LIST: OnceCell> = 249 | OnceCell::const_new(); 250 | static OFFICIAL_SUBSCRIBE_API_USE_INDEX: Lazy> = Lazy::new(|| Mutex::new(0)); 251 | 252 | let vec_list = &args.subscribe_key; 253 | 254 | OFFICIAL_SUBSCRIBE_API_LIST 255 | .get_or_init(|| async move { 256 | 257 | AzureSubscribeKey::from(vec_list) 258 | }) 259 | .await; 260 | 261 | if OFFICIAL_SUBSCRIBE_API_LIST.get().unwrap().is_empty() { 262 | error!("为了启用 subscribe api 最起码得添加一个有用的数据吧"); 263 | std::process::exit(1); 264 | } 265 | 266 | /// 官网 免费预览接口 socket 连接 267 | static SOCKET_TX_MAP_OFFICIAL_SUBSCRIBE: OnceCell< 268 | Mutex>>>, 269 | > = OnceCell::const_new(); 270 | 271 | // 设定程序配置的订阅key 272 | SOCKET_TX_MAP_OFFICIAL_SUBSCRIBE 273 | .get_or_init(|| async move { 274 | let mut h = HashMap::new(); 275 | for subscribe_key in OFFICIAL_SUBSCRIBE_API_LIST.get().unwrap().iter() { 276 | let info = MsSocketInfo { 277 | azure_api: AzureApiSubscribeToken::new_from_subscribe_key(subscribe_key), 278 | tx: Arc::new(Mutex::new(None)), 279 | new: AtomicBool::new(false), 280 | }; 281 | h.insert(subscribe_key.hash_str(), Arc::new(info)); 282 | } 283 | Mutex::new(h) 284 | }) 285 | .await; 286 | 287 | // 根据程序内订阅key获取发音人等数据 288 | if let Err(e) = AzureApiSubscribeToken::get_vices_mixed_list().await { 289 | error!("获取订阅key 的音频列表失败, {:?}", e); 290 | std::process::exit(1); 291 | } 292 | 293 | /// 官网 订阅API 响应数据缓存 294 | static MS_TTS_DATA_CACHE_OFFICIAL_SUBSCRIBE: Lazy< 295 | Arc>>>>, 296 | > = Lazy::new(|| { 297 | let kk = HashMap::new(); 298 | Arc::new(Mutex::new(kk)) 299 | }); 300 | 301 | #[macro_export] 302 | macro_rules! get_subscribe_api_tx_for_map { 303 | () => { 304 | SOCKET_TX_MAP_OFFICIAL_SUBSCRIBE.get().unwrap().lock().await 305 | }; 306 | } 307 | // 308 | 309 | crate::GLOBAL_EB 310 | .consumer("tts_ms_subscribe_api", |fn_msg| async move { 311 | let eb_msg = fn_msg.msg.clone(); 312 | let eb = Arc::clone(&fn_msg.eb); 313 | let ll = Bytes::from( 314 | eb_msg 315 | .body() 316 | .await 317 | .as_bytes() 318 | .expect("event_bus[tts_ms_subscribe_api]: body is not bytes") 319 | .to_vec(), 320 | ); 321 | let request = MsTtsMsgRequest::from_bytes(ll); 322 | 323 | let key_info = if request.region.is_some() && request.subscribe_key.is_some() 324 | { 325 | let key_tmp = String::from(request.subscribe_key.as_ref().unwrap()); 326 | let region_tmp = String::from(request.region.as_ref().unwrap()); 327 | 328 | let api_key = AzureSubscribeKey( 329 | key_tmp, 330 | AzureApiRegionIdentifier::from(®ion_tmp).unwrap(), 331 | ); 332 | let hash = api_key.hash_str(); 333 | let if_contains = get_subscribe_api_tx_for_map!().contains_key(&hash); 334 | let key_info = if if_contains { 335 | get_subscribe_api_tx_for_map!().get(&hash).unwrap().clone() 336 | } else { 337 | let key_info = Arc::new(MsSocketInfo { 338 | azure_api: AzureApiSubscribeToken::new_from_subscribe_key(&api_key), 339 | tx: Arc::new(Mutex::new(None)), 340 | new: AtomicBool::new(false), 341 | }); 342 | get_subscribe_api_tx_for_map!().insert(hash, key_info.clone()); 343 | key_info 344 | }; 345 | key_info 346 | } else { 347 | let index = *OFFICIAL_SUBSCRIBE_API_USE_INDEX.lock().await; 348 | let key_info = OFFICIAL_SUBSCRIBE_API_LIST 349 | .get() 350 | .unwrap() 351 | .get(index) 352 | .unwrap(); 353 | get_subscribe_api_tx_for_map!() 354 | .get(&key_info.hash_str()) 355 | .unwrap() 356 | .clone() 357 | }; 358 | let azure_api = key_info.azure_api.clone(); 359 | 360 | let tx_socket = key_info.tx.clone(); 361 | 362 | if !key_info.new.load(Ordering::Relaxed) 363 | && !tx_socket.clone().lock().await.is_some() 364 | { 365 | key_info.new.store(true, Ordering::Release); 366 | 367 | debug!("websocket is not connected"); 368 | // let mut info_mut = ; 369 | // let token_info = key_info.lock().await.azure_api.clone(); 370 | let mut result = azure_api.get_connection().await; 371 | // drop(info_mut); 372 | // let mut result = new_websocket_edge_free().await; 373 | 'outer: loop { 374 | // 'outer: 375 | trace!("进入循环,防止websocket连接失败"); 376 | let result_bool = result.is_ok(); 377 | 378 | if result_bool { 379 | trace!("websocket连接成功"); 380 | let (tx_tmp, rx_tmp) = result.unwrap().split(); 381 | *tx_socket.clone().lock().await = Some(tx_tmp); 382 | let tx_tmp1 = Arc::clone(&tx_socket); 383 | trace!("启动消息处理线程"); 384 | eb.runtime.spawn(async move { 385 | process_response_body( 386 | rx_tmp, 387 | tx_tmp1, 388 | MS_TTS_DATA_CACHE_OFFICIAL_SUBSCRIBE.clone(), 389 | ) 390 | .await; 391 | // 更新下一次进行连接的 Api 下标 392 | let num = *OFFICIAL_SUBSCRIBE_API_USE_INDEX.lock().await + 1; 393 | if num > OFFICIAL_SUBSCRIBE_API_LIST.get().unwrap().len() { 394 | *OFFICIAL_SUBSCRIBE_API_USE_INDEX.lock().await = 0; 395 | } else { 396 | *OFFICIAL_SUBSCRIBE_API_USE_INDEX.lock().await = num; 397 | } 398 | }); 399 | trace!("准备跳出循环"); 400 | break 'outer; 401 | } else { 402 | trace!("reconnection websocket"); 403 | sleep(Duration::from_secs(1)).await; 404 | result = azure_api.get_connection().await; 405 | } 406 | } 407 | trace!("循环已跳出"); 408 | key_info.new.store(false, Ordering::Release) 409 | } else { 410 | while key_info.new.load(Ordering::Relaxed) 411 | || !tx_socket.clone().lock().await.is_some() 412 | { 413 | tokio::time::sleep(Duration::from_millis(200)).await; 414 | } 415 | } 416 | trace!("存在websocket连接,继续处理"); 417 | 418 | let request_id = request.request_id.clone(); 419 | debug!("发送请求: {} | {:?}", request_id, request); 420 | 421 | let xmml = azure_api 422 | .generate_xmml(request) 423 | .await 424 | .expect("generate_xmml 错误"); 425 | 426 | // 向 websocket 发送消息 427 | MS_TTS_DATA_CACHE_OFFICIAL_SUBSCRIBE 428 | .clone() 429 | .lock() 430 | .await 431 | .insert( 432 | request_id, 433 | Arc::new(Mutex::new(MsTtsCache { 434 | data: BytesMut::new(), 435 | reply: eb_msg.clone(), 436 | file_type: None, 437 | })), 438 | ); 439 | 440 | let mut gg = tx_socket.lock().await; 441 | let socket = gg.as_mut(); 442 | if let Some(s) = socket { 443 | for i in xmml { 444 | debug!("\n >>>>>>>>>> xmml data\n{}\n <<<<<<<<<<\n", &i); 445 | s.send(Message::Text(i)).await.unwrap(); 446 | } 447 | drop(gg) 448 | } 449 | }) 450 | .await; 451 | } 452 | } 453 | 454 | /// 处理微软api 响应 455 | #[allow(dead_code)] 456 | async fn process_response_body( 457 | rx_r: SplitStream>>, 458 | tx_r: Arc>>, 459 | cache_db: Arc>>>>, 460 | ) { 461 | let mut rx_r = rx_r; 462 | loop { 463 | let msg = rx_r.next().await.unwrap(); 464 | match msg { 465 | Ok(m) => { 466 | trace!("收到消息"); 467 | match m { 468 | Message::Ping(s) => { 469 | trace!("收到ping消息: {:?}", s); 470 | } 471 | Message::Pong(s) => { 472 | trace!("收到pong消息: {:?}", s); 473 | } 474 | Message::Close(s) => { 475 | debug!("被动断开连接: {:?}", s); 476 | break; 477 | } 478 | Message::Text(s) => { 479 | let id = s[12..44].to_string(); 480 | // info!("到消息: {}", id); 481 | if let Some(_i) = s.find("Path:turn.start") { 482 | } else if let Some(_i) = s.find("Path:turn.end") { 483 | trace!("响应 {}, 结束", id); 484 | let data = { cache_db.lock().await.remove(&id) }; 485 | if let Some(data) = data { 486 | debug!("结束请求: {}", id); 487 | let data = data.lock().await; 488 | 489 | let body = MsTtsMsgResponse { 490 | request_id: id, 491 | data: data.data.to_vec().clone(), 492 | file_type: data.file_type.as_ref().unwrap().to_string(), 493 | }; 494 | data.reply.reply(body.to_vec().into()).await; 495 | // eb_msg.reply(data.to_vec().into()).await; 496 | } else { 497 | trace!("响应 不存在回复"); 498 | } 499 | // 测试代码 500 | // File::create(format!("tmp/{}.mp3", id)).await 501 | // .unwrap().write_all(&cache.get(&id).unwrap().to_vec()).await.unwrap(); 502 | // ; 503 | } 504 | } 505 | Message::Binary(s) => { 506 | if s.starts_with(&TAG_NONE_DATA_START) { 507 | let id = String::from_utf8(s[14..46].to_vec()).unwrap(); 508 | trace!("二进制响应体结束 TAG_NONE_DATA_START, {}", id); 509 | } else { 510 | let id = String::from_utf8(s[14..46].to_vec()).unwrap(); 511 | let mut body = BytesMut::from(s.as_slice()); 512 | let index = binary_search(&s, &TAG_BODY_SPLIT).unwrap(); 513 | let head = body.split_to(index + TAG_BODY_SPLIT.len()); 514 | let cache = { cache_db.lock().await.get(&id).unwrap().clone() }; 515 | let mut cache_map = cache.lock().await; 516 | cache_map.data.put(body); 517 | if cache_map.file_type.is_none() { 518 | let head = String::from_utf8(head.to_vec()[2..head.len()].to_vec()) 519 | .unwrap(); 520 | let head_list = head.split("\r\n").collect::>(); 521 | let content_type = 522 | head_list[1].to_string().split(':').collect::>()[1] 523 | .to_string(); 524 | trace!("content_type: {}", content_type); 525 | cache_map.file_type = Some(content_type); 526 | } 527 | drop(cache_map); 528 | drop(cache); 529 | // unsafe { Arc::get_mut_unchecked(&mut MS_TTS_DATA_CACHE.clone()).get_mut(&id).unwrap().lock().await.data.put(body) }; 530 | trace!("二进制响应体 ,{}", id); 531 | } /* else { 532 | trace!("其他二进制类型: {} ", unsafe { String::from_utf8_unchecked(s.to_vec()) }); 533 | }*/ 534 | } 535 | _ => {} 536 | } 537 | } 538 | Err(e) => { 539 | // trace!("收到错误消息:{:?}", e); 540 | debug!("收到错误消息,被动断开连接: {:?}", e); 541 | // websocket 错误的话就会断开连接 542 | break; 543 | } 544 | } 545 | } 546 | *tx_r.lock().await = None; 547 | } 548 | 549 | #[derive(Debug)] 550 | pub struct MsTtsConfig { 551 | pub voices_list: VoicesList, 552 | pub quality_list: Vec, 553 | } 554 | 555 | // 空白音频 556 | #[allow(dead_code)] 557 | pub(crate) const BLANK_MUSIC_FILE: &[u8] = include_bytes!("resource/blank.mp3"); 558 | -------------------------------------------------------------------------------- /src/resource/api/ms-api-edge.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_id": "ms-tts-edge", 3 | "api_name": "微软文本转语音", 4 | "api_desc": "微软语音合成 - Edge浏览器免费接口", 5 | "api_url": "/api/{{api_id}}", 6 | "params": [ 7 | { 8 | "index": 0, 9 | "param_type": "List", 10 | "param_name": "informant", 11 | "param_desc": "发音人", 12 | "list_data_url": "/api/{{api_id}}/informant" 13 | }, 14 | { 15 | "index": 1, 16 | "param_type": "Float", 17 | "param_name": "rate", 18 | "param_desc": "语速", 19 | "min_value": 0.0, 20 | "max_value": 3.0, 21 | "default_value": 1.0 22 | }, 23 | { 24 | "index": 2, 25 | "param_type": "Text", 26 | "param_name": "text", 27 | "param_desc": "待生成文本", 28 | "max_len": 1000 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /src/resource/api/ms-api-subscribe.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_id": "tts-ms-subscribe", 3 | "api_name": "微软文本转语音", 4 | "api_desc": "微软语音合成 - 订阅API接口", 5 | "api_url": "/api/{{api_id}}", 6 | "params": [ 7 | { 8 | "index": 0, 9 | "param_type": "Text", 10 | "param_name": "text", 11 | "param_desc": "待生成文本", 12 | "max_len": 5000 13 | }, 14 | { 15 | "index": 1, 16 | "param_type": "List", 17 | "param_name": "informant", 18 | "param_desc": "发音人", 19 | "list_data_url": "/api/{{api_id}}/informant" 20 | }, 21 | { 22 | "index": 2, 23 | "param_type": "List", 24 | "param_name": "style", 25 | "param_desc": "音频风格", 26 | "list_data_url": "/api/{{api_id}}/{{informant}}/style" 27 | }, 28 | { 29 | "index": 3, 30 | "param_type": "Float", 31 | "param_name": "rate", 32 | "param_desc": "语速", 33 | "min_value": 0.0, 34 | "max_value": 3.0, 35 | "default_value": 1.0 36 | }, 37 | { 38 | "index": 4, 39 | "param_type": "Float", 40 | "param_name": "pitch", 41 | "param_desc": "音调", 42 | "min_value": 0.0, 43 | "max_value": 2.0, 44 | "default_value": 1.0 45 | }, 46 | { 47 | "index": 5, 48 | "param_type": "List", 49 | "param_name": "quality", 50 | "param_desc": "音频格式", 51 | "list_data_url": "/api/{{api_id}}/quality" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /src/resource/blank.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litcc/tts-server/5d9ff18bf731a78a6fbbcdab679ef91a9b80bd0c/src/resource/blank.mp3 -------------------------------------------------------------------------------- /src/tests/azure_api_test.rs: -------------------------------------------------------------------------------- 1 | use futures::future::join_all; 2 | use log::{error, info, LevelFilter}; 3 | 4 | use crate::{ 5 | utils::{ 6 | azure_api::{ 7 | AzureApiEdgeFree, AzureApiNewWebsocket, 8 | AzureApiRegionIdentifier, AzureApiSubscribeToken, AzureAuthKey, VoicesItem, 9 | }, 10 | log::init_test_log, 11 | }, 12 | AppArgs, 13 | }; 14 | 15 | /// 16 | /// 订阅 Key 获取测试 17 | #[tokio::test] 18 | async fn test_azure_api_subscribe_create_oauth_token() { 19 | init_test_log(LevelFilter::Debug, None); 20 | let region = std::env::var("REGION"); 21 | let subscription_key = std::env::var("SUBSCRIPTION"); 22 | 23 | if region.is_err() || subscription_key.is_err() { 24 | error!("未找到订阅key,跳过测试"); 25 | std::process::exit(1); 26 | } 27 | let region = region.unwrap(); 28 | let subscription_key = subscription_key.unwrap(); 29 | let kk = AzureApiSubscribeToken::new( 30 | AzureApiRegionIdentifier::from(®ion).unwrap(), 31 | &subscription_key, 32 | ); 33 | let oauth = kk.get_oauth_token().await; 34 | info!("oauth: \n{:?}", oauth); 35 | assert!(oauth.is_ok()) 36 | } 37 | 38 | /// 订阅 Api Websocket 连接测试 39 | #[tokio::test] 40 | async fn test_azure_api_subscribe() { 41 | init_test_log(LevelFilter::Debug, None); 42 | let region = std::env::var("REGION").unwrap(); 43 | let subscription_key = std::env::var("SUBSCRIPTION").unwrap(); 44 | let kk = AzureApiSubscribeToken::new( 45 | AzureApiRegionIdentifier::from(®ion).unwrap(), 46 | &subscription_key, 47 | ); 48 | let oauth = kk.get_oauth_token().await.unwrap(); 49 | info!("{:?}", oauth); 50 | let jj = kk.get_connection().await; 51 | 52 | assert!(jj.is_ok()) 53 | } 54 | 55 | /// Edge 免费预览接口 Websocket 连接测试 56 | #[tokio::test] 57 | async fn test_azure_api_edge_free() { 58 | init_test_log(LevelFilter::Debug, None); 59 | let args = AppArgs::test_parse_macro(&["test", "--listen-port", "8989"]); 60 | info!("{:?}", args); 61 | let kk2 = AzureApiEdgeFree::new(); 62 | 63 | let jj = kk2.get_connection().await; 64 | 65 | assert!(jj.is_ok()) 66 | } 67 | 68 | /// 69 | #[tokio::test] 70 | async fn test_azure_api_edge_free_connectivity_cn_hk() -> anyhow::Result<()> { 71 | init_test_log(LevelFilter::Debug, None); 72 | 73 | let mut list = Vec::new(); 74 | for i in AzureApiEdgeFree::MS_TTS_SERVER_CHINA_HK_LIST { 75 | info!("[{}]", i); 76 | let kk = AzureApiEdgeFree::new_websocket_by_select_server(Some(i)); 77 | list.push(kk); 78 | } 79 | 80 | let list = join_all(list).await; 81 | 82 | for (index, x) in list.iter().enumerate() { 83 | info!( 84 | "ip: {}", 85 | AzureApiEdgeFree::MS_TTS_SERVER_CHINA_HK_LIST 86 | .get(index) 87 | .unwrap() 88 | ); 89 | info!("ok: {:?}", x.is_ok()) 90 | } 91 | 92 | Ok(()) 93 | } 94 | 95 | /// 96 | #[tokio::test] 97 | async fn test_azure_api_edge_free_connectivity_cn() -> anyhow::Result<()> { 98 | init_test_log(LevelFilter::Debug, None); 99 | 100 | let mut list = Vec::new(); 101 | for i in AzureApiEdgeFree::MS_TTS_SERVER_CHINA_LIST { 102 | info!("[{}]", i); 103 | let kk = AzureApiEdgeFree::new_websocket_by_select_server(Some(i)); 104 | list.push(kk); 105 | } 106 | 107 | let list = join_all(list).await; 108 | 109 | for (index, x) in list.iter().enumerate() { 110 | info!( 111 | "ip: {}", 112 | AzureApiEdgeFree::MS_TTS_SERVER_CHINA_LIST 113 | .get(index) 114 | .unwrap() 115 | ); 116 | info!("ok: {:?}", x.is_ok()) 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | /// 123 | #[tokio::test] 124 | async fn test_azure_api_edge_free_connectivity_cn_tw() -> anyhow::Result<()> { 125 | init_test_log(LevelFilter::Debug, None); 126 | 127 | let mut list = Vec::new(); 128 | for i in AzureApiEdgeFree::MS_TTS_SERVER_CHINA_TW_LIST { 129 | info!("[{}]", i); 130 | let kk = AzureApiEdgeFree::new_websocket_by_select_server(Some(i)); 131 | list.push(kk); 132 | } 133 | 134 | let list = join_all(list).await; 135 | 136 | for (index, x) in list.iter().enumerate() { 137 | info!( 138 | "ip: {}", 139 | AzureApiEdgeFree::MS_TTS_SERVER_CHINA_TW_LIST 140 | .get(index) 141 | .unwrap() 142 | ); 143 | info!("ok: {:?}", x.is_ok()) 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | /// 官网预览界面 免费接口 Websocket 连接测试 150 | #[tokio::test] 151 | async fn test_azure_api_serde_voices_list_data() -> anyhow::Result<()> { 152 | init_test_log(LevelFilter::Debug, None); 153 | 154 | let body = include_bytes!("../resource/azure_voices_list.json"); 155 | let voice_list: Vec = serde_json::from_slice(body.as_ref()).map_err(|e| { 156 | error!("{:?}", e); 157 | e 158 | })?; 159 | 160 | println!("{:?}", voice_list); 161 | 162 | let body2 = include_bytes!("../resource/edge_voices_list.json"); 163 | let voice_list2: Vec = serde_json::from_slice(body2.as_ref()).map_err(|e| { 164 | error!("{:?}", e); 165 | e 166 | })?; 167 | 168 | println!("{:?}", voice_list2); 169 | 170 | Ok(()) 171 | } 172 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod azure_api_test; 2 | pub(crate) mod other; 3 | -------------------------------------------------------------------------------- /src/tests/other.rs: -------------------------------------------------------------------------------- 1 | // use std::process::exit; 2 | // use crate::ms_tts::{MsTtsMsgRequest, TAG_BODY_SPLIT, TAG_NONE_DATA_START}; 3 | // use bytes::{BufMut, BytesMut}; 4 | // use fancy_regex::Regex; 5 | // use std::thread; 6 | // use std::time::Duration; 7 | // use chrono::Utc; 8 | // use futures::{SinkExt, StreamExt}; 9 | // use log::{debug, error, info, LevelFilter, trace}; 10 | // use tokio::fs::File; 11 | // use tokio::io::AsyncWriteExt; 12 | // use tokio_tungstenite::tungstenite::Message; 13 | // 14 | // // use json::JsonValue; 15 | // 16 | // use super::*; 17 | // 18 | // // 测试日志 19 | // fn init_log() { 20 | // utils::log::init_test_log(LevelFilter::Debug); 21 | // } 22 | // 23 | // #[test] 24 | // fn test_get_ms_tts_token() { 25 | // println!("ms tts websocket token: "); 26 | // let token: [u8; 32] = [ 27 | // 54, 65, 53, 65, 65, 49, 68, 52, 69, 65, 70, 70, 52, 69, 57, 70, 66, 51, 55, 69, 50, 51, 68, 28 | // 54, 56, 52, 57, 49, 68, 54, 70, 52, 29 | // ]; 30 | // 31 | // let _kk = "\r\nX-StreamId:"; 32 | // let _kk = "\r\nX-StreamId:"; 33 | // println!("{}", String::from_utf8(token.to_vec()).unwrap()) 34 | // } 35 | // 36 | // #[test] 37 | // fn test4() { 38 | // thread::spawn(|| { 39 | // for i in 1..10 { 40 | // println!("hi number {} from the spawned thread!", i); 41 | // thread::sleep(Duration::from_millis(1)); 42 | // } 43 | // }); 44 | // 45 | // for i in 1..5 { 46 | // println!("hi number {} from the main thread!", i); 47 | // thread::sleep(Duration::from_millis(1)); 48 | // } 49 | // } 50 | // 51 | // /// 52 | // /// 测试微软服务器连通性 53 | // #[tokio::test] 54 | // async fn test_ms_server_connectivity_cn() { 55 | // init_log(); 56 | // info!("开始测试服务器连通性"); 57 | // for i in AzureApiEdgeFree::MS_TTS_SERVER_CHINA_LIST { 58 | // info!("[{}]", i); 59 | // let kk = new_websocket_by_select_server(Some(i)).await; 60 | // error!("{:?}", kk); 61 | // assert!(kk.is_ok()); 62 | // } 63 | // } 64 | // 65 | // /// 测试微软服务器连通性 66 | // #[tokio::test] 67 | // async fn test_ms_server_connectivity_cn_tw() { 68 | // init_log(); 69 | // info!("开始测试服务器连通性"); 70 | // 71 | // for i in AzureApiEdgeFree::MS_TTS_SERVER_CHINA_TW_LIST { 72 | // info!("[{}]", i); 73 | // let kk = new_websocket_by_select_server(Some(i)).await; 74 | // assert!(kk.is_ok()); 75 | // } 76 | // } 77 | // 78 | // /// 测试微软服务器连通性 79 | // #[tokio::test] 80 | // async fn test_ms_server_connectivity_cn_hk() { 81 | // init_log(); 82 | // info!("开始测试服务器连通性"); 83 | // for i in AzureApiEdgeFree::MS_TTS_SERVER_CHINA_HK_LIST { 84 | // info!("[{}]", i); 85 | // let kk = new_websocket_by_select_server(Some(i)).await; 86 | // assert!(kk.is_ok()); 87 | // } 88 | // } 89 | // 90 | // 91 | // /// 测试微软服务器连通性 92 | // #[tokio::test] 93 | // async fn test_ms_server_connectivity() { 94 | // init_log(); 95 | // info!("开始测试服务器连通性"); 96 | // 97 | // let kk = new_websocket_by_select_server(None).await; 98 | // assert!(kk.is_ok()); 99 | // } 100 | // 101 | // 102 | // #[tokio::test] 103 | // async fn test_ms_tts_websocket_call() { 104 | // init_log(); 105 | // debug!("test_float_calculate"); 106 | // //Some(ms_tts::MS_TTS_SERVER_CHINA_LIST.last().unwrap()) 107 | // let ip = AzureApiEdgeFree::MS_TTS_SERVER_CHINA_LIST.first().unwrap(); 108 | // let ip = "182.61.148.24"; 109 | // let ip = "182.61.148.24"; 110 | // debug!("ip:{}",ip); 111 | // // 182.61.148.24 不行 112 | // let kk = new_websocket_by_select_server(None).await.unwrap(); 113 | // debug!("构造 ssml 消息"); 114 | // let id = random_string(32); 115 | // let create_time = Utc::now(); 116 | // let time = format!("{:?}", create_time); 117 | // 118 | // 119 | // let mut msg1 = format!("Path: speech.config\r\nX-Timestamp: {}\r\nX-RequestId: {}\r\nContent-Type: application/json\r\n\r\n", &time, &id); 120 | // msg1.push_str("{\"context\":{\"system\":{\"name\":\"SpeechSDK\",\"version\":\"1.19.0\",\"build\":\"JavaScript\",\"lang\":\"JavaScript\"},\"os\":{\"platform\":\"Browser/Win32\",\"name\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.39\",\"version\":\"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.39\"}}}"); 121 | // 122 | // let mut msg2 = String::new(); 123 | // msg2.push_str(format!("Path: synthesis.context\r\nX-RequestId: {}\r\nX-Timestamp: {}\r\nContent-Type: application/json", &id, &time).as_str()); 124 | // msg2.push_str("\r\n\r\n{\"synthesis\":{\"audio\":{\"metadataOptions\":{\"bookmarkEnabled\":false,\"sentenceBoundaryEnabled\":false,\"visemeEnabled\":false,\"wordBoundaryEnabled\":false},\"outputFormat\":\"ogg-16khz-16bit-mono-opus\"},\"language\":{\"autoDetection\":false}}}"); 125 | // 126 | // 127 | // // 128 | // // let msg3 = format!(r"Path: ssml\r\nX-RequestId:{}\r\nContent-Type:application/ssml+xml\r\nPath:ssml\r\n\r\n新华社北京6月15日电(记者魏玉坤、王悦阳)国家统计局15日发布数据显示,5月份,我国经济逐步克服疫情不利影响,生产需求逐步恢复,就业物价总体稳定,主要指标边际改善,国民经济呈现恢复势头。",id); 129 | // 130 | // 131 | // // 132 | // // let mut msg1 = String::new(); 133 | // // msg1.push_str("Content-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n"); 134 | // // msg1.push_str("{\"context\":{\"synthesis\":{\"audio\":{\"metadataoptions\":{\"sentenceBoundaryEnabled\":\"false\",\"wordBoundaryEnabled\":\"false\"},\"outputFormat\":\"webm-24khz-16bit-mono-opus\"}}}}\r\n"); 135 | // 136 | // //Path: ssml 137 | // // X-RequestId: 3508B5D2EDF1460292AE6F03EB6F4F32 138 | // // X-Timestamp: 2022-06-16T15:42:07.494Z 139 | // // Content-Type: application/ssml+xml 140 | // 141 | // let mut msg3 = String::new(); 142 | // 143 | // msg3.push_str(format!("Path:ssml\r\nX-RequestId:{}\r\nContent-Type:application/ssml+xml\r\n\r\n", &id).as_str()); 144 | // // 145 | // // Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural) 146 | // msg3.push_str("新华社北京6月15日电(记者魏玉坤、王悦阳)国家统计局15日发布数据显示,5月份,我国经济逐步克服疫情不利影响,生产需求逐步恢复,就业物价总体稳定,主要指标边际改善,国民经济呈现恢复势头。"); 147 | // 148 | // 149 | // // msg3.push_str(r#"也可以合成多角色多情感的有声,例如:黛玉冷笑道:“我说呢,亏了绊住,不然,早就飞了来了。” 宝玉道:“只许和你玩,替你解闷。不过偶然到他那里,就说这些闲话。””好没意思的话!去不去,关我什么事儿?又没叫你替我解闷儿,还许你从此我呢!”说着,便赌气回房去了。"#); 150 | // 151 | // debug!("{:#?}",msg1); 152 | // debug!("{:#?}",msg2); 153 | // debug!("{:#?}",msg3); 154 | // 155 | // let (mut a, mut b) = kk.split(); 156 | // // a.send(Message::Text(msg1)).await.unwrap(); 157 | // debug!("发送 ssml 消息"); 158 | // a.send(Message::Text(msg2)).await.unwrap(); 159 | // 160 | // a.send(Message::Text(msg3)).await.unwrap(); 161 | // let mut df = b.next().await; 162 | // let mut resp = BytesMut::new(); 163 | // while df.is_some() { 164 | // debug!("收到消息"); 165 | // let kk = df.unwrap(); 166 | // if let Ok(ss) = kk { 167 | // info!("{:?}",ss); 168 | // get_file_body(&mut resp, ss).await; 169 | // } else { 170 | // error!("{:?}",kk) 171 | // } 172 | // df = b.next().await; 173 | // } 174 | // 175 | // // 向 websocket 发送消息 176 | // // 177 | // // thread::sleep(Duration::from_secs(5)); 178 | // } 179 | // 180 | // async fn get_file_body(resp: &mut BytesMut, msg: Message) { 181 | // match msg { 182 | // Message::Ping(s) => { 183 | // trace!("收到ping消息: {:?}", s); 184 | // } 185 | // Message::Pong(s) => { 186 | // trace!("收到pong消息: {:?}", s); 187 | // } 188 | // Message::Close(s) => { 189 | // debug!("被动断开连接: {:?}", s); 190 | // } 191 | // Message::Text(s) => { 192 | // let id = s[12..44].to_string(); 193 | // // info!("到消息: {}", id); 194 | // if let Some(_i) = s.find("Path:turn.start") { 195 | // // MS_TTS_DATA_CACHE.lock().await.insert(id, BytesMut::new()); 196 | // } else if let Some(_i) = s.find("Path:turn.end") { 197 | // trace!("响应 {}, 结束", id); 198 | // File::create(format!("tmp/test-{}.mp3", &id)).await 199 | // .unwrap().write_all(&resp.to_vec()).await.unwrap(); 200 | // ; 201 | // exit(0) 202 | // } 203 | // } 204 | // Message::Binary(s) => { 205 | // if s.starts_with(&crate::ms_tts::TAG_NONE_DATA_START) { 206 | // // } 207 | // // drop(cache_map); 208 | // // drop(cache); 209 | // // unsafe { Arc::get_mut_unchecked(&mut MS_TTS_DATA_CACHE.clone()).get_mut(&id).unwrap().lock().await.data.put(body) }; 210 | // let id = String::from_utf8(s[14..46].to_vec()).unwrap(); 211 | // trace!("二进制响应体结束 TAG_NONE_DATA_START, {}",id); 212 | // // trace!("二进制响应体 ,{}",id); 213 | // }/* else if s.starts_with(&TAG_NONE_DATA_START) { 214 | // let id = String::from_utf8(s[14..46].to_vec()).unwrap(); 215 | // trace!("二进制响应体结束 TAG_NONE_DATA_START, {}",id); 216 | // } */else { 217 | // let id = String::from_utf8(s[14..46].to_vec()).unwrap(); 218 | // let mut body = BytesMut::from(s.as_slice()); 219 | // let index = binary_search(&s, &TAG_BODY_SPLIT).unwrap(); 220 | // let head = body.split_to(index + TAG_BODY_SPLIT.len()); 221 | // resp.put(body); 222 | // // if cache_map.file_type.is_none() { 223 | // let head = String::from_utf8(head.to_vec()[2..head.len()].to_vec()).unwrap(); 224 | // let head_list = head.split("\r\n").collect::>(); 225 | // let content_type = head_list[1].to_string().split(":").collect::>()[1].to_string(); 226 | // trace!("content_type: {}", content_type); 227 | // 228 | // trace!("其他二进制类型: {} ", unsafe { String::from_utf8_unchecked(s.to_vec()) }); 229 | // } 230 | // } 231 | // _ => {} 232 | // } 233 | // } 234 | // 235 | // 236 | // #[tokio::test] 237 | // async fn test_bytes() { 238 | // init_log(); 239 | // info!("test_ms_tts_websocket"); 240 | // 241 | // let _tag_some_data_start = [0, 128]; 242 | // let _tag_none_data_start = [0, 103]; 243 | // 244 | // let tag1 = "6A5AA1D4EAFF4E9FB37E23D68491D6F4"; 245 | // let _tag1_2: [u8; 12] = [80, 97, 116, 104, 58, 97, 117, 100, 105, 111, 13, 10]; 246 | // let tag2: [u8; 5] = [0, 128, 88, 45, 82]; 247 | // 248 | // info!("tag1: {:?}", tag1.as_bytes()); 249 | // info!("tag2: {}", unsafe { 250 | // String::from_utf8_unchecked(tag2.to_vec()) 251 | // }); 252 | // 253 | // let _b = BytesMut::new(); 254 | // 255 | // // b.put(&b"123"[..]); 256 | // // b.reserve(2); 257 | // // b.put_slice(b"xy"); 258 | // // info!("{:?}",b); 259 | // // info!("{:?}",b.capacity()); 260 | // } 261 | // 262 | // #[tokio::test] 263 | // async fn test_serialize() { 264 | // init_log(); 265 | // info!("test_serialize"); 266 | // let test = MsTtsMsgRequest { 267 | // text: "123".to_string(), 268 | // request_id: "123".to_string(), 269 | // informant: "123".to_string(), 270 | // style: "123".to_string(), 271 | // rate: "123".to_string(), 272 | // pitch: "123".to_string(), 273 | // quality: "123".to_string(), 274 | // }; 275 | // let encoded: Vec = bincode::serialize(&test).unwrap(); 276 | // 277 | // info!("test: {:?}", encoded); 278 | // //let decoded: MsTtsRequest = bincode::deserialize(&encoded[..]).unwrap(); 279 | // //let adsf:Vec = postcard::to_allocvec(&test).unwrap(); 280 | // } 281 | // 282 | // // #[tokio::test] 283 | // #[test] 284 | // fn test_regex() { 285 | // init_log(); 286 | // info!("test_regex"); 287 | // 288 | // let re = Regex::new(r"^\s*$").unwrap(); 289 | // let result = re 290 | // .is_match( 291 | // r#" 292 | // asdfasdf"#, 293 | // ) 294 | // .unwrap(); 295 | // info!("{}", result); 296 | // thread::sleep(Duration::from_secs(5)); 297 | // } 298 | // 299 | // #[tokio::test] 300 | // // #[test] 301 | // async fn test_get_ms_tts_config() { 302 | // init_log(); 303 | // debug!("test_get_ms_tts_config"); 304 | // 305 | // let kk = crate::ms_tts::get_ms_tts_config().await; 306 | // if let Some(s) = kk { 307 | // debug!("请求成功"); 308 | // info!("{:?}", s); 309 | // } else { 310 | // debug!("请求失败"); 311 | // } 312 | // 313 | // //info!("{}",result); 314 | // thread::sleep(Duration::from_secs(5)); 315 | // } 316 | // 317 | // #[tokio::test] 318 | // // #[test] 319 | // async fn test_ms_tts_config() { 320 | // init_log(); 321 | // debug!("test_get_ms_tts_config"); 322 | // 323 | // let kk_s = crate::ms_tts::get_ms_tts_config().await.unwrap(); 324 | // info!("{:?}", kk_s); 325 | // info!( 326 | // "en-US: {}", 327 | // kk_s.voices_list.by_locale_map.get("en-US").unwrap().len() 328 | // ); 329 | // 330 | // // 411 331 | // 332 | // // info!("{}",kk); 333 | // thread::sleep(Duration::from_secs(5)); 334 | // } 335 | // 336 | // #[tokio::test] 337 | // // #[test] 338 | // async fn test_float_calculate() { 339 | // init_log(); 340 | // debug!("test_float_calculate"); 341 | // let x: f32 = 3.14159; 342 | // 343 | // let _kk = num::BigInt::parse_bytes(b"2", 10); 344 | // let sin_x = x.sin(); 345 | // println!("sin({}) = {}", x, sin_x); 346 | // let style = 1.6666666666666666666666666666666; 347 | // let kk_s = 100.00 * style - 100.00; 348 | // 349 | // info!("{:.0}", kk_s); 350 | // 351 | // // 352 | // thread::sleep(Duration::from_secs(5)); 353 | // } 354 | // 355 | // use urlencoding::decode as url_decode; 356 | // use crate::{ms_tts, random_string, utils}; 357 | // use crate::utils::azure_api::{AzureApiEdgeFree}; 358 | // use crate::utils::binary_search; 359 | // 360 | // #[tokio::test] 361 | // async fn test_url_decode() { 362 | // init_log(); 363 | // debug!("test_float_calculate"); 364 | // let mut sss = "你好".to_string(); 365 | // 366 | // let text_tmp2: String = 'break_1: loop { 367 | // let decoded = url_decode(&sss); 368 | // if let Ok(s) = decoded { 369 | // if sss == s.to_string() { 370 | // break 'break_1 sss; 371 | // } 372 | // sss = s.to_string(); 373 | // } else { 374 | // break 'break_1 sss; 375 | // } 376 | // }; 377 | // 378 | // info!("{}", text_tmp2); 379 | // // 380 | // thread::sleep(Duration::from_secs(5)); 381 | // } 382 | -------------------------------------------------------------------------------- /src/utils/azure_api.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{hash_map::DefaultHasher, HashMap, HashSet}, 3 | hash::{Hash, Hasher}, 4 | net::Ipv4Addr, 5 | sync::Arc, 6 | }; 7 | 8 | 9 | use chrono::{Duration, TimeZone, Utc}; 10 | use event_bus::async_utils::BoxFutureSync; 11 | use futures::{future::join_all, SinkExt}; 12 | use itertools::Itertools; 13 | use log::{debug, error, info, trace, warn}; 14 | use once_cell::sync::{Lazy, OnceCell}; 15 | use rand::Rng; 16 | use serde::{Deserialize, Serialize}; 17 | use tokio::{ 18 | net::TcpStream, 19 | sync::{Mutex, RwLock}, 20 | }; 21 | use tokio_native_tls::{native_tls, TlsStream}; 22 | use tokio_tungstenite::{ 23 | client_async_with_config, tungstenite, 24 | tungstenite::{ 25 | handshake::client::{generate_key, Request}, 26 | http::{Method, Uri, Version}, 27 | protocol::WebSocketConfig, 28 | }, 29 | WebSocketStream, 30 | }; 31 | 32 | use crate::{cmd::ServerArea, error::TTSServerError, random_string, AppArgs}; 33 | 34 | // 发音人配置 35 | #[allow(dead_code)] 36 | pub(crate) const AZURE_SPEAKERS_LIST_FILE: &[u8] = 37 | include_bytes!("../resource/azure_voices_list.json"); 38 | 39 | // 发音人配置 40 | #[allow(dead_code)] 41 | pub(crate) const EDGE_SPEAKERS_LIST_FILE: &[u8] = 42 | include_bytes!("../resource/edge_voices_list.json"); 43 | 44 | /// 该程序实现的 Api 调用方式 45 | #[derive(Debug)] 46 | pub enum MsApiOrigin { 47 | /// 传统 edge 免费预览接口 48 | EdgeFree, 49 | /// 官方 api-key 接口 50 | Subscription, 51 | } 52 | 53 | impl TryFrom for MsApiOrigin { 54 | type Error = TTSServerError; 55 | 56 | fn try_from(value: String) -> Result { 57 | match value.as_str() { 58 | "ms-tts-edge" => Ok(MsApiOrigin::EdgeFree), 59 | "ms-tts-subscribe" => Ok(MsApiOrigin::Subscription), 60 | _ => Err(TTSServerError::ProgramError("不存在的接口名称".to_owned())), 61 | } 62 | } 63 | } 64 | 65 | impl From for String { 66 | fn from(val: MsApiOrigin) -> Self { 67 | match val { 68 | MsApiOrigin::EdgeFree => "ms-tts-edge".to_owned(), 69 | MsApiOrigin::Subscription => "ms-tts-subscribe".to_owned(), 70 | } 71 | } 72 | } 73 | 74 | /// Azure Api 地域标识符 75 | #[derive(Clone, Debug, PartialEq)] 76 | #[allow(dead_code)] 77 | pub enum AzureApiRegionIdentifier { 78 | ///南非北部 79 | SouthAfricaNorth, 80 | ///东亚 81 | EastAsia, 82 | ///东南亚 83 | SoutheastAsia, 84 | ///澳大利亚东部 85 | AustraliaEast, 86 | ///印度中部 87 | CentralIndia, 88 | /// Jio 印度西部 89 | JioIndiaWest, 90 | ///日本东部 91 | JapanEast, 92 | ///日本西部 93 | JapanWest, 94 | ///韩国中部 95 | KoreaCentral, 96 | ///加拿大中部 97 | CanadaCentral, 98 | ///北欧 99 | NorthEurope, 100 | ///西欧 101 | WestEurope, 102 | ///法国中部 103 | FranceCentral, 104 | ///德国中西部 105 | GermanyWestCentral, 106 | ///挪威东部 107 | NorwayEast, 108 | ///瑞士北部 109 | SwitzerlandNorth, 110 | ///瑞士西部 111 | SwitzerlandWest, 112 | ///英国南部 113 | UkSouth, 114 | ///阿拉伯联合酋长国北部 115 | UaeNorth, 116 | ///巴西南部 117 | BrazilSouth, 118 | ///美国中部 119 | CentralUs, 120 | ///美国东部 121 | EastUs, 122 | ///美国东部2 123 | EastUs2, 124 | ///美国中北部 125 | NorthCentralUs, 126 | ///美国中南部 127 | SouthCentralUs, 128 | ///USGov亚利桑那州 129 | UsGovArizona, 130 | ///USGov弗吉尼亚州 131 | UsGovVirginia, 132 | ///美国中西部 133 | WestCentralUs, 134 | ///美国西部 135 | WestUs, 136 | ///美国西部2 137 | WestUs2, 138 | ///美国西部3 139 | WestUs3, 140 | } 141 | 142 | impl AzureApiRegionIdentifier { 143 | /// 获取地域标识 144 | #[allow(dead_code)] 145 | pub fn value(&self) -> String { 146 | match self { 147 | AzureApiRegionIdentifier::SouthAfricaNorth => "southafricanorth", 148 | AzureApiRegionIdentifier::EastAsia => "eastasia", 149 | AzureApiRegionIdentifier::SoutheastAsia => "southeastasia", 150 | AzureApiRegionIdentifier::AustraliaEast => "australiaeast", 151 | AzureApiRegionIdentifier::CentralIndia => "centralindia", 152 | AzureApiRegionIdentifier::JapanEast => "japaneast", 153 | AzureApiRegionIdentifier::JapanWest => "japanwest", 154 | AzureApiRegionIdentifier::KoreaCentral => "koreacentral", 155 | AzureApiRegionIdentifier::CanadaCentral => "canadacentral", 156 | AzureApiRegionIdentifier::NorthEurope => "northeurope", 157 | AzureApiRegionIdentifier::WestEurope => "westeurope", 158 | AzureApiRegionIdentifier::FranceCentral => "francecentral", 159 | AzureApiRegionIdentifier::GermanyWestCentral => "germanywestcentral", 160 | AzureApiRegionIdentifier::NorwayEast => "norwayeast", 161 | AzureApiRegionIdentifier::SwitzerlandNorth => "switzerlandnorth", 162 | AzureApiRegionIdentifier::SwitzerlandWest => "switzerlandwest", 163 | AzureApiRegionIdentifier::UkSouth => "uksouth", 164 | AzureApiRegionIdentifier::UaeNorth => "uaenorth", 165 | AzureApiRegionIdentifier::BrazilSouth => "brazilsouth", 166 | AzureApiRegionIdentifier::CentralUs => "centralus", 167 | AzureApiRegionIdentifier::EastUs => "eastus", 168 | AzureApiRegionIdentifier::EastUs2 => "eastus2", 169 | AzureApiRegionIdentifier::NorthCentralUs => "northcentralus", 170 | AzureApiRegionIdentifier::SouthCentralUs => "southcentralus", 171 | AzureApiRegionIdentifier::UsGovArizona => "usgovarizona", 172 | AzureApiRegionIdentifier::UsGovVirginia => "usgovvirginia", 173 | AzureApiRegionIdentifier::WestCentralUs => "westcentralus", 174 | AzureApiRegionIdentifier::WestUs => "westus", 175 | AzureApiRegionIdentifier::WestUs2 => "westus2", 176 | AzureApiRegionIdentifier::WestUs3 => "westus3", 177 | AzureApiRegionIdentifier::JioIndiaWest => "jioindiawest", 178 | } 179 | .to_owned() 180 | } 181 | 182 | #[allow(dead_code)] 183 | pub fn from(region_str: &str) -> Result { 184 | let kk = match region_str { 185 | "southafricanorth" => AzureApiRegionIdentifier::SouthAfricaNorth, 186 | "eastasia" => AzureApiRegionIdentifier::EastAsia, 187 | "southeastasia" => AzureApiRegionIdentifier::SoutheastAsia, 188 | "australiaeast" => AzureApiRegionIdentifier::AustraliaEast, 189 | "centralindia" => AzureApiRegionIdentifier::CentralIndia, 190 | "japaneast" => AzureApiRegionIdentifier::JapanEast, 191 | "japanwest" => AzureApiRegionIdentifier::JapanWest, 192 | "koreacentral" => AzureApiRegionIdentifier::KoreaCentral, 193 | "canadacentral" => AzureApiRegionIdentifier::CanadaCentral, 194 | "northeurope" => AzureApiRegionIdentifier::NorthEurope, 195 | "westeurope" => AzureApiRegionIdentifier::WestEurope, 196 | "francecentral" => AzureApiRegionIdentifier::FranceCentral, 197 | "germanywestcentral" => AzureApiRegionIdentifier::GermanyWestCentral, 198 | "norwayeast" => AzureApiRegionIdentifier::NorwayEast, 199 | "switzerlandnorth" => AzureApiRegionIdentifier::SwitzerlandNorth, 200 | "switzerlandwest" => AzureApiRegionIdentifier::SwitzerlandWest, 201 | "uksouth" => AzureApiRegionIdentifier::UkSouth, 202 | "uaenorth" => AzureApiRegionIdentifier::UaeNorth, 203 | "brazilsouth" => AzureApiRegionIdentifier::BrazilSouth, 204 | "centralus" => AzureApiRegionIdentifier::CentralUs, 205 | "eastus" => AzureApiRegionIdentifier::EastUs, 206 | "eastus2" => AzureApiRegionIdentifier::EastUs2, 207 | "northcentralus" => AzureApiRegionIdentifier::NorthCentralUs, 208 | "southcentralus" => AzureApiRegionIdentifier::SouthCentralUs, 209 | "usgovarizona" => AzureApiRegionIdentifier::UsGovArizona, 210 | "usgovvirginia" => AzureApiRegionIdentifier::UsGovVirginia, 211 | "westcentralus" => AzureApiRegionIdentifier::WestCentralUs, 212 | "westus" => AzureApiRegionIdentifier::WestUs, 213 | "westus2" => AzureApiRegionIdentifier::WestUs2, 214 | "westus3" => AzureApiRegionIdentifier::WestUs3, 215 | "jioindiawest" => AzureApiRegionIdentifier::JioIndiaWest, 216 | _ => { 217 | return Err(TTSServerError::ProgramError("解析地域标签错误".to_owned())); 218 | } 219 | }; 220 | 221 | Ok(kk) 222 | } 223 | } 224 | 225 | /// 226 | /// 可用音质参数 227 | pub(crate) static MS_TTS_QUALITY_LIST: [&str; 32] = [ 228 | "audio-16khz-128kbitrate-mono-mp3", 229 | "audio-16khz-16bit-32kbps-mono-opus", 230 | "audio-16khz-16kbps-mono-siren", 231 | "audio-16khz-32kbitrate-mono-mp3", 232 | "audio-16khz-64kbitrate-mono-mp3", 233 | "audio-24khz-160kbitrate-mono-mp3", 234 | "audio-24khz-16bit-24kbps-mono-opus", 235 | "audio-24khz-16bit-48kbps-mono-opus", 236 | "audio-24khz-48kbitrate-mono-mp3", 237 | "audio-24khz-96kbitrate-mono-mp3", 238 | "audio-48khz-192kbitrate-mono-mp3", 239 | "audio-48khz-96kbitrate-mono-mp3", 240 | "ogg-16khz-16bit-mono-opus", 241 | "ogg-24khz-16bit-mono-opus", 242 | "ogg-48khz-16bit-mono-opus", 243 | "raw-16khz-16bit-mono-pcm", 244 | "raw-16khz-16bit-mono-truesilk", 245 | "raw-24khz-16bit-mono-pcm", 246 | "raw-24khz-16bit-mono-truesilk", 247 | "raw-48khz-16bit-mono-pcm", 248 | "raw-8khz-16bit-mono-pcm", 249 | "raw-8khz-8bit-mono-alaw", 250 | "raw-8khz-8bit-mono-mulaw", 251 | "riff-16khz-16bit-mono-pcm", 252 | // "riff-16khz-16kbps-mono-siren",/*弃用*/ 253 | "riff-24khz-16bit-mono-pcm", 254 | "riff-48khz-16bit-mono-pcm", 255 | "riff-8khz-16bit-mono-pcm", 256 | "riff-8khz-8bit-mono-alaw", 257 | "riff-8khz-8bit-mono-mulaw", 258 | "webm-16khz-16bit-mono-opus", 259 | "webm-24khz-16bit-24kbps-mono-opus", 260 | "webm-24khz-16bit-mono-opus", 261 | ]; 262 | 263 | /// 264 | /// 微软认证 265 | pub trait AzureAuthSubscription { 266 | /// 获取订阅key 267 | fn get_subscription_key(&self) -> BoxFutureSync>; 268 | } 269 | 270 | /// 271 | /// 微软认证 272 | pub trait AzureAuthKey { 273 | /// 获取地域标识 274 | fn get_region_identifier( 275 | &self, 276 | ) -> BoxFutureSync>; 277 | /// 获取认证key 278 | fn get_oauth_token(&self) -> BoxFutureSync>; 279 | } 280 | 281 | /// 282 | /// 微软获取 发音人列表 283 | pub trait AzureApiSpeakerList { 284 | /// 获取发音人列表 285 | fn get_vices_list(&self) -> BoxFutureSync>; 286 | } 287 | 288 | /// 创建新的请求 289 | pub trait AzureApiNewWebsocket { 290 | fn get_connection( 291 | &self, 292 | ) -> BoxFutureSync>, TTSServerError>>; 293 | } 294 | 295 | /// 生成 xmml 请求 296 | pub trait AzureApiGenerateXMML { 297 | fn generate_xmml( 298 | &self, 299 | data: MsTtsMsgRequest, 300 | ) -> BoxFutureSync, TTSServerError>>; 301 | } 302 | 303 | /// Azure 认证 Key 304 | #[derive(PartialEq, Debug)] 305 | pub struct AzureSubscribeKey( 306 | /// 订阅key 307 | pub String, 308 | /// 地域 309 | pub AzureApiRegionIdentifier, 310 | ); 311 | 312 | impl AzureSubscribeKey { 313 | pub fn hash_str(&self) -> String { 314 | let mut hasher = DefaultHasher::new(); 315 | self.hash(&mut hasher); 316 | format!("{:x}", hasher.finish()) 317 | } 318 | 319 | pub fn from(list: &Vec) -> Vec { 320 | let mut k_list = Vec::new(); 321 | for i in list.iter() { 322 | let item = AzureSubscribeKey::try_from(i.as_str()); 323 | if let Ok(d) = item { 324 | k_list.push(d) 325 | } 326 | } 327 | k_list 328 | } 329 | } 330 | 331 | impl TryFrom<&str> for AzureSubscribeKey { 332 | type Error = anyhow::Error; 333 | 334 | fn try_from(value: &str) -> Result { 335 | let l: Vec<_> = value.split(',').collect(); 336 | if l.len() != 2 { 337 | let err = 338 | anyhow::Error::msg("错误的订阅字符串, 请检查订阅key参数是否符合要求".to_owned()); 339 | warn!("{:?}", err); 340 | return Err(err); 341 | } 342 | let key = l.first().unwrap().to_string(); 343 | let region = l.get(1).unwrap().to_string(); 344 | let region = AzureApiRegionIdentifier::from(®ion)?; 345 | Ok(AzureSubscribeKey(key, region)) 346 | } 347 | } 348 | 349 | impl Hash for AzureSubscribeKey { 350 | fn hash(&self, state: &mut H) { 351 | self.0.hash(state); 352 | self.1.value().hash(state); 353 | } 354 | } 355 | 356 | /// 生成 xmml 的数据 357 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 358 | pub struct MsTtsMsgRequest { 359 | // 待生成文本 360 | pub text: String, 361 | // 请求id 362 | pub request_id: String, 363 | // 发音人 364 | pub informant: String, 365 | // 音频风格 366 | pub style: String, 367 | // 语速 368 | pub rate: String, 369 | // 音调 370 | pub pitch: String, 371 | // 音频格式 372 | pub quality: String, 373 | 374 | // 使用订阅 API 时可用传递 自定义订阅 key,以及自定义订阅地区 375 | #[serde(default)] 376 | pub subscribe_key: Option, 377 | #[serde(default)] 378 | pub region: Option, 379 | // 以前java版本支持的功能,目前没时间支持 380 | // text_replace_list:Vec, 381 | // phoneme_list:Vec 382 | } 383 | 384 | /// 385 | /// 发音人列表 386 | #[derive(Debug)] 387 | pub struct VoicesList { 388 | pub voices_name_list: HashSet, 389 | pub raw_data: Vec>, 390 | pub by_voices_name_map: HashMap>, 391 | pub by_locale_map: HashMap>>, 392 | } 393 | 394 | /// 395 | /// Azure 订阅版文本转语音官方相关接口实例 396 | pub(crate) struct AzureApiSubscribeToken { 397 | region_identifier: AzureApiRegionIdentifier, 398 | subscription_key: String, 399 | oauth_token: Arc>>, 400 | oauth_get_time: Arc>, 401 | voices_list: RwLock>>>, 402 | } 403 | 404 | impl Hash for AzureApiSubscribeToken { 405 | fn hash(&self, state: &mut H) { 406 | self.subscription_key.hash(state); 407 | self.region_identifier.value().hash(state); 408 | } 409 | } 410 | 411 | static MS_TTS_SUBSCRIBE_TOKEN_LIST: Lazy>>>> = 412 | Lazy::new(|| { 413 | let kk = HashMap::new(); 414 | Arc::new(Mutex::new(kk)) 415 | }); 416 | 417 | static MS_TTS_SUBSCRIBE_VICES_MIXED_LIST: tokio::sync::OnceCell>> = 418 | tokio::sync::OnceCell::const_new(); 419 | 420 | impl AzureApiSubscribeToken { 421 | /// 过期时间 422 | const EXPIRED_TIME: i64 = 8; 423 | /// 请求 user-agent 424 | const USER_AGENT: &'static str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1379.1"; 425 | 426 | #[inline] 427 | pub(crate) fn hash_str(&self) -> String { 428 | let mut hasher = DefaultHasher::new(); 429 | self.hash(&mut hasher); 430 | format!("{:x}", hasher.finish()) 431 | } 432 | 433 | /// 获取过期时间 434 | #[allow(dead_code)] 435 | #[inline] 436 | fn get_expired_time() -> Duration { 437 | Duration::minutes(AzureApiSubscribeToken::EXPIRED_TIME) 438 | } 439 | 440 | /// 实例化付费版 Api key 441 | #[allow(dead_code)] 442 | pub(crate) fn new(region: AzureApiRegionIdentifier, subscription_key: &str) -> Arc { 443 | let new_token = AzureApiSubscribeToken { 444 | region_identifier: region, 445 | subscription_key: subscription_key.to_owned(), 446 | oauth_token: Arc::new(Mutex::new(None)), 447 | oauth_get_time: Arc::new(Mutex::new(0)), 448 | voices_list: RwLock::new(None), 449 | }; 450 | let hash = new_token.hash_str(); 451 | let mut k_list = loop { 452 | let kk = MS_TTS_SUBSCRIBE_TOKEN_LIST.try_lock(); 453 | if let Ok(d) = kk { 454 | break d; 455 | } 456 | }; 457 | let arc_token = Arc::new(new_token); 458 | return if let std::collections::hash_map::Entry::Vacant(e) = k_list.entry(hash.clone()) { 459 | e.insert(arc_token.clone()); 460 | arc_token 461 | } else { 462 | k_list.get(&hash).unwrap().clone() 463 | }; 464 | } 465 | 466 | #[allow(dead_code)] 467 | pub(crate) fn new_from_subscribe_key(data: &AzureSubscribeKey) -> Arc { 468 | Self::new(data.1.clone(), &data.0) 469 | } 470 | 471 | // 获取程序中配置的所有订阅key 472 | pub(crate) async fn get_subscribe_key_list() -> Vec> { 473 | let mut list = Vec::new(); 474 | for x in MS_TTS_SUBSCRIBE_TOKEN_LIST.lock().await.values() { 475 | list.push(x.clone()) 476 | } 477 | list 478 | } 479 | 480 | /// 获取程序中配置所有订阅key的发音人列表 注:列表只取交集,防止部分地区发音人在随机api中无法使用 481 | pub(crate) async fn get_vices_mixed_list() -> Result { 482 | let list = MS_TTS_SUBSCRIBE_VICES_MIXED_LIST 483 | .get_or_init(|| async move { 484 | let list = Self::get_subscribe_key_list().await; 485 | let mut resp = Vec::new(); 486 | for x in list { 487 | let kk = x.clone(); 488 | let call = || async move { kk.get_vices_list().await }; 489 | resp.push(call()) 490 | } 491 | let resp: Vec> = join_all(resp).await; 492 | 493 | let mut tmp: Option>> = None; 494 | 495 | for re in resp { 496 | match re { 497 | Ok(d)=>{ 498 | let d = d.raw_data; 499 | if let Some(t) = tmp { 500 | let intersect = t 501 | .iter() 502 | .filter(|&u| d.contains(u)) 503 | .cloned() 504 | .collect::>(); 505 | tmp = Some(intersect); 506 | } else { 507 | tmp = Some(d.clone()) 508 | } 509 | } 510 | Err(re)=>{ 511 | error!("{:?}", re); 512 | std::process::exit(1); 513 | } 514 | }; 515 | } 516 | 517 | tmp.unwrap() 518 | }) 519 | .await; 520 | let arc_list = collating_list_of_pronouncers_arc(list); 521 | Ok(arc_list) 522 | } 523 | 524 | /// 判断认证 Token 是否过期 525 | #[inline] 526 | #[allow(dead_code)] 527 | pub(crate) async fn is_expired(&self) -> bool { 528 | let now = Utc::now(); 529 | let p_time = self.oauth_get_time.lock().await; 530 | let old = Utc.timestamp(*p_time, 0); 531 | drop(p_time); 532 | now - old > Self::get_expired_time() 533 | } 534 | 535 | /// 判断认证 oauth_token 是否为 None 536 | #[inline] 537 | #[allow(dead_code)] 538 | pub(crate) async fn token_is_none(&self) -> bool { 539 | let token = self.oauth_token.lock().await; 540 | let is = token.is_none(); 541 | drop(token); 542 | is 543 | } 544 | 545 | /// 判断认证 oauth_token 是否为 None 546 | #[inline] 547 | #[allow(dead_code)] 548 | pub(crate) async fn set_new_token(&self, token: String) { 549 | self.oauth_token.lock().await.replace(token); 550 | *self.oauth_get_time.lock().await = Utc::now().timestamp(); 551 | } 552 | 553 | /// 根据 subscription_key 获取新的 auth key 554 | #[inline] 555 | #[allow(dead_code)] 556 | pub(crate) async fn get_auth_key_by_subscription_key(&self) -> Result<(), TTSServerError> { 557 | let oauth_key = get_oauth_token(self).await?; 558 | self.set_new_token(oauth_key).await; 559 | Ok(()) 560 | } 561 | 562 | /// 563 | /// 使用 Azure 接口连接文本转语音服务 564 | pub(crate) async fn get_text_to_speech_connection( 565 | api_info: &T, 566 | ) -> Result>, TTSServerError> 567 | where 568 | T: AzureAuthKey + Sync + Send, 569 | { 570 | let region = api_info.get_region_identifier().await?.value(); 571 | let oauth_token = api_info.get_oauth_token().await?; 572 | let connect_id = random_string(32); 573 | let connect_domain = format!("{}.tts.speech.microsoft.com", ®ion); 574 | let uri = Uri::builder() 575 | .scheme("wss") 576 | .authority(connect_domain.clone()) 577 | .path_and_query(format!( 578 | "/cognitiveservices/websocket/v1?Authorization=bearer%20{}&X-ConnectionId={}", 579 | oauth_token, connect_id 580 | )) 581 | .build() 582 | .map_err(|e| { 583 | error!("{:?}", e); 584 | TTSServerError::ProgramError(format!("uri build error {:?}", e)) 585 | })?; 586 | 587 | let request_builder = Request::builder() 588 | .uri(uri) 589 | .method(Method::GET) 590 | .header("Accept-Encoding", "gzip, deflate, br") 591 | .header( 592 | "Accept-Language", 593 | "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", 594 | ) 595 | .header("Cache-Control", "no-cache") 596 | .header("Connection", "Upgrade") 597 | .header("Host", &connect_domain) 598 | // .header("Origin", "http://127.0.0.1:8080") 599 | .header("Pragma", "no-cache") 600 | .header( 601 | "Sec-webSocket-Extension", 602 | "permessage-deflate; client_max_window_bits", 603 | ) 604 | .header("Sec-WebSocket-Key", generate_key()) 605 | .header("Sec-WebSocket-Version", "13") 606 | .header("Upgrade", "websocket") 607 | .header("User-Agent", Self::USER_AGENT) 608 | .version(Version::HTTP_11); 609 | 610 | let request = request_builder.body(()).map_err(|e| { 611 | error!("{:?}", e); 612 | TTSServerError::ProgramError(format!("request_builder build error {:?}", e)) 613 | })?; 614 | 615 | // let jj = native_tls::TlsConnector::new().unwrap(); 616 | let jj = native_tls::TlsConnector::builder() 617 | .use_sni(true) 618 | .danger_accept_invalid_certs(true) 619 | .build() 620 | .map_err(|e| { 621 | error!("{:?}", e); 622 | TTSServerError::ProgramError(format!("创建Tcp连接失败! {:?}", e)) 623 | })?; 624 | let config = tokio_native_tls::TlsConnector::from(jj); 625 | 626 | let sock = TcpStream::connect(format!("{}:443", &connect_domain)) 627 | .await 628 | .map_err(|e| { 629 | error!("连接到微软服务器发生异常,tcp握手失败! 请检查网络! {:?}", e); 630 | TTSServerError::ProgramError(format!( 631 | "连接到微软服务器发生异常,tcp握手失败! 请检查网络! {:?}", 632 | e 633 | )) 634 | })?; 635 | 636 | let tls_stream = config.connect(&connect_domain, sock).await.map_err(|e| { 637 | error!("tls 握手失败 {:?}", e); 638 | TTSServerError::ProgramError(format!("tls 握手失败! {:?}", e)) 639 | })?; 640 | 641 | let websocket = client_async_with_config( 642 | request, 643 | tls_stream, 644 | Some(WebSocketConfig { 645 | max_send_queue: None, 646 | max_message_size: None, 647 | max_frame_size: None, 648 | accept_unmasked_frames: false, 649 | }), 650 | ) 651 | .await 652 | .map_err(|e| { 653 | error!("websocket 握手失败 {:?}", e); 654 | TTSServerError::ProgramError(format!("websocket 握手失败! {:?}", e)) 655 | })?; 656 | 657 | Ok(websocket.0) 658 | } 659 | 660 | // 以后的版本可能实现的功能,当拥有多个 api时,支持使用负载均衡处理,超过或接近免费额度则更换api 661 | // pub async fn get_free_quota(self){ 662 | // // https://management.azure.com/subscriptions/{subscription-id}/resourceGroups/{resource-group-name}/providers/{resource-provider-namespace}/{resource-type}/{resource-name}/providers/microsoft.insights/metrics?metricnames={metric}×pan={starttime/endtime}&$filter={filter}&resultType=metadata&api-version={apiVersion} 663 | // } 664 | } 665 | 666 | /// 667 | /// 为付费订阅版实现获取订阅key 668 | // #[async_trait] 669 | impl AzureAuthSubscription for AzureApiSubscribeToken { 670 | #[inline] 671 | fn get_subscription_key(&self) -> BoxFutureSync> { 672 | let su_k = self.subscription_key.clone(); 673 | Box::pin(async move { Ok(su_k) }) 674 | } 675 | } 676 | 677 | /// 678 | /// 为付费订阅版实现获取认证key以及地域 679 | // #[async_trait] 680 | impl AzureAuthKey for AzureApiSubscribeToken { 681 | /// 获取订阅地区 682 | #[inline] 683 | fn get_region_identifier( 684 | &self, 685 | ) -> BoxFutureSync> { 686 | let ri = self.region_identifier.clone(); 687 | // Ok(self.region_identifier.clone()) 688 | Box::pin(async move { Ok(ri) }) 689 | } 690 | /// 获取 Azure Oauth Token 691 | #[inline] 692 | fn get_oauth_token(&self) -> BoxFutureSync> { 693 | Box::pin(async move { 694 | if self.is_expired().await || self.token_is_none().await { 695 | self.get_auth_key_by_subscription_key().await?; 696 | } 697 | let jj = self.oauth_token.lock().await.clone().unwrap(); 698 | Ok(jj) 699 | }) 700 | } 701 | } 702 | 703 | /// 实现微软官网Api订阅接口获取发音人列表 704 | impl AzureApiSpeakerList for AzureApiSubscribeToken { 705 | #[inline] 706 | fn get_vices_list(&self) -> BoxFutureSync> { 707 | Box::pin(async move { 708 | if self.voices_list.read().await.is_none() { 709 | let mut voice_arc_list = Vec::new(); 710 | let kk = if let Ok(d) = get_voices_list_by_authkey(self).await { 711 | d 712 | } else { 713 | warn!("请求 AzureApiSubscribeToken 发音人列表出错,改用缓存数据!"); 714 | let data_str = String::from_utf8(AZURE_SPEAKERS_LIST_FILE.to_vec()).unwrap(); 715 | let tmp_list_1: Vec = serde_json::from_str(&data_str).unwrap(); 716 | tmp_list_1 717 | }; 718 | for voice in kk { 719 | voice_arc_list.push(Arc::new(voice)) 720 | } 721 | self.voices_list.write().await.replace(voice_arc_list); 722 | }; 723 | 724 | let data = self.voices_list.read().await; 725 | 726 | let list = collating_list_of_pronouncers_arc(data.as_ref().unwrap()); 727 | Ok(list) 728 | }) 729 | } 730 | } 731 | 732 | /// 实现微软官网Api订阅接口获取新连接 733 | impl AzureApiNewWebsocket for AzureApiSubscribeToken { 734 | #[inline] 735 | fn get_connection( 736 | &self, 737 | ) -> BoxFutureSync>, TTSServerError>> { 738 | Box::pin(async move { 739 | let mut new_socket = Self::get_text_to_speech_connection(self).await?; 740 | let mut msg1 = String::new(); 741 | let create_time = Utc::now(); 742 | let time = format!("{:?}", create_time); 743 | msg1.push_str(format!("Path: speech.config\r\nX-Timestamp: {}\r\nContent-Type: application/json; charset=utf-8", &time).as_str()); 744 | msg1.push_str("\r\n\r\n"); 745 | 746 | let user_agent = Self::USER_AGENT; 747 | msg1.push_str(r#"{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/Win32","name":""#); 748 | msg1.push_str(user_agent); 749 | msg1.push_str(r#"","version":""#); 750 | msg1.push_str(&user_agent[8..user_agent.len()]); 751 | msg1.push_str(r#""}}}"#); 752 | // xmml_data.push(msg1); 753 | new_socket 754 | .send(tungstenite::Message::Text(msg1)) 755 | .await 756 | .map_err(|e| { 757 | error!("发送配置数据错误; {:?}", e); 758 | TTSServerError::ProgramError("发送配置数据错误".to_owned()) 759 | })?; 760 | Ok(new_socket) 761 | }) 762 | } 763 | } 764 | 765 | impl AzureApiGenerateXMML for AzureApiSubscribeToken { 766 | fn generate_xmml( 767 | &self, 768 | data: MsTtsMsgRequest, 769 | ) -> BoxFutureSync, TTSServerError>> { 770 | Box::pin(async move { 771 | let mut xmml_data = Vec::new(); 772 | let create_time = Utc::now(); 773 | let time = format!("{:?}", create_time); 774 | 775 | let mut msg1 = String::new(); 776 | 777 | msg1.push_str("Path: synthesis.context\r\n"); 778 | msg1.push_str(format!("X-RequestId: {}\r\n", data.request_id).as_str()); 779 | msg1.push_str(format!("X-Timestamp: {}\r\n", time).as_str()); 780 | msg1.push_str("Content-Type: application/json\r\n\r\n"); 781 | msg1.push_str(r#"{"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":""#); 782 | msg1.push_str(data.quality.as_str()); 783 | msg1.push_str(r#""},"language":{"autoDetection":false}}}"#); 784 | xmml_data.push(msg1); 785 | 786 | let mut msg2 = String::new(); 787 | msg2.push_str(format!("Path: ssml\r\nX-RequestId: {}\r\nX-Timestamp:{}\r\nContent-Type:application/ssml+xml\r\n\r\n", &data.request_id, &time).as_str()); 788 | msg2.push_str(format!("{}", 789 | data.informant, data.style, data.rate, data.pitch, data.text).as_str()); 790 | xmml_data.push(msg2); 791 | Ok(xmml_data) 792 | }) 793 | } 794 | } 795 | 796 | // 797 | // unsafe impl Sync for AzureApiPreviewFreeToken {} 798 | // 799 | // unsafe impl Send for AzureApiPreviewFreeToken {} 800 | 801 | /// 802 | /// 微软 Edge浏览器 免费预览 api 803 | #[derive(Debug)] 804 | pub(crate) struct AzureApiEdgeFree { 805 | voices_list: RwLock>>>, 806 | } 807 | 808 | impl AzureApiEdgeFree { 809 | pub(crate) const TOKEN: [u8; 32] = [ 810 | 54, 65, 53, 65, 65, 49, 68, 52, 69, 65, 70, 70, 52, 69, 57, 70, 66, 51, 55, 69, 50, 51, 68, 811 | 54, 56, 52, 57, 49, 68, 54, 70, 52, 812 | ]; 813 | 814 | #[allow(dead_code)] 815 | pub(crate) const MS_TTS_SERVER_CHINA_LIST: [&'static str; 6] = [ 816 | // 北京节点 817 | "202.89.233.100", 818 | "202.89.233.101", 819 | "202.89.233.102", 820 | "202.89.233.103", 821 | "202.89.233.104", 822 | "182.61.148.24", 823 | ]; 824 | 825 | #[allow(dead_code)] 826 | pub(crate) const MS_TTS_SERVER_CHINA_HK_LIST: [&'static str; 8] = [ 827 | "149.129.121.248", 828 | // "103.200.112.245", 829 | // "47.90.51.125", 830 | // "61.239.177.5", 831 | "149.129.88.238", 832 | "103.68.61.91", 833 | "47.75.141.93", 834 | // "34.96.186.48", 835 | "47.240.87.168", 836 | "47.57.114.186", 837 | "150.109.51.247", 838 | // "20.205.113.91", 839 | "35.241.115.60", 840 | ]; 841 | 842 | #[allow(dead_code)] 843 | pub(crate) const MS_TTS_SERVER_CHINA_TW_LIST: [&'static str; 2] = 844 | ["34.81.240.201", "34.80.106.199"]; 845 | 846 | const USER_AGENT: &'static str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.39"; 847 | 848 | #[allow(dead_code)] 849 | pub(crate) fn new() -> Arc { 850 | static INSTANCE: OnceCell> = OnceCell::new(); 851 | 852 | INSTANCE 853 | .get_or_init(|| { 854 | Arc::new(Self { 855 | voices_list: RwLock::new(None), 856 | }) 857 | }) 858 | .clone() 859 | } 860 | 861 | /// 862 | /// edge 免费版本 863 | /// 根据命令行中地域配置进行连接 864 | /// 865 | pub(crate) async fn new_websocket_edge_free() 866 | -> Result>, TTSServerError> { 867 | #[cfg(test)] 868 | let args = AppArgs::test_parse_macro(&[]); 869 | #[cfg(not(test))] 870 | let args = AppArgs::parse_macro(); 871 | debug!("指定加速ip {:#?}", args.server_area); 872 | match args.server_area { 873 | ServerArea::Default => { 874 | info!("连接至官方服务器, 根据 dns 解析"); 875 | Self::new_websocket_by_select_server(None).await 876 | // new_websocket_by_select_server(Some("171.117.98.148")).await 877 | } 878 | ServerArea::China => { 879 | info!("连接至内陆服务器"); 880 | let select = rand::thread_rng() 881 | .gen_range(0..AzureApiEdgeFree::MS_TTS_SERVER_CHINA_LIST.len()); 882 | Self::new_websocket_by_select_server(Some( 883 | AzureApiEdgeFree::MS_TTS_SERVER_CHINA_LIST 884 | .get(select) 885 | .unwrap(), 886 | )) 887 | .await 888 | } 889 | ServerArea::ChinaHK => { 890 | info!("连接至香港服务器"); 891 | let select = rand::thread_rng() 892 | .gen_range(0..AzureApiEdgeFree::MS_TTS_SERVER_CHINA_HK_LIST.len()); 893 | Self::new_websocket_by_select_server(Some( 894 | AzureApiEdgeFree::MS_TTS_SERVER_CHINA_HK_LIST 895 | .get(select) 896 | .unwrap(), 897 | )) 898 | .await 899 | } 900 | ServerArea::ChinaTW => { 901 | info!("连接至台湾服务器"); 902 | let select = rand::thread_rng() 903 | .gen_range(0..AzureApiEdgeFree::MS_TTS_SERVER_CHINA_TW_LIST.len()); 904 | Self::new_websocket_by_select_server(Some( 905 | AzureApiEdgeFree::MS_TTS_SERVER_CHINA_TW_LIST 906 | .get(select) 907 | .unwrap(), 908 | )) 909 | .await 910 | } 911 | } 912 | } 913 | 914 | /// 915 | /// edge 免费版本 916 | /// 获取新的隧道连接 917 | pub(crate) async fn new_websocket_by_select_server( 918 | server: Option<&str>, 919 | ) -> Result>, TTSServerError> { 920 | let connect_id = random_string(32); 921 | let uri = Uri::builder() 922 | .scheme("wss") 923 | .authority("speech.platform.bing.com") 924 | .path_and_query(format!( 925 | "/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken={}&ConnectionId={}", 926 | String::from_utf8(AzureApiEdgeFree::TOKEN.to_vec()).unwrap(), 927 | &connect_id 928 | )) 929 | .build(); 930 | if let Err(e) = uri { 931 | return Err(TTSServerError::ProgramError(format!( 932 | "uri 构建错误 {:?}", 933 | e 934 | ))); 935 | } 936 | let uri = uri.unwrap(); 937 | 938 | let request_builder = Request::builder() 939 | .uri(uri) 940 | .method(Method::GET) 941 | .header("Cache-Control", "no-cache") 942 | .header("Pragma", "no-cache") 943 | .header("Accept", "*/*") 944 | .header("Accept-Encoding", "gzip, deflate, br") 945 | .header( 946 | "Accept-Language", 947 | "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", 948 | ) 949 | .header("User-Agent", Self::USER_AGENT) 950 | .header( 951 | "Origin", 952 | "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold", 953 | ) 954 | .header("Host", "speech.platform.bing.com") 955 | .header("Connection", "Upgrade") 956 | .header("Upgrade", "websocket") 957 | .header("Sec-WebSocket-Version", "13") 958 | .header("Sec-WebSocket-Key", generate_key()) 959 | .header( 960 | "Sec-webSocket-Extension", 961 | "permessage-deflate; client_max_window_bits", 962 | ) 963 | .version(Version::HTTP_11); 964 | let request = request_builder.body(()); 965 | if let Err(e) = request { 966 | return Err(TTSServerError::ProgramError(format!( 967 | "request_builder 构建错误 {:?}", 968 | e 969 | ))); 970 | } 971 | let request = request.unwrap(); 972 | 973 | let domain = "speech.platform.bing.com"; 974 | 975 | // let jj = native_tls::TlsConnector::new().unwrap(); 976 | let jj = native_tls::TlsConnector::builder() 977 | .use_sni(false) 978 | .danger_accept_invalid_certs(true) 979 | .build() 980 | .unwrap(); 981 | 982 | let config = tokio_native_tls::TlsConnector::from(jj); 983 | 984 | let sock = match server { 985 | Some(s) => { 986 | info!("连接至 {:?}", s); 987 | 988 | let kk: Vec<_> = s.split('.').map(|x| x.parse::().unwrap()).collect(); 989 | TcpStream::connect(( 990 | Ipv4Addr::new( 991 | *kk.first().unwrap(), 992 | *kk.get(1).unwrap(), 993 | *kk.get(2).unwrap(), 994 | *kk.get(3).unwrap(), 995 | ), 996 | 443, 997 | )) 998 | .await 999 | } 1000 | None => { 1001 | info!("连接至 speech.platform.bing.com"); 1002 | TcpStream::connect("speech.platform.bing.com:443").await 1003 | } 1004 | }; 1005 | 1006 | if let Err(e) = sock { 1007 | return Err(TTSServerError::ProgramError(format!( 1008 | "连接到微软服务器发生异常,tcp握手失败! 请检查网络! {:?}", 1009 | e 1010 | ))); 1011 | } 1012 | let sock = sock.unwrap(); 1013 | 1014 | let tsl_stream = config.connect(domain, sock).await; 1015 | 1016 | if let Err(e) = tsl_stream { 1017 | error!("{:?}", e); 1018 | return Err(TTSServerError::ProgramError(format!("tsl握手失败! {}", e))); 1019 | } 1020 | let tsl_stream = tsl_stream.unwrap(); 1021 | 1022 | let websocket = client_async_with_config( 1023 | request, 1024 | tsl_stream, 1025 | Some(WebSocketConfig { 1026 | max_send_queue: None, 1027 | max_message_size: None, 1028 | max_frame_size: None, 1029 | accept_unmasked_frames: false, 1030 | }), 1031 | ) 1032 | .await; 1033 | 1034 | match websocket { 1035 | Ok(_websocket) => { 1036 | trace!("websocket 握手成功"); 1037 | Ok(_websocket.0) 1038 | } 1039 | Err(e2) => Err(TTSServerError::ProgramError(format!( 1040 | "websocket 握手失败! {:?}", 1041 | e2 1042 | ))), 1043 | } 1044 | } 1045 | 1046 | async fn get_vices_list_request() -> Result>, TTSServerError> { 1047 | let url = format!( 1048 | "https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken={}", 1049 | String::from_utf8(Self::TOKEN.to_vec()).unwrap() 1050 | ); 1051 | let resp = reqwest::Client::new() 1052 | .get(url) 1053 | .header("Host", "speech.platform.bing.com") 1054 | .header("Connection", "keep-alive") 1055 | .header( 1056 | "sec-ch-ua", 1057 | r#""Microsoft Edge";v="107", "Chromium";v="107", "Not=A?Brand";v="24""#, 1058 | ) 1059 | .header("sec-ch-ua-mobile", r#"?0"#) 1060 | .header("User-Agent", Self::USER_AGENT) 1061 | .header("sec-ch-ua-platform", r#""Windows""#) 1062 | .header("Accept", "*/*") 1063 | .header("X-Edge-Shopping-Flag", "1") 1064 | .header("Sec-Fetch-Site", "none") 1065 | .header("Sec-Fetch-Mode", "cors") 1066 | .header("Sec-Fetch-Dest", "empty") 1067 | .header("Accept-Encoding", "gzip, deflate, br") 1068 | .header( 1069 | "Accept-Language", 1070 | "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", 1071 | ) 1072 | .send() 1073 | .await 1074 | .map_err(|e| { 1075 | error!("request build err: {:#?}", e); 1076 | TTSServerError::ThirdPartyApiCallFailed(format!("{:?}", e.to_string())) 1077 | })?; 1078 | 1079 | let body = if resp.status() == 200 { 1080 | Ok(resp.bytes().await.map_err(|e| { 1081 | error!("{:#?}", e); 1082 | TTSServerError::ProgramError(format!("Error parsing response body! {:?}", e)) 1083 | })?) 1084 | } else { 1085 | error!("{:#?}", resp); 1086 | Err(TTSServerError::ThirdPartyApiCallFailed( 1087 | "Third-party interface corresponding error".to_owned(), 1088 | )) 1089 | }?; 1090 | let voice_list: Vec = serde_json::from_slice(&body).map_err(|e| { 1091 | error!("{:?}", e); 1092 | TTSServerError::ProgramError(format!("Failed to deserialize voice list! {:?}", e)) 1093 | })?; 1094 | let mut voice_arc_list = Vec::new(); 1095 | for voice in voice_list { 1096 | voice_arc_list.push(Arc::new(voice)) 1097 | } 1098 | 1099 | Ok(voice_arc_list) 1100 | } 1101 | } 1102 | 1103 | /// 实现微软官网免费预览接口获取发音人列表 1104 | impl AzureApiSpeakerList for AzureApiEdgeFree { 1105 | #[inline] 1106 | fn get_vices_list(&self) -> BoxFutureSync> { 1107 | Box::pin(async move { 1108 | if self.voices_list.read().await.is_none() { 1109 | let kk = if let Ok(d) = Self::get_vices_list_request().await { 1110 | d 1111 | } else { 1112 | warn!("请求 AzureApiEdgeFree 发音人列表出错,改用缓存数据!"); 1113 | let data_str = String::from_utf8(EDGE_SPEAKERS_LIST_FILE.to_vec()).unwrap(); 1114 | let tmp_list_1: Vec = serde_json::from_str(&data_str).unwrap(); 1115 | let mut voice_arc_list = Vec::new(); 1116 | for voice in tmp_list_1 { 1117 | voice_arc_list.push(Arc::new(voice)) 1118 | } 1119 | voice_arc_list 1120 | }; 1121 | self.voices_list.write().await.replace(kk); 1122 | }; 1123 | 1124 | let data = self.voices_list.read().await; 1125 | 1126 | let list = collating_list_of_pronouncers_arc(data.as_ref().unwrap()); 1127 | Ok(list) 1128 | }) 1129 | } 1130 | } 1131 | 1132 | impl AzureApiNewWebsocket for AzureApiEdgeFree { 1133 | fn get_connection( 1134 | &self, 1135 | ) -> BoxFutureSync>, TTSServerError>> { 1136 | Box::pin(async move { 1137 | let mut new_socket = Self::new_websocket_edge_free().await?; 1138 | let mut msg1 = String::new(); 1139 | let create_time = Utc::now(); 1140 | let time = format!("{:?}", create_time); 1141 | msg1.push_str(format!("X-Timestamp: {}\r\nContent-Type: application/json; charset=utf-8\r\nPath: speech.config", &time).as_str()); 1142 | msg1.push_str("\r\n\r\n"); 1143 | msg1.push_str(r#"{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":"false","wordBoundaryEnabled":"false"},"outputFormat":"audio-24khz-96kbitrate-mono-mp3"}}}}"#); 1144 | // xmml_data.push(msg1); 1145 | new_socket 1146 | .send(tungstenite::Message::Text(msg1)) 1147 | .await 1148 | .map_err(|e| { 1149 | let err = format!("发送配置数据错误; {:?}", e); 1150 | error!("{}", err); 1151 | TTSServerError::ProgramError(err) 1152 | })?; 1153 | Ok(new_socket) 1154 | }) 1155 | } 1156 | } 1157 | 1158 | impl AzureApiGenerateXMML for AzureApiEdgeFree { 1159 | fn generate_xmml( 1160 | &self, 1161 | data: MsTtsMsgRequest, 1162 | ) -> BoxFutureSync, TTSServerError>> { 1163 | Box::pin(async move { 1164 | let mut xmml_data = Vec::new(); 1165 | let create_time = Utc::now(); 1166 | let time = format!("{:?}", create_time); 1167 | let mut msg2 = String::new(); 1168 | msg2.push_str(format!("X-RequestId:{}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:{}\r\nPath:ssml\r\n\r\n", &data.request_id, &time).as_str()); 1169 | msg2.push_str(format!("{}", 1170 | data.informant, data.rate, data.text).as_str()); 1171 | xmml_data.push(msg2); 1172 | Ok(xmml_data) 1173 | }) 1174 | } 1175 | } 1176 | 1177 | /// 1178 | /// 微软文本转语音认证 token 获取,使用官方 Api 1179 | pub(crate) async fn get_oauth_token(api_info: &T) -> Result 1180 | where 1181 | T: AzureAuthSubscription + AzureAuthKey + Sync + Send, 1182 | { 1183 | let region = api_info.get_region_identifier().await?.value(); 1184 | let key = api_info.get_subscription_key().await?; 1185 | let resp = reqwest::Client::new() 1186 | .post(format!( 1187 | "https://{}.api.cognitive.microsoft.com/sts/v1.0/issueToken", 1188 | ®ion 1189 | )) 1190 | .header("Ocp-Apim-Subscription-Key", key) 1191 | .header("Host", format!("{}.api.cognitive.microsoft.com", ®ion)) 1192 | .header("Content-type", "application/x-www-form-urlencoded") 1193 | .header("Content-Length", "0") 1194 | .send() 1195 | .await 1196 | .map_err(|e| { 1197 | let err = TTSServerError::ThirdPartyApiCallFailed(format!("{:?}", e.to_string())); 1198 | error!("{:?}", e); 1199 | err 1200 | })?; 1201 | let body = if resp.status() == 200 { 1202 | Ok(resp.bytes().await.map_err(|e| { 1203 | error!("Error parsing response body! {:#?}", e); 1204 | TTSServerError::ProgramError(format!("Error parsing response body! {}", e)) 1205 | })?) 1206 | } else if resp.status() == 401 { 1207 | error!( 1208 | "请检查您传入的 订阅KEY 以及 地域 是否正确, Azure Api 调用失败; {:?}", 1209 | resp 1210 | ); 1211 | Err(TTSServerError::ThirdPartyApiCallFailed( 1212 | "Call without permission!".to_owned(), 1213 | )) 1214 | } else { 1215 | error!("Third-party interface corresponding error: {:#?}", resp); 1216 | Err(TTSServerError::ThirdPartyApiCallFailed( 1217 | "Third-party interface corresponding error".to_owned(), 1218 | )) 1219 | }?; 1220 | 1221 | String::from_utf8(body.to_vec()).map_err(|e| { 1222 | error!("{:#?}", e); 1223 | TTSServerError::ProgramError(format!("Binary to string error! {}", e)) 1224 | }) 1225 | } 1226 | 1227 | #[derive(Serialize, Deserialize, Clone, Debug)] 1228 | #[serde(untagged)] 1229 | pub enum VoicesItem { 1230 | AzureApi { 1231 | #[serde(rename = "Name")] 1232 | name: String, 1233 | #[serde(rename = "DisplayName")] 1234 | display_name: String, 1235 | #[serde(rename = "LocalName")] 1236 | local_name: String, 1237 | #[serde(rename = "ShortName")] 1238 | short_name: String, 1239 | #[serde(rename = "Gender")] 1240 | gender: String, 1241 | #[serde(rename = "Locale")] 1242 | locale: String, 1243 | #[serde(rename = "LocaleName")] 1244 | locale_name: String, 1245 | #[serde(rename = "StyleList", skip_serializing_if = "Option::is_none")] 1246 | style_list: Option>, 1247 | #[serde(rename = "SampleRateHertz")] 1248 | sample_rate_hertz: String, 1249 | #[serde(rename = "VoiceType")] 1250 | voice_type: String, 1251 | #[serde(rename = "Status")] 1252 | status: String, 1253 | #[serde(rename = "RolePlayList", skip_serializing_if = "Option::is_none")] 1254 | role_play_list: Option>, 1255 | #[serde(rename = "WordsPerMinute", skip_serializing_if = "Option::is_none")] 1256 | words_per_minute: Option, 1257 | }, 1258 | EdgeApi { 1259 | #[serde(rename = "Name")] 1260 | name: String, 1261 | #[serde(rename = "ShortName")] 1262 | short_name: String, 1263 | #[serde(rename = "Gender")] 1264 | gender: String, 1265 | #[serde(rename = "Locale")] 1266 | locale: String, 1267 | #[serde(rename = "SuggestedCodec")] 1268 | suggested_codec: String, 1269 | #[serde(rename = "FriendlyName")] 1270 | friendly_name: String, 1271 | #[serde(rename = "Status")] 1272 | status: String, 1273 | #[serde(rename = "VoiceTag")] 1274 | voice_tag: HashMap>, 1275 | }, 1276 | } 1277 | 1278 | // /// 1279 | // /// Azure 官方文本转语音 发音人结构体 1280 | // #[derive(Serialize, Deserialize, Clone, Debug)] 1281 | // pub struct VoicesItem { 1282 | // #[serde(rename = "Name")] 1283 | // pub name: String, 1284 | // #[serde(rename = "DisplayName", skip_serializing_if = "Option::is_none")] 1285 | // pub display_name: Option, 1286 | // 1287 | // #[serde(rename = "LocalName")] 1288 | // pub local_name: String, 1289 | // #[serde(rename = "ShortName")] 1290 | // pub short_name: String, 1291 | // #[serde(rename = "Gender")] 1292 | // pub gender: String, 1293 | // #[serde(rename = "Locale")] 1294 | // pub locale: String, 1295 | // #[serde(rename = "LocaleName")] 1296 | // pub locale_name: String, 1297 | // #[serde(rename = "StyleList", skip_serializing_if = "Option::is_none")] 1298 | // pub style_list: Option>, 1299 | // #[serde(rename = "SampleRateHertz")] 1300 | // pub sample_rate_hertz: String, 1301 | // #[serde(rename = "VoiceType")] 1302 | // pub voice_type: String, 1303 | // #[serde(rename = "Status")] 1304 | // pub status: String, 1305 | // #[serde(rename = "RolePlayList", skip_serializing_if = "Option::is_none")] 1306 | // pub role_play_list: Option>, 1307 | // 1308 | // // edge api 接口独立数据 1309 | // #[serde(rename = "FriendlyName", skip_serializing_if = "Option::is_none")] 1310 | // pub friendly_name: Option, 1311 | // #[serde(rename = "SuggestedCodec", skip_serializing_if = "Option::is_none")] 1312 | // pub suggested_codec: Option, 1313 | // #[serde(rename = "WordsPerMinute", skip_serializing_if = "Option::is_none")] 1314 | // pub words_per_minute: Option>, 1315 | // #[serde(rename = "VoiceTag", skip_serializing_if = "Option::is_none")] 1316 | // pub voice_tag: Option>>, 1317 | // } 1318 | 1319 | impl VoicesItem { 1320 | #[inline] 1321 | pub fn get_short_name(&self) -> String { 1322 | match self { 1323 | VoicesItem::AzureApi { short_name, .. } => short_name.clone(), 1324 | VoicesItem::EdgeApi { short_name, .. } => short_name.clone(), 1325 | } 1326 | } 1327 | #[inline] 1328 | pub fn get_local(&self) -> &str { 1329 | return match self { 1330 | VoicesItem::AzureApi { locale, .. } => locale.as_str(), 1331 | VoicesItem::EdgeApi { locale, .. } => locale.as_str(), 1332 | }; 1333 | } 1334 | #[inline] 1335 | pub fn get_style(&self) -> Option> { 1336 | return match self { 1337 | VoicesItem::AzureApi { style_list, .. } => style_list.clone(), 1338 | VoicesItem::EdgeApi { voice_tag, .. } => { 1339 | voice_tag.get("ContentCategories").cloned() 1340 | } 1341 | }; 1342 | } 1343 | 1344 | pub fn get_desc(&self) -> String { 1345 | match self { 1346 | VoicesItem::AzureApi { 1347 | voice_type, 1348 | local_name, 1349 | display_name, 1350 | .. 1351 | } => { 1352 | if voice_type == "Neural" { 1353 | format!( 1354 | "Microsoft {} Online (Natural) - {}", 1355 | display_name, local_name 1356 | ) 1357 | } else { 1358 | format!("Microsoft {} Online - {}", display_name, local_name) 1359 | } 1360 | } 1361 | VoicesItem::EdgeApi { friendly_name, .. } => friendly_name.clone(), 1362 | } 1363 | } 1364 | } 1365 | 1366 | impl PartialEq for VoicesItem { 1367 | #[inline] 1368 | fn eq(&self, other: &Self) -> bool { 1369 | match self { 1370 | VoicesItem::AzureApi { 1371 | short_name: short_name1, 1372 | .. 1373 | } => { 1374 | if let VoicesItem::AzureApi { 1375 | short_name: short_name2, 1376 | .. 1377 | } = other 1378 | { 1379 | short_name1 == short_name2 1380 | } else { 1381 | false 1382 | } 1383 | } 1384 | VoicesItem::EdgeApi { 1385 | short_name: short_name1, 1386 | .. 1387 | } => { 1388 | if let VoicesItem::EdgeApi { 1389 | short_name: short_name2, 1390 | .. 1391 | } = other 1392 | { 1393 | short_name1 == short_name2 1394 | } else { 1395 | false 1396 | } 1397 | } 1398 | } 1399 | } 1400 | } 1401 | 1402 | /// 1403 | /// 根据 Api key 获取文本转语音 发音人列表 1404 | async fn get_voices_list_by_authkey(api_info: &T) -> Result, TTSServerError> 1405 | where 1406 | T: AzureAuthKey + Sync + Send, 1407 | { 1408 | let region = api_info.get_region_identifier().await?.value(); 1409 | let oauth_token = api_info.get_oauth_token().await?; 1410 | let resp = reqwest::Client::new() 1411 | .get(format!( 1412 | "https://{}.tts.speech.microsoft.com/cognitiveservices/voices/list", 1413 | ®ion 1414 | )) 1415 | .header("Authorization", format!("Bearer {}", oauth_token)) 1416 | .header("Host", format!("{}.tts.speech.microsoft.com", ®ion)) 1417 | .header("Accept", "application/json") 1418 | .send() 1419 | .await 1420 | .map_err(|e| TTSServerError::ThirdPartyApiCallFailed(format!("{:?}", e.to_string())))?; 1421 | 1422 | let body = if resp.status() == 200 { 1423 | Ok(resp.bytes().await.map_err(|e| { 1424 | TTSServerError::ProgramError(format!("Error parsing response body! {:?}", e)) 1425 | })?) 1426 | } else { 1427 | error!("{:#?}", resp); 1428 | Err(TTSServerError::ThirdPartyApiCallFailed( 1429 | "Third-party interface corresponding error".to_owned(), 1430 | )) 1431 | }?; 1432 | 1433 | let voice_list: Vec = serde_json::from_slice(&body).map_err(|e| { 1434 | error!("{:?}", e); 1435 | TTSServerError::ProgramError(format!("Failed to deserialize voice list! {:?}", e)) 1436 | })?; 1437 | 1438 | Ok(voice_list) 1439 | } 1440 | 1441 | pub(crate) fn collating_list_of_pronouncers(list: Vec) -> VoicesList { 1442 | let mut raw_data: Vec> = Vec::new(); 1443 | let mut voices_name_list: HashSet = HashSet::new(); 1444 | let mut by_voices_name_map: HashMap> = HashMap::new(); 1445 | 1446 | list.iter().for_each(|item| { 1447 | let new = Arc::new(item.clone()); 1448 | raw_data.push(new.clone()); 1449 | voices_name_list.insert(item.get_short_name()); 1450 | by_voices_name_map.insert(item.get_short_name(), new); 1451 | }); 1452 | 1453 | let mut by_locale_map: HashMap>> = HashMap::new(); 1454 | 1455 | let new_iter = raw_data.iter(); 1456 | for (key, group) in &new_iter.group_by(|i| i.get_local()) { 1457 | let mut locale_vec_list: Vec> = Vec::new(); 1458 | 1459 | group.for_each(|j| { 1460 | locale_vec_list.push(j.clone()); 1461 | }); 1462 | by_locale_map.insert(key.to_owned(), locale_vec_list); 1463 | } 1464 | 1465 | 1466 | VoicesList { 1467 | voices_name_list, 1468 | raw_data, 1469 | by_voices_name_map, 1470 | by_locale_map, 1471 | } 1472 | } 1473 | 1474 | pub(crate) fn collating_list_of_pronouncers_arc(raw_data: &[Arc]) -> VoicesList { 1475 | let mut voices_name_list: HashSet = HashSet::new(); 1476 | let mut by_voices_name_map: HashMap> = HashMap::new(); 1477 | 1478 | raw_data.iter().for_each(|item| { 1479 | let new = item.clone(); 1480 | voices_name_list.insert(item.get_short_name()); 1481 | by_voices_name_map.insert(item.get_short_name(), new); 1482 | }); 1483 | 1484 | let mut by_locale_map: HashMap>> = HashMap::new(); 1485 | 1486 | let new_iter = raw_data.iter(); 1487 | for (key, group) in &new_iter.group_by(|&i| i.get_local()) { 1488 | let mut locale_vec_list: Vec> = Vec::new(); 1489 | 1490 | group.for_each(|j| { 1491 | locale_vec_list.push(j.clone()); 1492 | }); 1493 | by_locale_map.insert(key.to_owned(), locale_vec_list); 1494 | } 1495 | 1496 | 1497 | VoicesList { 1498 | voices_name_list, 1499 | raw_data: raw_data.to_owned(), 1500 | by_voices_name_map, 1501 | by_locale_map, 1502 | } 1503 | } 1504 | -------------------------------------------------------------------------------- /src/utils/log.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use log::{debug, LevelFilter}; 4 | use log4rs::{ 5 | self, 6 | append::{console::ConsoleAppender, file::FileAppender}, 7 | config::{Appender, Config, Logger, Root}, 8 | encode::pattern::PatternEncoder, 9 | }; 10 | 11 | /// 12 | /// 初始化日志 13 | #[allow(dead_code)] 14 | pub(crate) fn init_log( 15 | log_level: LevelFilter, 16 | log_to_file: Option, 17 | log_path: Option<&str>, 18 | custom_level: Option>, 19 | ) { 20 | let log_to_file = log_to_file.unwrap_or(false); 21 | let log_path_default = format!( 22 | "{}/tts-server/server.log", 23 | std::env::temp_dir().to_str().unwrap() 24 | ); 25 | let log_path = if let Some(p) = log_path { 26 | p.to_owned() 27 | } else { 28 | log_path_default 29 | }; 30 | 31 | let stdout = ConsoleAppender::builder() 32 | .encoder(Box::new(PatternEncoder::new( 33 | "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {T} {I} {h({l})} - {m}{n}", 34 | ))) 35 | .build(); 36 | 37 | let mut config = Config::builder(); 38 | config = config.appender(Appender::builder().build("stdout", Box::new(stdout))); 39 | if log_to_file { 40 | let log_to_file = FileAppender::builder() 41 | .encoder(Box::new(PatternEncoder::new( 42 | "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {T} {I} {l} - {m}{n}", 43 | ))) 44 | .build(log_path.clone()) 45 | .unwrap(); 46 | config = config.appender(Appender::builder().build("file", Box::new(log_to_file))); 47 | } 48 | if let Some(c_l) = custom_level { 49 | for x in c_l { 50 | config = config.logger(Logger::builder().build(x.0, x.1)) 51 | } 52 | } 53 | 54 | let mut root = Root::builder().appender("stdout"); 55 | if log_to_file { 56 | root = root.appender("file"); 57 | } 58 | 59 | // let config_tmp = config 60 | // .build(root.build(LevelFilter::from_str(args.log_level.to_uppercase().as_str()).unwrap())) 61 | // .unwrap(); 62 | 63 | let config_tmp = config.build(root.build(log_level)).unwrap(); 64 | 65 | log4rs::init_config(config_tmp).unwrap(); 66 | if log_to_file { 67 | debug!("日志文件路径: {}", log_path); 68 | } 69 | } 70 | 71 | /// 72 | /// 初始化测试日志 73 | #[allow(dead_code)] 74 | #[cfg(test)] 75 | pub(crate) fn init_test_log( 76 | log_level: LevelFilter, 77 | custom_level: Option>, 78 | ) { 79 | let stdout = ConsoleAppender::builder() 80 | .encoder(Box::new(PatternEncoder::new( 81 | "{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {T} {I} {h({l})} - {m}{n}", 82 | ))) 83 | .build(); 84 | let mut config = Config::builder(); 85 | config = config.appender(Appender::builder().build("stdout", Box::new(stdout))); 86 | // .logger(Logger::builder().build("reqwest", LevelFilter::Warn)) 87 | // .logger(Logger::builder().build("rustls", LevelFilter::Warn)) 88 | // .logger(Logger::builder().build("actix_server::builder", LevelFilter::Warn)) 89 | // .logger(Logger::builder().build("hyper", LevelFilter::Warn)); 90 | 91 | if let Some(c_l) = custom_level { 92 | for x in c_l { 93 | config = config.logger(Logger::builder().build(x.0, x.1)) 94 | } 95 | } 96 | 97 | let root = Root::builder().appender("stdout"); 98 | let config_tmp = config.build(root.build(log_level)).unwrap(); 99 | 100 | log4rs::init_config(config_tmp).unwrap(); 101 | } 102 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod azure_api; 2 | pub mod log; 3 | 4 | use rand::Rng; 5 | 6 | /// 生成随机字符 7 | /// 8 | /// Examples 9 | /// 10 | /// ``` 11 | /// let x = random_string(32); 12 | /// ``` 13 | pub fn random_string(num: u32) -> String { 14 | // println!("num: {} ", num); 15 | let str = "123456789abcdef".chars().collect::>(); 16 | let mut ret_str = String::new(); 17 | for _i in 0..num { 18 | let nums = rand::thread_rng().gen_range(0..str.len()); 19 | let k = str[nums]; 20 | ret_str.push(k); 21 | // println!("添加: {} , 字符串总共: {}", k, ret_str); 22 | } 23 | ret_str 24 | } 25 | 26 | /// 二进制数组查询 27 | pub fn binary_search(bin: &[u8], search: &[u8]) -> Option { 28 | if bin.len() > usize::MAX - search.len() { 29 | panic!("binary_search: length overflow"); 30 | } 31 | let mut i = 0; 32 | let k: usize = bin.len() - search.len(); 33 | loop { 34 | if i > k { 35 | break; 36 | } 37 | let j = i + search.len(); 38 | if &bin[i..j] == search { 39 | return Some(i); 40 | } 41 | i += 1; 42 | } 43 | None 44 | } 45 | -------------------------------------------------------------------------------- /src/web/controller.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug}; 2 | 3 | use actix_web::{body::BoxBody, http::StatusCode, web, HttpRequest, HttpResponse}; 4 | use fancy_regex::Regex; 5 | use log::{debug, error, warn}; 6 | use serde::{Deserialize, Serialize}; 7 | use urlencoding::decode as url_decode; 8 | 9 | use crate::{ 10 | error::TTSServerError, 11 | info, 12 | ms_tts::MsTtsMsgResponse, 13 | random_string, 14 | utils::azure_api::{ 15 | AzureApiEdgeFree, AzureApiSpeakerList, AzureApiSubscribeToken, MsApiOrigin, 16 | MsTtsMsgRequest, MS_TTS_QUALITY_LIST, 17 | }, 18 | web::{ 19 | entity::ApiBaseResponse, error::ControllerError, middleware::token_auth::AuthTokenValue, 20 | }, 21 | AppArgs, 22 | }; 23 | 24 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 25 | pub struct MsTtsMsgRequestJson { 26 | // 待生成文本 27 | pub text: String, 28 | // 发音人 29 | pub informant: Option, 30 | // 音频风格 31 | pub style: Option, 32 | // 语速 33 | pub rate: Option, 34 | // 音调 35 | pub pitch: Option, 36 | // 音频格式 37 | pub quality: Option, 38 | /// 认证 Token 39 | pub token: Option, 40 | // text_replace_list:Vec, 41 | // phoneme_list:Vec 42 | } 43 | 44 | impl MsTtsMsgRequestJson { 45 | pub async fn to_ms_request( 46 | &self, 47 | api_name: MsApiOrigin, 48 | request_id_value: String, 49 | ) -> Result { 50 | let args = AppArgs::parse_macro(); 51 | let text_value: String = { 52 | let mut text_tmp1 = self.text.as_str().to_string(); 53 | // url 解码 54 | let text_tmp2: String = 'break_1: loop { 55 | let decoded = url_decode(&text_tmp1); 56 | if let Ok(s) = decoded { 57 | if text_tmp1 == s { 58 | break 'break_1 text_tmp1; 59 | } 60 | text_tmp1 = s.to_string(); 61 | } else { 62 | break 'break_1 text_tmp1; 63 | } 64 | }; 65 | if text_tmp2.is_empty() { 66 | // 如果文字为空则返回1秒空音乐 67 | return Err(ControllerError::new("文本为空")); 68 | } 69 | 70 | // 转义符号 71 | let result = Regex::new(r"<") 72 | .unwrap() 73 | .replace_all(&text_tmp2, "<") 74 | .to_string(); 75 | let result = Regex::new(r">") 76 | .unwrap() 77 | .replace_all(&result, ">") 78 | .to_string(); 79 | 80 | let result = Regex::new(r"?") 81 | .unwrap() 82 | .replace_all(&result, "? ") 83 | .to_string(); 84 | let result = Regex::new(r",") 85 | .unwrap() 86 | .replace_all(&result, ", ") 87 | .to_string(); 88 | let result = Regex::new(r"。") 89 | .unwrap() 90 | .replace_all(&result, ". ") 91 | .to_string(); 92 | let result = Regex::new(r":") 93 | .unwrap() 94 | .replace_all(&result, ": ") 95 | .to_string(); 96 | let result = Regex::new(r";") 97 | .unwrap() 98 | .replace_all(&result, "; ") 99 | .to_string(); 100 | let result = Regex::new(r"!") 101 | .unwrap() 102 | .replace_all(&result, "! ") 103 | .to_string(); 104 | 105 | result 106 | }; 107 | 108 | let ms_informant_list = match api_name { 109 | MsApiOrigin::EdgeFree => { 110 | if !args.close_edge_free_api { 111 | AzureApiEdgeFree::new().get_vices_list().await 112 | } else { 113 | Err(TTSServerError::ProgramError( 114 | "未开启 ms-tts-edge 接口,请勿调用".to_owned(), 115 | )) 116 | } 117 | } 118 | MsApiOrigin::Subscription => { 119 | if !args.close_official_subscribe_api { 120 | AzureApiSubscribeToken::get_vices_mixed_list().await 121 | } else { 122 | Err(TTSServerError::ProgramError( 123 | "未开启 ms-tts-subscribe 接口,请勿调用".to_owned(), 124 | )) 125 | } 126 | } 127 | } 128 | .map_err(|e| { 129 | let err = ControllerError::new(format!("获取发音人数据错误 {:?}", e)); 130 | error!("{:?}", err); 131 | err 132 | })?; 133 | 134 | // let ms_tts_config = &MS_TTS_CONFIG.get().unwrap(); 135 | 136 | let informant_value: String = { 137 | let default = "zh-CN-XiaoxiaoNeural".to_owned(); 138 | 139 | match &self.informant { 140 | Some(inf) => { 141 | if ms_informant_list.voices_name_list.contains(inf) { 142 | inf.to_string() 143 | } else { 144 | default 145 | } 146 | } 147 | None => default, 148 | } 149 | } 150 | .trim() 151 | .to_owned(); 152 | 153 | let informant_item = ms_informant_list 154 | .by_voices_name_map 155 | .get(&informant_value) 156 | .unwrap(); 157 | 158 | let style_value: String = { 159 | let default = "general".to_owned(); 160 | if let Some(style) = &self.style { 161 | match &informant_item.get_style() { 162 | Some(e) => { 163 | let s_t = style.to_lowercase(); 164 | if e.contains(&s_t) { 165 | s_t 166 | } else { 167 | default 168 | } 169 | } 170 | None => default, 171 | } 172 | } else { 173 | default 174 | } 175 | } 176 | .trim() 177 | .to_owned(); 178 | 179 | let rate_value: String = { 180 | let default = "0".to_owned(); 181 | 182 | if let Some(style) = &self.rate { 183 | // num::Num 184 | if style <= &0.0 { 185 | "-100".to_owned() 186 | } else if style >= &3.0 { 187 | "200".to_owned() 188 | } else { 189 | let tmp = 100.00 * style - 100.00; 190 | format!("{:.0}", tmp) 191 | } 192 | } else { 193 | default 194 | } 195 | } 196 | .trim() 197 | .to_owned(); 198 | 199 | let pitch_value: String = { 200 | let default = "0".to_owned(); 201 | if let Some(pitch) = &self.pitch { 202 | if pitch <= &0.0 { 203 | "-50".to_owned() 204 | } else if pitch >= &2.0 { 205 | "50".to_owned() 206 | } else { 207 | let tmp = 50.00 * pitch - 50.00; 208 | format!("{:.0}", tmp) 209 | } 210 | } else { 211 | default 212 | } 213 | } 214 | .trim() 215 | .to_owned(); 216 | 217 | let quality_value: String = { 218 | let default = "audio-24khz-48kbitrate-mono-mp3".to_owned(); 219 | if let Some(quality) = &self.quality { 220 | if MS_TTS_QUALITY_LIST.contains(&quality.as_str()) { 221 | quality.to_owned() 222 | } else { 223 | default 224 | } 225 | } else { 226 | default 227 | } 228 | } 229 | .trim() 230 | .to_owned(); 231 | 232 | Ok(MsTtsMsgRequest { 233 | text: text_value, 234 | request_id: request_id_value, 235 | informant: informant_value, 236 | style: style_value, 237 | rate: rate_value, 238 | pitch: pitch_value, 239 | quality: quality_value, 240 | subscribe_key: None, 241 | region: None, 242 | }) 243 | } 244 | } 245 | 246 | impl AuthTokenValue for MsTtsMsgRequestJson { 247 | fn get_token(&self) -> Option<&str> { 248 | if self.token.is_some() { 249 | Some(self.token.as_ref().unwrap()) 250 | } else { 251 | None 252 | } 253 | } 254 | } 255 | 256 | /// 监听 257 | pub(crate) async fn tts_ms_post_controller( 258 | _req: HttpRequest, 259 | body: web::Json, 260 | ) -> Result { 261 | let id = random_string(32); 262 | debug!("收到 post 请求{:?}", body); 263 | let request_tmp = body.to_ms_request(MsApiOrigin::EdgeFree, id.clone()).await; 264 | info!("解析 post 请求 {:?}", request_tmp); 265 | let re = request_ms_tts("tts_ms_edge_free", request_tmp).await; 266 | debug!("响应 post 请求 {}", &id); 267 | re 268 | } 269 | 270 | pub(crate) async fn tts_ms_get_controller( 271 | _req: HttpRequest, 272 | request: web::Query, 273 | ) -> Result { 274 | let id = random_string(32); 275 | debug!("收到 get 请求{:?}", request); 276 | let request_tmp = request 277 | .to_ms_request(MsApiOrigin::EdgeFree, id.clone()) 278 | .await; 279 | info!("解析 get 请求 {:?}", request_tmp); 280 | 281 | let re = request_ms_tts("tts_ms_edge_free", request_tmp).await; 282 | debug!("响应 get 请求 {}", &id); 283 | 284 | re 285 | } 286 | 287 | pub(crate) async fn tts_ms_subscribe_api_get_controller( 288 | _req: HttpRequest, 289 | request: web::Query, 290 | ) -> Result { 291 | let id = random_string(32); 292 | debug!("收到 get 请求 /api/tts-ms-subscribe {:?}", request); 293 | let request_tmp = request 294 | .to_ms_request(MsApiOrigin::Subscription, id.clone()) 295 | .await; 296 | info!("解析 get 请求 {:?}", request_tmp); 297 | let re = request_ms_tts("tts_ms_subscribe_api", request_tmp).await; 298 | debug!("响应 get 请求 {}", &id); 299 | re 300 | } 301 | 302 | pub(crate) async fn tts_ms_subscribe_api_post_controller( 303 | _req: HttpRequest, 304 | body: web::Json, 305 | ) -> Result { 306 | let id = random_string(32); 307 | debug!("收到 post 请求 /api/tts-ms-subscribe {:?}", body); 308 | let request_tmp = body 309 | .to_ms_request(MsApiOrigin::Subscription, id.clone()) 310 | .await; 311 | info!("解析 post 请求 /api/tts-ms-subscribe {:?}", request_tmp); 312 | let re = request_ms_tts("tts_ms_subscribe_api", request_tmp).await; 313 | debug!("响应 post 请求 {}", &id); 314 | re 315 | } 316 | 317 | async fn request_ms_tts( 318 | api_name: &str, 319 | data: Result, 320 | ) -> Result { 321 | match data { 322 | Ok(rd) => { 323 | let id = rd.request_id.clone(); 324 | // debug!("请求微软语音服务器"); 325 | let kk = crate::GLOBAL_EB.request(api_name, rd.into()).await; 326 | // debug!("请求微软语音完成"); 327 | match kk { 328 | Some(data) => { 329 | let data = MsTtsMsgResponse::from_vec(data.as_bytes().unwrap().to_vec()); 330 | 331 | let mut respone = HttpResponse::build(StatusCode::OK).body(data.data); 332 | respone.headers_mut().insert( 333 | actix_web::http::header::CONTENT_TYPE, 334 | data.file_type.parse().unwrap(), 335 | ); 336 | Ok(respone) 337 | } 338 | None => { 339 | warn!("生成语音失败 {}", id); 340 | 341 | let ll: HttpResponse = ApiBaseResponse::<()>::error("未知错误").into(); 342 | Ok(ll) 343 | } 344 | } 345 | } 346 | Err(e) => { 347 | if e.msg == "文本为空" { 348 | warn!("请求文本为空"); 349 | let mut respone = HttpResponse::build(StatusCode::OK) 350 | .body(crate::ms_tts::BLANK_MUSIC_FILE.to_vec()); 351 | respone.headers_mut().insert( 352 | actix_web::http::header::CONTENT_TYPE, 353 | "audio/mpeg".parse().unwrap(), 354 | ); 355 | Ok(respone) 356 | } else { 357 | error!("调用错误:{:?}", e); 358 | Err(e) 359 | } 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/web/entity.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use actix_web::{ 7 | body::{BoxBody, MessageBody}, 8 | http::{header, header::HeaderValue, StatusCode}, 9 | HttpResponse, 10 | }; 11 | use bytes::Bytes; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 15 | pub struct ApiBaseResponse 16 | where 17 | T: Serialize, 18 | { 19 | pub code: i32, 20 | // #[serde(skip_serializing_if = "Option::is_none")] 21 | pub data: Option, 22 | pub msg: String, 23 | } 24 | 25 | impl ToString for ApiBaseResponse 26 | where 27 | T: Serialize, 28 | { 29 | fn to_string(&self) -> String { 30 | serde_json::to_string(&self).unwrap() 31 | } 32 | } 33 | 34 | #[allow(dead_code)] 35 | impl ApiBaseResponse 36 | where 37 | T: Serialize, 38 | { 39 | #[allow(dead_code)] 40 | pub fn success(data: Option) -> ApiBaseResponse { 41 | ApiBaseResponse { 42 | code: 200, 43 | data, 44 | msg: "success".to_owned(), 45 | } 46 | } 47 | 48 | #[allow(dead_code)] 49 | pub fn error>(msg: Y) -> ApiBaseResponse { 50 | 51 | ApiBaseResponse { 52 | code: 500, 53 | data: None, 54 | msg: msg.into(), 55 | } 56 | } 57 | 58 | #[allow(dead_code)] 59 | pub fn error_by_status_code>(code: i32, msg: Y) -> ApiBaseResponse { 60 | ApiBaseResponse { 61 | code, 62 | data: None, 63 | msg: msg.into(), 64 | } 65 | } 66 | } 67 | 68 | #[allow(dead_code)] 69 | pub fn success_as_value(data: serde_json::Value) -> ApiBaseResponse { 70 | ApiBaseResponse { 71 | code: 200, 72 | data: Some(data), 73 | msg: "success".to_owned(), 74 | } 75 | } 76 | 77 | impl MessageBody for ApiBaseResponse 78 | where 79 | T: Serialize, 80 | { 81 | type Error = std::convert::Infallible; 82 | 83 | fn size(&self) -> actix_web::body::BodySize { 84 | let payload_string = ApiBaseResponse::to_string(self); 85 | let payload_bytes = Bytes::from(payload_string); 86 | actix_web::body::BodySize::Sized(payload_bytes.len() as u64) 87 | } 88 | 89 | fn poll_next( 90 | self: Pin<&mut Self>, 91 | _cx: &mut Context<'_>, 92 | ) -> Poll>> { 93 | let payload_string = ApiBaseResponse::to_string(&self); 94 | let payload_bytes = Bytes::from(payload_string); 95 | Poll::Ready(Some(Ok(payload_bytes))) 96 | } 97 | } 98 | 99 | impl From> for Bytes 100 | where 101 | T: Serialize, 102 | { 103 | fn from(val: ApiBaseResponse) -> Self { 104 | let payload_string = ApiBaseResponse::::to_string(&val); 105 | 106 | Bytes::from(payload_string) 107 | } 108 | } 109 | 110 | impl From> for HttpResponse 111 | where 112 | T: Serialize, 113 | { 114 | fn from(val: ApiBaseResponse) -> Self { 115 | let bytes: Bytes = val.into(); 116 | let mut res = HttpResponse::with_body(StatusCode::OK, bytes); 117 | res.headers_mut().insert( 118 | header::CONTENT_TYPE, 119 | HeaderValue::from_static("application/json; charset=utf-8"), 120 | ); 121 | res.map_into_boxed_body() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/web/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use actix_web::{error, HttpResponse}; 4 | 5 | use crate::web::entity::ApiBaseResponse; 6 | 7 | #[derive(Debug)] 8 | pub struct ControllerError { 9 | pub code: i32, 10 | pub msg: String, 11 | } 12 | 13 | impl ControllerError { 14 | pub fn new>(msg: T) -> Self { 15 | ControllerError { 16 | code: 500, 17 | msg: msg.into(), 18 | } 19 | } 20 | pub fn from_status_code>(code: i32, msg: T) -> Self { 21 | ControllerError { 22 | code, 23 | msg: msg.into(), 24 | } 25 | } 26 | } 27 | 28 | impl Display for ControllerError { 29 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 30 | write!( 31 | f, 32 | "Error Code: {}, Controller Error: {}", 33 | self.code, self.msg 34 | ) 35 | } 36 | } 37 | 38 | // Use default implementation for `error_response()` method 39 | impl error::ResponseError for ControllerError { 40 | fn error_response(&self) -> HttpResponse { 41 | ApiBaseResponse::<()>::error_by_status_code(self.code, &self.msg).into() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/web/middleware/error_handle.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::{ready, Ready}, 3 | rc::Rc, 4 | }; 5 | 6 | use actix_http::body::{to_bytes, BoxBody}; 7 | use actix_web::{ 8 | body::{EitherBody, MessageBody}, 9 | dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, 10 | http::{header, header::HeaderValue}, 11 | HttpResponse, 12 | }; 13 | use futures::future::LocalBoxFuture; 14 | use log::{debug, error, info, warn}; 15 | 16 | 17 | use crate::web::vo::{BaseResponse, EmptyVO}; 18 | 19 | /// Actix 错误处理中间件 20 | #[derive(Debug, Clone, Default)] 21 | pub(crate) struct ErrorHandle; 22 | 23 | impl Transform for ErrorHandle 24 | where 25 | S: 'static + Service, Error = actix_web::Error>, 26 | S::Future: 'static, 27 | B: 'static + MessageBody, 28 | { 29 | type Response = ServiceResponse>; 30 | type Error = actix_web::Error; 31 | type Transform = ErrorHandleMiddleware; 32 | type InitError = (); 33 | type Future = Ready>; 34 | 35 | fn new_transform(&self, service: S) -> Self::Future { 36 | ready(Ok(ErrorHandleMiddleware { 37 | service: Rc::new(service), 38 | })) 39 | } 40 | } 41 | 42 | pub(crate) struct ErrorHandleMiddleware { 43 | service: Rc, 44 | } 45 | 46 | impl Service for ErrorHandleMiddleware 47 | where 48 | S: 'static + Service, Error = actix_web::Error>, 49 | S::Future: 'static, 50 | B: 'static + MessageBody, 51 | { 52 | type Response = ServiceResponse>; 53 | type Error = actix_web::Error; 54 | type Future = LocalBoxFuture<'static, Result>; 55 | 56 | forward_ready!(service); 57 | 58 | fn call(&self, req: ServiceRequest) -> Self::Future { 59 | info!("wrap ErrorHandleMiddleware"); 60 | 61 | let res_call = self.service.call(req); 62 | Box::pin(async move { 63 | let res: ServiceResponse = res_call.await?.map_into_boxed_body(); 64 | 65 | if res.status().is_client_error() || res.status().is_server_error() { 66 | // 处理错误信息(如,序列化json数据出问题) 67 | let (req, resp) = res.into_parts(); // 将 ServiceResponse 拆分,获取所有权 68 | 69 | let head = resp.head().clone(); 70 | 71 | debug!("ErrorHandleMiddleware:: ErrorResponseHead {:?}", &head); 72 | 73 | let content_type = head.headers.get(header::CONTENT_TYPE).map(AsRef::as_ref); 74 | 75 | let error_body = if content_type.is_none() 76 | || content_type == Some(b"text/plain; charset=utf-8") 77 | { 78 | let status_code = head.status.as_u16() as i32; 79 | let status_str = head.status.canonical_reason().unwrap().to_string(); 80 | let body_error_msg_bytes = to_bytes(resp.into_body()).await; 81 | debug!( 82 | "ErrorHandleMiddleware:: ErrorResponseBody {:?}", 83 | &body_error_msg_bytes 84 | ); 85 | match body_error_msg_bytes { 86 | Ok(bytes) => { 87 | let body_err = String::from_utf8(bytes.to_vec()) 88 | .unwrap_or("from_utf8 error".to_owned()); 89 | warn!( 90 | "ErrorHandleMiddleware :: HandleError :: {:?} {} {}", 91 | status_code, 92 | &body_err, 93 | &req.path() 94 | ); 95 | let body = EitherBody::new( 96 | BaseResponse::::from_error_info::( 97 | head.status.into(), 98 | if body_err.is_empty() { 99 | status_str 100 | } else { 101 | body_err 102 | }, 103 | ) 104 | .to_string(), 105 | ); 106 | 107 | let mut res = HttpResponse::with_body(head.status, body); 108 | res.headers_mut().insert( 109 | header::CONTENT_TYPE, 110 | HeaderValue::from_static("application/json; charset=utf-8"), 111 | ); 112 | 113 | ServiceResponse::new(req, res) 114 | } 115 | Err(err) => { 116 | error!("match body_error_msg_bytes {:?}", err); 117 | let r = BaseResponse::::from_error_info::( 118 | head.status.into(), 119 | status_str, 120 | ) 121 | .http_resp(); 122 | ServiceResponse::new(req, r.map_into_boxed_body().map_into_right_body()) 123 | } 124 | } 125 | } else { 126 | ServiceResponse::new(req, resp.map_into_boxed_body().map_into_right_body()) 127 | }; 128 | 129 | Ok(error_body.map_into_boxed_body().map_into_right_body()) 130 | } else { 131 | debug!("ErrorHandleMiddleware :: Ignore"); 132 | Ok(res.map_into_right_body()) 133 | } 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/web/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod error_handle; 2 | pub(crate) mod token_auth; 3 | -------------------------------------------------------------------------------- /src/web/middleware/token_auth.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::{ready, Ready}, 3 | marker::PhantomData, 4 | rc::Rc, 5 | }; 6 | 7 | 8 | use actix_web::{ 9 | body::{EitherBody, MessageBody}, 10 | dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, 11 | http::{header, header::HeaderValue, StatusCode}, 12 | HttpResponse, 13 | }; 14 | use futures::future::LocalBoxFuture; 15 | use log::{info}; 16 | use serde::{de::DeserializeOwned, Serialize}; 17 | 18 | use crate::{ 19 | web::{entity::ApiBaseResponse, utils::get_request_body_to_entity}, 20 | AppArgs, 21 | }; 22 | 23 | /// 请求体中获取 token 24 | pub trait AuthTokenValue { 25 | fn get_token(&self) -> Option<&str>; 26 | } 27 | 28 | /// 29 | /// 权限认证中间件 30 | /// 31 | #[derive(Debug, Clone)] 32 | pub struct TokenAuthentication 33 | where 34 | T: Serialize + DeserializeOwned + AuthTokenValue, 35 | { 36 | _marker: PhantomData, 37 | } 38 | 39 | impl Default for TokenAuthentication 40 | where 41 | T: Serialize + DeserializeOwned + AuthTokenValue, 42 | { 43 | fn default() -> Self { 44 | TokenAuthentication:: { 45 | _marker: PhantomData, 46 | } 47 | } 48 | } 49 | 50 | impl Transform for TokenAuthentication 51 | where 52 | S: 'static + Service, Error = actix_web::Error>, 53 | S::Future: 'static, 54 | B: 'static + MessageBody, 55 | T: Serialize + DeserializeOwned + AuthTokenValue, 56 | { 57 | type Response = ServiceResponse>; 58 | type Error = actix_web::Error; 59 | type Transform = TokenAuthenticationMiddleware; 60 | type InitError = (); 61 | type Future = Ready>; 62 | 63 | fn new_transform(&self, service: S) -> Self::Future { 64 | ready(Ok(TokenAuthenticationMiddleware { 65 | service: Rc::new(service), 66 | _marker: PhantomData, 67 | })) 68 | } 69 | } 70 | 71 | pub struct TokenAuthenticationMiddleware 72 | where 73 | T: Serialize + DeserializeOwned + AuthTokenValue, 74 | { 75 | service: Rc, 76 | _marker: PhantomData, 77 | } 78 | 79 | impl Service for TokenAuthenticationMiddleware 80 | where 81 | S: 'static + Service, Error = actix_web::Error>, 82 | S::Future: 'static, 83 | B: 'static + MessageBody, 84 | T: Serialize + DeserializeOwned + AuthTokenValue, 85 | { 86 | type Response = ServiceResponse>; 87 | type Error = actix_web::Error; 88 | type Future = LocalBoxFuture<'static, Result>; 89 | 90 | forward_ready!(service); 91 | 92 | fn call(&self, mut req: ServiceRequest) -> Self::Future { 93 | info!("wrap TokenAuthenticationMiddleware"); 94 | let svc = self.service.clone(); 95 | let args = AppArgs::parse_macro(); 96 | let system_token = args.subscribe_api_auth_token.as_ref().unwrap().as_str(); 97 | 98 | // url query 中的 Token 99 | let req_path_token = { 100 | let query_list = req.request().query_string().split('&'); 101 | let mut query_list_tmp = Vec::new(); 102 | query_list.into_iter().for_each(|i| { 103 | let item: Vec<_> = i.split('=').collect(); 104 | let key_name = item.first().unwrap(); 105 | if key_name == &"token" { 106 | query_list_tmp.push(item.get(1).as_ref().unwrap().to_string()) 107 | } 108 | }); 109 | let token_one = query_list_tmp.get(0); 110 | if let Some(d) = token_one { 111 | d.to_owned() 112 | } else { 113 | "".to_owned() 114 | } 115 | }; 116 | if system_token == req_path_token { 117 | return Box::pin(async move { 118 | let fut = svc.call(req); 119 | let res = fut.await?; 120 | Ok(res.map_into_boxed_body().map_into_right_body()) 121 | }); 122 | } 123 | 124 | let header_token = req 125 | .headers() 126 | .get("token") 127 | .unwrap_or(&HeaderValue::from_str("").unwrap()) 128 | .to_str() 129 | .unwrap() 130 | .to_string(); 131 | 132 | if system_token == header_token { 133 | return Box::pin(async move { 134 | let fut = svc.call(req); 135 | let res = fut.await?; 136 | Ok(res.map_into_boxed_body().map_into_right_body()) 137 | }); 138 | } 139 | 140 | if req.request().method().as_str() == "POST" { 141 | Box::pin(async move { 142 | let body_entity: anyhow::Result = get_request_body_to_entity(&mut req).await; 143 | return if let Ok(data) = body_entity { 144 | if data.get_token().is_some() && data.get_token().unwrap() == system_token { 145 | let fut = svc.call(req); 146 | let res = fut.await?; 147 | Ok(res.map_into_boxed_body().map_into_right_body()) 148 | } else { 149 | let body = EitherBody::new( 150 | ApiBaseResponse::<()>::error_by_status_code( 151 | 403, 152 | "Token 错误,无法进行调用", 153 | ) 154 | .to_string(), 155 | ); 156 | 157 | let mut res = HttpResponse::with_body(StatusCode::FORBIDDEN, body); 158 | res.headers_mut().insert( 159 | header::CONTENT_TYPE, 160 | HeaderValue::from_static("application/json; charset=utf-8"), 161 | ); 162 | let new_resp_s = req.into_parts().0; 163 | let new_resp_s = ServiceResponse::new(new_resp_s, res); 164 | Ok(new_resp_s.map_into_boxed_body().map_into_right_body()) 165 | } 166 | } else { 167 | let body = EitherBody::new( 168 | ApiBaseResponse::<()>::error_by_status_code(500, "解析请求中的 Token 错误") 169 | .to_string(), 170 | ); 171 | 172 | let mut res = HttpResponse::with_body(StatusCode::FORBIDDEN, body); 173 | res.headers_mut().insert( 174 | header::CONTENT_TYPE, 175 | HeaderValue::from_static("application/json; charset=utf-8"), 176 | ); 177 | let new_resp_s = req.into_parts().0; 178 | let new_resp_s = ServiceResponse::new(new_resp_s, res); 179 | Ok(new_resp_s.map_into_boxed_body().map_into_right_body()) 180 | }; 181 | }) 182 | } else { 183 | Box::pin(async move { 184 | let body = EitherBody::new( 185 | ApiBaseResponse::<()>::error_by_status_code(403, "Token 错误,无法进行调用") 186 | .to_string(), 187 | ); 188 | 189 | let mut res = HttpResponse::with_body(StatusCode::FORBIDDEN, body); 190 | res.headers_mut().insert( 191 | header::CONTENT_TYPE, 192 | HeaderValue::from_static("application/json; charset=utf-8"), 193 | ); 194 | let new_resp_s = req.into_parts().0; 195 | let new_resp_s = ServiceResponse::new(new_resp_s, res); 196 | Ok(new_resp_s.map_into_boxed_body().map_into_right_body()) 197 | }) 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/web/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod controller; 2 | // #[cfg(feature = "web-entrance")] 3 | mod entity; 4 | pub(crate) mod error; 5 | pub(crate) mod utils; 6 | pub(crate) mod web_entrance; 7 | 8 | pub(crate) mod middleware; 9 | mod vo; 10 | 11 | use actix_web::{ 12 | middleware::{Compress, Condition}, 13 | web, App, HttpServer, 14 | }; 15 | use log::{error, info}; 16 | 17 | // #[cfg(feature = "web-entrance")] 18 | use crate::web::web_entrance::register_router; 19 | use crate::{ 20 | web::{ 21 | controller::*, 22 | middleware::{error_handle::ErrorHandle, token_auth::TokenAuthentication}, 23 | }, 24 | AppArgs, 25 | }; 26 | 27 | /// 28 | /// 注册 web 服务 29 | /// 30 | /// 31 | pub(crate) async fn register_service() { 32 | let args = AppArgs::parse_macro(); 33 | let web_server = HttpServer::new(|| { 34 | let app = App::new(); 35 | let mut app = app.wrap(ErrorHandle).wrap(Compress::default()); 36 | 37 | // 微软 TTS 文本转语音 相关接口 38 | 39 | // if !args.close_official_subscribe_api { 40 | app = app.service( 41 | // 新版本网页接口地址 (使用api收费访问) 42 | web::resource("/api/tts-ms-subscribe") 43 | .wrap(Condition::new( 44 | args.subscribe_api_auth_token.is_some(), 45 | TokenAuthentication::::default(), 46 | )) 47 | .route(web::get().to(tts_ms_subscribe_api_get_controller)) 48 | .route(web::post().to(tts_ms_subscribe_api_post_controller)), 49 | ); 50 | // } 51 | 52 | // if !args.close_edge_free_api { 53 | app = app.service( 54 | // 旧版本 edge 预览接口 55 | web::resource("/api/tts-ms-edge") 56 | .route(web::get().to(tts_ms_get_controller)) 57 | .route(web::post().to(tts_ms_post_controller)), 58 | ); 59 | // } 60 | 61 | // 根据功能 62 | // #[cfg(feature = "web-entrance")] 63 | // { 64 | 65 | app = app.configure(register_router); 66 | 67 | // } 68 | app 69 | }); 70 | let web_server = web_server.bind(format!("{}:{}", args.listen_address, args.listen_port)); 71 | match web_server { 72 | Ok(server) => { 73 | let local_ip = local_ipaddress::get(); 74 | info!( 75 | "启动 Api 服务成功 接口地址已监听至: {}:{} 自行修改 ip 以及 port", 76 | args.listen_address, args.listen_port 77 | ); 78 | if local_ip.is_some() { 79 | info!( 80 | "您当前局域网ip可能为: {} 请自行替换上面的监听地址", 81 | local_ip.unwrap() 82 | ); 83 | } 84 | server 85 | .workers(1) 86 | .max_connections(1000) 87 | .backlog(1000) 88 | .run() 89 | .await 90 | .unwrap(); 91 | } 92 | Err(_e) => { 93 | error!( 94 | "启动 Api 服务失败,无法监听 {}:{}", 95 | args.listen_address, args.listen_port 96 | ); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/web/utils.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | use actix_http::h1::Payload; 4 | use actix_web::{dev::ServiceRequest, http::header, HttpMessage}; 5 | use anyhow::{Error, Result}; 6 | use bytes::BytesMut; 7 | use futures::StreamExt; 8 | use log::error; 9 | use serde::{de::DeserializeOwned, Serialize}; 10 | 11 | 12 | pub async fn get_request_body_to_entity(sr: &mut ServiceRequest) -> Result 13 | where 14 | T: Serialize + DeserializeOwned, 15 | { 16 | let content_type = sr 17 | .headers() 18 | .get(header::CONTENT_TYPE) 19 | .map(|v| v.clone().to_str().unwrap_or("").to_string()) 20 | .unwrap_or_else(|| "".to_string()) 21 | .to_lowercase(); 22 | 23 | // Content type that is not application json won't be logged since it can cause 24 | // harm in some setups, this might be a feature to implement sometimes in the future, 25 | // once we get a proper chance to test it and figure out all the bugs that keep happening, 26 | // but for now we will simply set it as Null value in the log. 27 | // 28 | // Issue that we got was that some multipart forms weren't recognized properly after 29 | // the things we did here below to them, the issue couldn't be reproduced in a local 30 | // setting, but it was happening within the cluster. 31 | // 32 | // Payload would apear okay in treblle.com, but later methods that were supposed 33 | // to handle that payload reported invalid multipart data, or form data. 34 | if content_type != "application/json" { 35 | return Err(Error::msg("Content type is not application/json")); 36 | } 37 | let mut body_bytes = BytesMut::new(); 38 | let mut stream = sr.take_payload(); 39 | while let Some(chunk) = stream.next().await { 40 | body_bytes.extend_from_slice(&chunk?); 41 | } 42 | let bytes = body_bytes.freeze(); 43 | 44 | let (_sender, mut orig_payload) = Payload::create(true); 45 | orig_payload.unread_data(bytes.clone()); 46 | sr.set_payload(actix_http::Payload::from(orig_payload)); 47 | 48 | if bytes.is_empty() { 49 | return Err(Error::msg("Empty body received")); 50 | } 51 | let serde_obj = serde_json::from_slice::(&bytes) 52 | .map_err(|e| { 53 | let err_msg = format!("Error deserializing request body: {}", e); 54 | error!("{}", err_msg); 55 | Error::msg(err_msg) 56 | })?; 57 | Ok(serde_obj) 58 | } 59 | -------------------------------------------------------------------------------- /src/web/vo/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | use actix_http::body::BoxBody; 4 | use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder, Responder}; 5 | 6 | use log::{error}; 7 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 8 | 9 | use crate::error::{Error, Result}; 10 | 11 | /// Http 响应码 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct HttpStatus { 14 | pub code: u16, 15 | pub reason: Option, 16 | } 17 | 18 | impl From for HttpStatus { 19 | fn from(value: StatusCode) -> Self { 20 | let reason = value.canonical_reason().map(|i| i.to_string()); 21 | HttpStatus { 22 | code: value.as_u16(), 23 | reason, 24 | } 25 | } 26 | } 27 | 28 | impl From for StatusCode { 29 | fn from(value: HttpStatus) -> Self { 30 | StatusCode::from_u16(value.code).expect("错误的状态码") 31 | } 32 | } 33 | 34 | /// 基础响应体 35 | #[derive(Debug, Clone, Serialize, Deserialize)] 36 | pub struct BaseResponse { 37 | /// http 响应码 38 | pub code: u16, 39 | /// 具体响应内容 (可选) 40 | pub data: Option, 41 | /// 响应信息或错误信息 (可选) 42 | pub msg: Option, 43 | } 44 | 45 | impl BaseResponse 46 | where 47 | T: Serialize + DeserializeOwned, 48 | { 49 | pub fn from_result>(arg: Result) -> Self { 50 | match arg { 51 | Ok(data) => { 52 | let status: HttpStatus = StatusCode::OK.into(); 53 | Self { 54 | code: status.code, 55 | msg: status.reason, 56 | data: Some(data), 57 | } 58 | } 59 | Err(e) => { 60 | let err = e.into(); 61 | error!("BaseResponse InternalError: {:?}", err); 62 | let status: HttpStatus = StatusCode::INTERNAL_SERVER_ERROR.into(); 63 | Self { 64 | code: status.code, 65 | msg: Some(format!("错误信息: {}", err)), 66 | data: None, 67 | } 68 | } 69 | } 70 | } 71 | 72 | pub fn from(arg: T) -> Self { 73 | let status: HttpStatus = StatusCode::OK.into(); 74 | Self { 75 | code: status.code, 76 | msg: status.reason, 77 | data: Some(arg), 78 | } 79 | } 80 | 81 | pub fn from_error(code: HttpStatus, arg: &Error) -> Self { 82 | error!("BaseResponse from_error: {:?} {:?}", code, arg); 83 | Self { 84 | code: code.code, 85 | msg: Some(format!("请求失败, {}", arg)), 86 | data: None, 87 | } 88 | } 89 | 90 | pub fn from_error_info(code: HttpStatus, info: V) -> Self { 91 | let err_str = info.to_string(); 92 | error!("BaseResponse from_error_info: {:?} {:?}", code, err_str); 93 | Self { 94 | code: code.code, 95 | msg: Some(err_str), 96 | data: None, 97 | } 98 | } 99 | 100 | pub fn http_resp(&self) -> HttpResponse { 101 | return HttpResponseBuilder::new(StatusCode::from_u16(self.code).unwrap()) 102 | .insert_header(("Access-Control-Allow-Origin", "*")) 103 | .insert_header(("Cache-Control", "no-cache")) 104 | .insert_header(("Content-Type", "text/json;charset=utf-8")) 105 | .body(self.to_string()); 106 | } 107 | } 108 | 109 | impl ToString for BaseResponse 110 | where 111 | T: Serialize + DeserializeOwned, 112 | { 113 | fn to_string(&self) -> String { 114 | serde_json::to_string(self) 115 | .map_err(|e| error!("BaseResponse ToString 序列化错误 {:?}", e)) 116 | .unwrap() 117 | } 118 | } 119 | 120 | impl Responder for BaseResponse 121 | where 122 | T: Serialize + DeserializeOwned, 123 | { 124 | type Body = BoxBody; 125 | 126 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 127 | self.http_resp() 128 | } 129 | } 130 | 131 | #[derive(Serialize, Deserialize, Clone, Debug)] 132 | pub struct EmptyVO {} 133 | 134 | pub const EMPTY_VO: EmptyVO = EmptyVO {}; 135 | -------------------------------------------------------------------------------- /src/web/web_entrance.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpRequest, HttpResponse, Responder}; 2 | use log::error; 3 | // use mime_guess::from_path; 4 | // use rust_embed::RustEmbed; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::{ 8 | utils::azure_api::{ 9 | AzureApiEdgeFree, AzureApiSpeakerList, AzureApiSubscribeToken, MsApiOrigin, 10 | MS_TTS_QUALITY_LIST, 11 | }, 12 | web::{entity::ApiBaseResponse, error::ControllerError, vo::BaseResponse}, 13 | AppArgs, 14 | }; 15 | 16 | /// 17 | /// 注册 web访问界面 18 | pub(crate) fn register_router(cfg: &mut web::ServiceConfig) { 19 | cfg.service( 20 | // 获取 tts 支持列表 21 | web::resource("/api/list") 22 | .route(web::get().to(get_api_list)) 23 | .route(web::post().to(get_api_list)), 24 | ) 25 | .service( 26 | web::resource("/api/ms-tts/style/{api_name}/{informant}") 27 | .route(web::get().to(get_ms_tts_style)), 28 | ) 29 | .route( 30 | "/api/ms-tts/informant/{api_name}", 31 | web::get().to(get_ms_tts_informant), 32 | ) 33 | .route("/api/ms-tts/quality", web::get().to(get_ms_tts_quality)); 34 | // 等待web UI 适配 35 | // .service(web::resource("/").route(web::get().to(html_index))) 36 | // .service(web::resource("/{_:.*}").route(web::get().to(dist))); 37 | } 38 | 39 | // 等待WebUi适配 40 | /*#[derive(RustEmbed)] 41 | #[folder = "web/dist/"] 42 | struct WebAsset; 43 | 44 | fn handle_embedded_file(path: &str) -> HttpResponse { 45 | // let index_html = WebAsset::get("prefix/index.html").unwrap(); 46 | //RustEmbed::get() Asset::get 47 | match WebAsset::get(path) { 48 | Some(content) => { 49 | let kk = content.data; 50 | let hh = kk.into_owned(); 51 | HttpResponse::Ok() 52 | .content_type(from_path(path).first_or_octet_stream().as_ref()) 53 | .body(hh) 54 | } 55 | None => HttpResponse::NotFound().body("404 Not Found"), 56 | } 57 | } 58 | 59 | async fn favicon_ico() -> HttpResponse { 60 | handle_embedded_file("favicon.ico") 61 | } 62 | 63 | pub(crate) async fn html_index() -> HttpResponse { 64 | handle_embedded_file("index.html") 65 | } 66 | 67 | pub(crate) async fn dist(path: web::Path) -> HttpResponse { 68 | let patd = path.into_inner(); 69 | handle_embedded_file(&patd) 70 | }*/ 71 | 72 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 73 | pub struct ApiListResponse { 74 | list: Vec, 75 | } 76 | 77 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 78 | pub struct ApiListItem { 79 | pub api_id: String, 80 | pub api_name: String, 81 | pub api_desc: String, 82 | pub api_url: String, 83 | pub params: Vec, 84 | } 85 | 86 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 87 | #[serde(tag = "param_type")] 88 | pub enum ApiParam { 89 | Text { 90 | index: usize, 91 | param_name: String, 92 | param_desc: String, 93 | max_len: Option, 94 | min_len: Option, 95 | }, 96 | Float { 97 | index: usize, 98 | param_name: String, 99 | param_desc: String, 100 | // #[serde(skip_serializing_if = "Option::is_none")] 101 | float_min: Option, 102 | // #[serde(skip_serializing_if = "Option::is_none")] 103 | float_max: Option, 104 | // #[serde(skip_serializing_if = "Option::is_none")] 105 | default_value: Option, 106 | }, 107 | List { 108 | index: usize, 109 | param_name: String, 110 | param_desc: String, 111 | list_data_url: String, 112 | }, 113 | } 114 | 115 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 116 | pub struct ListDataItem { 117 | pub key: String, 118 | pub desc: String, 119 | #[serde(skip_serializing_if = "serde_json::Value::is_null")] 120 | pub data: serde_json::Value, 121 | } 122 | 123 | /// 124 | /// /api/list 125 | /// 获取可用语音合成接口列表 126 | /// 127 | pub(crate) async fn get_api_list(_req: HttpRequest) -> Result { 128 | let mut api_list: Vec = Vec::new(); 129 | let args = AppArgs::parse_macro(); 130 | if !args.close_edge_free_api { 131 | let data = include_bytes!("../resource/api/ms-api-edge.json"); 132 | api_list.push(serde_json::from_slice(data).unwrap()) 133 | } 134 | if !args.close_official_subscribe_api { 135 | let data = include_bytes!("../resource/api/ms-api-subscribe.json"); 136 | api_list.push(serde_json::from_slice(data).unwrap()) 137 | } 138 | Ok(BaseResponse::from(api_list)) 139 | } 140 | 141 | /// 142 | /// /api/ms-tts/informant/{api_name} 143 | /// 获取微软文本转语音接口发音人列表 144 | /// 145 | pub(crate) async fn get_ms_tts_informant( 146 | path_params: web::Path, 147 | ) -> Result { 148 | let api_name = MsApiOrigin::try_from(path_params.into_inner()).map_err(|e| { 149 | let err = format!("接口配置数据不存在 {:?}", e); 150 | error!("{}", err); 151 | ControllerError::new(err) 152 | })?; 153 | let args = AppArgs::parse_macro(); 154 | let mut list: Vec = Vec::new(); 155 | 156 | let vices_list = match api_name { 157 | MsApiOrigin::EdgeFree => { 158 | if args.close_edge_free_api { 159 | None 160 | } else { 161 | Some( 162 | AzureApiEdgeFree::new() 163 | .get_vices_list() 164 | .await 165 | .map_err(|e| { 166 | let err = ControllerError::new(format!( 167 | "获取 ms-tts-edge 接口发音人列表失败 {:?}", 168 | e 169 | )); 170 | error!("{:?}", err); 171 | err 172 | })?, 173 | ) 174 | } 175 | } 176 | MsApiOrigin::Subscription => { 177 | if args.close_official_subscribe_api { 178 | None 179 | } else { 180 | Some( 181 | AzureApiSubscribeToken::get_vices_mixed_list() 182 | .await 183 | .map_err(|e| { 184 | let err = ControllerError::new(format!( 185 | "获取 ms-tts-subscribe 接口发音人列表失败 {:?}", 186 | e 187 | )); 188 | error!("{:?}", err); 189 | err 190 | })?, 191 | ) 192 | } 193 | } 194 | }; 195 | if vices_list.is_none() { 196 | let err = ControllerError::new("配置数据不存在"); 197 | error!("{:?}", err); 198 | return Err(err); 199 | } 200 | let vices_list = vices_list.unwrap(); 201 | vices_list.voices_name_list.iter().for_each(|v| { 202 | let voice_item = vices_list.by_voices_name_map.get(v).unwrap(); 203 | 204 | let desc = voice_item.get_desc(); 205 | let tmp = serde_json::to_value(voice_item.as_ref()).unwrap(); 206 | 207 | list.push(ListDataItem { 208 | key: v.clone(), 209 | desc, 210 | data: tmp, 211 | }); 212 | }); 213 | 214 | Ok(ApiBaseResponse::success(Some(list)).into()) 215 | } 216 | 217 | /// 218 | /// /ms-tts/quality 219 | /// 获取微软文本转语音接口音质列表 220 | 221 | pub(crate) async fn get_ms_tts_quality(_req: HttpRequest) -> Result { 222 | let list_tmp = MS_TTS_QUALITY_LIST; 223 | 224 | let mut list: Vec = Vec::new(); 225 | 226 | list_tmp.iter().for_each(|v| { 227 | list.push(ListDataItem { 228 | key: v.to_string(), 229 | desc: "".to_owned(), 230 | data: serde_json::Value::Null, 231 | }); 232 | }); 233 | 234 | Ok(ApiBaseResponse::success(Some(list)).into()) 235 | } 236 | 237 | // 238 | // #[get("/ms-tts/style/{informant}")] 239 | #[derive(Deserialize, Debug)] 240 | pub(crate) struct PathParams { 241 | pub api_name: String, 242 | pub informant: String, 243 | } 244 | 245 | pub(crate) async fn get_ms_tts_style( 246 | path_params: web::Path, 247 | ) -> Result { 248 | let params = path_params.into_inner(); 249 | let informant = params.informant; 250 | let api_name = MsApiOrigin::try_from(params.api_name).map_err(|e| { 251 | let err = format!("接口配置数据不存在 {:?}", e); 252 | error!("{}", err); 253 | ControllerError::new(err) 254 | })?; 255 | let args = AppArgs::parse_macro(); 256 | 257 | let vices_list = match api_name { 258 | MsApiOrigin::EdgeFree => { 259 | if args.close_edge_free_api { 260 | None 261 | } else { 262 | Some( 263 | AzureApiEdgeFree::new() 264 | .get_vices_list() 265 | .await 266 | .map_err(|e| { 267 | let err = ControllerError::new(format!( 268 | "获取 ms-tts-edge 接口发音人列表失败 {:?}", 269 | e 270 | )); 271 | error!("{:?}", err); 272 | err 273 | })?, 274 | ) 275 | } 276 | } 277 | MsApiOrigin::Subscription => { 278 | if args.close_official_subscribe_api { 279 | None 280 | } else { 281 | Some( 282 | AzureApiSubscribeToken::get_vices_mixed_list() 283 | .await 284 | .map_err(|e| { 285 | let err = ControllerError::new(format!( 286 | "获取 ms-tts-subscribe 接口发音人列表失败 {:?}", 287 | e 288 | )); 289 | error!("{:?}", err); 290 | err 291 | })?, 292 | ) 293 | } 294 | } 295 | }; 296 | 297 | if vices_list.is_none() { 298 | let err = ControllerError::new("配置数据不存在,请查看是否参数正确,或是否开启该接口"); 299 | error!("{:?}", err); 300 | return Err(err); 301 | } 302 | let vices_list = vices_list.unwrap(); 303 | 304 | let mut list_data: Vec = Vec::new(); 305 | let voice_item = vices_list.by_voices_name_map.get(&informant).unwrap(); 306 | let vec_style = if voice_item.get_style().is_some() { 307 | let mut ff = vec!["general".to_owned()]; 308 | let mut kk = voice_item 309 | .get_style() 310 | .as_ref() 311 | .unwrap().to_vec(); 312 | ff.append(&mut kk); 313 | ff 314 | } else { 315 | let ff = vec!["general".to_owned()]; 316 | ff 317 | }; 318 | 319 | vec_style.iter().for_each(|v| { 320 | list_data.push(ListDataItem { 321 | key: v.clone(), 322 | desc: "".to_owned(), 323 | data: serde_json::Value::Null, 324 | }); 325 | }); 326 | 327 | Ok(ApiBaseResponse::success(Some(list_data)).into()) 328 | } 329 | --------------------------------------------------------------------------------