最佳实践!用腾讯云AI语音合成打造自己的第一本有声书

2022-08-22 14:36:23 浏览数 (2)

现代生活中,我们不可避免会遇到很多碎片时间,等公交、倒地铁、排核酸、买早点等等。这些时间累积起来,无疑是一笔很大的个人资源,而想利用这部分时间,听显然是最好的方式。

国内云计算语音合成服务已经非常成熟,基于开源工具整合 TTS PaaS 服务,可以非常方便地打造一款个人定制的有声书制作工具。

好了,需求有了,可行性也没问题,开始搞起!Get hands dirty!

一、分析调研

有声书需求,一句话来讲就是,把电子书制作成有声音音频,并提供下载链接。

先细化下需求,拆解成不同过程,以及看看都需要哪些能力来支持:

  1. 我们根据个人喜好,下载好电子书(商用务必确保已获授权)。由于我之前用 Kindle,手上有大量 mobi 的电子书。
  2. 打开制作工具,上传指定的 mobi 电子书。(调研 Web 交互库)
  3. 制作工具需要先解析 mobi,获取其中的文本内容。(调研 mobi 解析库)
  4. 基于文本内容,调用语音合成服务,获取有声书音频内容。(调研云计算语音合成 PaaS 服务)
  5. 提供有声书音频下载。

经过一番调研,准备使用工具栈如下。

二、代码开发

工具栈到位,开始编码。

第一步:电子书文件解析

解析模块,先引入外部库 mobi,通过 mobi.extract 函数读取电子书文件,解析为 html 格式的文件 tmp_html。

mobi 库使用可以参见文档 mobi - library for unpacking mobi files

代码语言:javascript复制
import mobi
def load_file(self, file_name):
        logging.info('begin to parse file')
        start_t = time.time()
        tmp_dir, tmp_html = mobi.extract(file_name) # 解析 mobi 文件
        end_t = time.time()
        logging.info('extract {} to {}. cost {}ms'.format(file_name, tmp_html, int((end_t-start_t)*1000)))

        with open(tmp_html, 'r') as fp:
            lines = fp.readlines()
        self.html_content = ''.join(lines) # 读取 html 
        logging.info('load file total {} chars'.format(len(self.html_content)))

        shutil.rmtree(tmp_dir)
        logging.info('clean temp dir {}'.format(tmp_dir))

得到 html 文件后,通过 lxml.etree 将其解析为一棵 DOM 树,然后就可以通过 xpath 这个大杀器,可以得到其中想要的任意内容了。

比如特定属性的元素,特定位置的段落、标题等等,不了解的同学可以看下 XPath教程。

代码语言:javascript复制
from lxml import etree
def parse_html(self):
        logging.info('parse html')
        # pre process
        self.html_content = self.pre_process(self.html_content)
        
        # parse dom
        dom = etree.fromstring(self.html_content)
        plist = dom.xpath('//p/text()')
        audio_texts = []

		# 示例,比如从 1010 段开始,获取后面 10 个段落
        idx_start = 1010
        for p in plist[idx_start:idx_start 10]:
            #logging.info('{}'.format(p))
            audio_texts.append(p)
        
        self.text = ''.join(audio_texts)
        logging.info('content length {}'.format(len(self.text)))

以上就是电子书解析模块,封装在 AudioBookGenerator 类,详见 src/audio_book_generator.py。

第二步:有声语音合成

有声语音合成,需要基于第三方的语音合成 TTS 服务。调研了市面上常见云计算厂商 PaaS 服务后,决定采用腾讯云 TTS 服务。

我觉得比较好的点有三个

  1. 开发文档是基于开发者视角的,看起来非常顺畅。
  2. 长文本合成接口最长支持 10 万字,可以完整合成一个章节,适合合成有声书的场景,不用频繁拆分文本。
  3. 其中的智逍遥音色,以及旁对白支持的能力,适合小说场景;尤其是其中包含人物对话,旁白和对白分开后层次分明。

