[聚合文章] 腾讯云语音合成TTS

c# 2017-05-07 9 阅读

腾讯云语音合成 C# 示例

语音合成官方API文档

按着官方文档的顺序开始写吧。

一、请求 url 拼接

首先的坑点就是url必须是按升序排列进行拼接,不然接下来的鉴权肯定失败

基础链接:

https://aai.qcloud.com/tts/v1/<appid>?

参数:

{
    "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&timestamp=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&timestamp=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;的格式就不介绍了,网上一大堆

-------------------123456abcdefg
Content-Disposition: form-data; name="text"; filename="file1.txt"

这里是你需要转换的内容
-------------------123456abcdefg--

这里的 -----------------123456abcdefg 是边界符,以 --自定义字符串 开头,以 --自定义字符串-- 结尾。
name="text" 和 filename="file1.txt" 都是任意取名

Demo代码:

 #region 语音合成
        /// <summary>
        /// 语音合成
        /// </summary>
        /// <param name="appid"></param>
        /// <param name="secretid"></param>
        /// <param name="secretkey"></param>
        /// <param name="text"></param>
        /// <param name="mp3AbsolutePath"></param>
        /// <param name="mp3Filename"></param>
        /// <returns></returns>
        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<string>("code").Equals("0"))
            {
                //转换为mp3保存
                Base64ToAudio(resJson.Value<string>("speech"), mp3AbsolutePath, mp3Filename);
            }
            else
            {
                //转换失败,具体查看错误码
            }
            return responseResult;
        }
        #endregion

        #region 使用密钥(secretkey)对url进行加密,得到签名
        /// <summary>
        /// 使用密钥(secretkey)对url进行加密,得到签名
        /// </summary>
        /// <param name="urlSorted">排序后的url地址</param>
        /// <param name="secretkey">密钥</param>
        /// <returns></returns>
        private string GetAuthorization(string urlSorted, string secretkey)
        {
            return HmacSha1AndBase64("POST" + urlSorted.Substring(8),secretkey);
        }
        #endregion
        
        #region HMACSHA1 & Base64 加密
        /// <summary>
        /// HMACSHA1 & Base64 加密
        /// </summary>
        /// <param name="mk">原文</param>
        /// <param name="secretkey">密钥</param>
        /// <returns></returns>
        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 获取排序后的完整路径地址
        /// <summary>
        /// 获取排序后的完整路径地址
        /// </summary>
        /// <param name="appid"></param>
        /// <param name="secretid"></param>
        /// <returns></returns>
        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
             * &timestamp=1484109983
             * &volume=3
             * 
             */

            StringBuilder url = new StringBuilder("https://aai.qcloud.com/tts/v1/" + appid + "?");
            SortedDictionary<string, object> dic = new SortedDictionary<string, object>();
            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
        /// <summary>
        /// 设置Header
        /// </summary>
        /// <param name="webRequest"></param>
        /// <param name="url"></param>
        /// <param name="secretkey"></param>
        /// <param name="boundary"></param>
        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
        /// <summary>
        /// 获取设置文字内容后的Body
        /// </summary>
        /// <param name="boundary"></param>
        /// <param name="text"></param>
        /// <returns></returns>
        private string GetRequestBody(string boundary, string text)
        {
            //设置需要转语音的文字内容
            string strBody = GetTextBody(boundary, text);

            //在这里可以添加其他的Body内容
            //strBody += GetOtherBody();

            //设置结尾
            strBody += "--" + boundary + "--\r\n";
            return strBody;
        }
        #endregion

        #region 获取设置文字内容后的Body段
        /// <summary>
        /// 设置文字内容
        /// </summary>
        /// <param name="boundary"></param>
        /// <param name="text"></param>
        /// <returns></returns>
        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流写入到请求流中
        /// <summary>
        /// 将body流写入到请求流中
        /// </summary>
        /// <param name="webRequest"></param>
        /// <param name="memStream"></param>
        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 获取响应结果
        /// <summary>
        /// 获取响应内容
        /// </summary>
        /// <param name="webRequest"></param>
        /// <returns></returns>
        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
        /// <summary>
        /// base64转换mp3
        /// </summary>
        /// <param name="strBase64"></param>
        /// <param name="absolutePath"></param>
        /// <param name="filename"></param>
        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, "你好,世界!");

代码有些地方封装的欠妥,懒得弄了,先这样写着了,以后看情况重构吧。


腾讯的文档我已经不想吐槽了,可能是刚好赶上了文档的修改时期吧,我刚去看,发现很多地方已经修改了。

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。