From fc987599a03d3a9abcbb94bd4edefe764d2aafe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E4=BF=8A=E7=91=9C?= <2933582448@qq.com> Date: Sat, 9 Dec 2023 11:08:37 +0800 Subject: [PATCH] 23.12.09 --- .idea/.gitignore | 8 + .idea/VoiceSynthAssistant.iml | 8 + .idea/deployment.xml | 175 + .idea/inspectionProfiles/Project_Default.xml | 59 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + README.md | 24 + SSML.xml | 7 + __pycache__/tts.cpython-310.pyc | Bin 0 -> 4406 bytes main.py | 207 + output_2023-12-09 10-37-43.mp3 | Bin 0 -> 13248 bytes requirements.txt | 2 + tts.py | 146 + voice.png | Bin 0 -> 7833 bytes ...\351\237\263\345\210\227\350\241\250.json" | 5461 +++++++++++++++++ 16 files changed, 6118 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/VoiceSynthAssistant.iml create mode 100644 .idea/deployment.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 README.md create mode 100644 SSML.xml create mode 100644 __pycache__/tts.cpython-310.pyc create mode 100644 main.py create mode 100644 output_2023-12-09 10-37-43.mp3 create mode 100644 requirements.txt create mode 100644 tts.py create mode 100644 voice.png create mode 100644 "\346\224\257\346\214\201\345\243\260\351\237\263\345\210\227\350\241\250.json" diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/VoiceSynthAssistant.iml b/.idea/VoiceSynthAssistant.iml new file mode 100644 index 0000000..f34f703 --- /dev/null +++ b/.idea/VoiceSynthAssistant.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..378e4a3 --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..9ab2ca3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,59 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ba2d063 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8c3b270 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4bee11 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# 语音合成助手 + +## 介绍 +语音合成助手是一个基于Python的应用程序,旨在将文本转换为语音。它利用语音合成技术,将输入的文本转化为自然流畅的语音输出。该助手提供了一个用户友好的图形界面,使用户能够轻松输入文本并选择所需的语音合成设置。 + +通过语音合成助手,你可以轻松地将文本转换为语音,用于各种应用场景,如语音导航、语音提示、语音辅助等。 + +它为用户提供了一种简单而强大的方式来将文本转换为自然流畅的语音输出。 + +## 使用 +使用语音合成助手,你可以通过以下步骤实现语音合成: + +1、在文本编辑框中输入要转换为语音的文本。 + +2、从可用的声音选择列表中选择所需的语音类型。 + +3、调整速度和语等参数,以获得所需的语音效果。 + +4、指定要保存的文件名和路径。 + +5、点击生成按钮开始语音合成过程。 + +## 参考 +[tts](https://github.com/skygongque/tts) diff --git a/SSML.xml b/SSML.xml new file mode 100644 index 0000000..f2f84e3 --- /dev/null +++ b/SSML.xml @@ -0,0 +1,7 @@ + + + + 我是语音助手 + + + diff --git a/__pycache__/tts.cpython-310.pyc b/__pycache__/tts.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..990d4ffb8c5be876c54e7d461062a8751b19bf71 GIT binary patch literal 4406 zcmaJ^OKcm*8QvGkB}LMbEI;BrO0ZB7w&F`-Bw3D}$dX(eu@xhd>ttboUG9v?mADVj z4lQd#0!fsjX$v>br3jK5mC#dA1UVFF4*^mX?YTg&du{B*hXMr(6ljpf{r{mP%Wl$L z%*@U|uYdm6_pg`9q$GHPzXWuCM3Vl7or6ytI?qE1?m|T*Ln1O!+Oi>wRxuQ4)wb%W zhAQ{RG(!_(F(W40xDgj^!bl_~qLJ8}lA)70NkE$B^g8Hh%ro#8i&a-V}u+xj?gR}ribYWJwhJ4sToJf z33_Nt+CD}eCr|7s$0Xx8Rkk17k+x6J3_VJZZz(&nEYZiwlUvXdWm%NMQ=L!XvZ*_7 zzw!BNTc3XX>(91dPqjGrgPC%>x1i$$-){SJ@f{kC%)PWbc>x9@!TuH#SK{l$+z{rGpEz4zwlzyIjYCvToD zXpsiXd8D;y+mAH9>C;GO^jgPeloVo-ZnCCtvVca}Io9kr)a943%c8VNNF7E6Em8x{ zqLi44#Gs%=anWOuhW!sDHVBM-F}pBx_0p!*GMRIUd27_-PcIa%`kTDvxx0j`Kk#ge zE^oTLH5UYSz)hE51u=tYqomjqI0=SPdOB2Tc|_J_O=d{3WT^HY6f_ngXmBtPB$1DU zogD>Xa;YnCp+ME8d${Nx%Bdi;G&V}4w3v|u<6^)eC4%>UH3Z{Pti|dUI{}}822~mu zU`OHOq2pB`U=hbp!v~Rj;J|ROv;}PRlrCs`U1P_)GSQ|`zY2*>@6&^HSMEy3Bw#VV z6iN5+CJZ@^~nRE^W(TgA!M`3O1^3f$)fYY_A`~{xP`WJ|(_O0oR8$39iGc zB+|q@b$?#!e<%aH6bJiDC0F{TLnogT)phT+#iR)o59^kc6ZL9F3h|g@9AQm z{$L(YZl}oLj@(OJD@>L_$r2KcucmZbFvueMNE=S9axVSNL#O z27Zs+R>;vUd{Yo~HICToSN22IajprXEXYO%q{Pg6 z3uI1N5T;))qVEE>IQ~rd_e=`+hnd9{0Kgd&u7zk0GA*wKo_i{XfE7?a+u^Il=}hX9 z$y+nKItLA}+RaQVlX^7|--ylmnOF0BiVm=2c8KlaheJ6iotvIOYcUqUvDCHb8L#6K zlWi`#W}{6>erDBd2XrjIAu^mt#yIQH-dMg5@n^x}OgOZEOmV!n-U^Gg>Gg&USLU<` zfzK-f_~(1@WKwwfmjL16_UFk=Sp7ej4+5wCy&FzDlX^&AgT|f+U>t8AW`sm|hq)z4 z!~TguZY4|ucvAa1xH8wd7 zB5f5+T7Y+?c|LWc*aovXEgXuH%gYxp)KP;`sxMk(*oZ?uR0sA(R$jWiyt24ZpTBT^ zacQN#^5P4NOGe_#@^bz1OBakJFyec_?YNQLXV*xAl?7mr_3DGqaU%_L>x>3|-8Z?B z79v6Xrlw;+`W5&PHmDJIDJ5-cB*9J@uLIjgvOg8LdlZm{K-q4vO4-E#GTH?&vMO9U z_#srvYgt4lRl(;g<$g96mjRvj-{CKmL|Or~O6sbnXkRIs_9gVcg9@K9QQ>VE03?0GokA#_8eLr@mzhM5tfTNhI<|02B5~>LPG#o%u!2ZG$QKDWao>i~22^>n8fFzdf^)p*d+-DLOma#&6 z7e78CrS0dum}&~25-uVnyMHlM9qf$B=hyKjd@xEd3WLEGm*-2ehiU zJCedK!N$0{j`$Bt5P__zMA>&{bUrT1rpo X&*_?;)isPX{dZ6vRz_g9_OJf}l&P2+ literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100644 index 0000000..fe15471 --- /dev/null +++ b/main.py @@ -0,0 +1,207 @@ +import sys +from PySide6.QtCore import Qt +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QComboBox, \ + QSpinBox, QTextEdit, QPushButton, QVBoxLayout, QWidget, QScrollArea, \ + QPlainTextEdit, QHBoxLayout, QLineEdit, QFileDialog, QMessageBox +from qt_material import apply_stylesheet + +from tts import * + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + ''' + 设置状态栏 + ''' + # 创建状态栏 + self.statusBar = self.statusBar() + # 设置版本号和作者信息 + version = "2023.V1.0" # 替换为实际的版本号 + author = "作者:鲸鱼工作室" # 替换为实际的作者名字 + self.statusBar.showMessage(f"版本号:{version}") + # 创建作者信息标签 + author_label = QLabel(author) + author_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) # 设置标签靠右对齐 + # 将作者信息标签添加为状态栏的永久部件 + self.statusBar.addPermanentWidget(author_label) + + # 初始值 + self.save_path = '' + + self.setWindowTitle("语音合成助手") + self.setGeometry(100, 100, 400, 600) # 调整窗口大小为800x600 + + # 创建文本输入框 + self.text_edit = QTextEdit(self) + self.text_edit.setGeometry(40, 40, 720, 200) # 调整文本输入框的尺寸和位置 + self.text_edit.setPlaceholderText("请输入或粘贴需要合成的文本") + + # 创建名称下拉框 + self.name_label = QLabel("支持声音列表:", self) + self.name_label.setGeometry(40, 280, 80, 30) # 调整名称标签的位置 + self.name_combo = QComboBox(self) + self.name_combo.setGeometry(140, 280, 600, 30) # 调整名称下拉框的尺寸和位置 + self.name_combo.addItem("01-女:zh-HK-HiuGaaiNeural") + self.name_combo.addItem("02-女:zh-HK-HiuMaanNeural") + self.name_combo.addItem("03-男:zh-HK-WanLungNeural") + self.name_combo.addItem("04-女:zh-CN-XiaoxiaoNeural") + self.name_combo.addItem("05-女:zh-CN-XiaoyiNeural") + self.name_combo.addItem("06-男:zh-CN-YunjianNeural") + self.name_combo.addItem("07-男:zh-CN-YunxiNeural") + self.name_combo.addItem("08-男:zh-CN-YunxiaNeural") + self.name_combo.addItem("09-男:zh-CN-YunyangNeural") + self.name_combo.addItem("10-女:zh-CN-liaoning-XiaobeiNeural") + self.name_combo.addItem("11-女:zh-TW-HsiaoChenNeural") + self.name_combo.addItem("12-男:zh-TW-YunJheNeural") + self.name_combo.addItem("13-女:zh-TW-HsiaoYuNeural") + self.name_combo.addItem("14-女:h-CN-shaanxi-XiaoniNeural") + # 添加更多的名称选项... + + # 创建语速微调器 + self.rate_label = QLabel("速度:", self) + self.rate_label.setGeometry(40, 340, 80, 30) # 调整语速标签的位置 + self.rate_spinbox = QSpinBox(self) + self.rate_spinbox.setGeometry(140, 340, 600, 30) # 调整语速微调器的尺寸和位置 + self.rate_spinbox.setRange(0, 100) + self.rate_spinbox.setValue(0) + + # 创建音调微调器 + self.pitch_label = QLabel("语调:", self) + self.pitch_label.setGeometry(40, 400, 80, 30) # 调整音调标签的位置 + self.pitch_spinbox = QSpinBox(self) + self.pitch_spinbox.setGeometry(140, 400, 600, 30) # 调整音调微调器的尺寸和位置 + self.pitch_spinbox.setRange(0, 100) + self.pitch_spinbox.setValue(0) + + # 创建选择路径 + path_layout = QHBoxLayout() + self.save_path_label = QLabel("音频保存路径:", self) + self.select_path_button = QPushButton("选择路径", self) + self.select_path_button.clicked.connect(self.select_output_path) + # 将选择路径按钮和文件名输入框放在同一行 + path_layout.addWidget(self.save_path_label) + path_layout.addWidget(self.select_path_button) + + # 创建文件名输入框 + self.filename_label = QLabel("音频文件名(无需加后缀名):", self) + self.filename_edit = QLineEdit(self) + self.filename_edit.setPlaceholderText("请输入音频文件名,如test") + # 将选择路径按钮和文件名输入框放在同一行 + filename_layout = QHBoxLayout() + filename_layout.addWidget(self.filename_label) + filename_layout.addWidget(self.filename_edit) + + # 创建状态标签 + self.status_label = QLabel("", self) + self.status_label.setGeometry(40, 520, 700, 20) # 调整状态标签的尺寸和位置 + + # 创建控制台 + self.console = QPlainTextEdit(self) + self.console.setGeometry(40, 500, 700, 80) # 调整控制台的尺寸和位置 + self.console.setReadOnly(True) # 设置控制台为只读模式 + + # 创建生成按钮 + self.generate_button = QPushButton("开始生成语音", self) + self.generate_button.clicked.connect(self.generate_speech) + + # 创建垂直布局 + layout = QVBoxLayout() + layout.addWidget(self.text_edit) + layout.addWidget(self.name_label) + layout.addWidget(self.name_combo) + layout.addWidget(self.rate_label) + layout.addWidget(self.rate_spinbox) + layout.addWidget(self.pitch_label) + layout.addWidget(self.pitch_spinbox) + layout.addLayout(path_layout) # 将标签和按钮放在同一行 + layout.addLayout(filename_layout) # 将选择路径按钮和文件名输入框放在同一行 + layout.addWidget(self.filename_edit) + layout.addWidget(self.generate_button) + layout.addWidget(self.console) + layout.addWidget(self.status_label) + + # 创建主部件和滚动区域 + main_widget = QWidget() + scroll_area = QScrollArea() + scroll_area.setWidget(main_widget) + scroll_area.setWidgetResizable(True) + main_widget.setLayout(layout) + + # 设置主窗口的中心部件为滚动区域 + self.setCentralWidget(scroll_area) + + def generate_speech(self): + text = self.text_edit.toPlainText().strip() + name = self.name_combo.currentText().split(':')[-1] + rate = self.rate_spinbox.value() + pitch = self.pitch_spinbox.value() + filename = self.filename_edit.text() + + if text == '' or filename == '' or self.save_path == '': + # 弹出警告 + QMessageBox.warning(self, '警告', '请检查【文本】或【选择路径】或【文件名】是否为空') + else: + # 将文件名和输出路径组合以生成完整的文件路径 + # 输出生成语音的相关信息到控制台 + self.console.appendPlainText(f"文本: {text}") + self.console.appendPlainText(f"声音:'{name}':") + self.console.appendPlainText(f"速度: {rate}%") + self.console.appendPlainText(f"语调: {pitch}%") + + # 调用生成语音的代码 + self.console.appendPlainText(f"【{now_time()}】开始生成语音.... 请耐心等待....") + rate = f'{rate}%' + pitch = f'{pitch}%' + SSML_text = get_SSML(name=name, rate=rate, pitch=pitch, text=text) + output_path = self.save_path + '/' + filename + asyncio.get_event_loop().run_until_complete(mainSeq(SSML_text, output_path)) + self.console.appendPlainText(f"【{now_time()}】语音生成成功!音频文件保存至:{output_path}.mp3") + self.console.appendPlainText("\n") + + def select_output_path(self): + print('选择路径') + self.save_path = QFileDialog.getExistingDirectory(self, '选择保存路径', '.') + # 弹窗表示选择路径成功 + QMessageBox.information(self, '提示', '已设置保存路径') + self.console.appendPlainText('音频保存路径为:{}'.format(self.save_path)) + + +if __name__ == '__main__': + # 创建应用程序实例 + app = QApplication(sys.argv) + + theme_list = ['dark_amber.xml', + 'dark_blue.xml', + 'dark_cyan.xml', + 'dark_lightgreen.xml', + 'dark_pink.xml', + 'dark_purple.xml', + 'dark_red.xml', + 'dark_teal.xml', + 'dark_yellow.xml', + 'light_amber.xml', + 'light_blue.xml', + 'light_cyan.xml', + 'light_cyan_500.xml', + 'light_lightgreen.xml', + 'light_pink.xml', + 'light_purple.xml', + 'light_red.xml', + 'light_teal.xml', + 'light_yellow.xml'] + apply_stylesheet(app, theme='dark_cyan.xml') + + # 创建主窗口 + window = MainWindow() + window.show() + + # 创建图标对象 + icon = QIcon("voice.png") # 将 "path_to_icon_file.ico" 替换为实际的图标文件路径 + # 设置应用程序的图标 + app.setWindowIcon(icon) + + # 运行应用程序 + sys.exit(app.exec()) diff --git a/output_2023-12-09 10-37-43.mp3 b/output_2023-12-09 10-37-43.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..57a79f2e732b9c25d601f31fbf161fcc52310982 GIT binary patch literal 13248 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 BY*_#R literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..573b879 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests==2.15.1 +websockets==10.2 diff --git a/tts.py b/tts.py new file mode 100644 index 0000000..f73c224 --- /dev/null +++ b/tts.py @@ -0,0 +1,146 @@ +''' +参考代码 +https://github.com/OS984/DiscordBotBackend/blob/3b06b8be39e4dbc07722b0afefeee4c18c136102/NeuralTTS.py +https://github.com/rany2/edge-tts/blob/master/src/edge_tts/communicate.py +''' + +import websockets +import asyncio +from datetime import datetime +import time +import re +import uuid +import argparse + +'''命令行参数解析''' + + +def parseArgs(): + parser = argparse.ArgumentParser(description='text2speech') + parser.add_argument('--output', dest='output', help='保存mp3文件的路径', type=str, required=False) + args = parser.parse_args() + return args + + +# Fix the time to match Americanisms +def hr_cr(hr): + corrected = (hr - 1) % 24 + return str(corrected) + + +# Add zeros in the right places i.e 22:1:5 -> 22:01:05 +def fr(input_string): + corr = '' + i = 2 - len(input_string) + while (i > 0): + corr += '0' + i -= 1 + return corr + input_string + + +# Generate X-Timestamp all correctly formatted +def getXTime(): + now = datetime.now() + return fr(str(now.year)) + '-' + fr(str(now.month)) + '-' + fr(str(now.day)) + 'T' + fr(hr_cr(int(now.hour))) + ':' + fr( + str(now.minute)) + ':' + fr(str(now.second)) + '.' + str(now.microsecond)[:3] + 'Z' + + +# Async function for actually communicating with the websocket +async def transferMsTTSData(SSML_text, outputPath): + req_id = uuid.uuid4().hex.upper() + print(req_id) + # TOKEN来源 https://github.com/rany2/edge-tts/blob/master/src/edge_tts/constants.py + # 查看支持声音列表 https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=6A5AA1D4EAFF4E9FB37E23D68491D6F4 + TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4" + WSS_URL = ( + "wss://speech.platform.bing.com/consumer/speech/synthesize/" + + "readaloud/edge/v1?TrustedClientToken=" + + TRUSTED_CLIENT_TOKEN + ) + endpoint2 = f"{WSS_URL}&ConnectionId={req_id}" + async with websockets.connect(endpoint2, extra_headers={ + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "Origin": "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + " (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 Edg/91.0.864.41"}) as websocket: + message_1 = ( + f"X-Timestamp:{getXTime()}\r\n" + "Content-Type:application/json; charset=utf-8\r\n" + "Path:speech.config\r\n\r\n" + '{"context":{"synthesis":{"audio":{"metadataoptions":{' + '"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},' + '"outputFormat":"audio-24khz-48kbitrate-mono-mp3"' + "}}}}\r\n" + ) + await websocket.send(message_1) + + message_2 = ( + f"X-RequestId:{req_id}\r\n" + "Content-Type:application/ssml+xml\r\n" + f"X-Timestamp:{getXTime()}Z\r\n" # This is not a mistake, Microsoft Edge bug. + "Path:ssml\r\n\r\n" + f"{SSML_text}") + await websocket.send(message_2) + + # Checks for close connection message + end_resp_pat = re.compile('Path:turn.end') + audio_stream = b'' + while (True): + response = await websocket.recv() + print('receiving...') + # print(response) + # Make sure the message isn't telling us to stop + if (re.search(end_resp_pat, str(response)) == None): + # Check if our response is text data or the audio bytes + if type(response) == type(bytes()): + # Extract binary data + try: + needle = b'Path:audio\r\n' + start_ind = response.find(needle) + len(needle) + audio_stream += response[start_ind:] + except: + pass + else: + break + with open(f'{outputPath}.mp3', 'wb') as audio_out: + audio_out.write(audio_stream) + + +async def mainSeq(SSML_text, outputPath): + await transferMsTTSData(SSML_text, outputPath) + + +def get_SSML(name, rate, pitch, text): + SSML = f""" + + + + {text} + + + + """ + + return SSML + + +def now_time(): + return datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + +if __name__ == "__main__": + args = parseArgs() + name = 'zh-CN-XiaoxiaoNeural' + rate = '40%' + pitch = '60%' + text = '我是tts语音合成助手' + SSML_text = get_SSML(name=name, rate=rate, pitch=pitch, text=text) + + output_path = 'output_' + now_time().replace(':', '-') + asyncio.get_event_loop().run_until_complete(mainSeq(SSML_text, output_path)) + print('completed') + # python tts.py --input SSML.xml + # python tts.py --input SSML.xml --output 保存文件名 diff --git a/voice.png b/voice.png new file mode 100644 index 0000000000000000000000000000000000000000..02c8c51b7dc66b955822955e5341370dd336fda2 GIT binary patch literal 7833 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