关于服务注册开通,官方文档写的很详细,就不赘述了,大家可以参见 腾讯云TTS。

服务开通后,在控制台打开 API 密钥管理页面,拷贝如下的访问密钥,配置到 config 文件中即可。

配置文件 src/config.py

class Config(object): SECRET_ID = 'XXXX' # 对应上面的 SecretId SECRET_KEY = 'XXXX' # 对应上面的 SecretKey

下面看看怎么使用官网提供的 SDK ,调用语音合成服务。

打开 长文本合成官方开发文档,滚动下下面,找到对应的 sdk,这里我们用 python sdk

集成 SDK 到我们的工程。

长文本合成是个异步服务,提供两个接口用于服务调用。

  1. 创建合成任务接口:CreateTtsTask
  2. 查询任务状态及结果接口:DescribeTtsTaskStatus

下面的 create_task 函数和 query_task 函数,分别针对这两个函数进行了封装。

需要注意的是,查询任务状态时,任务可能并未执行完成,所以需要间隔一段时间后循环查询,直到任务完成(成功或失败)。

创建任务:CreateTtsTask

调用时,要注意两个参数

  • VoiceType:音色 id,用于选择不同的发音人,这里使用之前调研时确定的智逍遥(100510000),感觉非常适合武侠或玄幻小说的场景
  • VoiceoverDialogueSplit:旁对白支持选项,需要设置为 True,可以将文本中的对话和旁白分割,并分别用对应的音色进行合成

请求成功后,返回该任务的唯一 ID:TaskId

代码语言:javascript复制
def create_task(self) -> str:
        task_id = ''

        req = models.CreateTtsTaskRequest()
        req.Text = self.text # 合成文本
        req.VoiceType = self.voice_type # 设置音色id,此处选用 智逍遥100510000
        req.VoiceoverDialogueSplit = self.voiceover_dialogue_split # 打开旁对白支持
        req.Codec = self.codec
        req.SampleRate = self.sample_rate
        req.ModelType = self.model_type
        try:
            resp = self.client.CreateTtsTask(req)
            task_id = resp.Data.TaskId
            req_id = resp.RequestId
            print('call CreateTtsTask succeed, task_id: {} request_id: {}'.format(task_id, req_id))
        except TencentCloudSDKException as err:
            print('call CreateTtsTask failed, err: {}'.format(str(err)))
        
        return task_id

查询任务状态及结果:DescribeTtsTaskStatus

调用是,将上面得到的 TaskId 作为参数传进去,请求会实时返回任务的相关信息,主要包含

  • Status:任务状态
  • ErrorMsg:任务错误信息(任务失败时)
  • ResultUrl:合成音频地址
代码语言:javascript复制
def query_task(self, task_id):
        req = models.DescribeTtsTaskStatusRequest()
        req.TaskId = task_id
        try:
            resp = self.client.DescribeTtsTaskStatus(req)
            data = resp.Data
            req_id = resp.RequestId
            print('call DescribeTtsTaskStatus succeed, data: {} request_id: {}'.format(str(data), req_id))
        except TencentCloudSDKException as err:
            print('call DescribeTtsTaskStatus failed, err: {}'.format(str(err)))

        if data:
            return data.Status, data.ErrorMsg, data.ResultUrl # 任务状态、错误信息、音频文件地址
        else:
            return 3, 'internal error', ''

以上就是有声书语音合成模块,封装在 TencentSDK 类,详见 src/tencent_sdk.py。

第三步:完成有声书制作脚本

通过 main 脚本,将上两步的电子书解析模块、语音合成模块集成到一起,再增加文件下载功能,即可完成有声书制作脚本。

腾讯云 TTS 服务返回的合成音频 url,新增 HttpAgent 类,将音频二进制文件下载到本地。

代码语言:javascript复制
from audio_book_generator import AudioBookGenerator
from http_agent import HttpAgent

