自制有声书阅读器:用PaddleSpeech打开读书新方式

news2025/1/12 20:39:53

吕声辉,飞桨开发者技术专家(PPDE),某网络科技公司研发工程师。主要研究方向为图像识别,自然语言处理等。 • AI Studio主页
https://aistudio.baidu.com/aistudio/personalcenter/thirdview/227158

项目背景

随着互联网的发展,普通用户对于书籍展示形式的需求已由纯文字变成了图文、语音、视频等多种形式,因此将文本书籍转换为有声读物具有很大的市场需求。本文以飞桨语音模型库PaddleSpeech提供的语音合成技术为核心,通过音色克隆、语速设置、音量调整等附加功能,展示有声书籍的技术可行方案。
在这里插入图片描述
最终呈现效果如
player.bilibili.com/player.html?bvid=BV1x84y1V7SR

网页体验访问地址
https://book.weixin12306.com/

环境准备

PaddleSpeech 是基于飞桨的语音方向开源模型库,用于语音和音频中的各种关键任务的开发,包含大量基于深度学习的前沿和有影响力的模型。首先进行PaddleSpeech安装环境的配置,配置如下:

# 注意如果之前运行过这步 下次就不用再运行了,这个目录重启项目也不会清空的
# 下载解压说话人编码器
!wget -P data https://bj.bcebos.com/paddlespeech/Parakeet/released_models/ge2e/ge2e_ckpt_0.3.zip
!unzip -o -d work data/ge2e_ckpt_0.3.zip
# 下载解压声码器
!wget -P data https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_aishell3_ckpt_0.5.zip
!unzip -o -d work data/pwg_aishell3_ckpt_0.5.zip
# 下载解压声学模型
!wget -P data https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_aishell3_vc1_ckpt_0.5.zip
!unzip -o -d work data/fastspeech2_nosil_aishell3_vc1_ckpt_0.5.zip
#  下载解压nltk包
!wget -P data https://paddlespeech.bj.bcebos.com/Parakeet/tools/nltk_data.tar.gz
!tar zxvf data/nltk_data.tar.gz

# 安装PaddleSpeech
!pip install pytest-runner
!pip install paddlespeech

# 将nltk_data 拷贝到 /home/aistudio 目录
!cp -r /home/aistudio/work/nltk_data /home/aistudio

# 安装moviepy 
!pip install moviepy==1.0.3

数据处理

每本书的内容均以json格式存放在txt文本中,路径为
/work/books/inputs/bookname.txt。为方便演示,这里以三国演义为例。

{
   “name”: “三国演义”, 
   “lists”: [{
      “title”: “第一回 宴桃园豪杰三结义 斩黄巾英雄首立功”
      “content”: “滚滚长江东逝水,浪花淘尽英雄。是非成败转头空。青山依
}, {
      “title”: “第二回 张翼德怒鞭督邮 何国舅谋诛宦竖”,
      “content”: “且说董卓字仲颖,陇西临洮人也,官拜河东太守,自来骄傲
   }]
}

音频合成

段落句子分割

以换行符"\n"分割为段落,以"。"分割为句子。

# 段落和句子分割
def lists(self, lists):
    results = []
    for i in range(len(lists)):
        item = lists[i]
        title = item['title']
        content = item['content']
        sections = []
        sentences = []
        contents = content.split('\n')
        for citem in contents:
            if len(citem) > 1:
                sections.append(citem)

        sentenceIndex = 0
        for sitems in sections:
            sitems_ = []
            for tmp  in sitems.split('。'):
                if len(tmp) > 1:
                    sitems_.append(tmp)

            for j in range(len(sitems_)):
                sentence = {
                    'id':sentenceIndex,
                    'sentence': sitems_[j],
                    'end': 0 if j < len(sitems_) - 1 else 1
                }
                sentences.append(sentence)
                sentenceIndex += 1
        result = {
            'id':i,
            'title':title,
            'sentences':sentences
        }
        results.append(result)
    return results

特殊字符处理

在国学书籍中,有可能出现很多生僻字或者特殊符号,这里需要做针对性的替换。

