草庐IT

前端文字转语音(tts+mp3拼接)

Nic_LittleCow 2023-06-30 原文

1.功能场景

有时候需要在网页上面播报一段语音,而这段语音是动态的。例如收银时播报请出示付款吗,收钱成功后播报某某某为您收到金额XX元。

2.思路

第一种思路是前端不需要怎么动手写代码的也是最容易实现的,调用语音合成api。但是api的局限性就在于免费的没有语音包,收钱的就有点贵了,不适用于重复调用(我们系统目前规模不大,但是每天也能产生1-2万条成功的交易订单)。

第二种思路是调用windows本地的tts语音合成服务,这是能免费使用且可以支持每次根据不同的内容来合成不同的语音的一个功能。

第三种思路使用video元素直接组装一些零散的文字来形成一段完整的音频。

这里就讲一下第二种跟第三种思路

3.实现

        3.1windows本地的tts语音合成服务

        这里使用的是SpeechSynthesisUtterance这个html5新的api,这个对象主要用来构建语音合成实例,具体的属性如下。

  • text – 要合成的文字内容:string。
  • lang – 使用的语言:string, 例如:"zh-cn"
  • voiceURI – 指定希望使用的声音和服务:string。
  • volume – 声音的音量:number,范围是0-1,默认11
  • rate – 语速:number,范围是0.1-10,默认1。
  • pitch – 表示说话的音高:number,范围是0-2。默认为1

当然,这个实例对象也包括一些方法

  • onstart – 合成开始的回调。
  • onpause – 合成暂停的回调。
  • onresume – 合成重新开始的回调。
  • onend – 合成结束时的回调。

这是mdn上面对SpeechSynthesisUtterance这个对象的说明:SpeechSynthesisUtterance - Web API 接口参考 | MDN

 然后还有一个跟SpeechSynthesisUtterance搭配使用的SpeechSynthesis对象。该接口是语音服务的控制接口,它可以用于获取设备上关于可用的合成声音的信息,开始、暂停语音,或除此之外的其他命令。SpeechSynthesisUtterance - Web API 接口参考 | MDN

  • speak() – 只能接收SpeechSynthesisUtterance作为唯一的参数,作用是读合成的话语。

  • stop() – 立即终止合成过程。

  • pause() – 暂停合成过程。

  • resume() – 重新开始合成过程。

  • getVoices – 此方法不接受任何参数,用来返回浏览器支持的语音包列表,是个数组。

  • 谷歌浏览器getVoices获取的声音列表,国内能使用的应该就前三个

  •  

let synth;
let msg;
const initSpeak = () => {
    synth = window.speechSynthesis
    msg = new SpeechSynthesisUtterance()
    msg.text = '收到新的订单'
    msg.lang = 'zh-CN'
    msg.pitch = 1.1
    msg.rate = 1.8
    msg.volume = 10
    //getVoices() 是一个异步的方法,需要使用一个定时器来保证每次都能获取到值
    setTimeout(()=>{
        synth.getVoices().find(i=>i.lang=='zh-CN'&&i.localService==true)
    },100)
}

initSpeak()

// 调用这个方法传入一个你需要合成的文字的话就能开始使用浏览器来播报语音了
const handleSpeak = (message) => {
    msg.text = message
    synth.speak(msg)
}

到这里就完成了语音合成了,不过由于我们项目中有的客户电脑使用的阉割版的windows或者没有去安装语音合成引擎,这个版本上线一周就被pass掉了,所以使用这个方案的前提是你能保证客户机上面是安装了语音合成引擎。

        3.2 组装一些零散的mp3片段来形成一段完整的音频(伪语音播报)

        结合到实际的应用场景项目中最终选择了这种方式来实现语音合成,虽然方法很笨,但是至少所有客户机都能满足需求且可以做到客户自定义语音包。

        实现:

1.需要一个将数字转换成中文读数的方法(网上找的)

