大模型高级工程师实践 - 将课程内容转为音频
音频内容可以随时随地播放,让学习变得更加灵活和便捷。本节课程介绍如何借助文本生成模型 Qwen-Max、语音合成模型 CosyVoice 和视频编辑和处理 moviepy,将课程内容快速转换为音频,并生成对应的字幕
1. 原理介绍
除了之前用到的 Qwen-Max, 本次课程你将用到以下模型和工具:
CosyVoice:CosyVoice 是通义实验室依托大规模预训练语言模型,深度融合文本理解和语音生成的新一代生成式语音合成大模型,支持文本至语音的实时流式合成。
moviepy:一个 Python 库,用于视频编辑和处理。它提供了许多方便的功能,可以帮助开发者创建、修改和合成视频文件。
使用 CosyVoice 和 moviepy 将课程内容转换为音频的过程如下:
2. 代码实践
接下来,让我们执行以下代码,将第一节课生成的内容转换为音频,并生成字幕。
2.1. 环境准备
安装 Python 库。
! pip install -r requirements.txt -q
pdf2image==1.17.0
openai==1.40.8
python-dotenv==1.0.1
requests==2.32.3
dash==2.18.1
dashscope==1.20.12
moviepy==1.0.3
ffmpeg-python==0.2.0
pydub==0.25.1
natsort==8.4.0
导入必要的模块。
import os
import openai
from dashscope.audio.tts_v2 import SpeechSynthesizer
import json
import re
import time
import traceback
import dashscope
from moviepy.editor import concatenate_audioclips, AudioFileClip
from typing import List
from utils import create_directory, save_file, read_text_from_file,load_config
设置环境变量
在这里插入代码片
import os
import sys
sys.path.append("../")
from config.load_key import load_key
load_key()
print(f'''你配置的 API Key 是:{os.environ["DASHSCOPE_API_KEY"][:5]+"*"*5}''')
加载配置文件。
project_config = load_config("config.json")
2.2. 设置 API 客户端
设置 OpenAI 的 API 客户端,用于后续调用阿里云百炼的 Qwen-Max 模型和 Flux-Merged 模型。
client = openai.OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
2.3. 将课程内容转换为录音稿
首先,我们将 Markdown 格式的课程内容转换为 JSON 格式,然后使用 Qwen-Max 对录音稿中的内容进行优化,便于之后进行语音合成。
定义一个 create_speech_script 函数,用于将课程内容转换为录音稿。
def create_speech_script(course_script):
"""
创建演讲稿脚本,从给定的课程脚本中提取标题和内容。
该函数将输入的课程脚本按行拆分,并提取其中的标题和内容,
形成一个结构化的输出,包含标题及其对应的内容(去除 Markdown 格式)。
参数:
course_script (str): 包含课程内容的 Markdown 格式字符串。
返回:
str: 返回一个 JSON 格式的字符串,包含所有标题和内容。
"""
output = [] # 用于保存输出的最终结果
lines = course_script.splitlines() # 将脚本按行拆分
current_title = None # 当前标题
current_content = [] # 当前内容列表
for line in lines:
line = line.strip() # 去除每行的首尾空白字符
# 处理图片标签,去除 Markdown 图片格式
if line.startswith('!['):
continue # 跳过图片行,直接进入下一个循环
# 处理一级标题
if line.startswith('# '): # 检测到一级标题
if current_title: # 如果已有标题,保存当前内容
output.append({
"title": current_title,
"content": "\n".join(current_content).replace('*', '').replace('**', '')
})
current_content = [] # 重置内容
current_title = line[2:] # 获取当前标题的文本内容(跳过 '# ')
# 处理二级标题
elif line.startswith('## '): # 检测到二级标题
if current_title: # 如果已有标题,保存当前内容
output.append({
"title": current_title,
"content": "\n".join(current_content).replace('*', '').replace('**', '')
})
current_content = [] # 重置内容
current_title = line[3:] # 获取当前标题的文本内容(跳过 '## ')
else:
# 处理列表和其他 Markdown 内容,移除多余的符号
clean_line = re.sub(r'\*\s*', '', line) # 移除以 '*' 开头的内容
clean_line = re.sub(r'#+\s*', '', clean_line) # 移除 Markdown 标题格式
clean_line = clean_line.strip() # 去除多余的空白字符
if clean_line: # 确保不添加空行
current_content.append(clean_line) # 将清理后的内容添加到当前内容列表
# 辐忘记添加最后一个标题与内容
if current_title:
output.append({
"title": current_title,
"content": "\n".join(current_content).replace('*', '').replace('**', '')
})
# 将输出转换为 JSON 格式并返回
generated_content = json.dumps(output, ensure_ascii=False, indent=4)
# 打印生成的录音稿初稿
print("生成的录音稿初稿:" + generated_content)
return generated_content
调用 create_speech_script 函数将含插画课程脚本转换为录音稿。
# 读取配置文件中的课程脚本文件路径
course_script_with_illustrations_file_path = project_config["course_script_with_illustrations_file_path"].format(title=project_config["title"])
# 根据课程脚本文件路径读取课程内容
script = read_text_from_file(course_script_with_illustrations_file_path)
# 调用函数生成录音稿初稿
speech_script_draft = create_speech_script(script)
# 读取配置文件中的录音稿初稿文件路径
speech_script_draft_file_path = project_config["speech_script_draft_file_path"].format(title=project_config["title"])
# 保存录音稿初稿文件
save_file(speech_script_draft, speech_script_draft_file_path)
成功读取文件:./output/course_script_with_illustrations/云计算_含插画课程脚本.md
生成的录音稿初稿:[
{
"title": "云计算基础课程",
"content": ""
},
{
"title": "引言:云上之旅",
"content": "- 想象一下,如果我们的数据和应用不再被限制在小小的硬盘或单一的计算机里,而是可以自由地漂浮在天空中,随取随用。这听起来是不是有点像魔法?其实这就是我们今天要探索的世界——云计算。\n- 在开始这段奇妙旅程之前,请确保你的想象力已经调整到最佳状态,因为我们将一起揭开隐藏在这片“云”背后的秘密。"
},
{
"title": "什么是云计算?",
"content": "- 定义:简单来说,云计算就是通过互联网提供计算资源(如服务器、存储空间等)和服务的技术。它允许用户按需访问网络上的各种资源,而无需自己购买和维护物理硬件。\n- 比喻说明:可以把云计算想象成一个巨大的图书馆。这个图书馆不仅收藏了世界上所有的书籍(即信息),还拥有无限扩展的空间来存放新书。更重要的是,无论你身在何处,只要连接上网,就能随时借阅这些书籍。"
},
{
"title": "云计算的关键组件",
"content": "- 基础设施即服务 (IaaS): 基础设施就像盖房子的地基一样重要。IaaS为用户提供虚拟化的计算资源,比如虚拟机、存储设备等。就像是给你一块土地让你自己设计建造梦想中的家园。\n- 平台即服务 (PaaS): 如果说IaaS是土地,那么PaaS就是已经为你准备好了建筑材料甚至是基本框架的房子。开发者可以直接在此基础上开发应用程序,而不用从零开始搭建环境。\n- 软件即服务 (SaaS): SaaS则是完全装修好的成品房,用户只需拎包入住即可享受服务。常见的例子包括在线办公软件、电子邮件服务等。"
},
{
"title": "云计算的优势",
"content": "- 灵活性与可扩展性:根据需要轻松增加或减少资源,就像给气球充气或放气那样简单快捷。\n- 成本效益:避免了一次性投入大量资金购买硬件设备的风险,转而采用按使用量付费的方式。\n- 易于管理与维护:服务商负责处理所有底层技术细节,让用户能够专注于核心业务的发展。\n- 安全性增强:虽然很多人担心把数据放在云端不安全,但实际上专业的云服务提供商通常会采取更加严格的安全措施来保护用户的信息。"
},
{
"title": "云计算的应用场景",
"content": "- 个人生活:从在线存储照片到流媒体音乐播放,甚至智能家居控制,云计算让日常生活变得更加便捷。\n- 企业运营:帮助企业实现远程工作、协作办公,并支持大规模数据分析等复杂任务。\n- 教育领域:利用云计算提供的强大计算能力和丰富教育资源,促进个性化学习体验的发展。"
},
{
"title": "结语:未来的云",
"content": "- 随着技术不断进步,未来云计算将变得更加智能高效。也许有一天,我们真的能生活在这样一个世界里:所有东西都连接在一起,信息无处不在,触手可及。\n- 但在此之前,让我们先学会如何驾驭这片神奇的“云”,开启属于自己的数字时代冒险吧!"
}
]
目标目录:./output/speech_script_draft
文件路径:./output/speech_script_draft/云计算_录音稿_草稿.json
文件已成功保存为:./output/speech_script_draft/云计算_录音稿_草稿.json
定义一个 improve_speech_script 函数,用于优化录音稿中的内容。
def improve_speech_script(script):
"""
优化录音稿的内容,使其更适合口语表达。
此函数调用外部 API,对输入的录音稿 JSON 格式的内容进行处理,
生成更适合发言的纯文本内容。
参数:
script (str): JSON 格式的字符串,包含待优化的内容。
返回:
str: 生成的优化后的口语化文本。
"""
# 系统消息,设定机器人的角色
system_message = "您是录音稿专家。"
# 提示词创建,构建用于 API 请求的 prompt 字符串
prompt = (
f"处理以下 JSON 中的 content 字段,并将内容转换为适合录音的纯文本形式。"
f"返回处理后的 JSON,不要任何额外的说明。内容格式要求:\n"
"1. 对于英文的专有术语缩写,替换为全称。\n" # 英文缩写的全称替换
"2. 去除星号、井号等 Markdown 格式。\n" # 移除 Markdown 标记
"3. 去除换行符和段落分隔。\n" # 删除换行符和段落分割符
"4. 对于复杂的长难句,使用中文句号分割,便于口语表达。\n" # 处理长难句使其更流畅
" content 中的内容使用于发言使用。\n" # 明确内容用途
f"{script} " # 添加待处理的 JSON 内容
"输出格式为 JSON。不包含任何额外的文字、解释或评论。" # 指明输出格式
)
# 调用 API 获取插图数据
# 使用预设的系统消息和用户提示词来生成回复
completion = client.chat.completions.create(
model="qwen-max-latest", # 指定使用的模型
messages=[
{"role": "system", "content": system_message}, # 系统角色消息
{"role": "user", "content": prompt}, # 用户请求部分
],
)
# 解析 API 返回的 JSON 格式结果
dumped_json = json.loads(completion.model_dump_json()) # 从响应中加载 JSON 数据
# 返回生成的内容,提取优化后的文本
generated_content = dumped_json['choices'][0]['message']['content']
# 打印生成的优化后的录音稿
print("生成的优化后的录音稿:" + generated_content)
return generated_content # 返回优化后的文本
调用 improve_speech_script 函数优化录音稿中的内容。
# 调用函数优化录音稿初稿
improved_speech_script = improve_speech_script(speech_script_draft)
# 读取配置文件中的优化后的录音稿文件路径
speech_script_file_path = project_config["speech_script_file_path"].format(title=project_config["title"])
# 保存优化后的录音稿文件
save_file(improved_speech_script, speech_script_file_path)
生成的优化后的录音稿:[
{
"title": "云计算基础课程",
"content": ""
},
{
"title": "引言:云上之旅",
"content": "想象一下,如果我们的数据和应用不再被限制在小小的硬盘或单一的计算机里,而是可以自由地漂浮在天空中,随取随用。这听起来是不是有点像魔法?其实这就是我们今天要探索的世界——云计算。在开始这段奇妙旅程之前,请确保你的想象力已经调整到最佳状态,因为我们将一起揭开隐藏在这片“云”背后的秘密。"
},
{
"title": "什么是云计算?",
"content": "定义:简单来说,云计算就是通过互联网提供计算资源(如服务器、存储空间等)和服务的技术。它允许用户按需访问网络上的各种资源,而无需自己购买和维护物理硬件。比喻说明:可以把云计算想象成一个巨大的图书馆。这个图书馆不仅收藏了世界上所有的书籍(即信息),还拥有无限扩展的空间来存放新书。更重要的是,无论你身在何处,只要连接上网,就能随时借阅这些书籍。"
},
{
"title": "云计算的关键组件",
"content": "基础设施即服务 (基础设施即服务): 基础设施就像盖房子的地基一样重要。基础设施即服务为用户提供虚拟化的计算资源,比如虚拟机、存储设备等。就像是给你一块土地让你自己设计建造梦想中的家园。平台即服务 (平台即服务): 如果说基础设施即服务是土地,那么平台即服务就是已经为你准备好了建筑材料甚至是基本框架的房子。开发者可以直接在此基础上开发应用程序,而不用从零开始搭建环境。软件即服务 (软件即服务): 软件即服务则是完全装修好的成品房,用户只需拎包入住即可享受服务。常见的例子包括在线办公软件、电子邮件服务等。"
},
{
"title": "云计算的优势",
"content": "灵活性与可扩展性:根据需要轻松增加或减少资源,就像给气球充气或放气那样简单快捷。成本效益:避免了一次性投入大量资金购买硬件设备的风险,转而采用按使用量付费的方式。易于管理与维护:服务商负责处理所有底层技术细节,让用户能够专注于核心业务的发展。安全性增强:虽然很多人担心把数据放在云端不安全,但实际上专业的云服务提供商通常会采取更加严格的安全措施来保护用户的信息。"
},
{
"title": "云计算的应用场景",
"content": "个人生活:从在线存储照片到流媒体音乐播放,甚至智能家居控制,云计算让日常生活变得更加便捷。企业运营:帮助企业实现远程工作、协作办公,并支持大规模数据分析等复杂任务。教育领域:利用云计算提供的强大计算能力和丰富教育资源,促进个性化学习体验的发展。"
},
{
"title": "结语:未来的云",
"content": "随着技术不断进步,未来云计算将变得更加智能高效。也许有一天,我们真的能生活在这样一个世界里:所有东西都连接在一起,信息无处不在,触手可及。但在此之前,让我们先学会如何驾驭这片神奇的“云”,开启属于自己的数字时代冒险吧!"
}
]
目标目录:./output/speech_script
文件路径:./output/speech_script/云计算_录音稿.json
文件已成功保存为:./output/speech_script/云计算_录音稿.json
2.4. 转换为音频
接下来,我们使用语音合成模型 CosyVoice 将录音稿转换为音频。
定义一个 read_json_file 函数,用于从指定路径读取 JSON 文件并返回内容。
def read_json_file(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
return data
定义一个 split_into_sentences 函数,用于将输入文本按中文标点和括号分割成句子。
def split_into_sentences(text):
# 中文标点符号列表
punctuation = [',', '。', ';', '?', '!']
brackets = {'(': ')', '[': ']', '{': '}', '(': ')', '【': '】', '《': '》'}
# 初始化结果列表和临时句子存储
sentences = []
temp_sentence = ''
bracket_stack = []
# 遍历文本中的每一个字符
for char in text:
# 如果是左括号,压入栈
if char in brackets:
bracket_stack.append(char)
# 如果是右括号且与栈顶匹配,弹出栈
elif char in brackets.values() and bracket_stack and brackets[bracket_stack[-1]] == char:
bracket_stack.pop()
# 如果字符是中文标点之一且括号栈为空,表示句子结束
if char in punctuation and not bracket_stack:
# 添加临时句子到结果列表,并清空临时句子
sentences.append(temp_sentence.strip())
temp_sentence = ''
# 如果字符是空格,也可以视为句子结束
elif char == ' ':
# 如果临时句子不是空,将其添加到结果列表
if temp_sentence.strip(): # 仅在临时句子不为空时添加
sentences.append(temp_sentence.strip())
temp_sentence = ''
else:
# 否则,将字符添加到临时句子中
temp_sentence += char
# 处理最后一个可能没有标点结尾的句子
if temp_sentence:
sentences.append(temp_sentence.strip())
return sentences
定义一个 save_sentences_to_markdown 函数,用于将分割后的句子保存为 Markdown 文件。
def save_sentences_to_markdown(sentences, base_dir, index1):
for index2, sentence in enumerate(sentences, start=1):
# 创建目录
dir_name = f'audio_for_paragraph_{index1}'
dir_path = os.path.join(base_dir, dir_name)
os.makedirs(dir_path, exist_ok=True)
# 构建文件名
file_name = f'paragraph_{index1}_sentence_{index2}.md'
file_path = os.path.join(dir_path, file_name)
# 写入Markdown文件
with open(file_path, 'w', encoding='utf-8') as file:
file.write(sentence + '\n')
定义一个 process_json_file 函数,用于处理指定的 JSON 文件并生成 Markdown 文件。
def process_json_file(json_file_path, base_dir):
if not os.path.exists(base_dir):
os.makedirs(base_dir)
file_prefix = os.path.splitext(os.path.basename(json_file_path))[0]
# base_dir = os.path.join(base_dir, file_prefix)
# 读取JSON文件
json_data = read_json_file(json_file_path)
# 处理JSON数据中的每个条目
for index1, item in enumerate(json_data):
if 'content' in item:
content = item['content']
# 检查content是否为链接
if not is_url(content):
sentences = split_into_sentences(content)
save_sentences_to_markdown(sentences, base_dir, index1+1)
定义一个 is_url 函数,用于检查给定字符串是否为有效 URL。
def is_url(s):
url_pattern = re.compile(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')
return bool(url_pattern.match(s))
定义一个 synthesize_md_to_speech 函数,用于将指定目录下的所有 Markdown 文件内容转换为语音并保存为 MP3 文件。
def synthesize_md_to_speech(base_directory):
"""
识别指定目录下的所有.md文件,读取其内容并使用DashScope API将其转换为语音,
保存为同名.mp3文件在同一目录下。
参数:
base_directory (str): 包含.md文件的顶层目录路径。
"""
# 确保环境变量中存在DashScope API密钥
if 'DASHSCOPE_API_KEY' not in os.environ:
raise ValueError("DashScope API key must be set in the environment variables.")
# 遍历指定目录及其子目录
for root, dirs, files in os.walk(base_directory):
for file in files:
if file.endswith('.md'):
# 构建完整文件路径
md_file_path = os.path.join(root, file)
# 读取.md文件内容
with open(md_file_path, 'r', encoding='utf-8') as f:
text = f.read()
# 初始化语音合成器
speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v1', voice='longxiaochun')
# 合成语音
audio_data = speech_synthesizer.call(text)
# 构建输出.mp3文件路径
mp3_file_path = os.path.splitext(md_file_path)[0] + '.mp3'
# 保存音频到文件
with open(mp3_file_path, 'wb') as f:
f.write(audio_data)
print(f'Synthesized text from file "{md_file_path}" to file: {mp3_file_path}')
调用 process_json_file 切分录音稿,然后调用 synthesize_md_to_speech 函数将录音稿片段转换为语音。
# 读取配置文件中的音频文件所在目录
audio_file_folder = project_config["audio_file_folder"].format(title=project_config["title"])
# 切分录音稿
process_json_file(speech_script_file_path, audio_file_folder)
# 将录音稿片段转换为语音
synthesize_md_to_speech(audio_file_folder)
2.5. 生成字幕
最后,我们基于音频的时长和录音稿的文本生成音频的字幕。
定义一个 format_time 函数,用于将给定的时间(以秒为单位)格式化为“时:分:秒,毫秒”的字符串表示。
def format_time(seconds):
hours, remainder = divmod(seconds, 3600)
minutes, seconds = divmod(remainder, 60)
milliseconds = int((seconds - int(seconds)) * 1000)
seconds = int(seconds)
return f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d},{milliseconds:03d}"
定义一个 get_audio_duration 函数,用于获取音频文件的时长。
def get_audio_duration(file_path):
audio = AudioFileClip(file_path)
duration = audio.duration
audio.close()
return duration
定义一个 create_srt_line 函数,用于生成SRT格式的字幕行。
def create_srt_line(index, start_time, end_time, text):
return f"{index}\n{start_time} --> {end_time}\n{text}\n\n"
定义一个 generate_srt_from_audio 函数,用于生成字幕。
def generate_srt_from_audio(base_dir: str, output_dir: str, output_srt_file: str) -> None:
"""
从指定目录下的音频文件夹生成SRT字幕文件。
:param base_dir: 包含音频文件夹的根目录。
:param output_dir: 输出SRT文件的目录。
:param output_srt_file: 输出SRT文件的完整路径。
"""
# 创建输出目录,如果它不存在
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 确保输出文件名有.srt后缀
if not output_srt_file.endswith('.srt'):
output_srt_file += '.srt'
# 初始化当前时间
current_time = 2.000 # 初始时间
# 打开SRT文件进行写入
with open(output_srt_file, 'w', encoding='utf-8') as srt_file:
srt_index = 1
# 获取所有符合条件的子目录,并按索引排序
sub_dirs = [d for d in os.listdir(base_dir) if d.startswith('audio_for_paragraph_')]
sub_dirs.sort(key=lambda x: int(re.search(r'\d+', x).group()))
# 遍历所有子目录
for sub_dir in sub_dirs:
sub_dir_path = os.path.join(base_dir, sub_dir)
# 查找所有的.md和.mp3文件
files = [f for f in os.listdir(sub_dir_path) if f.endswith('.md') or f.endswith('.mp3')]
md_files = [f for f in files if f.endswith('.md')]
# 按照index1和index2排序.md文件
md_files.sort(key=lambda x: (int(x.split('_')[1]), int(x.split('_')[3].split('.')[0])))
# 处理每个.md文件
for md_file in md_files:
md_file_path = os.path.join(sub_dir_path, md_file)
mp3_file_path = os.path.splitext(md_file_path)[0] + '.mp3'
# 确保对应的.mp3文件存在
if os.path.exists(mp3_file_path):
# 读取.md文件内容
with open(md_file_path, 'r', encoding='utf-8') as f:
text = f.read().strip()
# 获取.mp3文件时长
duration = get_audio_duration(mp3_file_path)
# 生成SRT格式的字幕行
start_time_str = format_time(current_time)
end_time_str = format_time(current_time + duration)
srt_line = create_srt_line(srt_index, start_time_str, end_time_str, text)
# 写入SRT文件
srt_file.write(srt_line)
# 更新当前时间
current_time += duration + 0.3 # 加上0.5秒以避免时间重叠
srt_index += 1
else:
print(f"No corresponding MP3 file found for {md_file}")
print("成功生成字幕文件:" + output_srt_file)
调用 generate_srt_from_audio 函数生成一个字幕文件。
# 读取配置文件中的音频文件所在目录
audio_file_folder = project_config["audio_file_folder"].format(title=project_config["title"])
# 读取配置文件中的字幕文件所在目录
srt_file_folder = project_config["srt_file_folder"]
# 读取配置文件中的字幕文件路径
srt_file_path = project_config["srt_file_path"].format(title=project_config["title"])
# 生成 SRT 文件
generate_srt_from_audio(audio_file_folder, srt_file_folder, srt_file_path)
本节小结
在本次学习和实践中,我们了解了 CosyVoice 和 moviepy,并使用它们生成了音频和字幕。
为了提升课程生动性,我们可以基于已有的图像、文本和音频素材生成视频。接下来,我们将学习如何剪辑这些素材以制作视频。