# 特殊处理示例,工程化最好用字典自动判断替换
def dealText(self, text):
    text = text.replace('-','')
    text = text.replace(' ', '')
    text = text.replace('’','')
    text = text.replace('﨑','崎')
    text = text.replace("[",' ')
    text = text.replace("]",' ')
    text = text.replace(' ',' ')
    text = text.replace(",]","")
    text = text.replace("1","1")
    text = text.replace("2",'2')
    text = text.replace("6","6")
    text = text.replace("〔","")
    text = text.replace("─","")
    text = text.replace("┬","")
    text = text.replace("┼","")
    text = text.replace("┴","")
    text = text.replace("〖"," ")
    text = text.replace("〗"," ")
    text = text.replace("礻殳","祋")
    return text

音频合成

根据分割的ID,保存到对应位置。

# 音频合成
def audio(self, contents):
    self.tts = TTSExecutor()
    for i in range(len(contents['lists'])):
        item = contents['lists'][i]
        basePath = self.bookPathOutput+'/'+self.bookname+'/'+str(i)
        if os.path.exists(basePath) is False:
            os.makedirs(r''+basePath)

        # 生成每回标题音频
        self.text2audio(item['title'], basePath+'/title.wav')

        # 生成每句内容音频
        for j in range(len(item['sentences'])):
            sitem = item['sentences'][j]
            self.text2audio(sitem['sentence'], basePath+'/'+str(sitem['id'])+'.wav')

def text2audio(self, text, path):
    text = self.dealText(text)
    self.voice_cloning(text, path)
#self.tts(text=text, output=path)

音色克隆

可以事先将不同音色音频放置在 /work/sounds 目录下。此处音色克隆部分的功能主要参考自PaddleSpeech语音克隆项目。

项目链接
https://aistudio.baidu.com/aistudio/projectdetail/4265795?channelType=0&channel=0

def clone_pre(self):

        # Init body.
        with open(self.am_config) as f:
            am_config = CfgNode(yaml.safe_load(f))

        self.am_config_ = am_config
        with open(self.voc_config) as f:
            voc_config = CfgNode(yaml.safe_load(f))

        # speaker encoder
        p = SpeakerVerificationPreprocessor(
            sampling_rate=16000,
            audio_norm_target_dBFS=-30,
            vad_window_length=30,
            vad_moving_average_width=8,
            vad_max_silence_length=6,
            mel_window_length=25,
            mel_window_step=10,
            n_mels=40,
            partial_n_frames=160,
            min_pad_coverage=0.75,
            partial_overlap_ratio=0.5)
        print("Audio Processor Done!")
        self.p = p

        speaker_encoder = LSTMSpeakerEncoder(
            n_mels=40, num_layers=3, hidden_size=256, output_size=256)
        speaker_encoder.set_state_dict(paddle.load(self.ge2e_params_path))
        speaker_encoder.eval()
        self.speaker_encoder = speaker_encoder
        print("GE2E Done!")

        with open(self.phones_dict, "r") as f:
            phn_id = [line.strip().split() for line in f.readlines()]
        vocab_size = len(phn_id)
        print("vocab_size:", vocab_size)

        # acoustic model
        odim = am_config.n_mels
        # model: {model_name}_{dataset}
        am_name = self.am[:self.am.rindex('_')]
        am_dataset = self.am[self.am.rindex('_') + 1:]

        am_class = dynamic_import(am_name, self.model_alias)
        am_inference_class = dynamic_import(
            am_name + '_inference', self.model_alias)

      if am_name == 'fastspeech2':
            am = am_class(
                idim=vocab_size, odim=odim, spk_num=None, **am_config["model"])
        elif am_name == 'tacotron2':
            am = am_class(idim=vocab_size, odim=odim, **am_config["model"])

        am.set_state_dict(paddle.load(self.am_ckpt)["main_params"])
        am.eval()
        am_mu, am_std = np.load(self.am_stat)
        am_mu = paddle.to_tensor(am_mu)
        am_std = paddle.to_tensor(am_std)
        am_normalizer = ZScore(am_mu, am_std)
        am_inference = am_inference_class(am_normalizer, am)
        am_inference.eval()
        self.am_inference = am_inference
        print("acoustic model done!")


        # vocoder
        # model: {model_name}_{dataset}
        voc_name = self.voc[:self.voc.rindex('_')]
        voc_class = dynamic_import(voc_name, self.model_alias)
        voc_inference_class = dynamic_import(
            voc_name + '_inference', self.model_alias)
        voc = voc_class(**voc_config["generator_params"])
        voc.set_state_dict(paddle.load(self.voc_ckpt)["generator_params"])
        voc.remove_weight_norm()
        voc.eval()
        voc_mu, voc_std = np.load(self.voc_stat)
        voc_mu = paddle.to_tensor(voc_mu)
        voc_std = paddle.to_tensor(voc_std)
        voc_normalizer = ZScore(voc_mu, voc_std)
        voc_inference = voc_inference_class(voc_normalizer, voc)
        voc_inference.eval()
        self.voc_inference = voc_inference
        print("voc done!")

        self.frontend = Frontend(phone_vocab_path=self.phones_dict)
        print("frontend done!")

        # 获取音色
        ref_audio_path = self.soundsInput+'/'+str(self.sound)+'.mp3'
        mel_sequences = self.p.extract_mel_partials(self.p.preprocess_wav(ref_audio_path))
        # print("mel_sequences: ", mel_sequences.shape)
        with paddle.no_grad():
            spk_emb = self.speaker_encoder.embed_utterance(paddle.to_tensor(mel_sequences))
        # print("spk_emb shape: ", spk_emb.shape)
        self.spk_emb = spk_emb

