最近一直在研究TTS文本转语音的实现,虽然当下的这个技术已经很成熟了,但尝试遍了各大语音平台,基本都是要收费。也参考过很多帖子,其中提到的免费也只是临时的试用一段时间,并不能完全免费的使用。
这里通过微软提供的edge_tts来自由的实现想要的tts效果。
由于我是用在Android客户端中进行语音播放,Android中虽然自带了TTS的相关包TextToSpeech,但是音色选择实在是一言难尽,网上能搜索到的相关内容都是围绕setPitch这个函数来达到改变音色的目的。根据实际测试后,发现这只是改变了声音的尖锐程度,并不算完全的改变音色。
微软提供了一个edge_tts的工具可以直接在python中实现文本至语音的转换,但刚开始使用发现它还是有很大的限制的。这个工具可以在命令行中使用,也可以在python中调用。
相关包:
edge-tts==6.1.10
接下来看它常规的调用方式:
async def my_function():
tts = edge_tts.Communicate(text, voice)
await tts.save(output)
if __name__ == '__main__':
asyncio.run(my_function())
通过上面的代码片段可以看出,tts转换成语音后,获取结果数据相关的函数只有save这一个,必须写入文件。我也是卡在这一步琢磨了好久,最后终于发现,它提供的stream函数所返回的音频流,我们可以在python中定义一个“虚拟文件”以避免实际的磁盘读写。相当于是“骗过了”微软edge_tts库的API。最终实现的代码如下:
async def tts_task(text, voice="zh-CN-YunxiNeural"):
base64_data = ""
memory_file = io.BytesIO()
try:
communicate = edge_tts.Communicate(text, voice)
async for chunk in communicate.stream():
if chunk["type"] == "audio":
memory_file.write(chunk["data"])
memory_file.seek(0)
read_data = memory_file.read()
base64_data = base64.b64encode(read_data).decode("utf-8")
finally:
memory_file.close()
return base64_data
在上面的代码块中,将最后的音频流结果转换成了base64数据。
在Django WebSocket调用效果如下:
@database_sync_to_async
def chat_flow(self, json_data):
print("接收数据:", json.dumps(json_data, ensure_ascii=False))
user_msg = json_data["content"]
audio_data = ""
content = ""
try:
# 此处省略对content进行处理,生成实际要转成语音的文本内容
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
audio_data = loop.run_until_complete(pyTTSUtils.tts_task(content))
loop.close()
except requests.exceptions.RequestException as e:
logger.debug(e)
except openai.APIConnectionError as e:
logger.debug(e)
return audio_data, content
async def websocket_receive(self, message):
text = message["text"]
json_data = json.loads(text)
if self.assistant is None:
audio_data, content = await self.chat_flow(json_data)
await self.send(json.dumps({"audio_data": audio_data, "content": content}))
上面的代码块中我是在AsyncWebsocketConsumer中实现,通过websocket的方式将最终通过文本生成的语音数据转成base64数据返回给了客户端。
下面附带上Android客户端接收数据的处理:
okHttpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
Log.d(TAG, "onOpen: websocket已连接");
empWebSocket = webSocket;
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
Log.d(TAG, "onTextMessage: ");
Log.d(TAG, text);
String audioData = parseJson(text, "audio_data");
String content = parseJson(text, "content");
if (!"".equals(audioData)) {
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource("data:audio/mpeg;base64," + audioData);
mediaPlayer.prepareAsync();
mediaPlayer.setVolume(100f, 100f);
mediaPlayer.setLooping(false);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
sendActivityMessage("message",content);
visualizer = new Visualizer(mediaPlayer.getAudioSessionId());
visualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[0]);
visualizer.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
@Override
public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
int numBars = 4;
int barWidth = 32;
int[] amplitudes = new int[numBars];
for (int i = 0; i < numBars; i++){
int sum = 0;
int startIndex = i * barWidth;
int endIndex = (i + 1) * barWidth;
for (int j = startIndex; j < endIndex; j++) {
sum += Math.abs(waveform[j]);
}
amplitudes[i] = sum / barWidth;
}
// Log.d(TAG, "播放音频波形: " + Arrays.toString(amplitudes));
sendActivityMessage("wave",Arrays.toString(amplitudes));
}
@Override
public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
}
},Visualizer.getMaxCaptureRate() / 5,true,false);
visualizer.setEnabled(true);
mediaPlayer.start();
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
visualizer.setEnabled(false);
mediaPlayer.stop();
mediaPlayer.release();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
Log.d(TAG, "onBytesMessage: ");
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
Log.d(TAG, "onClosing: 正在关闭连接...");
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
Log.d(TAG, "onClosed: websocket连接已关闭");
sendActivityMessage("status","close");
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
Log.d(TAG, "onFailure: " + t.getMessage());
sendActivityMessage("status","close");
}
});
这里Android客户端接收到base64音频数据后,使用MediaPlayer进行播放,并且还做了音频波形的转换处理,用于在界面上显示,这里也顺带记录下。