以下是「 豆包MarsCode 体验官」优秀文章,作者X2046。
我们都知道外网上有很多优秀的视频教程平台,比如 Coursera 和 deeplearning.ai。尤其是后者,由吴恩达老师与OpenAI、Langchain、LlamaIndex、AutoGen等公司和作者合作,推出了一系列广受好评的LLM教程,如Prompt Engineering、Langchain教程、LlamaIndex教程和AutoGen教程。deeplearning.ai 的课程紧跟时下热点,是大语言模型爱好者和从业者不可或缺的资源。然而,deepleaning.ai 的课程通常没有中文字幕,这无疑提高了学习的门槛。即使有些同学坚持学习,也可能因为语言障碍只能学到皮毛。我肝了4天,我成功地将这些课程转换成流畅自然的普通话。话不多说,让我们直接看看下面的效果视频。
https://www.ixigua.com/7386982418232574464?utm_source=iframe_share
下面我将详细介绍实现的过程。本文以 deeplearning.ai 上的 ChatGPT Prompt Engineering for Developers 课程为例,通过下载视频和字幕、使用LLama3和反思策略翻译字幕、然后使用 ChatTTS 将字幕转换为流畅的普通话,最终通过 FFmpeg 将字幕、音频和视频合并在一起。你或许会有疑问为什么我的TTS说话如此流畅,音色如此统一?且听我娓娓道来。如何下载视频和字幕文件不在本文讨论范围,本文仅作为教育用途。
注:本文中有很多关于字幕处理和音视频处理的库都是与MarsCode交互得知,我本人对字幕和音视频处理并不是很了解~,如果其中对于音视频处理有疑问的地方,还望指出,感谢~
1. ChatTTS
ChatTTS最近开源后引起了广泛关注,相信大家已经有所耳闻。我在周末简单学习了一下,发现其使用非常简单。你可以先去官网chattts体验一下生成的语音,非常自然和流畅,可以说非常丝滑自然了。
按照官方ChatTTS安装,注意你可能需要使用conda或者venv提前建立虚拟环境,此处按下不表。
pip install git+https://github.com/2noise/ChatTTS
如果希望使用WebUI,可以按照官方说明文档启用python examples/web/webui.py。这里介绍的是如何通过编程方式使用ChatTTS。首先,我们导入必要的库并初始化 ChatTTS 实例,然后加载模型,并使用 ChatTTS 将文本转换为语音,最后保存生成的音频文件。
import ChatTTS
import torch
import torchaudio
chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance
texts = ["你好,我是X二零四六,欢迎关注LLM深潜:Agent框架与应用揭秘"]
wavs = chat.infer(texts)
torchaudio.save("output1.wav", torch.from_numpy(wavs[0]), 24000)
注意:不要使用官网的教程,API已经落后无法运行了,以github官方repo为准。
但是我的Apple M1使用torchaudio.save会报错,如下所示。
把上述错误拷贝给MarCode进行一番交流,终于解决了运行错误。但在与MarsCode交流中,我觉得MarsCode对于指令的跟随性不太好,我需要的是mp3,结果给出的代码是存储为wav文件。
此外,我觉得还可以改进的地方是上下文,看起来它已经忘记我使用torchaudio报错的,这仅仅只有3轮对话。
后来我要求使用pydup实现将numpy array存储mp3,MarCode给出一版初稿,然后我运行报错,再次贴报错给它,最终给出如下代码。此段代码,可以运行,但是生成的mp3没有声音。于是我查询资料得知,它在将数组归一化之后量化到int16时,没有将数据缩放导致没有声音。加入numpy_array = (numpy_array * 32767).astype(np.int16)
如下代码即可,但生成的MP3听起来很快,按照官方教程中将frame_rate修改为24000,语音变得正常。
最终采用的是pydup的方法,得到了可运行的numpy_array_to_mp3
函数,可以将numpy数组转换成mp3,需要安装pydub。
def numpy_array_to_mp3(numpy_array, output_file):
# 将 numpy 数组规范化到 [-1, 1] 范围
numpy_array = np.clip(numpy_array, -1, 1)
numpy_array = (numpy_array * 32767).astype(np.int16)
# 将 numpy 数组转换为 int16 类型的 WAV 格式字节流
byte_stream = numpy_array.tobytes()
# 创建一个新的 AudioSegment 对象
audio_segment = AudioSegment(
data=byte_stream,
sample_width=2,
frame_rate=24000,
channels=1
)
# 将 AudioSegment 对象保存为 MP3 文件
audio_segment.export(output_file, format="mp3")
新的运行代码如下:
mp3_filename = "output_audio1.mp3"
numpy_array_to_mp3(wavs[0], mp3_filename)
生成的语音极为流畅,你可以听听看。
<<掘金无法添加音频,抱歉>>
如果你亲自尝试,你会发现为什么我的音色不固定啊,我这一长串句子怎么一会这个人一会儿那个人,甚至是一会儿语气大一会儿语气小。音色就像抽卡一样,好在已经有大神对音色评测并开源HuggingFace,如下图所示。你可以根据需要过滤音色,选择合适的seed_id进行试听,如果满意,可以下载pt文件来固定音色。
根据我个人测试,seed_1332发音语气平稳、中英文穿插也能很好的合成,在长句合成上也未见到音色切换,推荐大家使用,上面那段欢迎公众号的音频就是来自seed_1332。但是现在新发布的ChatTTS已经不支持直接采用pt权重文件固定音色了,如何办呢?
我在issue中看到有人提到这个PRhttps://github.com/2noise/ChatTTS/pull/463提交导致了不再支持直接加载,再次经过与MarsCode一番友好交流得到如下回答,在编辑这个提示时候,MarsCode无法使用Shift Enter
换行,不知道怎么换行。
上述代码运行后依然会报错,RuntimeError: The expanded size of the tensor (768) must match the existing size (1536) at non-singleton dimension 2. Target sizes: [1, 35, 768]. Tensor sizes: [1, 1, 1536]
。把这段报错给到MarsCode它无法解决,从这个错误来看,其实我们只需要将tensor转换为FP16即可,spk_emb_np = spk_emb.cpu().numpy().astype(np.float16)
。经过修改后,得到如下函数tensor_to_str
。
def tensor_to_str(spk_emb):
# 将Tensor转换为NumPy数组
spk_emb_np = spk_emb.cpu().numpy().astype(np.float16)
# 将NumPy数组编码为字符串
spk_emb_str = b14.encode_to_string(lzma.compress(spk_emb_np, format=lzma.FORMAT_RAW, filters=[
{"id": lzma.FILTER_LZMA2, "preset": 9 | lzma.PRESET_EXTREME}]))
return spk_emb_str
所以在下载好音色之后,我们就可以通过如下方式加载pt文件来固定音色。
spk = torch.load("asset/seed_1332_restored_emb.pt", map_location=torch.device('cpu')).detach()
spk_emb_str = tensor_to_str(spk)
print(spk_emb_str) # save it for later timbre recovery
params_infer_code = ChatTTS.Chat.InferCodeParams(
spk_emb=spk_emb_str, # add sampled speaker
temperature=.0003, # using custom temperature
top_P=0.7, # top P decode
top_K=20, # top K decode
)
text = "你好,我是X二零四六,欢迎关注LLM深潜:Agent框架与应用揭秘"
wavs = chat.infer([text], use_decoder=True, params_infer_code=params_infer_code)
这就是ChatTTS的基本使用和音色固定。接下来,我们看看如何使用LLM翻译字幕。
2. 翻译字幕文件
我原本采用的是通义千问大模型,只是在6月29号免费的Token到期了。幸运的是,我还可以使用Groq提供的免费Llama3模型。字幕文件是WEBVTT(Web Video Text Tracks)格式,常用于web前端加载。而在本地使用FFmpeg合并时,我们通常采用SRT(SubRip Subtitle)格式,这是一种常见的字幕文件格式,用于存储字幕文本及其时间信息。SRT文件通常是纯文本文件,使用特定的格式来标记每个字幕条目。两种格式的主要区别在于时间戳格式。
WEBVTT格式:
WEBVTT
1
00:00:02.080 --> 00:00:05.660
Retrieval Augmented Generation, or RAG, has become a key
SRT格式:
1
00:00:02,080 --> 00:00:05,660
Retrieval Augmented Generation, or RAG, has become a key
根据豆包MarsCode 沟通得知使用FFmpeg可将WEBVTT字幕文件转换为SRT文件:
ffmpeg -i subtitle.vtt subtitle.srt
但是我发现转换后第一行字幕被移除了,这是因为VTT文件的第一行包含"WEBVTT",被识别为非字幕块。移除这一行后转换就可以正常进行。不过,MarsCode告诉我可以使用pysrt直接转换,看起来更加简单,也不需要使用FFmpeg这种重量级工具。
pysrt.open(vtt_path, encoding="utf-8").save(srt_path, encoding="utf-8")
虽然pysrt可以直接转换,但是第一行依然会被移除。因此,我们需要编写代码移除第一行WEBVTT字幕,使用MarsCode自动编码,写出函数名然后编写comment,他就会自动提示,此时我们只需要按Tab键即可将这段自动生成代码输入。可以看出对于这种比较简单的功能,生成的代码基本不用考虑再次修改,基本上随着comment输入完成,代码自动生成就会提示,速度挺快的。
字幕文件已经转换完成,接下来需要考虑如何翻译字幕。通过pysrt可以逐行读取并翻译,但测试发现这种方法的翻译质量较差,因为一个字幕块通常不是完整的句子。例如:
3
00:00:11,224 --> 00:00:14,627
teach this along with me. She
is a member of
最初,我尝试逐行将字幕输入LLM进行翻译,但结果并不理想。翻译内容缺乏上下文且质量较差,甚至会拒绝翻译,而且耗时也较长,需要多次往返请求。接着尝试将字幕合并为完整的句子,即遇到句号、感叹号、问号等标点符号时将其与前面的字幕合并,并更新字幕的起始时间戳。但发现一个字幕可能长达几分钟,原本的119条字幕最终合并成了16条,而且每条字幕并可能包含多个完整的句子。所以这种方法也不可取,最终的解决方案是提取几十条完整的字幕,包括时间戳信息,设置字符数限制(如500字符),最好不要超过模型的最大字符限制。遇到标点符号时将这一组字幕发送给LLM翻译,这样翻译出的句子非常通顺。有时LLM不会按照字幕格式返回翻译结果,解决方法是提供一个示例。以下是我设定的Prompt,仅供参考。如需更多Prompt指南,请查看我之前的总结文章《敲黑板!吴恩达LLM Agent工作流Prompt精华全解析》。
content="""你是一个专业的翻译官,尤其擅长科技类中英文翻译, 必须保持原本的字幕格式,要求保持精简,不要肆意添加东西,不要漏翻。
比如:
原文:
1
00:00:03,270 --> 00:00:06,292
how to set up both a basic and advanced RAG pipeline
翻译:
1
00:00:03,270 --> 00:00:06,292
如何设置基本和高级的RAG管道
"""
注意Prompt加入了必须这个词,要求保持字幕样式,还有要求保持精简,不要肆意添加东西,也不要漏翻。这是因为我发现有时候翻译出的中文太啰嗦了,导致后面使用chattts合成的音频要比原视频多出几分钟,这是无法接受的。
这样我们就能得到类似如下的中文字幕。
1
00:00:04,760 --> 00:00:08,162
欢迎来到这门关于ChatGPT的课程
开发者Prompt Engineering。
2
00:00:08,162 --> 00:00:11,224
我很高兴与
Isa Fulford一起教学。
但依然不够,现在的翻译略显生硬,如何能让翻译过来的字母更地道呢?我们知道吴恩达老师前段时间开源了一个translate_agent,代码较为简单,它采用了反思工作流。
- 初始翻译
- 反思
- 根据反思内容修改翻译
但我的内容是字幕,因此对于翻译本身还有要求。因此我参考吴恩达老师的反思策略写了translator_agent
。
def translate_agent(source_text):
translation_1 = initial_translation(
source_text
)
reflection = reflect_on_translation(
source_text, translation_1
)
translation_2 = improve_translation(
source_text, translation_1, reflection
)
return translation_2
最早的Prompt设定,经常出现几个连续字幕是重复的内容,就是通过反思他也只是删除重复内容,但不能让字幕在整句翻译的时候考虑字幕块的分布问题,所以Prompt在初始翻译的设定要考虑到这一点。
system_message = f"你是一个专业的翻译官,尤其擅长科技类英文到中文翻译。"
translation_prompt = f"""你的任务是将英文字幕翻译为中文字幕, 必须保持原本的字幕格式,要求保持精简,翻译字幕和要与原文基本一致,不要肆意添加任何解释或者其他文字,不要漏翻。
字幕中一句话通常分布在几个字幕块中,翻译时候,务必整句翻译,并将其按照原格式分布在多个字幕块,保持字符数量和字幕时间段一致。
举个例子:
英文字幕:
8
00:00:25,514 --> 00:00:29,257
She's also
contributed to the OpenAI Cookbook that
9
00:00:29,257 --> 00:00:30,958
teaches people prompting.
So thrilled
10
00:00:30,958 --> 00:00:32,660
to have you with me.
中文字幕:
8
00:00:25,514 --> 00:00:29,257
她还为OpenAI Cookbook做出了贡献,
9
00:00:29,257 --> 00:00:30,958
专门教人们如何进行prompting。很高兴
10
00:00:30,958 --> 00:00:32,660
你能与我一起。
开始你的翻译:
英文字幕: {source_text}
中文字幕:"""
正如上文《敲黑板!吴恩达LLM Agent工作流Prompt精华全解析》对Prompt解析提到,在初始翻译的Prompt设定中仍然是按照任务说明,输入输出说明,样例和输入几个要素进行设定。我们再看反思Prompt设定,反思Prompt在设计的时候,需要输入源字幕和初始翻译字幕,然后要求LLM提出具体的建议,并给出这些建议可以考虑的方法等等。
system_message = f"你是一名翻译专家,专门从事从英文字幕到中文字幕的翻译工作。你将得到一段源文本及其翻译,你的任务是改进这段翻译。"
reflection_prompt = f"""你的任务是仔细阅读从英文字幕翻译为中文字幕,然后提出建设性的批评和有帮助的建议,以改进翻译。
源文本和初始翻译如下,以XML标签<SOURCE_TEXT></SOURCE_TEXT>和分隔:
<SOURCE_TEXT>
{source_text}
</SOURCE_TEXT>
<TRANSLATION>
{translation}
</TRANSLATION>
在写建议时,请注意是否有以下改进翻译的方法:
(i) 准确性(通过纠正添加错误、误译、遗漏或未翻译的文本),
(ii) 流畅性(应用中文的语法、拼写和标点规则,确保没有不必要的重复),
(iii) 风格(确保翻译反映源文本的风格,并考虑任何文化背景),
(iv) 术语(确保术语使用一致,并反映源文本的领域;并确保仅使用等效的中文术语)。
(v) 针对字幕本身具有时效性,切勿增长单个字幕块的内容长度,保持前后字幕连贯。
写出一份具体、有帮助和建设性的建议清单,以改进翻译。
每个建议应解决翻译中的一个特定部分。
只输出建议,不要包含其他内容。
"""
下面是LLM提出的反思建议,你可以看到反思的建议非常具体,而且更为地道和流畅。
最后是根据反思的建议进行改进的的Prompt设定,它的系统Prompt设定不变,但是要求按照专家的建议进行改进,我还告诉他干得好,奖励5000美元(不知道人民币有没有效果😅)。在改进过程中,我经常看到它会把移除的字幕换成提示语(remove duplicate),所以我们在Prompt种要求在无需改进时候就返回原始翻译或者移除的情况下就展示为空等。
system_message = f"你是一个专业的翻译官,尤其擅长科技类英文到中文翻译。"
prompt = f"""你的任务是仔细阅读并编辑从英文字幕到中文字幕的翻译,考虑专家的建议和建设性的批评。
针对字幕本身具有时效性,必须考虑原始英文字幕和中文字幕块的一致性,不要导致字幕本身的时效性变化,不要增加单个字幕块的内容长度,务必保持前后字幕连贯,前后字幕不要重复翻译!!!干得好,奖励5000美元。
源英文字幕、初始翻译和专家语言学家的建议如下,以XML标签<SOURCE_TEXT></SOURCE_TEXT>、和<EXPERT_SUGGESTIONS></EXPERT_SUGGESTIONS>分隔:
<SOURCE_TEXT>
{source_text}
</SOURCE_TEXT>
<TRANSLATION>
{translation}
</TRANSLATION>
<EXPERT_SUGGESTIONS>
{reflection}
</EXPERT_SUGGESTIONS>
请在编辑翻译时考虑专家的建议。编辑翻译时请确保:
(i) 准确性(通过纠正添加错误、误译、遗漏或未翻译的文本),
(ii) 流畅性(应用中文的语法、拼写和标点规则,确保没有不必要的重复),
(iii) 风格(确保翻译反映源文本的风格,并考虑任何文化背景),
(iv) 术语(确保术语使用一致,并反映源文本的领域;并确保仅使用等效的中文术语)。
(v) 针对字幕本身具有时效性,切勿增长单个字幕块的内容长度,保持前后字幕连贯。
只输出新的字幕翻译,不要包含其他内容,如果无需修改,返回原始翻译,如果移除内容,就展示为空,不要有任何提示内容。
"""
最后,可以看到每个Prompt中对于输入或者输出都是采用了明确的说明以此提示大模型。另外Prompt的设定并不是一簇而就,而是经过不断的改进达到要求。
3. 生成普通话音频文件
我们已经知道pysrt可以处理字幕文件,接下来可以使用pysrt读取中文字幕,并遍历调用ChatTTS的infer接口生成音频,如在第一节所示。音频的连接我们采用pydup工具,它支持插入静音片段和使用+号进行连接,也可以直接读取mp3、wav等文件。尽管我没有深入研究其全部功能,但主要是在MarsCode上生成的代码基础上进行了一些修改。
full_audio = AudioSegment.silent(duration=0)
subtitles = pysrt.open(subtitle_path, encoding="utf-8")
for i, subtitle in enumerate(subtitles):
text = subtitle.text
start_time = time_to_ms(subtitle.start)
end_time = time_to_ms(subtitle.end)
duration_ms = end_time - start_time
wavs = chat.infer([text.replace('\n', ' ')], use_decoder=True, params_infer_code=params_infer_code)
mp3_filename = f"temp_{i}.mp3"
wav_to_mp3(mp3_filename, wavs[0])
speech = AudioSegment.from_mp3(mp3_filename)
full_audio += speech
虽然看起来比较简单,但实际生成的音频和字幕大部分都无法匹配。需要处理诸如空字幕、开始前后加入静音片段。最主要的问题是合成的音频长度和字幕不匹配,这意味视频和音频也不会匹配,这是一个严重影响体验的问题。与MarsCode沟通得知,FFmpeg中大约有几种方法,其中asspeed我们不考虑,第2个方法的输入源必须是视频,因此也不考虑。
- 使用 atempo 过滤器,atempo 过滤器用于调整音频的播放速度而不改变音调,它可以用来加速或减速音频。
<<掘金无法添加音频,抱歉>>
- 改变采样率,通过改变音频的采样率,可以达到加速或减速音频的效果。然而,这种方法会改变音调,所以通常不推荐直接使用这种方法。
<<掘金无法添加音频,抱歉>>
经过测试,我感觉第一种使用atempo过滤器的方法效果最好。因此,我要求MarsCode使用pydup实现第一种方法如下,最好将倍速调整到0.5-2.0之间,超过2.0之后基本听不太清楚了。
def change_audio_speed(audio, speed=1.0):
# 调整音频的帧率以改变播放速度
audio_with_altered_frame_rate = audio._spawn(audio.raw_data, overrides={
"frame_rate": int(audio.frame_rate * speed)
})
# 将帧率恢复到正常,以便播放音频
return audio_with_altered_frame_rate.set_frame_rate(audio.frame_rate)
因此,在chat生成的speech之后,通过计算audio的长度和字幕长度来确定倍速或者增加静默时间,从而提升体验。其中使用MarsCode这个生成倍速的代码时候,我发现MarsCode对于上下文的指令跟随略差,还有幻觉API产生。pydup没有speedup方法,但它反复提供这种方法的代码,即使我告诉它speedup方法,它依然坚持自我。
4. 合并视频、字幕和普通话音频文件
既然我们已经生成了中文字幕和普通话音频,最后一步就是通过FFmpeg将所有文件合并,生成最终的视频。在反复询问MarsCode后,我了解到由于字幕的合并需要重新编码视频,因此最终FFmpeg命令如下:
ffmpeg -y -i "input_video.mp4" -i "translated_audio.mp3" -vf "subtitles=subtitle.srt:force_style='PlayResX=640,PlayResY=360,MarginV=10,Alignment=2',setpts=PTS+1.0/TB" -c:v libx264 -c:a aac -strict experimental -map 0:v -map 1:a "output_video.mp4"
5. 总结
总结来说,我们介绍了如何使用 ChatTTS 进行语音合成、如何使用反思策略使用LLM 更好地翻译出地道流畅的字幕,以及如何调整 ChatTTS 生成的音频以更好地适配视频时间。在此过程中,我们使用MarsCode辅助编程调试代码,自动生成代码,帮助我减少了不少代码的编写,同时在我不了解的字幕和音视频处理领域也能提供不少新的见解。但我感觉MarsCode的指令跟随和上下文管理还有待改进,幻觉API依然存在。
最终,我们成功地将一些只有英文字幕而没有中文配音的教程转译为自然地道的中文配音,期望能更好地普及优秀的英文视频教程。虽然现阶段对于英文转中文发音后,导致字幕和合成语音有些许同步的问题,此外对于音频的速度控制,仍然不够理想,但我想这已经是一个很大的进步,这里也欢迎亲爱的读者提出更好的建议。此外,我们还可以使用ChatTTS克隆音色,并控制在不同人说话时候的音色,这样就能做到完全拟真的效果,也不会有违和的感觉,或许这需要使用whisper等ASR自动语音识别判别声音,或许有一天多模态大模型可以完全自动克隆音色并翻译合成。如果你有想看的英文教程,欢迎留言,不要私信,私信需要我回关才能回复。