2.再将这个中文读数构造成一个数组(urlList)

3.调用组件进行播报

 数字转换成中文读数的方法

/**
    * 数字转成汉字
    * @params num === 要转换的数字
    * @return 汉字
    * @eg 例如,输入0.41, 返回"零点四一元"
    * */
const numToChines = (tranvalue) => {
    if (typeof tranvalue == "number") {
        tranvalue = tranvalue + ''
    }
    //拆分整数与小数
    let splits = function (tranvalue) {
        var value = new Array('', '');
        temp = tranvalue.split(".");
        for (var i = 0; i < temp.length; i++) {
            value[i] = temp[i];
        }
        return value;
    }
    try {
        var i = 1;
        var dw2 = new Array("", "万", "亿");//大单位
        var dw1 = new Array("十", "百", "千");//小单位
        var dw = new Array("零", "一", "二", "三", "四", "五", "六", "七", "八", "九");//整数部分用
        //以下是小写转换成大写显示在合计大写的文本框中
        //分离整数与小数
        var source = splits(tranvalue);
        var num = source[0];
        var dig = source[1];
        //转换整数部分
        var k1 = 0;//计小单位
        var k2 = 0;//计大单位
        var sum = 0;
        var str = "";
        var len = source[0].length;//整数的长度
        for (i = 1; i <= len; i++) {
            var n = source[0].charAt(len - i);//取得某个位数上的数字
            var bn = 0;
            if (len - i - 1 >= 0) {
                bn = source[0].charAt(len - i - 1);//取得某个位数前一位上的数字
            }
            sum = sum + Number(n);
            if (sum != 0) {
                str = dw[Number(n)].concat(str);//取得该数字对应的大写数字,并插入到str字符串的前面
                if (n == '0') sum = 0;
            }
            if (len - i - 1 >= 0) {//在数字范围内
                if (k1 != 3) {//加小单位
                    if (bn != 0) {
                        str = dw1[k1].concat(str);
                    }
                    k1++;
                } else {//不加小单位,加大单位
                    k1 = 0;
                    var temp = str.charAt(0);
                    if (temp == "万" || temp == "亿")//若大单位前没有数字则舍去大单位
                        str = str.substr(1, str.length - 1);
                    str = dw2[k2].concat(str);
                    sum = 0;
                }
            }
            if (k1 == 3)//小单位到千则大单位进一
            { k2++; }
        }
        //转换小数部分
        var strdig = "";
        if (dig != "") {
            var n = dig.charAt(0)
            var nn = dig.charAt(1)
            if (nn !== "") {
                strdig = "点" + dw[Number(n)] + dw[Number(nn)]
            } else {
                strdig = "点" + dw[Number(n)]
            }

        }
        if (str) {
            str += strdig + "元";
        } else {
            str = "零" + strdig + "元"
        }

    } catch (e) {
        return "零元";
    }
    return str;
}

 构建待播报的数组

//将中文枚举成对应的英文路径
const voiceEnum = {
    "万": "ten_thousand",
    "十": "ten",
    "百": "hundred",
    "千": "thousand",
    "零": "0",
    "一": "1",
    "二": "2",
    "三": "3",
    "四": "4",
    "五": "5",
    "六": "6",
    "七": "7",
    "八": "8",
    "九": "9",
    "点": "point",
    "元": "element"
}

const speak = () => {
    let list = []
    let chineseNum = numToChines(0.41)
    for (let i = 0; i < chineseNum.length; i++) {
        const item = chineseNum[i];
        //这里的地址拼接成你自己存放零散mp3片段的地址,voicePacket是语音包的配置,需要自己去定义
           urlList.push(`https://oss.aliyuncs.com/voice/${voicePacket.value}/amount/${voiceEnum[item]}.mp3`)
    }
    //调用一个组件来播报这个数组形式的url
    audioLoopRef.value && audioLoopRef.value.start(list)
}

 附上audioLoop这个组件代码,构建好urlList之后通过ref调用组件内的方法即可

