最近在調研使用各個雲平台提供的AI服務,有個語音合成的需求因此就使用了一下科大訊飛的TTS服務,也用.NET Core寫了一個小示例,下面就是這個小示例及其相關背景知識的介紹。
一、什么是語音合成(TTS)
1.1 What is 語音合成?
將文字信息轉化為聲音信息,給應用配上“嘴巴”,這就是語音合成。
Note:語音合成和語音識別技術是實現人機語音通信,建立一個有聽和講能力的口語系統所必需的兩項關鍵技術。使電腦具有類似於人一樣的說話能力,是當今時代信息產業的重要競爭市場。和語音識別相比,語音合成的技術相對說來要成熟一些,並已開始向產業化方向成功邁進,大規模應用指日可待。
1.2 語音合成的應用場景
目前,語音合成技術在我們生活中具有廣泛的應用,如機器人發聲、有聲讀物制作、語音播報等等,這些應用場景都離不開語音合成。
Note:在語音導航應用、新聞類 APP 中,語音合成可以快速生成高質量的播報音頻,實現在開車、走路等不方便閱讀消息的情況下,音頻消息的即時傳達。
1.3 語音合成的基本原理
這里借用網易智能的一篇文章中的介紹如下:
簡單來說語音合成分為文本分析、韻律分析和聲學分析三個部分。通過文本分析提取出文本特征,在此基礎上預測基頻、時長、節奏等多種韻律特征,然后通過聲學模型實現從前端參數到語音參數的映射,最后通過聲碼器合成語音。整個過程類似於“編碼、信息匹配,解碼的過程”。
對語音合成有興趣的朋友,可以閱讀以下這篇文章《吳恩達盛贊的Deep Voice詳解教程,教你快速理解百度的語音合成原理》。
二、使用.NET Core調用訊飛API
2.1 科大訊飛TTS服務
訊飛提供了眾多極具特色的發音人(音庫)供您選擇。其合成音在音色、自然度等方面的表現均接近甚至超過了人聲。這種語音合成體驗,達到了真正可商用的標准。在我對百度、阿里、騰訊及訊飛的TTS服務對比中,訊飛的TTS體驗好一些,且發音人(音庫)最為豐富,還有四川話(准確來說是成都話)音庫,碉堡了。大家可以去訊飛TTS服務網頁體驗一下。
2.2 .NET Core調用示例
(1)首先得去訊飛開放平台注冊一個賬號,並申請一個勾選有“在線語音合成”的應用
這里主要是拿到AppID及ApiKey,並且可以通過發音人管理增加發音庫,當然,高級版的音庫都是要單獨收費的。免費版本的每天有500次的免費API調用機會。
(2)參考官方API文檔和C# DEMO
PS:由於訊飛官方提供的C# DEMO是一個基於.NET Framework的比較老的DEMO,在.NET Core下無法正常使用,我將其簡單地改寫成了.NET Core版本,並封裝了一個XunFeiCloudTtsService類如下:(每個屬性都有注釋,不再贅述,只是對API調用需要的一些屬性的簡單封裝)
public class XunFeiCloudTtsService { // Header Type : audio/mpeg private const string AUDIO_MPEG_TYPE = "audio/mpeg"; // AppID, AppKey 從訊飛開放雲平台獲取 public string AppID { get; set; } = "your AppID"; public string ApiKey { get; set; } = "your ApiKey"; // 要進行合成的文字 public string TextToSpeech { get; set; } = "這是一段測試文字"; // 訊飛TTS服務API地址 public string ServiceUrl { get; set; } = "http://api.xfyun.cn/v1/service/v1/tts"; // aue = raw, 音頻文件保存類型為 wav // aue = lame, 音頻文件保存類型為 mp3 public string AUE { get; set; } = "raw"; // 音頻采樣率,可選值:audio/L16;rate=8000,audio/L16;rate=16000 public string AUF { get; set; } = "audio/L16;rate=16000"; // 發音人,可選值:詳見控制台-我的應用-在線語音合成服務管理-發音人授權管理 public string VoiceName { get; set; } = "xiaoyan"; // 引擎類型,可選值:aisound(普通效果),intp65(中文),intp65_en(英文), // mtts(小語種,需配合小語種發音人使用),x(優化效果), // 默認為intp65 public string EngineType { get; set; } = "intp65"; // 語速,可選值:[0-100],默認為50 public string Speed { get; set; } = "50"; // 音量,可選值:[0-100],默認為50 public string Volume { get; set; } = "50"; // 音高,可選值:[0-100],默認為50 public string Pitch { get; set; } = "50"; // URL加密后的TextToSpeech public string Bodys { get; set; } // 要保存的文件夾路徑 public string SavePath { get; set; } = "./Output/"; // 要保存的文件名 public string FileName { get; set; } = $"TTS-{ Guid.NewGuid().ToString() }"; static XunFeiCloudTtsService() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } public XunFeiCloudTtsService() { } public XunFeiCloudTtsService(string appID, string apiKey, string serviceUrl) { AppID = appID; ApiKey = apiKey; ServiceUrl = serviceUrl; } public string GetTtsResult() { SetTextDataBodys(); string param = "{\"aue\":\"" + AUE + "\",\"auf\":\"" + AUF + "\",\"voice_name\":\"" + VoiceName + "\",\"engine_type\":\"" + EngineType + "\",\"speed\":\"" + Speed + "\",\"volume\":\"" + Volume + "\",\"pitch\":\"" + Pitch + "\"}"; // 獲取十位的時間戳 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); string curTime = Convert.ToInt64(ts.TotalSeconds).ToString(); // 對參數先utf-8然后用base64編碼 byte[] paramData = Encoding.UTF8.GetBytes(param); string paraBase64 = Convert.ToBase64String(paramData); // 形成簽名 string checkSum = Md5(ApiKey + curTime + paraBase64); // 設置HttpHeaders var ttsRequest = (HttpWebRequest)WebRequest.Create(ServiceUrl); ttsRequest.Method = "POST"; ttsRequest.ContentType = "application/x-www-form-urlencoded"; ttsRequest.Headers.Add("X-Param", paraBase64); ttsRequest.Headers.Add("X-CurTime", curTime); ttsRequest.Headers.Add("X-Appid", AppID); ttsRequest.Headers.Add("X-CheckSum", checkSum); using (Stream requestStream = ttsRequest.GetRequestStream()) { using (StreamWriter streamWriter = new StreamWriter(requestStream, Encoding.GetEncoding("gb2312"))) { streamWriter.Write(Bodys); } } string responseText = string.Empty; HttpWebResponse ttsResponse = ttsRequest.GetResponse() as HttpWebResponse; using (Stream responseStream = ttsResponse.GetResponseStream()) { using (StreamReader streamReader = new StreamReader(responseStream, Encoding.GetEncoding("UTF-8"))) { string headerType = ttsResponse.Headers["Content-Type"]; if (headerType.Equals(AUDIO_MPEG_TYPE)) { responseText = GetSuccessResponseText(ttsResponse); } else { responseText = streamReader.ReadToEnd(); } } } return responseText; } #region 私有輔助方法 /// <summary> /// 獲取請求成功的響應文本 /// </summary> /// <param name="ttsResponse">HttpWebResponse</param> /// <returns>響應Headers文本</returns> private string GetSuccessResponseText(HttpWebResponse ttsResponse) { string responseText = string.Empty; using (Stream stream = ttsResponse.GetResponseStream()) { MemoryStream memoryStream = StreamToMemoryStream(stream); if (!Directory.Exists(SavePath)) { Directory.CreateDirectory(SavePath); } string fileType = string.Empty; switch (AUE.ToLower()) { case "raw": fileType = "wav"; break; case "lame": fileType = "lame"; break; } File.WriteAllBytes($"{SavePath}{FileName}.{fileType}", streamTobyte(memoryStream)); responseText = ttsResponse.Headers.ToString(); } return responseText; } /// <summary> /// 對要合成語音的文字先用utf-8然后進行URL加密 /// </summary> private void SetTextDataBodys() { byte[] textData = Encoding.UTF8.GetBytes(TextToSpeech); TextToSpeech = HttpUtility.UrlEncode(textData); Bodys = string.Format("text={0}", TextToSpeech); } /// <summary> /// 生成令牌 :X-CheckSum /// 計算方法:MD5(apiKey + curTime + param),三個值拼接的字符串,進行MD5哈希計算(32位小寫),其中apiKey由訊飛提供,調用方管理。 /// </summary> /// <param name="token">apiKey + curTime + param</param> /// <returns>X-CheckSum</returns> private string Md5(string token) { MD5 md5 = new MD5CryptoServiceProvider(); byte[] bytes = Encoding.UTF8.GetBytes(token); bytes = md5.ComputeHash(bytes); md5.Clear(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.Length; i++) { sb.Append(Convert.ToString(bytes[i], 16).PadLeft(2, '0')); } return sb.ToString().PadLeft(32, '0'); } /// <summary> /// 將流轉換為緩存流 /// </summary> /// <param name="instream">輸入流</param> /// <returns>輸出流</returns> private MemoryStream StreamToMemoryStream(Stream instream) { MemoryStream outstream = new MemoryStream(); const int bufferLen = 4096; byte[] buffer = new byte[bufferLen]; int count = 0; while ((count = instream.Read(buffer, 0, bufferLen)) > 0) { outstream.Write(buffer, 0, count); } return outstream; } /// <summary> /// 把緩存流轉換成字節組 /// </summary> /// <param name="memoryStream">緩存劉</param> /// <returns>字節數組</returns> private byte[] streamTobyte(MemoryStream memoryStream) { byte[] buffer = new byte[memoryStream.Length]; memoryStream.Seek(0, SeekOrigin.Begin); memoryStream.Read(buffer, 0, buffer.Length); return buffer; } #endregion }
*.由於.NET Core下Encoding默認不支持GB2312,因此需要事先引入一個包:System.Text.Encoding.CodePages,然后在程序啟動時注冊一下,這里已經封裝靜態構造函數里面了。
NuGet>Install-Package System.Text.Encoding.CodePages
客戶端調用:
public class Program { public static void Main(string[] args) { XunFeiCloudTtsService ttsService = new XunFeiCloudTtsService(); ttsService.AppID = "5c3806f12121"; ttsService.ApiKey = "a99fff9fc537d883a181231231a37bf56dc8f28"; ttsService.TextToSpeech = "大家好我是第一個語音合成!"; ttsService.SavePath = "./TTS-Result/"; ttsService.FileName = "Hello-TTS"; ttsService.Speed = "60"; var result = ttsService.GetTtsResult(); Console.WriteLine(result); Console.ReadKey(); } }
*.這里AppID和ApiKey請使用你自己的,我博文這里是亂寫的。
調用結果:
(1)響應結果
*.如果提示illegal client ip,請到控制台中加入白名單列表:
(2)語音文件
So,播放一下?
三、小結
有了語音合成,我們可以在我們的業務系統或者App中有了更多的玩法,雖然我們不了解語音合成的具體實現原理。此文只是一個簡單的使用示例,無更多的內容,希望對你有幫助,本文的示例代碼可以點擊這里。
參考資料
(1)科大訊飛開放雲平台,《在線語音合成服務》
(2)科大訊飛TTS服務,API說明文檔
(3)網易智能,《讓機器說話更自然,語音合成還能干什么》