通过整合之前生成的文字、音频、PPT,我们能够制作出引人入胜的科普课程视频,使表达更加生动且多样化。本节课程将介绍如何利用音视频处理工具ffmpeg和moviepy,快速将课程内容转化为视频。
1. 原理介绍
当前的大模型文生视频或图生视频方案还不足以直接生成符合我们预期的科普课程视频,因此,我们会采用传统的方案,使用音视频处理工具进行合成。 本次课程除了上次课程用到的 moviepy 外,你还将用到以下工具:
ffmpeg:一个开源的跨平台音视频处理工具,它提供了强大的音视频编解码功能、转换格式、录制和流媒体功能。FFmpeg 包含了丰富的命令行工具和库,使用户能够灵活地处理各种媒体文件。
使用 ffmpeg 和 moviepy 将课程内容转换为视频的过程如下:
2. 代码实践
接下来,让我们执行以下代码,将第一节课生成的内容转换为音频,并生成字幕。
2.1. 环境准备
安装 ffmpeg。
可以取消这里的注释来安装 ffmpeg库,如果是 Windows 系统,请参考ffmpeg 官网 安装。
python">!sudo apt update
!sudo apt install ffmpeg
!ffmpeg -version
安装 python 库。
python">! pip install -r requirements.txt -q
python">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
导入必要的模块。
python">import os
import json
import re
import time
import traceback
from pydub import AudioSegment
from typing import List
from moviepy.editor import *
from PIL import Image
import natsort
import math
import numpy as np
from glob import glob
import subprocess
from utils import create_directory,read_text_from_file, save_file,load_config
2.2. 剪辑视频
首先,我们将 PPT 剪辑为视频。
python">project_config = load_config("config.json")
title = project_config["title"]
定义一个 calculate_durations_for_each_image 函数,用于计算每一张演示文稿在视频中的持续时间。
python">def calculate_audio_durations(directory):"""计算指定目录下所有以 audio_for_paragraph_{index} 命名的文件夹中 mp3 文件的总持续时间(以秒为单位)。参数:directory (str): 需要扫描的根目录路径。返回:list: 每个 audio_for_paragraph_{index} 文件夹中 mp3 文件总持续时间(秒)的列表。"""# 初始化结果列表durations = []# 遍历目录下的所有子目录for entry in os.scandir(directory):if entry.is_dir() and entry.name.startswith("audio_for_paragraph_"):# 提取 indexindex = int(entry.name.split("_")[-1])# 初始化当前文件夹的总持续时间为0total_duration_ms = 0# 遍历子目录中的所有文件for file_entry in os.scandir(entry.path):if file_entry.name.endswith(".mp3"):# 加载 mp3 文件并计算持续时间audio = AudioSegment.from_mp3(file_entry.path)delay = 300total_duration_ms += len(audio) + delay# 将当前文件夹的总持续# 时间转换为秒,并添加到结果列表中total_duration_seconds = total_duration_ms / 1000.0durations.append((index, total_duration_seconds))# 按照 index 排序结果列表durations.sort(key=lambda x: x[0])# 只保留持续时间(秒)durations = [duration for _, duration in durations]durations.insert(0, 2)return durations
调用 calculate_durations_for_each_image 函数计算每一张演示文稿在视频中的持续时间。
python"># 计算各段落的所有音频时长
audio_file_folder = project_config["audio_file_folder"].format(title=project_config["title"])# 计算音频时长
durations = calculate_audio_durations(audio_file_folder)durations_file=project_config["durations_folder"].format(title=project_config["title"])create_directory(durations_file)# 打印结果
print("各段落的音频时长(秒):")
with open(durations_file, "w") as f:for index, duration in enumerate(durations):f.write(f"段落 {index + 1}: {duration:.2f} 秒\n")print(f"段落 {index + 1}: {duration:.2f} 秒")print(f"时长信息已保存到 {durations_file}")
python">目标目录:./output
各段落的音频时长(秒):
段落 1: 2.00 秒
段落 2: 30.96 秒
段落 3: 38.33 秒
段落 4: 57.61 秒
段落 5: 38.97 秒
段落 6: 28.35 秒
段落 7: 25.71 秒
时长信息已保存到 ./output/durations
定义一个 images_to_video_with_durations 函数,用于将所有输入演示文稿按顺序剪辑为视频。
python">def images_to_video_with_durations(input_image_path, output_video_path, durations, fps, base_name):# 获取所有符合条件的图片,并按文件名中的数字排序# pattern = r'^' + re.escape(base_name) + r'_(\d+)\.png$'pattern = r".*_(\d+)\.png"image_files = [f"{input_image_path}/{file}"for file in os.listdir(input_image_path)if re.match(pattern, file)]print("Matching files:", image_files) # 调试输出,查看匹配的文件image_files = natsort.natsorted(image_files, key=lambda x: int(re.match(pattern, os.path.basename(x)).group(1)))# 确定视频的背景尺寸target_width, target_height = 1280, 720background_size = (target_width, target_height)clips = []for i, file in enumerate(image_files):print(f"Processing file: {file}, duration: {durations[i]}") # 再次调试输出img = Image.open(file)width, height = img.sizeratio = width / heightif width > target_width or height > target_height:if ratio > target_width / target_height:new_width = target_widthnew_height = math.floor(new_width / ratio)else:new_height = target_heightnew_width = math.floor(new_height * ratio)else:new_width, new_height = width, heightimg = img.resize((new_width, new_height), resample=Image.Resampling.LANCZOS)img_clip = ImageClip(np.array(img)).set_duration(durations[i])img_clip = img_clip.set_position('center')bg_clip = ColorClip(size=background_size, color=(255, 255, 255), duration=durations[i])composite_clip = CompositeVideoClip([bg_clip, img_clip])clips.append(composite_clip)# 使用concatenate_videoclips函数将所有剪辑串联在一起final_clip = concatenate_videoclips(clips, method="compose")output_filename = os.path.join(output_video_path, f"{base_name}.mp4")create_directory(output_filename)final_clip.write_videofile(output_filename, fps=fps)
调用 images_to_video_with_durations 将 PPT 按顺序剪辑为视频。
python">marp_export_image_folder = project_config["marp_export_image_folder"].format(title=project_config["title"])
srt_and_video_folder = project_config["srt_and_video_folder"]
fps = project_config["fps"]# 检查输入图像
# pattern = r'^' + re.escape(input_base_name) + r'_(\d+)\.png$'
pattern = r".*_(\d+)\.png"image_files = [f"{marp_export_image_folder}/{file}"for file in os.listdir(marp_export_image_folder)if re.match(pattern, file)
]print("Matching files:", image_files) # 输出匹配的文件if not image_files:raise ValueError("No matching image files found.")# 检查 durations 的数量
if len(durations) != len(image_files):raise ValueError("The number of durations must match the number of image files.")# 调用函数
images_to_video_with_durations(marp_export_image_folder,srt_and_video_folder,durations,fps,project_config["title"]
)
2.3. 嵌入音频和字幕
接下来,我们将上一课制作的音频和字幕添加到视频中。
定义一个 merge_audio_and_add_to_video 函数,用于合成音频并将音频添加到视频中。
python">def merge_audio_and_add_to_video(video_path, audio_base_dir, output_path):"""合并多个音频文件并添加到视频中。:param video_path: 视频文件的路径。:param audio_base_dir: 包含音频文件夹的基目录。:param output_path: 输出视频的路径。"""# 加载视频文件video_clip = VideoFileClip(video_path)# 初始化音频列表audio_clips = []silent_audio_start = AudioClip(lambda t: [0,0], duration=2)audio_clips.append(silent_audio_start)# 遍历所有子目录,按数字大小排序audio_dirs = glob(os.path.join(audio_base_dir, "audio_for_paragraph_*"))audio_dirs.sort(key=lambda x: int(re.search(r'\d+', os.path.basename(x)).group()))# 遍历所有子目录for audio_dir in audio_dirs:# 获取当前目录的indexindex = int(os.path.basename(audio_dir).split("_")[-1])# 遍历目录中的所有mp3文件mp3_files = glob(os.path.join(audio_dir, f"paragraph_{index}_sentence_*.mp3"))mp3_files.sort(key=lambda x: int(re.search(r'_sentence_(\d+)', os.path.basename(x)).group(1)))# 遍历排序后的mp3文件列表for mp3_file in mp3_files:# 加载音频文件audio_clip = AudioFileClip(mp3_file)# 添加到音频列表if audio_clips:# 如果不是第一个音频,则在前一个音频之后添加0.5秒的静音# 替换原有的 AudioNullClip 代码silent_audio = AudioClip(lambda t: [0,0], duration=0.3)audio_clips.append(silent_audio)audio_clips.append(audio_clip)# 合并所有音频片段final_audio = concatenate_audioclips(audio_clips)# 将音频添加到视频中video_with_audio = video_clip.set_audio(final_audio)# 输出带有新音频的视频文件video_with_audio.write_videofile(output_path, codec='libx264', audio_codec='aac')# 关闭剪辑对象,释放资源video_clip.close()
调用函数 merge_audio_and_add_to_video 添加音频。
python"># 合成路径
video_raw = project_config["video_raw"].format(title=project_config["title"])# 视频文件的路径
audio_file_folder = project_config["audio_file_folder"].format(title=project_config["title"])
# audio_base_dir = "./output/audio/"+title+"_课程脚本_speech_script_plus" # 音频文件夹的基目录
video_with_audio = project_config["video_with_audio"].format(title=project_config["title"]) # 输出视频的路径# 检查视频和音频路径是否存在
if not os.path.exists(video_raw):raise ValueError(f"Video file not found: {video_raw}")if not os.path.exists(audio_file_folder):raise ValueError(f"Audio directory not found: {audio_file_folder}")# 调用函数
merge_audio_and_add_to_video(video_raw, audio_file_folder, video_with_audio)
定义一个 merge_video_and_subtitle 函数,用于将字幕添加到视频中。
python">def merge_video_and_subtitle(video_path, srt_path, output_path):# 如果输出文件已存在,删除它if os.path.exists(output_path):os.remove(output_path)command = ['ffmpeg','-i', video_path,'-vf', f'subtitles={srt_path}','-c:a', 'copy',output_path]subprocess.run(command, check=True)
调用 merge_video_and_subtitle 函数将字幕添加到视频中。
python"># 构建文件路径
video_with_audio = project_config["video_with_audio"].format(title=project_config["title"])
srt_file_path = project_config["srt_file_path"].format(title=project_config["title"])video_with_audio_and_subtitles = project_config["video_with_audio_and_subtitles"].format(title=project_config["title"])# 调用函数
merge_video_and_subtitle(video_with_audio, srt_file_path, video_with_audio_and_subtitles)
播放生成好的视频
python">from IPython.display import Video# 本地视频文件路径
video_path = project_config["video_with_audio_and_subtitles"].format(title=project_config["title"])
print("视频地址:",video_path)# 播放视频
Video(video_path, width=768, height=512)
本节小结
在本次学习和实践中,我们了解了 ffmpeg,并使用该工具生成了视频。