腾讯云语音合成 C# 示例
按着官方文档的顺序开始写吧。
一、请求 url 拼接
首先的坑点就是url必须是按升序
排列进行拼接,不然接下来的鉴权
肯定失败
基础链接:
https://aai.qcloud.com/tts/v1/?
参数:
{ "projectid": "0", "sub_service_type": "0", "speech_format": "mp3", "volume": "3", "person": "0", "speed": "0", "secretid": "AKIDlfdHxN0ntSVt4KPH0xXWnGl21UUFNoO5", "timestamp": "1484109983", "expired": "1484113583", "nonce": "1675199141"}
这里以<appid> = 20170111, <SecretKey>=oaYWFO70LGDmcpfwo8uF1IInayysGtgZ
为例拼接签名原文,则拼接的签名原文为:
https://aai.qcloud.com/tts/v1/20170111?expired=1484113583&nonce=1675199141&person=0&projectid=0&secretid=AKIDlfdHxN0ntSVt4KPH0xXWnGl21UUFNoO5&speech_format=mp3&speed=0&sub_service_type=0×tamp=1484109983&volume=3
记得 url 一定要排序,不然鉴权肯定会失败的
二、加密
接上一步的结果,我们要去掉协议名 https://
加上POST(必须是大写)
POSTaai.qcloud.com/tts/v1/20170111?expired=1484113583&nonce=1675199141&person=0&projectid=0&secretid=AKIDlfdHxN0ntSVt4KPH0xXWnGl21UUFNoO5&speech_format=mp3&speed=0&sub_service_type=0×tamp=1484109983&volume=3
对原文进行加密处理:
Base64Encode(HmacSha1(签名原文, SecretKey))
C#方法示例:
public static string HmacSha1AndBase64(string mk, string secretkey) { HMACSHA1 hmacsha1 = new HMACSHA1(); hmacsha1.Key = Encoding.UTF8.GetBytes(secretkey); byte[] dataBuffer = Encoding.UTF8.GetBytes(mk); byte[] hashBytes = hmacsha1.ComputeHash(dataBuffer); return Convert.ToBase64String(hashBytes); }
最终得到签名串为:
HRCKlbwPhWtVvfGn914qE5O1rwc=
三:Body拼接(需要转换成语音的文本设置)
如果返回 111 错误,那么百分之百是这里拼接出错
multipart/form-data;
的格式就不介绍了,网上一大堆 -------------------123456abcdefgContent-Disposition: form-data; name="text"; filename="file1.txt"这里是你需要转换的内容-------------------123456abcdefg--
这里的 -----------------123456abcdefg 是边界符,以 --自定义字符串
开头,以 --自定义字符串--
结尾。
text
" 和 filename="file1
.txt" 都是任意取名 Demo代码:
#region 语音合成 ////// 语音合成 /// /// /// /// /// /// /// ///public string TextToSpeech(string appid, string secretid, string secretkey, string text, string mp3AbsolutePath = "", string mp3Filename = "") { //边界符 var boundary = "-------------------" + DateTime.Now.Ticks.ToString("x"); var url = GetFullUrl(appid, secretid); //创建请求 var webRequest = (HttpWebRequest)WebRequest.Create(url); //设置请求头 SetHeaders(webRequest, url, secretkey, boundary); //将数据写入Body并获取写入后的数据 var strBody = GetRequestBody(boundary, text); //将Body写入到请求流中 WriteRequestStreamFormBody(webRequest, strBody); //获取结果 string responseResult = GetResponseResult(webRequest); var resJson = JObject.Parse(responseResult); if (resJson.Value ("code").Equals("0")) { //转换为mp3保存 Base64ToAudio(resJson.Value ("speech"), mp3AbsolutePath, mp3Filename); } else { //转换失败,具体查看错误码 } return responseResult; } #endregion #region 使用密钥(secretkey)对url进行加密,得到签名 /// /// 使用密钥(secretkey)对url进行加密,得到签名 /// /// 排序后的url地址 /// 密钥 ///private string GetAuthorization(string urlSorted, string secretkey) { return HmacSha1AndBase64("POST" + urlSorted.Substring(8),secretkey); } #endregion #region HMACSHA1 & Base64 加密 /// /// HMACSHA1 & Base64 加密 /// /// 原文 /// 密钥 ///public static string HmacSha1AndBase64(string mk, string secretkey) { HMACSHA1 hmacsha1 = new HMACSHA1(); hmacsha1.Key = Encoding.UTF8.GetBytes(secretkey); byte[] dataBuffer = Encoding.UTF8.GetBytes(mk); byte[] hashBytes = hmacsha1.ComputeHash(dataBuffer); return Convert.ToBase64String(hashBytes); } #endregion #region 获取排序后的完整路径地址 /// /// 获取排序后的完整路径地址 /// /// /// ///private string GetFullUrl(string appid, string secretid) { /* * 返回值示例: * * https://aai.qcloud.com/tts/v1/20170111 * ?expired=1484113583 * &nonce=1675199141 * &person=0 * &projectid=0 * &secretid=AKIDlfdHxN0ntSVt4KPH0xXWnGl21UUFNoO5 * &speech_format=mp3 * &speed=0 * &sub_service_type=0 * ×tamp=1484109983 * &volume=3 * */ StringBuilder url = new StringBuilder("https://aai.qcloud.com/tts/v1/" + appid + "?"); SortedDictionary dic = new SortedDictionary (); dic.Add("projectid", 0); //腾讯云项目 ID,不填为默认项目,即0,总长度不超过1024字节 dic.Add("sub_service_type", 0); //子服务类型。0:短文本实时合成。目前只支持短文本实时合成 dic.Add("speech_format", "mp3"); //合成语音格式,目前支持MP3格式 dic.Add("volume", 3); //音量,默认为5,取值范围为0-10 dic.Add("person", 0); //发音人,目前仅支持0,女声 dic.Add("speed", 0); //语速,默认值为0,取值范围为-40到40,1表示加速到原来的1.1倍,-1为相对于正常语速放慢1.1倍 dic.Add("secretid", secretid); //官网云API密钥中获得的SecretId dic.Add("timestamp", DateTime.Now.ToUnixDate()); //当前时间戳,是一个符合 UNIX Epoch 时间戳规范的数值,单位为秒 dic.Add("expired", DateTime.Now.AddMinutes(5).ToUnixDate()); //签名的有效期,是一个符合 UNIX Epoch 时间戳规范的数值,单位为秒;expired 必须大于 timestamp 且 expired - timestamp 小于90天 dic.Add("nonce", new Random().Next(10000000, 99999999)); //随机正整数。用户需自行生成,最长10位 foreach (var item in dic) { url.Append(item.Key); url.Append("="); url.Append(item.Value); url.Append("&"); } return url.Remove(url.Length - 1, 1).ToString(); //移除末尾的 & } #endregion #region 设置Header /// /// 设置Header /// /// /// /// /// private void SetHeaders(HttpWebRequest webRequest, string url, string secretkey, string boundary) { //设置属性 webRequest.Host = "aai.qcloud.com"; webRequest.Method = "POST"; webRequest.Timeout = 30000; webRequest.ContentType = "multipart/form-data; boundary=" + boundary; webRequest.Headers.Add("Authorization", GetAuthorization(url, secretkey)); } #endregion #region 获取设置文字内容后的Body ////// 获取设置文字内容后的Body /// /// /// ///private string GetRequestBody(string boundary, string text) { //设置需要转语音的文字内容 string strBody = GetTextBody(boundary, text); //在这里可以添加其他的Body内容 //strBody += GetOtherBody(); //设置结尾 strBody += "--" + boundary + "--\r\n"; return strBody; } #endregion #region 获取设置文字内容后的Body段 /// /// 设置文字内容 /// /// /// ///private string GetTextBody(string boundary, string text) { return string.Format("--" + boundary + "\r\nContent-Disposition: form-data; name=\"text\"; filename=\"file1.txt\"" + //"\r\nContent-Type: text/plain" + "\r\n\r\n{0}\r\n", text); } #endregion #region 将body流写入到请求流中 /// /// 将body流写入到请求流中 /// /// /// private static void WriteRequestStreamFormBody(HttpWebRequest webRequest, string body) { var strBodyBytes = Encoding.UTF8.GetBytes(body); using (var requestStream = webRequest.GetRequestStream()) { requestStream.Write(strBodyBytes, 0, strBodyBytes.Length); } } #endregion #region 获取响应结果 ////// 获取响应内容 /// /// ///private static string GetResponseResult(HttpWebRequest webRequest) { string responseResult; using (var httpWebResponse = (HttpWebResponse)webRequest.GetResponse()) { using (var httpStreamReader = new StreamReader(httpWebResponse.GetResponseStream(), Encoding.GetEncoding("utf-8"))) { responseResult = httpStreamReader.ReadToEnd(); } } webRequest.Abort(); return responseResult; } #endregion #region base64转换mp3 /// /// base64转换mp3 /// /// /// /// public void Base64ToAudio(string strBase64, string absolutePath = "", string filename = "") { try { absolutePath = (absolutePath != null && !"".Equals(absolutePath.Trim())) ? absolutePath : Server.MapPath("~/audio/"); filename = (filename != null && !"".Equals(filename.Trim())) ? filename : DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".mp3"; byte[] audioByteArray = Convert.FromBase64String(strBase64); using (var mp3File = System.IO.File.Create(absolutePath + filename, audioByteArray.Length)) { mp3File.Write(audioByteArray, 0, audioByteArray.Length); } } catch (Exception ex) { throw ex; } } #endregion
调用代码:
TextToSpeech(appid, secretid, secretkey, "你好,世界!");
代码有些地方封装的欠妥,懒得弄了,先这样写着了,以后看情况重构吧。
腾讯的文档我已经不想吐槽了,可能是刚好赶上了文档的修改时期吧,我刚去看,发现很多地方已经修改了。