代码演示视频是怎么用python制作,我的解决思路

代码演示视频是怎么用python制作,我的解决思路
ytkz在半年前,我曾想过出一些关于编程、图像处理的视频教程。
在往常附上代码的以文本的形式,那如果要制作视频,那应该怎么制作呢?
我有认真思考过这个问题。
视频的本质就是多帧的图片叠加起来。
大致就是周星驰电影《苏乞儿》里那本武功秘籍,把书快速翻动,里面的小人就打一套功夫。
所以用python制作 一个类似 代码演示的视频,这是一件不难的事情。
写完了之后,我和朋友聊起这件事。
整体流程逻辑(大框架)
代码主要分 4 个阶段:
① 初始化环境与加载字体
- 清空输出目录
- 找字体 → 成功用之,不成功就换 → 最后用默认
- 根据 Pygments 配色方案(dracula)获取各种 token 的颜色
② 按字符绘制代码,模拟打字动画
核心逻辑:
- 代码内容
CODE_TO_ANIMATE→ 清洗成干净的字符串 for char in clean_code:
每多一个字符,就重新画一张“当前代码”的截图- 每生成一帧,文本要
- 用 PIL 绘制
- 按 token 上色(和 Python 语法高亮一样)
- 计算光标位置(像打字机一样挪动)
- 遇到换行
\n,会停顿一些帧(让动画更自然) - 每一帧保存为一张 PNG
③ 结尾做光标闪烁
结束后停几秒,用如下方式制造光标闪烁:
显示光标 → 隐藏光标 → 显示光标 → ...
同样是重新绘制帧。
④ 最后用 FFmpeg 把所有 PNG 合成视频
关键命令类似:
ffmpeg -framerate 60 -i frame_%05d.png ... typing_animation.mp4
最后产出
一堆 PNG 帧
一个“代码逐字打出来”的高品质 MP4 动画
核心解决思路总结
用 PIL 逐帧渲染代码(带语法高亮 + 光标),把每一次字符变化都画成一张图片,然后再用 FFmpeg 把所有图片拼成视频。
原理讲明白了,code2video的完整代码如下:
import os
import shutil
import subprocess
from PIL import Image, ImageDraw, ImageFont
from pygments.lexers import PythonLexer
from pygments.token import Token
from pygments.styles import get_style_by_name
# --- 1. 配置中心 (所有参数都在这里修改) ---
CONFIG = {
# 代码内容
"CODE_TO_ANIMATE": """import numpy as np
def calculate_pi(n_terms: int) -> float:
numerator = 4.0
denominator = 1.0
operator = 1.0
pi = 0.0
for _ in range(n_terms):
pi += operator * (numerator / denominator)
denominator += 2.0
operator *= -1.0
return pi
if __name__ == "__main__":
pi_estimate = calculate_pi(100000)
print(f"Pi approximation: {pi_estimate}")
""",
# 视频参数
"VIDEO_WIDTH": 1920,
"VIDEO_HEIGHT": 1080,
"FPS": 60,
# 视觉样式
"BACKGROUND_COLOR": (14, 18, 25),
"FONT_SIZE": 38,
"LINE_SPACING": 20,
"PADDING": 90,
"FONT_PATHS_TO_TRY": ["Consolas", "Menlo", "DejaVuSansMono.ttf", "DejaVu Sans Mono"],
"PYGMENTS_STYLE": 'dracula',
"CURSOR_COLOR": (248, 248, 242),
"CURSOR_WIDTH": 3,
# 动画参数
"LINE_PAUSE_FRAMES": 20,
"END_PAUSE_SECONDS": 5,
"SUPER_SAMPLING_SCALE": 2,
# 输出设置
"OUTPUT_DIR": "frames_manim_style",
"OUTPUT_FILENAME": "typing_animation_manim_style.mp4"
}
def setup_environment():
output_dir = CONFIG["OUTPUT_DIR"]
if os.path.exists(output_dir):
shutil.rmtree(output_dir)
os.makedirs(output_dir)
return output_dir
def load_font():
font_size = CONFIG["FONT_SIZE"] * CONFIG["SUPER_SAMPLING_SCALE"]
for font_path in CONFIG["FONT_PATHS_TO_TRY"]:
try:
font = ImageFont.truetype(font_path, font_size)
print(f"✅ 字体加载成功: {font_path}")
return font
except IOError:
print(f"🟡 提示: 字体 '{font_path}' 未找到, 尝试下一个...")
print("⚠️ 警告: 所有指定字体均未找到, 使用Pillow默认字体。")
return ImageFont.load_default()
def get_token_colors():
style = get_style_by_name(CONFIG["PYGMENTS_STYLE"])
colors = {}
for token, s in style:
if s['color']:
colors[token] = tuple(int(s['color'][i:i + 2], 16) for i in (0, 2, 4))
default_text_color = (248, 248, 242)
if Token.Text in colors:
default_text_color = colors[Token.Text]
return colors, default_text_color
def draw_code_frame(code_to_draw, show_cursor, font, token_colors, default_color):
scale = CONFIG["SUPER_SAMPLING_SCALE"]
img_size = (CONFIG["VIDEO_WIDTH"] * scale, CONFIG["VIDEO_HEIGHT"] * scale)
img = Image.new('RGB', img_size, color=CONFIG["BACKGROUND_COLOR"])
draw = ImageDraw.Draw(img)
padding = CONFIG["PADDING"] * scale
line_height = (CONFIG["FONT_SIZE"] + CONFIG["LINE_SPACING"]) * scale
lexer = PythonLexer()
tokens = list(lexer.get_tokens(code_to_draw)) # Convert to list for easier handling
x, y = padding, padding
cursor_x, cursor_y = x, y # Initialize cursor position
char_index = 0 # Track position in code_to_draw string
# Iterate through tokens for rendering text with correct colors
for token_type, token_text in tokens:
color = default_color
current_token = token_type
while current_token not in token_colors and current_token.parent:
current_token = current_token.parent
if current_token in token_colors:
color = token_colors[current_token]
# Process each character in the token
for char in token_text:
if char_index < len(code_to_draw):
# Use the actual character from code_to_draw to determine cursor movement
actual_char = code_to_draw[char_index]
if actual_char == '\n':
x = padding
y += line_height
cursor_x, cursor_y = x, y # Move cursor to start of next line
elif actual_char == '\t':
x += font.getlength(' ')
cursor_x, cursor_y = x, y
else:
draw.text((x, y), char, font=font, fill=color)
x += font.getlength(char)
cursor_x, cursor_y = x, y # Update cursor after character
char_index += 1
# Draw cursor at the final position
if show_cursor:
cursor_height = CONFIG["FONT_SIZE"] * scale * 1.1
cursor_width = CONFIG["CURSOR_WIDTH"] * scale
draw.rectangle(
[cursor_x, cursor_y, cursor_x + cursor_width, cursor_y + cursor_height],
fill=CONFIG["CURSOR_COLOR"]
)
final_img = img.resize((CONFIG["VIDEO_WIDTH"], CONFIG["VIDEO_HEIGHT"]), Image.Resampling.LANCZOS)
return final_img
def main():
output_dir = setup_environment()
font = load_font()
token_colors, default_color = get_token_colors()
clean_code = CONFIG["CODE_TO_ANIMATE"].strip().replace('\u00a0', ' ')
frames = []
code_to_display = ""
print("🎬 开始生成动画帧...")
for char in clean_code:
code_to_display += char
frame = draw_code_frame(code_to_display, True, font, token_colors, default_color)
frames.append(frame)
if char == '\n':
for _ in range(CONFIG["LINE_PAUSE_FRAMES"]):
frames.append(frame)
total_end_frames = CONFIG["END_PAUSE_SECONDS"] * CONFIG["FPS"]
blink_interval = CONFIG["FPS"] // 2
for i in range(total_end_frames):
show_cursor = (i // blink_interval) % 2 == 0
frame = draw_code_frame(code_to_display, show_cursor, font, token_colors, default_color)
frames.append(frame)
print(f"🖼️ 动画帧生成完毕,总共 {len(frames)} 帧。开始保存图片...")
for i, frame in enumerate(frames):
filename = os.path.join(output_dir, f"frame_{i:05d}.png")
frame.save(filename)
print(f"正在保存第 {i + 1}/{len(frames)} 帧", end='\r', flush=True)
print("\n✅ 所有帧已保存。开始使用 FFmpeg 合成视频...")
ffmpeg_command = [
'ffmpeg', '-y',
'-framerate', str(CONFIG["FPS"]),
'-i', os.path.join(output_dir, 'frame_%05d.png'),
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-crf', '18',
'-preset', 'slow',
CONFIG["OUTPUT_FILENAME"]
]
try:
subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)
print(f"🎉 视频生成成功!文件名为: {CONFIG['OUTPUT_FILENAME']}")
except FileNotFoundError:
print("\n❌ 错误: FFmpeg 未找到。请确保它已安装并已添加到系统 PATH。")
except subprocess.CalledProcessError as e:
print(f"\n❌ FFmpeg 合成视频时出错: {e}")
print(f"FFmpeg 输出:\n{e.stderr}")
if __name__ == '__main__':
main()
写代码是不难的,难的是解决问题的思路。你有思路了,你可以在ai辅助下完成很多有意思的事情。
保持热爱,开心每一天。
万分感谢大家的支持和厚爱,我们明天见。





