如何在uni-app使用微軟的文字轉(zhuǎn)語音服務(wù)
前言
嘗試過各種TTS的方案,一番體驗(yàn)下來,發(fā)現(xiàn)微軟才是這個(gè)領(lǐng)域的王者,其Azure文本轉(zhuǎn)語音服務(wù)的轉(zhuǎn)換出的語音效果最為自然,但Azure是付費(fèi)服務(wù),注冊操作付費(fèi)都太麻煩了。但在其官網(wǎng)上竟然提供了一個(gè)完全體的演示功能,能夠完完整整的體驗(yàn)所有角色語音,說話風(fēng)格...
但就是不能下載成mp3文件,所以有一些小伙伴逼不得已只好通過轉(zhuǎn)錄電腦的聲音來獲得音頻文件,但這樣太麻煩了。其實(shí),能在網(wǎng)頁里看到聽到的所有資源,都是解密后的結(jié)果。也就是說,只要這個(gè)聲音從網(wǎng)頁里播放出來了,我們必然可以找到方法提取到音頻文件。
本文就是記錄了這整個(gè)探索實(shí)現(xiàn)的過程,請盡情享用~
本文大部分內(nèi)容寫于今年年初一直按在手里未發(fā)布,我深知這個(gè)方法一旦公之于眾,可能很快會(huì)迎來微軟的封堵,甚至直接取消網(wǎng)頁體驗(yàn)的入口和相關(guān)接口。
解析Azure官網(wǎng)的演示功能
使用Chrome瀏覽器打開調(diào)試面板,當(dāng)我們在Azure官網(wǎng)中點(diǎn)擊播放功能時(shí),可以從network標(biāo)簽中監(jiān)控到一個(gè)wss://的請求,這是一個(gè)websocket的請求。
兩個(gè)參數(shù)
在請求的URL中,我們可以看到有兩個(gè)參數(shù)分別是Authorization和X-ConnectionId
有意思的是,第一個(gè)參數(shù)就在網(wǎng)頁的源碼里,使用axios對這個(gè)Azure文本轉(zhuǎn)語音的網(wǎng)址發(fā)起get請求就可以直接提取到
const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/"); const reg = /token: \"(.*?)\"/; if(reg.test(res.data)){ const token = RegExp.$1; }
通過查看發(fā)起請求的JS調(diào)用棧,加入斷點(diǎn)后再次點(diǎn)擊播放
可以發(fā)現(xiàn)第二個(gè)參數(shù)X-ConnectionId
來自一個(gè)createNoDashGuid
的函數(shù)
this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),
這就是一個(gè)uuid v4
格式的字符串,nodash
就是沒有-
的意思。
三次發(fā)送
請求時(shí)URL里的兩個(gè)參數(shù)已經(jīng)搞定了,我們繼續(xù)分析這個(gè)webscoket
請求,從Message標(biāo)簽中可以看到
每次點(diǎn)擊播放時(shí),都向服務(wù)器上報(bào)了三次數(shù)據(jù),明顯可以看出來三次上報(bào)數(shù)據(jù)各自的作用
第一次的數(shù)據(jù):SDK版本,系統(tǒng)信息,UserAgent
Path: speech.config X-RequestId: 818A1E398D8D4303956D180A3761864B X-Timestamp: 2022-05-27T16:45:02.799Z Content-Type: application/json {"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/MacIntel","name":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","version":"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36"}}}
第二次的數(shù)據(jù):轉(zhuǎn)語音輸出配置,從outputFormat
可以看出來,最終的音頻格式為audio-24khz-160kbitrate-mono-mp3
,這不就是我們想要的mp3
文件嗎?!
Path: synthesis.context X-RequestId: 091963E8C7F342D0A8E79125EA6BB707 X-Timestamp: 2022-05-27T16:48:43.340Z Content-Type: application/json {"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-160kbitrate-mono-mp3"},"language":{"autoDetection":false}}}
第三次的數(shù)據(jù):要轉(zhuǎn)語音的文本信息和角色voice name
,語速rate
,語調(diào)pitch
,情感等配置
Path: ssml X-RequestId: 091963E8C7F342D0A8E79125EA6BB707 X-Timestamp: 2022-05-27T16:48:49.594Z Content-Type: application/ssml+xml <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US"><voice name="zh-CN-XiaoxiaoNeural"><prosody rate="0%" pitch="0%">我叫大帥,一個(gè)熱愛編程的老程序猿</prosody></voice></speak>
接收的二進(jìn)制消息
既然從前三次上報(bào)的信息已經(jīng)看出來返回的格式就是mp3文件了,那么我們是不是把所有返回的二進(jìn)制數(shù)據(jù)合并就可以拼接成完整的mp3文件了呢?答案是肯定的!
每次點(diǎn)擊播放后接收的所有來自websocket
的消息的最后一條,都有明確的結(jié)束標(biāo)識(shí)符
turn.end
代表轉(zhuǎn)換結(jié)束!
用Node.js實(shí)現(xiàn)它
既然都解析出來了,剩下的就是在Node.js
中重新實(shí)現(xiàn)這個(gè)過程。
兩個(gè)參數(shù)
- Authorization,直接通過axios的get請求抓取網(wǎng)頁內(nèi)容后通過正則表達(dá)式提取
const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/"); const reg = /token: \"(.*?)\"/; if(reg.test(res.data)){ const Authorization = RegExp.$1; }
- X-ConnectionId,直接使用
uuid
庫即可
//npm install uuid const { v4: uuidv4 } = require('uuid'); const XConnectionId = uuidv4().toUpperCase();
創(chuàng)建WebSocket連接
//npm install nodejs-websocket const ws = require("nodejs-websocket"); const url = `wss://eastus.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=${Authorization}&X-ConnectionId=${XConnectionId}`; const connect = ws.connect(url);
三次發(fā)送
第一次發(fā)送
function getXTime(){ return new Date().toISOString(); } const message_1 = `Path: speech.config\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"Browser/Linux x86_64","name":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0","version":"5.0 (X11)"}}}}`; connect.send(message_1);
第二次發(fā)送
const message_2 = `Path: synthesis.context\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-16khz-32kbitrate-mono-mp3"}}}`; connect.send(message_2);
第三次發(fā)送
const SSML = ` <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US"> <voice name="zh-CN-XiaoxiaoNeural"> <mstts:express-as style="general"> <prosody rate="0%" pitch="0%"> 我叫大帥,一個(gè)熱愛編程的老程序猿 </prosody> </mstts:express-as> </voice> </speak> ` const message_3 = `Path: ssml\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/ssml+xml\r\n\r\n${SSML}` connect.send(message_3);
接收二進(jìn)制消息拼接mp3
當(dāng)三次發(fā)送結(jié)束后我們通過connect.on('binary')
監(jiān)聽websocket
接收的二進(jìn)制消息。
創(chuàng)建一個(gè)空的Buffer對象final_data
,然后將每一次接收到的二進(jìn)制內(nèi)容拼接到final_data
里,一旦監(jiān)聽到普通文本消息中包含Path:turn.end
標(biāo)識(shí)時(shí)則將final_data
寫入創(chuàng)建一個(gè)mp3
文件中。
let final_data=Buffer.alloc(0); connect.on("text", (data) => { if(data.indexOf("Path:turn.end")>=0){ fs.writeFileSync("test.mp3",final_data); connect.close(); } }) connect.on("binary", function (response) { let data = Buffer.alloc(0); response.on("readable", function () { const newData = response.read() if (newData)data = Buffer.concat([data, newData], data.length+newData.length); }) response.on("end", function () { const index = data.toString().indexOf("Path:audio")+12; final_data = Buffer.concat([final_data,data.slice(index)]); }) });
這樣我們就成功的保存出了mp3
音頻文件,連Azure官網(wǎng)都不用打開!
命令行工具
我已經(jīng)將整個(gè)代碼打包成一個(gè)命令行工具,使用非常簡單
npm install -g mstts-js mstts -i 文本轉(zhuǎn)語音 -o ./test.mp3
已全部開源: github.com/ezshine/mst…
在uni-app中使用
新建一個(gè)云函數(shù)
新建一個(gè)云函數(shù),命名為mstts
由于mstss-js
已經(jīng)封裝好了,只需要在云函數(shù)中npm install mstts-js
然后require
即可,代碼如下
'use strict'; const mstts = require('mstts-js') exports.main = async (event, context) => { const res = await mstts.getTTSData('要轉(zhuǎn)換的文本','CN-Yunxi'); //res為buffer格式 });
下載播放mp3文件
要在uniapp中播放這個(gè)mp3格式的文件,有兩種方法
方法1. 先上傳到云存儲(chǔ),通過云存儲(chǔ)地址訪問
exports.main = async (event, context) => { const res = await mstts.getTTSData('要轉(zhuǎn)換的文本','CN-Yunxi'); //res為buffer格式 var uploadRes = await uniCloud.uploadFile({ cloudPath: "xxxxx.mp3", fileContent: res }) return uploadRes.fileID; });
前端用法:
uniCloud.callFunction({ name:"mstts", success:(res)=>{ const aud = uni.createInnerAudioContext(); aud.autoplay = true; aud.src = res; aud.play(); } })
- 優(yōu)點(diǎn):云函數(shù)安全
- 缺點(diǎn):文件上傳到云存儲(chǔ)不做清理機(jī)制的話會(huì)浪費(fèi)空間
方法2. 利用云函數(shù)的URL化+集成響應(yīng)來訪問
這種方法就是直接將云函數(shù)的響應(yīng)體變成一個(gè)mp3文件,直接通過audio.src
賦值即可訪問`
exports.main = async (event, context) => { const res = await mstts.getTTSData('要轉(zhuǎn)換的文本','CN-Yunxi'); return { mpserverlessComposedResponse: true, isBase64Encoded: true, statusCode: 200, headers: { 'Content-Type': 'audio/mp3', 'Content-Disposition':'attachment;filename=\"temp.mp3\"' }, body: res.toString('base64') } };
前端用法:
const aud = uni.createInnerAudioContext(); aud.autoplay = true; aud.src = 'https://ezshine-274162.service.tcloudbase.com/mstts'; aud.play();
- 優(yōu)點(diǎn):用起來很簡單,無需保存文件到云存儲(chǔ)
- 缺點(diǎn):URL化后的云函數(shù)如果沒有安全機(jī)制,被抓包后可被其他人肆意使用
小結(jié)
這么好用的tts庫,如果對你有所幫助別忘了在github里點(diǎn)個(gè)star支持一下。
總結(jié)
到此這篇關(guān)于如何在uni-app使用微軟的文字轉(zhuǎn)語音服務(wù)的文章就介紹到這了,更多相關(guān)uni-app文字轉(zhuǎn)語音服務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IE6下javasc#ipt:void(0) 無效的解決方法
本篇文章主要是對IE6下javasc#ipt:void(0) 無效的解決方法進(jìn)行了介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-12-12js表格排序?qū)嵗治觯ㄖС謎nt,float,date,string四種數(shù)據(jù)類型)
這篇文章主要介紹了js表格排序?qū)嵗治觯ㄖС謎nt,float,date,string四種數(shù)據(jù)類型),涉及javascript常用的升序、降序及數(shù)據(jù)類型轉(zhuǎn)換等相關(guān)技巧,需要的朋友可以參考下2015-05-05Bootstrap中文本框的寬度變窄并且加入一副驗(yàn)證碼圖片的實(shí)現(xiàn)方法
這篇文章主要介紹了Bootstrap中文本框的寬度變窄并且加入一副驗(yàn)證碼圖片的實(shí)現(xiàn)方法的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06JS實(shí)現(xiàn)超簡潔網(wǎng)頁title標(biāo)題跑動(dòng)閃爍提示效果代碼
這篇文章主要介紹了JS實(shí)現(xiàn)超簡潔網(wǎng)頁title標(biāo)題跑動(dòng)閃爍提示效果代碼,涉及JavaScript結(jié)合定時(shí)函數(shù)動(dòng)態(tài)操作頁面元素屬性的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10