中文小说 tts 生成音视频
一、背景
平时里工作学习都用眼过度,一直喜欢听什么东西来填补碎片化的时间。除了播客、有声书等他人产出,有时候自己可以找到喜欢的文本作品,也希望可以简单的生成音频来听,所以有了这个项目。
GitHub - Wizna/txt2audio: transform txt book to audio book
二、方案
-
整个项目分为几个部分:
-
cli 交互部分
-
小说文本处理
-
tts
-
音频转视频
-
-
最终产物示例
2.1 cli 交互部分
-
用户首先进入项目目录,然后通过类似如下的指令开启交互:
python3 src/transform_to_audio.py demo/《英雄志》(校对第1-22卷)作者:孙晓.txt
,其中第二个输入即我们的目标小说文件路径 -
输入完成后,代码即会读取目标小说文件,然后处理看一共有多少章节并标号
-
之后会有 prompt 提示输入需要 tts 的章节,支持 e.g.
3-10
来 tts 第 3 到第 10 个章节 -
代码会自动检测已经产生过的内容(在 output 路径下)然后跳过,其它则正常生成
2.2 小说文本处理
-
会对常见的 “序言”,“楔子”, “后记” 等做处理,作为单独的章节 special_delimiter
-
目前分章节方式只有识别 “卷”,“章”,目前小说(来源 精校吧 )基本上使用这两者,后续也会考虑支持更多分段关键字
-
标志每一个章节的其实是一个 string list: [book_name, 第 x 卷, 第 y 章, special_delimiter]
2.3 tts
-
识别出的每一个章节会分片进行 tts,每片 6300 个汉字,大约 27 min (目前快手有 30 分钟的视频上传限制)
-
每片会进行句子分割(根据标点符号),保证每个单句长度相近,30 字左右,此时 tts 语速、效果比较好
-
def split_long_sentences(input_str, model_limit=30) -> List[str]: if not input_str: return [] pieces = math.ceil(len(input_str) / model_limit) character_for_each_piece = len(input_str) // pieces candidates = re.split(r'([,。?!:“”])', input_str) result = [] current_s = [] for v in candidates: current_s.append(v) if not v or v in ',。?!:“”': continue possible = ''.join(current_s) if len(possible) > character_for_each_piece: if len(current_s) > 1: result.append(''.join(current_s[:-1])) current_s = [v] else: result.append(v) current_s = [] if current_s: result.append(''.join(current_s)) return result
-
目前使用的是 coqui tts 的 xtts_v2, 多语言,后续可扩展英语 tts
- 结果保存本地 wav
2.4 音频转视频
- tts 结果还是希望分发出去让别人也可以利用(毕竟是我的 mac 辛辛苦苦运行出来的),所以 wav 转换成 mp4 方便分发
-
每一本书希望封面一致,所以底色是通过书名 hash 得到
-
def get_color_from_text(s, lightness=127): value = int(hashlib.sha1(s.encode("utf-8")).hexdigest(), 16) r = value % lightness value //= lightness g = value % lightness value //= lightness b = value % lightness return r, g, b
-
之后就是简单的 textbbox 写上书名、作者、章节等信息
-
def draw_underlined_text(draw, pos, text, font, **options): (left, top, right, bottom) = draw.textbbox(xy=(0, 0), text=text, font=font) text_width = right - left text_height = bottom - top lx, ly = pos[0], pos[1] + text_height + 20 draw.text(pos, text, font=font, **options) draw.line((lx, ly, lx + text_width, ly), width=4, **options) def create_image_from_text(number, toc, audio, max_w=720, max_h=1280): r, g, b = get_color_from_text(s=toc.split('/')[0]) img = Image.new('RGB', (max_w, max_h), color=(r, g, b)) font = ImageFont.truetype( f'{os.path.dirname(__file__)}/../resources/YunFengFeiYunTi-2.ttf', 80) smaller_font = ImageFont.truetype( f'{os.path.dirname(__file__)}/../resources/YangRenDongZhuShiTi-Extralight-2.ttf', 70) number_font = ImageFont.truetype(f'{os.path.dirname(__file__)}/../resources/DTM-Mono-1.otf', 40) d = ImageDraw.Draw(img) current_h, pad = 200, 40 for idx, sub_para in enumerate(toc.split('/')): sub_para = re.sub(r'(.+)', ' ', sub_para) for line in sub_para.split(' '): line = line.strip() if not line: continue selected_font = font if idx == 0 else smaller_font (left, top, right, bottom) = d.textbbox(xy=(0, 0), text=line, font=selected_font) w = right - left h = bottom - top d.text(((max_w - w) / 2, current_h), line, font=selected_font) current_h += h + pad (left, top, right, bottom) = d.textbbox(xy=(0, 0), text=f'{number}', font=number_font) w = right - left h = bottom - top draw_underlined_text(d, ((max_w - w) / 2, max_h - 300), f'{number}', font=number_font) result = f'{os.path.dirname(audio)}/cover.jpg' img.save(result) return result
- 最后通过 subprocess 使用 ffmpeg 转化成视频
ffmpeg -loop 1 -i {image} -i {audio} -c:v libx264 -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest {video_path} && rm -f {audio}
三、总结
-
项目并不复杂,目前也没涉及 tts model 的 fine-tuning,未来提升质量应该会从数据集、模型上继续下功夫
-
整体看还是很有价值的,没事可以听一听