alwaysol 发表于 2021-3-31 15:22

nodejs版讯飞文字合成语音

本帖最后由 alwaysol 于 2021-6-30 14:51 编辑

最近想听一部小说但是没语音版,看到讯飞有文字转语音的接口所有就玩玩,官方原版例子一次只能生成3000个字左右,所以我修改了下把整个小说分片段生成语音,如有不足的地方望大佬们指正
步骤:
1.去nodejs官网下载win版的nodejs.exe,直接安装就行,不需要任何设置
2.去讯飞开放平台注册账号,开通语音合成业务,可以免费用50000次,复制APPID等信息到以下代码config对应的位置
3.把下载的小说txt文件放到当前目录下,txt要utf-8编码的,如果不是另存txt为utf-8编码
4.修改代码里的txt文件名和你想保存的音频文件名
5.双击start.bat文件就会出现dos窗口,音频文件生成在当前目录下MP3文件夹里
源代码下载地址:https://wwa.lanzouj.com/iPLAJniey6f

/* Created by iflytek on 2020/03/01.
*
* 运行前:请先填写 appid、apiSecret、apiKey
*
* 在线语音合成调用demo
* 此demo只是一个简单的调用示例,不适合用到实际生产环境中
*
* 在线语音合成 WebAPI 接口调用示例 接口文档(必看):https://www.xfyun.cn/doc/tts/online_tts/API.html
* 错误码链接:
* https://www.xfyun.cn/document/error-code (code返回错误码时必看)
*
*/
const CryptoJS = require('crypto-js');
const WebSocket = require('ws');
const log = require('log4node');
const fs = require('fs');


//在控制台-我的应用-在线语音合成(流式版)获取
let appid = "";
//在控制台-我的应用-在线语音合成(流式版)获取
let apiSecret = "";
//在控制台-我的应用-在线语音合成(流式版)获取
let apiKey = "";

let vcn = "xiaoyan";//声音

var txt = fs.readFileSync('新建文本文档.txt', 'utf8');//读取的文件名
var flie = "newtxt";//保存的文件名
var min = 10;//每个音频时长,分钟

var array = (txt.length % 3000) > 0 ? parseInt(txt.length / 3000 + 1) : parseInt(txt.length / 3000);
min = min % 10 > 0 ? parseInt(min / 10) + 1 : parseInt(min / 10);

(async () => {

    var page = (array % 100) > 0 ? parseInt(array / 100 + 1) : parseInt(array / 100);
    for (let i = 0; i < page; i++) {
      for (let index = i * 100; index < (i * 100) + 100; index++) {
            main(index);
      }
      await wait(1000 * 20);
    }
})();

function main(index) {

    // 系统配置
    var config = {
      // 请求地址
      hostUrl: "wss://tts-api.xfyun.cn/v2/tts",
      host: "tts-api.xfyun.cn",
      appid: appid,
      apiSecret: apiSecret,
      apiKey: apiKey,
      text: txt.substr(3000 * index, 3000),
      uri: "/v2/tts",
    }

    // 获取当前时间 RFC1123格式
    let date = (new Date().toUTCString());

    // 设置当前临时状态为初始化
    let wssUrl = config.hostUrl + "?authorization=" + getAuthStr(date) + "&date=" + date + "&host=" + config.host;
    let ws = new WebSocket(wssUrl);

    if (!fs.existsSync('./mp3/' + flie)) fs.mkdirSync('./mp3/' + flie);

    // 连接建立完毕,读取数据进行识别
    ws.on('open', () => {
      log.info("websocket connect!");
      send();

      // 如果之前保存过音频文件,删除之
      var ss = (index + 1);
      ss = ss % min > 0 ? parseInt(ss / min) + 1 : ss / min;
      if (fs.existsSync('./mp3/' + flie + '/' + flie + ss + '.mp3')) {
            fs.unlink('./mp3/' + flie + '/' + flie + ss + '.mp3', (err) => {
                if (err) {
                  log.error('remove error: ' + err);
                }
            })
      }
    })

    // 得到结果后进行处理,仅供参考,具体业务具体对待
    ws.on('message', async (data, err) => {
      if (err) {
            log.error('message error: ' + err);
            return;
      }

      let res = JSON.parse(data);

      if (res.code != 0) {
            log.error(`${res.code}: ${res.message}`);
            ws.close();
            return;
      }

      let audio = res.data.audio;
      let audioBuf = Buffer.from(audio, 'base64');

      await save(audioBuf);

      if (res.code == 0 && res.data.status == 2) {
            await wait(1000);
            var ss = (index + 1);
            ss = ss % min > 0 ? parseInt(ss / min) + 1 : ss / min;
            log.info('第' + ss + '个文件合并完成,共' + (array % min > 0 ? parseInt(array / min) + 1 : parseInt(array / min)) + '个文件*********************!');
            ws.close();
      }
    })

    // 资源释放
    ws.on('close', () => {
      log.info('connect close!');
    });

    // 连接错误
    ws.on('error', (err) => {
      log.error("websocket connect err: " + err);
    })

    // 鉴权签名
    function getAuthStr(date) {
      let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`;
      let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
      let signature = CryptoJS.enc.Base64.stringify(signatureSha);
      let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
      let authStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin));
      return authStr;
    }

    // 传输数据
    function send() {
      let frame = {
            // 填充common
            "common": {
                "app_id": config.appid
            },
            // 填充business
            "business": {
                "aue": "lame",
                "sfl": 1,
                "auf": "audio/L16;rate=16000",
                "vcn": vcn,
                "tte": "UTF8"
            },
            // 填充data
            "data": {
                "text": Buffer.from(config.text).toString('base64'),
                "status": 2
            }
      }

      ws.send(JSON.stringify(frame));

    }

    // 保存文件
    async function save(data) {
      return new Promise(async resolve => {
            var ss = (index + 1);
            ss = ss % min > 0 ? parseInt(ss / min) + 1 : ss / min;
            fs.writeFile('./mp3/' + flie + '/' + flie + ss + '.mp3', data, { flag: 'a' }, (err) => {
                if (err) {
                  log.error('save error: ' + err);
                  resolve();
                  return;
                }
                log.info('正下载第' + ss + '个文件碎片!');
                resolve();
            })
      })
    }

}

async function wait(time) {
    return new Promise(resolve => {
      setTimeout(() => {
            resolve();
      }, time);
    })
}




fj198602 发表于 2021-6-9 23:33

感谢楼主分享,软件非常好用,使用后,发现一个小问题,因为合成的音频,是由很多小片段合并而来,合并处(小片段的拼接处)会出现破音或者撕裂的声音,一部小说转下来,很多都会有这个问题,不知道有没有优化的可能?

alwaysol 发表于 2021-6-11 09:51

fj198602 发表于 2021-6-9 23:33
感谢楼主分享,软件非常好用,使用后,发现一个小问题,因为合成的音频,是由很多小片段合并而来,合并处( ...

好的,有时间我看看

fj198602 发表于 2021-6-29 01:37

大神,什么时候有时间,能优化一下。查了好多资料,看到一个答案:说是JS数据格式的问题。   本人小白,不太懂,不知道能不能给大神一点帮助。
页: [1]
查看完整版本: nodejs版讯飞文字合成语音