def main():
    file_name = sys.argv[1]
    logging.info('upload file: {}'.format(file_name))

    # gen audio
    generator = AudioBookGenerator()
    generator.process(file_name)
    audio_url = generator.get_audio_url()
    logging.info('get audo url: {}'.format(audio_url))
    
    # download audio
    session_path = os.environ.get('SESSION_PATH', './')
    audio_name = os.path.join(session_path, 'result.mp3')
    agent = HttpAgent()
    agent.download(audio_url, audio_name)
    logging.info('download audio to: {}'.format(audio_name))

HttpAgent 详见文件 src/http_agent.py。

本地工具已完成,可以通过下列命令调用看下效果。

代码语言:javascript复制
(venv) justin@VM_centos:[~/audio_book/src]: python main.py ../dou.mobi 
2022-06-21 10:36:44,959 - main.py[line:13] - INFO: upload file: ../dou.mobi
2022-06-21 10:36:44,959 - /home/justin/audio_book/src/audio_book_generator.py[line:26] - INFO: begin to parse file
2022-06-21 10:36:47,253 - /home/justin/audio_book/src/audio_book_generator.py[line:30] - INFO: extract ../dou.mobi to /tmp/mobiexk287bwzw/mobi7/book.html. cost 2294ms
2022-06-21 10:36:47,293 - /home/justin/audio_book/src/audio_book_generator.py[line:35] - INFO: load file total 4988080 chars
2022-06-21 10:36:47,295 - /home/justin/audio_book/src/audio_book_generator.py[line:38] - INFO: clean temp dir /tmp/mobiexk287bwzw
2022-06-21 10:36:47,295 - /home/justin/audio_book/src/audio_book_generator.py[line:45] - INFO: parse html
2022-06-21 10:36:47,506 - /home/justin/audio_book/src/audio_book_generator.py[line:60] - INFO: content length 625
2022-06-21 10:36:47,549 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py[line:1005] - DEBUG: Starting new HTTPS connection (1): tts.tencentcloudapi.com:443
2022-06-21 10:36:47,699 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py[line:465] - DEBUG: https://tts.tencentcloudapi.com:443 "POST / HTTP/1.1" 200 125
2022-06-21 10:36:47,701 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/tencentcloud/common/http/request.py[line:112] - DEBUG: GetResponse Status: 200
Header: Server: nginx
Date: Tue, 21 Jun 2022 02:36:41 GMT
Content-Type: application/json
Content-Length: 125
Connection: keep-alive
Data: {"Response":{"RequestId":"ffb6f632-bd56-427d-ae21-xxxx","Data":{"TaskId":"gz-27ac44ab-c21e-4e58-b0b3-xxxx"}}}

call CreateTtsTask succeed, task_id: gz-27ac44ab-c21e-4e58-b0b3-xxxx request_id: ffb6f632-bd56-427d-ae21-xxxx

2022-06-21 10:37:27,964 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py[line:1005] - DEBUG: Starting new HTTPS connection (1): tts.tencentcloudapi.com:443
2022-06-21 10:37:28,016 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py[line:465] - DEBUG: https://tts.tencentcloudapi.com:443 "POST / HTTP/1.1" 200 576
2022-06-21 10:37:28,017 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/tencentcloud/common/http/request.py[line:112] - DEBUG: GetResponse Status: 200
Header: Server: nginx
Date: Tue, 21 Jun 2022 02:37:21 GMT
Content-Type: application/json
Content-Length: 576
Connection: keep-alive
Data: {"Response":{"RequestId":"7c4c20d3-ad79-47ea-86a8-xxxx","Data":{"TaskId":"gz-27ac44ab-c21e-4e58-b0b3-xxxx","Status":2,"StatusStr":"success","ResultUrl":"https://xxxx","ErrorMsg":""}}}

