在python中使用edge-tts将字幕合成配音并对齐-免费用
微软edge浏览器中有个大声朗读功能,在任何页面均可免费使用,效果也还不错。那么能不能在任意地方调用这个朗读功能,从而实现将自己的文本合成语音呢?自然是可以的,pip中有个edge-tts库,可以很方便的实现该功能。
在我的开源项目pyVideoTrans中也大量使用了该库以及字幕对齐相关处理,整理下以便记录。如果能帮到有需要的掘友,那就更棒了。 github.com/jianchang51…
首先配置 python3 和 ffmpeg
这是必须的哦,如果不会安装,可以看下该教程,和安装普通软件几乎一样,很容易.
安装 edge-tts 和 pydub 模块
安装好python3后,创建个空白英文目录,文件夹地址栏输入 cmd
回车,打开cmd窗口,先输入命令创建并激活虚拟环境
python -m venv venv && .\venv\scripts\activate
然后安装 edge-tts 和 pydub 库, pip install edge-tts pydub
pydub 是个很棒的音频处理库,可以方便的读取、处理音频数据
可以使用的声音角色
从edge浏览器大声朗读的语音选项中就能看到,支持的语言和角色非常多,每种语言至少都有2种以上不同角色
以下是中文语言下可用的角色名称
完整的语言支持列表和对应的角色名称,可直接打开该链接查看
编写代码将 txt 文件中的文本合成语音
将你想要合成的文本复制到 "mytext.txt" 中,注意要多分行哦,一行不要太长。
创建一个 start.py
文件,输入以下代码,实现按行合成,最后再使用 pydub
将语音片段连接
import edge_tts
import os
import time
from pydub import AudioSegment
from datetime import datetime
# 要合成语音的 txt文件名
file="./mytext.txt"
# 保存合成后的语音片段
merged_audio = AudioSegment.empty()
# 按行合成txt文件里文本
with open(file,'r',encoding='utf-8') as f:
for line in f.readlines():
if not line.strip():
continue
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural')
name=f'./tmp/{time.time()}.mp3'
communicate.save_sync(name)
merged_audio+=AudioSegment.from_file(name, format="mp3")
print(f'已合成文本:{line.strip()}')
# 合并结果为 output.mp3
merged_audio.export("output.mp3", format="mp3")
cmd窗口中输入 python start.py
执行一下,十秒左右即合成完毕。
调整语音参数实现不同效果:加减语速/加减音量/变化声调控制
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural')
这行代码是关键,line
里是要合成的文本,zh-CN-XiaoyiNeural
是角色名,你可以任意更换,除此参数外,还支持
rate语速:
加速使用 +百分比%
表示, 比如 +50%
表示语速增加50%,-50%
表示语速降低50%,+ -
加速符号不可缺少
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural',rate='+50%')
,在正常语速基础上加速50%播放
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural',rate='-50%')
,在正常语速基础上减速50%播放
volume音量:
同样以 +-百分比%
表示,-50%
表示音量降低50%,+50%
代表音量增加50%
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural',rate='+20%',volume='+150%')
语速增加20%,音量调高150%
pitch语调:
语调使用 +-数字Hz
表示,比如 +50Hz
,-50Hz
,音调改变将影响音色。
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural',rate='+20%',volume='+150%',pitch="+50Hz")
增加Hz音色将变的尖细,降低Hz将变的浑厚。
将srt字幕合成语音
如果你有srt格式的字幕,想直接将该字幕合成语音,而不是txt,继续往下看
先创建一个工具函数,用于读取srt字幕并转为列表。
def get_subtitle_from_srt(srtfile):
with open(srtfile, 'r', encoding='utf-8') as f:
content = f.read().strip().splitlines()
# remove whitespace
content=[c for c in content if c.strip()]
result = []
maxindex = len(content) - 1
# 时间格式
timepat = r'^\s*?\d+:\d+:\d+([\,\.]\d*?)?\s*?-->\s*?\d+:\d+:\d+([\,\.]\d*?)?\s*?$'
textpat = r'^[,./?`!@#$%^&*()_+=\\|\[\]{}~\s \n-]*$'
for i, it in enumerate(content):
# 当前空行跳过
if not it.strip():
continue
it = it.strip()
is_time = re.match(timepat, it)
if is_time:
# 当前行是时间格式,则添加
result.append({"time": it, "text": []})
elif i == 0:
# 当前是第一行,并且不是时间格式,跳过
continue
elif re.match(r'^\s*?\d+\s*?$', it) and i < maxindex and re.match(timepat, content[i + 1]):
# 当前不是时间格式,不是第一行,并且都是数字,并且下一行是时间格式,则当前是行号,跳过
continue
elif len(result) > 0 and not re.match(textpat, it):
# 当前不是时间格式,不是第一行,(不是行号),并且result中存在数据,则是内容,可加入最后一个数据
result[-1]['text'].append(it.capitalize())
# 再次遍历,去掉text为空的
result = [it for it in result if len(it['text']) > 0]
if len(result) > 0:
for i, it in enumerate(result):
srt={
"line":i+1,
"text":"\n".join(it['text']),
"time":it['time'].replace('.', ',').strip()
}
st,end=srt["time"].split('-->')
# 加入开始和结束时间毫秒
srt['start_time'] = int((datetime.strptime(st.strip(), '%H:%M:%S,%f') - datetime(1900, 1, 1)).total_seconds() * 1000)
srt['end_time'] = int((datetime.strptime(end.strip(), '%H:%M:%S,%f') - datetime(1900, 1, 1)).total_seconds() * 1000)
result[i]=srt
return result
这个函数读取srt字幕,并转为list,每个list元素都是一个dict字典,分别存储一条字幕的行号、文本、时间戳、开始和结束时间毫秒值。
再创建一个方法 srt_audio,用来将srt合成语音,和之前txt合成类似
def srt_audio(file):
# 保存合成后的语音片段
merged_audio = AudioSegment.empty()
with open(file,'r',encoding='utf-8') as f:
content=get_subtitle_from_srt(f.read())
for it in content:
communicate = edge_tts.Communicate(it["text"], 'zh-CN-XiaoyiNeural',rate='+50%',volume='+200%',pitch="+50Hz")
name=f'./tmp/{time.time()}.mp3'
communicate.save_sync(name)
merged_audio+=AudioSegment.from_file(name, format="mp3")
print(f'已合成文本:{it["text"]}')
merged_audio.export(f"{file}.mp3", format="mp3")
该方法接收一个srt字幕文件的地址参数,然后调用get_subtitle_from_srt函数转为list
使用方法:srt_audio("./zimu.srt")
即启动合成,将目录下的 zimu.srt 字幕合成语音文件,合成后的mp3文件名为zimu.srt.mp3
综合一下,自动根据txt或srt合成
只需要写一个启动函数,判断传入的文件后缀名,自动调用txt或srt合成方法
# 根据 file 格式调用对应合成方法
def main(file):
if file[-4:].lower()=='.txt':
txt_audio(file)
else:
srt_audio(file)
之前的txt合成方法修改为
# 将txt文件合成为语音
def txt_start(file):
# 保存合成后的语音片段
merged_audio = AudioSegment.empty()
with open(file,'r',encoding='utf-8') as f:
for line in f.readlines():
if not line.strip():
continue
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural',rate='+50%',volume='+200%',pitch="+50Hz")
name=f'./tmp/{time.time()}.mp3'
communicate.save_sync(name)
merged_audio+=AudioSegment.from_file(name, format="mp3")
print(f'已合成文本:{line.strip()}')
merged_audio.export("output.mp3", format="mp3")
调用方式
# 文件名
main("./mytext.txt")
main("./myzimu.srt")
到此可以实现txt文本文件和srt字幕文件使用edge-tts合成为音频文件了。
不过仔细对照字幕聆听你会发现,配音和字幕很可能无法对齐,这是为什么呢?其实很好理解,在合成语音时,并没有指定要合成为多少秒时长的音频,当然即使指定了也是无用。
有什么办法实现对齐呢?
将语音和字幕对齐
例如这条字幕
5
00:00:09,960 --> 00:00:10,960
最近也传回来
6
00:00:11,910 --> 00:00:13,180
许多过去难以拍摄到的照片
字幕时长为 1s,然而配音后时长为 1560ms,多了560ms
已合成line-5:最近也传回来,dub_time=1560ms
并且第6条字幕和第5条字幕之间存在 950ms 的间隔。该怎么办呢?最简单的方式就是强制加快语速对齐,并在适当的地方添加静音片段。
首先判断第一条字幕开始时间是否是0,如果不是,则在开头先添加静音片段,
然后在srt_audio
函数中,每条字幕合成完毕后,判断当前语音时长若大于原字幕时长,则强制加速配音到对齐,如果语音小于原时长,则在末尾添加差值的静音片段。
同时第二条字幕直到最后一条,分别判断当前字幕开始时间和前一条字幕结束之间是否存在时间间隔,如果存在,则添加静音片段。
修改后代码如下
def srt_audio(file):
# 保存合成后的语音片段
merged_audio = AudioSegment.empty()
content=get_subtitle_from_srt(file)
if content[0]['start_time']>0:
# 第一个开始时间不是0,添加静音片段
merged_audio+=AudioSegment.silent(duration=content[0]['start_time'])
for i,it in enumerate(content):
communicate = edge_tts.Communicate(it["text"], 'zh-CN-XiaoyiNeural',rate='+50%',volume='+100%',pitch="+0Hz")
name=f'./tmp/{time.time()}.mp3'
communicate.save_sync(name)
seg=AudioSegment.from_file(name, format="mp3")
# 原字幕时长
raw_duartion=it['end_time']-it['start_time']
#配音时长
dubb_time=len(seg)
print(f'{it["line"]}:配音时长:{dubb_time},原字幕时长:{raw_duartion}')
if dubb_time>raw_duartion:
# 配音时长大于字幕时长,强制加速播放到对齐
seg=precise_speed_up_audio(audio=seg,target_duration_ms=raw_duartion)
merged_audio+=seg
elif dubb_time<raw_duartion:
# 小于原时长,添加静音片段
merged_audio+=seg
merged_audio+=AudioSegment.silent(duration=raw_duartion - dubb_time)
# 距离前一个字幕结束有间隔,添加静音片段
if i>0 and it['start_time']>content[i-1]['end_time']:
merged_audio+=AudioSegment.silent(duration=it['start_time'] - content[i-1]['end_time'])
merged_audio.export(f"{file}.mp3", format="mp3")
def precise_speed_up_audio(*, audio=None, target_duration_ms=None, max_rate=100):
# 首先确保原时长和目标时长单位一致(毫秒)
current_duration_ms = len(audio)
# 计算速度变化率
speedup_ratio = current_duration_ms / target_duration_ms
if target_duration_ms <= 0 or speedup_ratio <= 1:
return audio
rate = min(max_rate, speedup_ratio)
# 变速处理
try:
fast_audio = audio.speedup(playback_speed=rate)
# 如果处理后的音频时长稍长于目标时长,进行剪裁
if len(fast_audio) > target_duration_ms:
fast_audio = fast_audio[:target_duration_ms]
except Exception:
fast_audio = audio[:target_duration_ms]
# 返回速度调整后的音频
return audio
这样就实现了srt字幕合成语音并对齐,虽然语速可能时快时慢。
完整代码
import edge_tts
import os,sys
import re
import time
from pydub import AudioSegment
from datetime import datetime
import asyncio
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
else:
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
if not os.path.isdir("./tmp"):
os.makedirs("./tmp",exist_ok=True)
# 读取srt字幕并返回整理好的list
def get_subtitle_from_srt(srtfile):
with open(srtfile, 'r', encoding='utf-8') as f:
content = f.read().strip().splitlines()
# remove whitespace
content=[c for c in content if c.strip()]
result = []
maxindex = len(content) - 1
# 时间格式
timepat = r'^\s*?\d+:\d+:\d+([\,\.]\d*?)?\s*?-->\s*?\d+:\d+:\d+([\,\.]\d*?)?\s*?$'
textpat = r'^[,./?`!@#$%^&*()_+=\\|\[\]{}~\s \n-]*$'
for i, it in enumerate(content):
# 当前空行跳过
if not it.strip():
continue
it = it.strip()
is_time = re.match(timepat, it)
if is_time:
# 当前行是时间格式,则添加
result.append({"time": it, "text": []})
elif i == 0:
# 当前是第一行,并且不是时间格式,跳过
continue
elif re.match(r'^\s*?\d+\s*?$', it) and i < maxindex and re.match(timepat, content[i + 1]):
# 当前不是时间格式,不是第一行,并且都是数字,并且下一行是时间格式,则当前是行号,跳过
continue
elif len(result) > 0 and not re.match(textpat, it):
# 当前不是时间格式,不是第一行,(不是行号),并且result中存在数据,则是内容,可加入最后一个数据
result[-1]['text'].append(it.capitalize())
# 再次遍历,去掉text为空的
result = [it for it in result if len(it['text']) > 0]
if len(result) > 0:
for i, it in enumerate(result):
srt={
"line":i+1,
"text":"\n".join(it['text']),
"time":it['time'].replace('.', ',').strip()
}
st,end=srt["time"].split('-->')
# 将时间字符串转换为毫秒
srt['start_time'] = int((datetime.strptime(st.strip(), '%H:%M:%S,%f') - datetime(1900, 1, 1)).total_seconds() * 1000)
srt['end_time'] = int((datetime.strptime(end.strip(), '%H:%M:%S,%f') - datetime(1900, 1, 1)).total_seconds() * 1000)
result[i]=srt
print(result)
return result
# 加快语速
def precise_speed_up_audio(*, audio=None, target_duration_ms=None, max_rate=100):
# 首先确保原时长和目标时长单位一致(毫秒)
current_duration_ms = len(audio)
# 计算速度变化率
speedup_ratio = current_duration_ms / target_duration_ms
if target_duration_ms <= 0 or speedup_ratio <= 1:
return audio
rate = min(max_rate, speedup_ratio)
# 变速处理
try:
fast_audio = audio.speedup(playback_speed=rate)
# 如果处理后的音频时长稍长于目标时长,进行剪裁
if len(fast_audio) > target_duration_ms:
fast_audio = fast_audio[:target_duration_ms]
except Exception:
fast_audio = audio[:target_duration_ms]
# 返回速度调整后的音频
return audio
# 将txt文件合成为语音
def txt_audio(file):
# 保存合成后的语音片段
merged_audio = AudioSegment.empty()
with open(file,'r',encoding='utf-8') as f:
for line in f.readlines():
if not line.strip():
continue
communicate = edge_tts.Communicate(line, 'zh-CN-XiaoyiNeural',rate='+50%',volume='+200%',pitch="+50Hz")
name=f'./tmp/{time.time()}.mp3'
communicate.save_sync(name)
merged_audio+=AudioSegment.from_file(name, format="mp3")
print(f'已合成文本:{line.strip()}')
merged_audio.export(f"{file}.mp3", format="mp3")
def srt_audio(file):
# 保存合成后的语音片段
merged_audio = AudioSegment.empty()
content=get_subtitle_from_srt(file)
if content[0]['start_time']>0:
# 第一个开始时间不是0,添加静音片段
merged_audio+=AudioSegment.silent(duration=content[0]['start_time'])
for i,it in enumerate(content):
communicate = edge_tts.Communicate(it["text"], 'zh-CN-XiaoyiNeural',rate='+50%',volume='+100%',pitch="+0Hz")
name=f'./tmp/{time.time()}.mp3'
communicate.save_sync(name)
seg=AudioSegment.from_file(name, format="mp3")
# 原字幕时长
raw_duartion=it['end_time']-it['start_time']
#配音时长
dubb_time=len(seg)
print(f'{it["line"]}:配音时长:{dubb_time},原字幕时长:{raw_duartion}')
if dubb_time>raw_duartion:
# 配音时长大于字幕时长,强制加速播放到对齐
seg=precise_speed_up_audio(audio=seg,target_duration_ms=raw_duartion)
merged_audio+=seg
elif dubb_time<raw_duartion:
# 小于原时长,添加静音片段
merged_audio+=seg
merged_audio+=AudioSegment.silent(duration=raw_duartion - dubb_time)
# 距离前一个字幕结束有间隔,添加静音片段
if i>0 and it['start_time']>content[i-1]['end_time']:
merged_audio+=AudioSegment.silent(duration=it['start_time'] - content[i-1]['end_time'])
merged_audio.export(f"{file}.mp3", format="mp3")
# 根据 file 格式调用对应合成方法
def main(file):
if file[-4:].lower()=='.txt':
txt_audio(file)
else:
srt_audio(file)
# 文件名
file="./1.srt"
main(file)
注意:edge-tts使用 asyncio 异步方式发送请求,虽提供了 save_sync 同步,但仍可能出现"event close"报错,为避免报错,在代码开头加入了 asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
有用的资源
edge-tts 还支持流式输出,感兴趣的可以去研究研究
edge-tts: github.com/rany2/edge-…
pydub: github.com/jiaaro/pydu…
转载自:https://juejin.cn/post/7386476988879913000