def voice_cloning(self, text, path):

    input_ids = self.frontend.get_input_ids(text, merge_sentences=True)
    phone_ids = input_ids["phone_ids"][0]

    with paddle.no_grad():
        wav = self.voc_inference(self.am_inference(phone_ids, spk_emb=self.spk_emb))

    sf.write(path, wav.numpy(), samplerate=self.am_config_.fs)

语速和音量调整

def post_del(self, path):
    old_au = AudioFileClip(path)
    new_au = old_au.fl_time(lambda t:  self.speed*t, apply_to=['mask', 'audio'])
    new_au = new_au.set_duration(old_au.duration/self.speed)
    new_au = (new_au.fx(afx.volumex, self.volumex))
    final_path = path.replace('outputs','final')
    print(path, final_path)
    new_au.write_audiofile(final_path)
    print('^^^^^^')

音色、语速和音量需要在 main.py 的头部中设置。

class Main(object):

    def __init__(self):
        self.bookPathInput = './books/inputs' # 书籍输入目录
        self.bookPathOutput = './books/outputs' # 常规输出目录
        self.bookPathFinal = './books/final' # 最终输出目录
        self.bookname = 'sanguoyanyi'
        self.tts = None
        self.soundsInput = './sounds' # 音色文件存放目录
        self.sound = '001' # 音色编号
        self.speed = 1.0 # 语速
        self.volumex = 1.1 # 音量

# 音频合成,一键命令
%cd /home/aistudio/work/
!python main.py

查看生成结果

最终切分好的数据在
/work/outputs/sanguoyanyi目录下,原始语速和音量音频在outputs目录下,指定语速和音量音频在final目录下。其中的outputs.txt为切分内容,而音频会按照每个章节以及每个章节的句子索引排序好。

以下为outputs.txt 内容:

{
   “name”: “三国演义”,
   “lists”: [{
      “id”: 0,
      “title”: “第一回 宴桃园豪杰三结义 斩黄巾英雄首立功”, 
      “sentence”: [{
         “id”: 0
         “sentence”: “滚滚长江东逝水,浪花淘尽英雄”,
         “end”: 0
   }, {
      “id”: 1,
      “sentence”: “是否成败转头空”,
      “end”: 0
   }, {
      “id”: 2,
      “sentence”: “青山依旧在,几度夕阳红”,
      “end”: 0
   }, {
      “id”: 3,
      “sentence”: “白发渔樵江渚上,惯看秋月春风”,
      “end”: 0
}, {
      “id”: 4,
      “sentence”: “一壶浊酒喜相逢”,
      “end”: 0
}, {

以下为第一回的每个句子wav格式音频。

在这里插入图片描述

客户端展示

输出第三部分生成好的内容和音频。这里用H5页面简单展示一下有声书阅读的效果,包括内容展示和逐句朗读高亮两种功能。

用PaddleSpeech实现有声书阅读

H5的具体代码已放在GitHub 上,大家可在下方链接中查看
https://github.com/lvsh2012/book2audio

手机或者PC也可直接体验
https://book.weixin12306.com/

总结

通过PaddleSpeech可以简单快速地实现语音合成功能,轻松实现书籍有声化。使用者在这里需要关注下,当以H5展示播放效果时,需要注意内容和音频的对应关系。除了语音合成功能外,PaddleSpeech还提供了包括语音识别、声纹提取、标点恢复等其他功能。相信大家基于PaddleSpeech可以在该领域挖掘出更多的可能性!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/390888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第十四届蓝桥杯三月真题刷题训练——第 3 天

目录 题目1&#xff1a;门牌制作 题目描述 运行限制 代码&#xff1a; 题目2&#xff1a;货物摆放_long 题目描述 答案提交 运行限制 代码&#xff1a; 题目3&#xff1a;跳跃_dp 题目描述 输入描述 输出描述 输入输出样例 运行限制 代码&#xff1a; 题目4&a…

AAAI顶会行人重识别算法详解——Relation Network for Person Re-identification

1.论文整体框架概述 在行人重识别任务中,通常都是对整个输入数据进行特征提取,但是缺少了局部信息。能不能既考虑局部与整体信息,也同时加入他们的联系呢?这篇论文主要的思想就是局部信息和全局信息的融合。 整体流程如上图所示, 首先对整体进行特征提取, 通常采用…

【FPGA】Verilog:MSI/LSI 组合电路之解码器 | 多路分解器

写在前面&#xff1a;本章将理解编码器与解码器、多路复用器与多路分解器的概念&#xff0c;通过使用 Verilog 实现多样的解码器与多路分解器&#xff0c;通过 FPGA 并使用 Verilog 实现。 Ⅰ. 前置知识 0x00 解码器与编码器&#xff08;Decoder / Encoder&#xff09; 解码器…

素数分类的猜想==素数,划分分类,就分为真素质数与非真素质数

素数分类的猜想 根据哥德巴赫公理&#xff08;我暂不称之为猜想了&#xff09;&#xff0c;普通素数加一&#xff0c;成为合数&#xff0c;必可分解成两个素数&#xff0c;所以&#xff0c;两个素数Pi,Pj相加减一&#xff0c;是有可能为素数Pn的&#xff0c;这样的素数Pi,Pj&am…

扩散模型DDPM开源代码的剖析【对应公式与作者给的开源项目,diffusion model】

扩散模型DDPM开源代码的剖析【对应公式与作者给的开源项目&#xff0c;diffusion model】一、简介二、扩散过程&#xff1a;输入是x_0和时刻num_steps&#xff0c;输出是x_t三、逆扩散过程&#xff1a;输入x_t&#xff0c;不断采样最终输出x_0四、具体参考算法流程图五、模型mo…

Android Framework 启动流程必知必会

课前预习在了解启动流程之前先了解一下下面两个概念:1、子进程与父进程的区别1.除了文件锁以外,其他的锁都会被继承2.各自的进程ID和父进程ID不同3.子进程的未决告警被清除4.子进程的未决信号集设置为空集2、什么是写时拷贝(copy-on-write)Linux 的 fork() 使用是通过「写时拷贝…

【Linux】Linux项目自动化构建工具make makefile

文章目录1. 背景2.实例3.原理4.项目清理5. 文件属性中的三个时间6. Linux下第一个小程序——进度条6.1 前置知识1&#xff1a;缓冲区6.2前置知识2&#xff1a;回车换行6.3进度条的实现7 Linux下git的”三板斧“1. 背景 一个工程中的源文件不计其数&#xff0c;其按类型、功能、…

【C++】位图+哈希切割+布隆过滤器

文章目录一、位图1.1 位图概念1.2 位图实现1.2.1 把x对应比特位0置11.2.2 把x对应比特位1置01.2.1 查看x对应比特位1.3 位图源码1.4 位图的应用二、哈希切割&#xff08;处理海量数据&#xff09;三、布隆过滤器3.1 布隆过滤器的概念3.2 布隆过滤器的应用场景3.3 布隆过滤器的实…

zookeeper安装使用

一、因使用kafka 需使用zookeeper,此处使用单节点 ZooKeeper有两种安装模式&#xff0c;最简单的方式是单机模式&#xff08;standalone mode&#xff09;&#xff0c;它只需要在一台机器上面运行&#xff0c;另一种方式是集群模式&#xff0c;集群模式需要多台服务器部署。 Z…

Java中垃圾回收(GC)基本概念

如果想真正理解GC&#xff0c;则需要循序渐进&#xff0c;由浅入深的了解GC&#xff0c;从本篇文章开始我们详细介绍Java中的GC&#xff0c;本篇文章我们通过4个主题先介绍垃圾回收的基本概念一、Java中什么是GC&#xff0c;为什么需要GC二、早期垃圾回收三、Java垃圾回收机制四…

DockQuery x 达梦 国产数据库生态“加速跑”

「数字化」是当今社会最先进和最具穿透力的生产力&#xff0c;近十年里开展着气势磅礴的发展。而信创产业则是保障中国经济数字化转型平稳健康发展的基础。 随着信创产业规模不断扩大&#xff0c;国产数据库市场释放出前所未有的活力。达梦作为国产数据库领头羊&#xff0c;坚…

Redis进阶之事物持久化

Reis进阶Redis事物Redis管道Redis持久化RDB持久化RDB持久化优缺点分析RBD文件修复&禁用RDB快照AOF持久化AOF优缺点&AOF重写机制AOF&RDB混合写机制Redis事物 什么是事物&#xff1f;相信学过数据库的铁子们都知道事物是什么。在MySQL当中事物是指和数据库连接的一次…

在面试时候,如何简明扼要简述产品流程

下面这个图是我今天总结的&#xff0c;我把它上传到这里来&#xff0c;然后逐一按点来解释&#xff0c;为了迎合面试&#xff0c;所以每个点尽量不超过140字。(觉得OK的&#xff0c;请点赞哦&#xff01;)图片过大&#xff0c;请点击放大后按“F”键查看原图。或下载后查看&…

IT项目经理的自我修养手册

在不断进步的时代&#xff0c;任何岗位职责都是一个责任、权力与义务的综合体&#xff0c;有多大的权力就应该承担多大的责任&#xff0c;有多大的权力和责任应该尽多大的义务&#xff0c;任何割裂开来的做法都会发生问题。那么作为IT项目经理的岗位职责&#xff0c;我大概列举…

vue实现输入框中输完后光标自动跳到下一个输入框中

前言 最近接到这么一个需求&#xff0c;做一个安全码的输入框&#xff0c;限制为6位数&#xff0c;但是每一个写入的值都是一个输入框&#xff0c;共计6个输入框&#xff0c;当前输入框写入值后&#xff0c;光标自动跳到下一个输入框中&#xff0c;删除当前输入框写入的值后再自…

秒懂算法 | 基于主成分分析法、随机森林算法和SVM算法的人脸识别问题

本文的任务与手写数字识别非常相似,都是基于图片的多分类任务,也都是有监督的。 01、数据集介绍与分析 ORL人脸数据集共包含40个不同人的400张图像,是在1992年4月至1994年4月期间由英国剑桥的Olivetti研究实验室创建。 此数据集下包含40个目录,每个目录下有10张图像,每个…

Mysql下载安装详细笔记

MySQL 是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database Management System&#xff0c;关系数据库管理系统) 应用软件之一。 一、下载mysql安装包 1. 登录官方网站https://www.mysql.com/ 2. 进入Down…

[Latex]参考文献的格式:数字,作者+年份

参考资料: 《使用 natbib 进行参考文献管理》 《bibliographystyle类型》 《\usepackage{natbib}在latex模板写作》 《LaTeX中的参考文献——作者年代引用》 文章目录[TOC]一、共同的参考文献和正文二、参考文献的引入方法2.1 声明引入的使用包(usepackage)2.2 正文的引用\…

C语言-基础了解-09-C循环

C循环 一、C循环 循环语句允许我们多次执行一个语句或语句组&#xff0c;下面是大多数编程语言中循环语句的流程图&#xff1a; 二、循环类型 2.1 while 循环 当给定条件为真时&#xff0c;重复语句或语句组。它会在执行循环主体之前测试条件。 语法 while(condition) { …

【虹科案例】虹科任意波形发生器在量子计算中的应用

虹科AWG在量子计算中的应用精度在研究中始终很重要&#xff0c;很少有研究领域需要比量子研究更高的精度。奥地利因斯布鲁克大学的量子光学和量子信息研究所需要一个任意波形发生器&#xff08;AWG&#xff09;来为他们的研究生成各种各样的信号。01无线电频率第一个应用是在射…