现代生活中,我们不可避免会遇到很多碎片时间,等公交、倒地铁、排核酸、买早点等等。这些时间累积起来,无疑是一笔很大的个人资源,而想利用这部分时间,听显然是最好的方式。
国内云计算语音合成服务已经非常成熟,基于开源工具整合 TTS PaaS 服务,可以非常方便地打造一款个人定制的有声书制作工具。
好了,需求有了,可行性也没问题,开始搞起!Get hands dirty!
一、分析调研
有声书需求,一句话来讲就是,把电子书制作成有声音音频,并提供下载链接。
先细化下需求,拆解成不同过程,以及看看都需要哪些能力来支持:
- 我们根据个人喜好,下载好电子书(商用务必确保已获授权)。由于我之前用 Kindle,手上有大量 mobi 的电子书。
- 打开制作工具,上传指定的 mobi 电子书。(调研 Web 交互库)
- 制作工具需要先解析 mobi,获取其中的文本内容。(调研 mobi 解析库)
- 基于文本内容,调用语音合成服务,获取有声书音频内容。(调研云计算语音合成 PaaS 服务)
- 提供有声书音频下载。
经过一番调研,准备使用工具栈如下。
二、代码开发
工具栈到位,开始编码。
第一步:电子书文件解析
解析模块,先引入外部库 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 服务。
我觉得比较好的点有三个
- 开发文档是基于开发者视角的,看起来非常顺畅。
- 长文本合成接口最长支持 10 万字,可以完整合成一个章节,适合合成有声书的场景,不用频繁拆分文本。
- 其中的智逍遥音色,以及旁对白支持的能力,适合小说场景;尤其是其中包含人物对话,旁白和对白分开后层次分明。
关于服务注册开通,官方文档写的很详细,就不赘述了,大家可以参见 腾讯云TTS。
服务开通后,在控制台打开 API 密钥管理页面,拷贝如下的访问密钥,配置到 config 文件中即可。
配置文件 src/config.py
class Config(object): SECRET_ID = 'XXXX' # 对应上面的 SecretId SECRET_KEY = 'XXXX' # 对应上面的 SecretKey |
---|
下面看看怎么使用官网提供的 SDK ,调用语音合成服务。
打开 长文本合成官方开发文档,滚动下下面,找到对应的 sdk,这里我们用 python sdk
集成 SDK 到我们的工程。
长文本合成是个异步服务,提供两个接口用于服务调用。
- 创建合成任务接口:CreateTtsTask
- 查询任务状态及结果接口: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:合成音频地址
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语音合成产品信息:语音合成_语音定制_文本转语音服务 - 腾讯云