call DescribeTtsTaskStatus succeed, data: {"TaskId": "gz-27ac44ab-c21e-4e58-b0b3-xxxx", "Status": 2, "StatusStr": "success", "ResultUrl": "https://xxxx", "ErrorMsg": ""} request_id: 7c4c20d3-ad79-47ea-86a8-xxxx
2022-06-21 10:37:28,580 - /home/justin/audio_book/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py[line:465] - DEBUG: https://xxxx:443 "GET /xxxx HTTP/1.1" 200 535248
http download succ: https://xxxx -> ./result.mp3
2022-06-21 10:37:29,001 - main.py[line:26] - INFO: download audio to: ./result.mp3

可以正常生成音频文件 result.mp3。附录中有一个 demo 音频,大家可以听下,效果蛮不错。

第四步:脚本可视化

有声书制作脚本已完成,但脚本用起来还是不方便,而且没办法给他人使用。

这里需要对脚本进行可视化,将其部署为一个 Web 工具。

这里采用 Wooey 开源库,有如下优点:

  • 通过编译一个适配类,将脚本工具非常方便地转化为 Web 交互页面
  • 支持常见UI交互组件,如下拉框、文件上传等,通过代码配置的方式展示到页面上,无需任何前端知识
  • 支持任务启动、回显执行过程,结果文件下载等功能

适配类如下,通过 parseer 增加了文件上传组件

代码语言:javascript复制
import os
import sys
import argparse

parser = argparse.ArgumentParser(description="convert mobi file to audio")
parser.add_argument('--audio', help='the mobi file to make audio', type=argparse.FileType('r'), required=True) # 文件上传组件

def audio_book(mobi_file):
    _format = mobi_file.split('.')[-1].lower()
    if _format != 'mobi':
        print('only mobi is supported')
        return
    
    # TODO: 此处填写业务逻辑

if __name__ == '__main__':

    args = parser.parse_args()
    audio_book(args.audio.name)

调用电子书制作脚本工具,通过 python venv 方式,隔离 wooey 与 工具脚本的环境变量,方便 wooey 平台集成其他任意脚本。

代码语言:javascript复制
SCRIPT_PATH = '/root/audio_book'

def audio_book(mobi_file):
    # ...
    # TODO: 此处填写业务逻辑
    cmd = []
    cmd.append('export SESSION_PATH={}'.format(os.getcwd())) # 传输本次执行 session 路径到脚本
    cmd.append('cd {}'.format(SCRIPT_PATH))
    cmd.append('source {}/venv/bin/activate'.format(SCRIPT_PATH))
    cmd.append('cd src')
    cmd.append('python main.py {}'.format(mobi_file))
    cmd.append('cd ')
    cmd = '&&'.join(cmd)

    print(cmd)
    os.system(cmd)

添加脚本到可视化平台

[root@VM-centos ~/TOOLS]# python manage.py addscript ../audio_book/audio_book_adaptor.py --group 小工具 Converting ../audio_book/audio_book_adaptor.py Converted 0 scripts

所有工作已完成,让我们 enjoy 下效果。

三、产品体验

合成自己的第一本有声书

打开工具平台,选择有声书制作工具

点击【选择文件】按钮,上传想要转换的电子书文件

启动任务,从页面可以看到脚本执行日志

任务执行结束后,状态显示成功,可以从页面底部的文件列表中,点击 result.mp3 进行下载。

到此,整个有声书制作工具已经完成,试听音频以及工程代码放在附录中。

关于有声书的效果,感兴趣的同学可以从附录下载听下,个人觉得还蛮不错。

工程代码部分,基本上是开箱可用的,感兴趣的可以下载下来跑一下,也可以基于此增加自己需要的一些 feature。

好了,那就到这里 ~~

附录

  • 有声书声音效果试听:result.mp3
  • 有声书制作工程代码:GitHub - jizhouli/audio_book_generator

了解更多腾讯云AI语音合成产品信息:语音合成_语音定制_文本转语音服务 - 腾讯云

0 人点赞