<template>
    <div v-if="audioUrlList.length > 0">
        <audio @ended="voiceEnded" id="voice">
            <source :src="audioUrlList[currentIndex]" type="audio/mpeg">
            您的浏览器不支持 audio 元素。
        </audio>
    </div>
</template>
 
<script setup>
import { ref, nextTick } from "vue"
// 用来保存传过来的需要播放的urlList:[]
const audioUrlList = ref([])
// 默认从头开始播报
const currentIndex = ref(0)
// 开始播报
const start = (urlList) => {
    audioUrlList.value = urlList
    nextTick(() => {
        let dom = document.getElementById("voice")
        dom.play()
    })
}

// 停止播报
const stop = () => {
    let dom = document.getElementById("voice")
    nextTick(() => {
        dom.pause()
        dom.setAttribute("src", "xxxx")
        currentIndex.value = 0
        audioUrlList.value = []
    })
}

// 播放完一个就继续播放下一个 
const voiceEnded = () => {
    if (currentIndex.value == (audioUrlList.value.length - 1)) {
        audioUrlList.value = []
        currentIndex.value = 0
    } else {
        currentIndex.value++
        let dom = document.getElementById("voice")
        dom.setAttribute("src", audioUrlList.value[currentIndex.value])
        dom.play()
    }
}
defineExpose({
    start, stop
})
</script>

 语音包的实现其实就是将相同的文字多录制几个语音包放在不同的oss目录下面,到时候通过前端的配置动态生成urlList时去对应到不同的语音包。

 

 

到这里就完成了自己手动合成一段语音并播报了。

