├── .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 | 
2 | [](https://github.com/litcc/tts-server/actions/workflows/rust.yml)
3 | 
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