有关前端文字转语音(tts+mp3拼接)的更多相关文章

  1. ruby - 如何使用文字标量样式在 YAML 中转储字符串? - 2

    我有一大串格式化数据(例如JSON),我想使用Psychinruby​​同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解

  2. ruby - 字符串文字中的转义状态作为 `String#tr` 的参数 - 2

    对于作为String#tr参数的单引号字符串文字中反斜杠的转义状态,我觉得有些神秘。你能解释一下下面三个例子之间的对比吗?我特别不明白第二个。为了避免复杂化,我在这里使用了'd',在双引号中转义时不会改变含义("\d"="d")。'\\'.tr('\\','x')#=>"x"'\\'.tr('\\d','x')#=>"\\"'\\'.tr('\\\d','x')#=>"x" 最佳答案 在tr中转义tr的第一个参数非常类似于正则表达式中的括号字符分组。您可以在表达式的开头使用^来否定匹配(替换任何不匹配的内容)并使用例如a-f来匹配一

  3. ruby - 字符串文字前面的 * 在 ruby​​ 中有什么作用? - 2

    这段代码似乎创建了一个范围从a到z的数组,但我不明白*的作用。有人可以解释一下吗?[*"a".."z"] 最佳答案 它叫做splatoperator.SplattinganLvalueAmaximumofonelvaluemaybesplattedinwhichcaseitisassignedanArrayconsistingoftheremainingrvaluesthatlackcorrespondinglvalues.Iftherightmostlvalueissplattedthenitconsumesallrvaluesw

  4. Ruby:如何将数组拼接成 Lisp 风格的列表? - 2

    这是我发现自己偶尔想做的事情。假设我有一个参数列表。在Lisp中,我可以像这样`(imaginary-function,@args)为了调用将数组从一个元素转换为正确数量的参数的函数。Ruby中是否有类似的功能?或者我只是在这里使用了一个完全错误的成语? 最佳答案 是的!它被称为splat运算符。a=[1,44]p(*a) 关于Ruby:如何将数组拼接成Lisp风格的列表?,我们在StackOverflow上找到一个类似的问题: https://stackov

  5. ruby - 如何以编程方式将 mp3 转换为 itunes 可播放的 aac/m4a 文件? - 2

    我一直在寻找一种以编程方式或通过命令行将mp3转换为aac的方法,但没有成功。理想情况下,我有一段代码可以从我的Rails应用程序中调用,将mp3转换为aac。我安装了ffmpeg和libfaac,并能够使用以下命令创建aac文件:ffmpeg-itest.mp3-acodeclibfaac-ab163840dest.aac当我将输出文件的名称更改为dest.m4a时,它无法在iTunes中播放。谢谢! 最佳答案 FFmpeg提供AAC编码功能(如果您已编译它们)。如果您使用的是Windows,则可以从here获取完整的二进制文件。

  6. ruby - 如何播放 mp3 文件? - 2

    我如何用ruby​​编写一个脚本,当从命令行执行时播放mp3文件(背景音乐)?我试过了run="mplayer#{"/Users/bhushan/resume/m.mp3"}-aosdl-vox11-framedrop-cache16384-cache-min20/100"system(run)但它也不起作用,以上是播放器特定的。如果用户没有安装mplayer怎么办。有没有更好的办法? 最佳答案 我一般都是这样pid=fork{exec'mpg123','-q',file} 关于ruby

  7. ruby - 如何在不使用 HERE-DOCUMENT 语法的情况下在 Ruby 中制作多行字符串文字? - 2

    问题总结我想尝试使用Ruby来完成我在Python中所做的事情。在Python中它有r"""syntaxtosupportrawstrings,这很好,因为它允许将原始字符串与代码内联,并以更自然的方式连接它们,而无需特殊缩进。在Ruby中,当使用原始字符串时,必须使用其次是EOT在单独的行中,这会破坏代码布局。你可能会问,为什么不使用Ruby的%q{}?嗯,因为%q{}与Python的r"""相比有局限性因为它不会转义多个\\\并且只处理单个\.我正在动态生成Latex代码并写入一个文件,该文件稍后用pdflatex编译。Latex代码包含类似\\\的内容在许多地方。如果我使用Rub

  8. Ruby:为什么等号在文字正则表达式中会导致解析错误? - 2

    这些解析和执行良好:"=".scan(/=/)"=".scan(/=/)这会导致“未终止的正则表达式遇到文件结尾”:"=".scan/=/如果我在=之前插入一些内容,错误就会消失:"=".scan/^=/这是怎么回事? 最佳答案 我猜你正在点击thisintheparser:case'/':if(IS_BEG()){lex_strterm=NEW_STRTERM(str_regexp,'/',0);returntREGEXP_BEG;}if((c=nextc())=='='){set_yylval_id('/');lex_state

  9. ruby-on-rails - 在 Rails 应用程序的前端获取实时日志 - 2

    在Rails3.x应用程序中,我正在使用net::ssh并向远程pc运行一些命令。我想向用户的浏览器显示实时日志。比如,如果两个命令在net中运行::ssh执行即echo"Hello",echo"Bye"被传递然后"Hello"应该在执行后立即显示在浏览器中。这是代码我在ruby​​onrails应用程序中使用ssh连接和运行命令Net::SSH.start(@servers['local'],@machine_name,:password=>@machine_pwd,:timeout=>30)do|ssh|ssh.open_channeldo|channel|channel.requ

  10. ruby - 如何在转换器插件中访问页面属性(YAML 前端) - 2

    我正在为Jekyll编写一个转换器插件,需要访问一些页眉(YAML前端)属性。只有内容被传递给主要的转换器方法,似乎无法访问上下文。例子:moduleJekyllclassUpcaseConverter关于如何在转换器插件中访问页眉数据有什么想法吗? 最佳答案 基于Jekyll源代码,无法在转换器中检索YAML前端内容。根据您的情况,我看到了两种可行的解决方案。您的文件扩展名可以具有足够的描述性,以提供您本应包含在前言中的信息。看起来Converter插件的设计就是这么基本的。如果修改Jekyll是一个选项,您可以更改Convert

随机推荐