manim 分场景生成代码
1. 思路
在传统的 Manim 渲染流程中,所有场景的帧生成、视频合成以及音频配音都会在全部完成后,才统一输出为最终的视频文件。这种方式虽能保证完整性,但等待时间较长,不利于实时播放或快速反馈。
为提升响应速度和用户体验,本方案采用“分段处理”的策略:每个场景单独渲染生成视频和音频,并在完成后立即输出。这样,用户无需等待全部场景渲染完成,就能开始播放第一个场景的视频内容。
本方案主要针对当前使用大语言模型一次性生成完整代码时遇到的两个核心问题:
- 生成和处理耗时过长
- 生成代码出错概率高,影响整体流程
为解决上述问题,我们引入了“分场景生成+实时处理”机制:
- 第一个场景生成并渲染完成后,前端即可开始播放;
- 后续场景的代码则按顺序依次生成,渲染成视频切片后追加至前方的 m3u8 播放列表中;
- 若某个场景运行时出现错误,系统会启动交互流程,调用大模型修复出错代码,修复成功后继续处理下一个场景。
完整流程概述
- 调用大模型生成第 1 个场景的 Python 渲染脚本;
- 将代码发送至远程 Java-Linux 服务端,由后者负责自动执行渲染、生成视频,并提供支持 HLS 的流式播放接口;
- 若运行失败,由系统提示错误,并自动调用大模型生成修复后的代码;
- 每个场景生成完成后,其对应的
.m3u8
视频片段立即可被前端播放; - 所有场景处理完毕后,服务端将各视频段合并生成完整的最终视频文件。
此方案大大降低了因生成错误导致的全流程中断风险,并显著提高了渲染系统的响应速度与用户交互体验。
如需,我可以继续补充交互流程图、各场景提示模板或示例代码。是否继续?
2. 提示词和交互流程示例
2.1 用户主题
例如本次场景生成的主题是:
为什么羽毛下落得比石头慢?
2.2 系统提示词
系统提示词(此处略)用于指导大模型生成代码,但在示例中不做展开,后续提示词均以“请仅输出可运行的 Python 脚本代码”作为主要要求。
2.3 第 1 个提示
提示词
下面是第 1 个场景的提示词,用来生成场景 1 的 Python 脚本代码。代码中要求包含主方法、语音合成、所需的 import 以及常量定义:
The Topic is:为什么羽毛下落得比石头慢?
The generated subtitles and narration must use the Chinese.
Please only output the runnable Python script code for scene 1, including the main method, speech synthesis, imports, and constant definitions.
Please output code without including any comments in the code.
To save processing and output time, please make the code as concise as possible while ensuring it runs correctly.
Because I execute the code by calling Python commands through Java, please ensure that the Python process does not block.
推理与输出示例
生成代码的推理耗时约 41.7 秒,生成的 Python 脚本代码如下:
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone
# --- Font Check ---
# Specify a preferred Chinese font
DEFAULT_FONT = "Noto Sans CJK SC" # Example: Noto Sans Simplified Chinese
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
# Add more fallback Chinese fonts if needed
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
# final_font remains None
# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6" # Light blue background
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937" # Dark text for contrast on light background
MY_WHITE = "#FFFFFF" # For subtitles if needed on a darker overlay
MY_GOLD = "#B8860B" # For potential highlights
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text):
"""Generates a unique filename based on the text hash."""
# Use text hash for filename consistency
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
# print(f"Using cached TTS for: {text[:30]}...")
pass # Use cached file
else:
# print(f"Requesting TTS for: {text[:30]}...")
try:
input_text_encoded = requests.utils.quote(text)
# Ensure the API call uses the provided text
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
# print("TTS downloaded and cached.")
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file) # Clean up partial file
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
# Get duration
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
# print(f"Audio duration: {duration:.2f}s")
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
# print(f"TTS audio file not found or not created: {audio_file}")
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass # Keep cache
# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
"""
Explains why feathers fall slower than stones.
Scene 1: Introduction of the question.
Uses Chinese narration and subtitles.
"""
def setup(self):
Scene.setup(self)
# Set default font if found
if final_font:
Text.set_default(font=final_font)
# Variable to hold the current scene number mobject
self.current_scene_num_mob = None
# Store elements needed across sections (though only one section here)
self.section_elements = VGroup()
# Set background color for the entire scene
self.camera.background_color = MY_SKY_BLUE
def update_scene_number(self, number_str, color=MY_DARK_TEXT):
"""Fades out the old scene number and fades in the new one."""
new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
# Ensure the previous mob is valid before fading out
if self.current_scene_num_mob is not None:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
# Always play the animations when rendering a video
self.play(*animations)
self.current_scene_num_mob = new_scene_num # Update the reference
def clear_section(self):
"""Clears elements specific to the current section."""
# Clear updaters from section elements first
for mob in self.section_elements:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
# Fade out section elements
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
# Use Group for simultaneous fade out
# Always play the animation when rendering
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
# Clear the VGroup container
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
# --- Scene Setup ---
# Background color is set in setup
# --- Play Section 1 ---
self.play_section_01()
# --- Cleanup ---
# Clear elements at the end of the scene
self.clear_section()
# Final wait before ending
self.wait(1)
# Fade out the last scene number
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Section 1: Introduction ---
def play_section_01(self):
"""Section 1: Introduce the question and objects."""
self.update_scene_number("01") # Update scene number
# Title in Chinese
title = Text("为什么羽毛下落得比石头慢?", font_size=48, color=MY_DARK_TEXT)
title.to_edge(UP, buff=MED_LARGE_BUFF)
self.section_elements.add(title) # Add to group for clearing
# Feather (Simple Polygon Representation)
feather_shape = Polygon(
[-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
[0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
).scale(0.8).rotate(-PI / 12)
feather = VGroup(feather_shape).move_to(LEFT * 3 + UP * 0.5)
feather_label = Text("羽毛", font_size=24, color=MY_DARK_TEXT).next_to(feather, DOWN)
feather_group = VGroup(feather, feather_label)
self.section_elements.add(feather_group)
# Stone (Irregular Polygon)
stone_shape = Polygon(
[-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
[0.1, 0.9, 0], [-0.7, 0.5, 0],
color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
).scale(1.2)
stone = VGroup(stone_shape).move_to(RIGHT * 3 + UP * 0.5)
stone_label = Text("石头", font_size=24, color=MY_DARK_TEXT).next_to(stone, DOWN)
stone_group = VGroup(stone, stone_label)
self.section_elements.add(stone_group)
# Narration 1 (Chinese)
voice_text_01 = "大家好!你有没有想过,为什么轻飘飘的羽毛,和沉甸甸的石头,从同样的高度落下时,羽毛总是慢很多呢?"
with custom_voiceover_tts(voice_text_01) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 1 TTS failed.")
# Subtitle setup
subtitle_voice = Text(
voice_text_01,
font_size=32,
color=MY_DARK_TEXT, # Dark text on light blue background
width=config.frame_width - 2, # Wrap text
should_center=True
).to_edge(DOWN, buff=MED_SMALL_BUFF)
# Animation Sequence
self.play(FadeIn(title), run_time=1.5)
self.play(
AnimationGroup(
FadeIn(subtitle_voice),
FadeIn(feather_group, shift=UP*0.5),
FadeIn(stone_group, shift=UP*0.5),
lag_ratio=0.0 # Start simultaneously
),
run_time=2.0
)
# Synchronization: Wait for audio duration, considering animation time
anim_duration = 1.5 + 2.0 # Title fadein + group fadein
# Calculate remaining time, ensuring it's not negative
# Subtract subtitle fadeout time (0.5s)
remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
if remaining_wait_time > 0:
self.wait(remaining_wait_time)
# Fade out subtitle
self.play(FadeOut(subtitle_voice), run_time=0.5)
# Keep objects on screen for a moment before clearing in construct()
self.wait(1)
# --- Main execution block ---
if __name__ == "__main__":
# Basic configuration
config.pixel_height = 1080 # Set resolution height
config.pixel_width = 1920 # Set resolution width
config.frame_rate = 30 # Set frame rate
config.output_file = "CombinedScene" # Specify output filename
config.disable_caching = True # Disable caching
# Set output directory using placeholder
config.media_dir = r"#(output_path)" # IMPORTANT: Use the placeholder
# Create and render the scene
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
运行代码
运行完成后,返回的 m3u8 地址为:
/data/hls/500673260100370432/main.m3u8
2.4 第 2 个提示
提示词
下面是第 2 个提示的提示词,用于生成下一个场景的 Python 脚本代码,要求不包含前一场景的代码,但包含主方法、语音合成、imports 与常量定义,并要求与前一场景的代码接口风格、坐标系统和元素保持一致。如果无后续场景,则输出 done。
Please only output the runnable Python script code for next scene do not include the code from the previous scene but including the main method, speech synthesis, imports, and constant definitions.
Please output code without including any comments in the code.
Please ensure that the generated code for the next scene is synchronized with the previous scene's code in terms of interface style, coordinate system, and elements.
To save processing and output time, please make the code as concise as possible while ensuring it runs correctly.
Because I execute the code by calling Python commands through Java, please ensure that the Python process does not block.
If there is no next scene, output done.
推理与输出示例
推理耗时约 24.9 秒,生成的 Scene 2 代码如下:
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone
# --- Font Check ---
# Specify a preferred Chinese font
DEFAULT_FONT = "Noto Sans CJK SC" # Example: Noto Sans Simplified Chinese
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
# Add more fallback Chinese fonts if needed
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
# final_font remains None
# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6" # Light blue background
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937" # Dark text for contrast on light background
MY_WHITE = "#FFFFFF" # For subtitles if needed on a darker overlay
MY_GOLD = "#B8860B" # For potential highlights
MY_GRAVITY_RED = "#E53935" # Color for gravity arrows
MY_AIR_RESISTANCE_BLUE = "#03A9F4" # Light blue for air resistance arrows
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text):
"""Generates a unique filename based on the text hash."""
# Use text hash for filename consistency
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
# print(f"Using cached TTS for: {text[:30]}...")
pass # Use cached file
else:
# print(f"Requesting TTS for: {text[:30]}...")
try:
input_text_encoded = requests.utils.quote(text)
# Ensure the API call uses the provided text
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
# print("TTS downloaded and cached.")
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file) # Clean up partial file
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
# Get duration
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
# print(f"Audio duration: {duration:.2f}s")
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
# print(f"TTS audio file not found or not created: {audio_file}")
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass # Keep cache
# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
"""
Explains why feathers fall slower than stones.
Scene 2: Introduce Gravity.
Uses Chinese narration and subtitles.
"""
def setup(self):
Scene.setup(self)
# Set default font if found
if final_font:
Text.set_default(font=final_font)
# Variable to hold the current scene number mobject
self.current_scene_num_mob = None
# Store elements needed across sections
self.section_elements = VGroup()
# Set background color for the entire scene
self.camera.background_color = MY_SKY_BLUE
def update_scene_number(self, number_str, color=MY_DARK_TEXT):
"""Fades out the old scene number and fades in the new one."""
new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
if self.current_scene_num_mob is not None:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*animations)
self.current_scene_num_mob = new_scene_num
def clear_section(self):
"""Clears elements specific to the current section."""
for mob in self.section_elements:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
self.play_section_02()
self.clear_section()
self.wait(1)
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Section 2: Gravity ---
def play_section_02(self):
"""Section 2: Introduce Gravity."""
self.update_scene_number("02")
# Title/Explanation Text
gravity_title = Text("首先,是重力的作用", font_size=40, color=MY_DARK_TEXT)
gravity_title.to_edge(UP, buff=MED_LARGE_BUFF)
gravity_expl = Text("地球会对所有物体产生向下的引力,这就是重力。", font_size=32, color=MY_DARK_TEXT)
gravity_expl.next_to(gravity_title, DOWN, buff=MED_SMALL_BUFF)
text_group = VGroup(gravity_title, gravity_expl)
self.section_elements.add(text_group)
# Feather (Positioned higher)
feather_shape = Polygon(
[-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
[0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
).scale(0.8).rotate(-PI / 12)
feather = VGroup(feather_shape).move_to(LEFT * 3 + UP * 1.5)
self.section_elements.add(feather)
# Stone (Positioned higher)
stone_shape = Polygon(
[-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
[0.1, 0.9, 0], [-0.7, 0.5, 0],
color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
).scale(1.2)
stone = VGroup(stone_shape).move_to(RIGHT * 3 + UP * 1.5)
self.section_elements.add(stone)
# Gravity Arrows
gravity_arrow_feather = Arrow(
start=feather.get_center() + UP * 0.2,
end=feather.get_center() + DOWN * 1.5,
color=MY_GRAVITY_RED,
buff=0.1,
stroke_width=6,
max_tip_length_to_length_ratio=0.2
)
gravity_arrow_stone = Arrow(
start=stone.get_center() + UP * 0.2,
end=stone.get_center() + DOWN * 1.5,
color=MY_GRAVITY_RED,
buff=0.1,
stroke_width=6,
max_tip_length_to_length_ratio=0.2
)
gravity_elements = VGroup(gravity_arrow_feather, gravity_arrow_stone)
self.section_elements.add(gravity_elements)
# Narration 2 (Chinese)
voice_text_02 = "首先,我们需要了解重力。地球吸引着它附近的所有物体,无论是羽毛还是石头,都会受到一个向下的拉力,这就是重力。"
with custom_voiceover_tts(voice_text_02) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 2 TTS failed.")
subtitle_voice = Text(
voice_text_02,
font_size=32,
color=MY_DARK_TEXT,
width=config.frame_width - 2,
should_center=True
).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(text_group), FadeIn(subtitle_voice), run_time=1.5)
self.play(FadeIn(feather), FadeIn(stone), run_time=1.0)
self.play(
Create(gravity_arrow_feather),
Create(gravity_arrow_stone),
run_time=2.0
)
anim_duration = 1.5 + 1.0 + 2.0
remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
if remaining_wait_time > 0:
self.wait(remaining_wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Main execution block ---
if __name__ == "__main__":
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 30
config.output_file = "CombinedScene"
config.disable_caching = True
config.media_dir = r"#(output_path)"
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
运行代码
运行后返回的 m3u8 地址为:
/data/hls/500674430197940224/main.m3u8
2.5 第 3 个提示
提示词
omit
推理与输出示例
推理耗时约 30.5 秒,生成的 Scene 3 代码如下(本场景用于介绍空气阻力的初步效果):
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone
# --- Font Check ---
# Specify a preferred Chinese font
DEFAULT_FONT = "Noto Sans CJK SC" # Example: Noto Sans Simplified Chinese
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text):
"""Generates a unique filename based on the text hash."""
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
pass
else:
try:
input_text_encoded = requests.utils.quote(text)
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file)
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass
# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
"""
Explains why feathers fall slower than stones.
Scene 3: Introduce Air Resistance.
Uses Chinese narration and subtitles.
"""
def setup(self):
Scene.setup(self)
if final_font:
Text.set_default(font=final_font)
self.current_scene_num_mob = None
self.section_elements = VGroup()
self.camera.background_color = MY_SKY_BLUE
def update_scene_number(self, number_str, color=MY_DARK_TEXT):
new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
if self.current_scene_num_mob is not None:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*animations)
self.current_scene_num_mob = new_scene_num
def clear_section(self):
for mob in self.section_elements:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
self.play_section_03()
self.clear_section()
self.wait(1)
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Section 3: Air Resistance ---
def play_section_03(self):
"""Section 3: Introduce Air Resistance."""
self.update_scene_number("03")
# Title/Explanation Text
air_res_title = Text("但是,还有空气阻力!", font_size=40, color=MY_DARK_TEXT)
air_res_title.to_edge(UP, buff=MED_LARGE_BUFF)
air_res_expl = Text("物体在空气中下落时,会受到空气向上的阻力。", font_size=32, color=MY_DARK_TEXT, width=config.frame_width - 4)
air_res_expl.next_to(air_res_title, DOWN, buff=MED_SMALL_BUFF)
text_group = VGroup(air_res_title, air_res_expl)
self.section_elements.add(text_group)
# Feather (Positioned higher)
feather_shape = Polygon(
[-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
[0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
).scale(0.8).rotate(-PI / 12)
feather = VGroup(feather_shape).move_to(LEFT * 3 + UP * 0.5)
self.section_elements.add(feather)
# Stone (Positioned higher)
stone_shape = Polygon(
[-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
[0.1, 0.9, 0], [-0.7, 0.5, 0],
color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
).scale(1.2)
stone = VGroup(stone_shape).move_to(RIGHT * 3 + UP * 0.5)
self.section_elements.add(stone)
# Gravity Arrows (Slightly faded or smaller)
gravity_arrow_feather = Arrow(
start=feather.get_center() + UP * 0.1,
end=feather.get_center() + DOWN * 1.0,
color=MY_GRAVITY_RED,
buff=0.1, stroke_width=5, max_tip_length_to_length_ratio=0.2
).set_opacity(0.7)
gravity_arrow_stone = Arrow(
start=stone.get_center() + UP * 0.1,
end=stone.get_center() + DOWN * 1.0,
color=MY_GRAVITY_RED,
buff=0.1, stroke_width=5, max_tip_length_to_length_ratio=0.2
).set_opacity(0.7)
gravity_elements = VGroup(gravity_arrow_feather, gravity_arrow_stone)
self.section_elements.add(gravity_elements)
# Air Resistance Arrows (Upwards)
air_res_arrow_feather = Arrow(
start=feather.get_center() + DOWN * 0.8,
end=feather.get_center() + UP * 1.2,
color=MY_AIR_RESISTANCE_BLUE,
buff=0.1,
stroke_width=6,
max_tip_length_to_length_ratio=0.2
)
air_res_arrow_stone = Arrow(
start=stone.get_center() + DOWN * 0.8,
end=stone.get_center() + UP * 0.4,
color=MY_AIR_RESISTANCE_BLUE,
buff=0.1,
stroke_width=6,
max_tip_length_to_length_ratio=0.2
)
air_res_label = Text("空气阻力 (Air Resistance)", font_size=24, color=MY_AIR_RESISTANCE_BLUE)
air_res_label.next_to(air_res_arrow_feather, LEFT, buff=SMALL_BUFF)
air_res_elements = VGroup(air_res_arrow_feather, air_res_arrow_stone, air_res_label)
self.section_elements.add(air_res_elements)
# Subtle Air Particles (Optional)
air_particles = VGroup(*[
Dot(point=[np.random.uniform(-config.frame_width/2+1, config.frame_width/2-1),
np.random.uniform(-config.frame_height/2+1, config.frame_height/2-1),
0],
radius=0.02, color=MY_WHITE, fill_opacity=0.5)
for _ in range(50)
]).set_z_index(-1)
self.section_elements.add(air_particles)
# Narration 3 (Chinese)
voice_text_03 = "但是,除了重力,还有一个重要的力在起作用,那就是空气阻力。当物体在空气中下落时,空气会给物体一个向上的阻力。这个阻力的大小和物体的形状、表面积以及下落速度有关。"
with custom_voiceover_tts(voice_text_03) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 3 TTS failed.")
subtitle_voice = Text(
voice_text_03,
font_size=32,
color=MY_DARK_TEXT,
width=config.frame_width - 2,
should_center=True
).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(text_group), FadeIn(subtitle_voice), run_time=1.5)
self.play(FadeIn(feather), FadeIn(stone), FadeIn(gravity_elements), FadeIn(air_particles), run_time=1.0)
self.play(
Create(air_res_arrow_feather),
Create(air_res_arrow_stone),
FadeIn(air_res_label),
run_time=2.5
)
anim_duration = 1.5 + 1.0 + 2.5
remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
if remaining_wait_time > 0:
self.wait(remaining_wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Main execution block ---
if __name__ == "__main__":
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 30
config.output_file = "CombinedScene"
config.disable_caching = True
config.media_dir = r"#(output_path)"
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
运行代码
运行完成后返回的 m3u8 地址为:
/data/hls/500675171104964608/main.m3u8
2.6 第 4 个提示
提示词
omit
推理与输出示例
推理耗时约 32.7 秒,生成的 Scene 4 代码如下(本场景展示合力的影响和物体下落过程):
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone
# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC"
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
MY_NET_FORCE_GREEN = "#4CAF50"
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text):
"""Generates a unique filename based on the text hash."""
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
pass
else:
try:
input_text_encoded = requests.utils.quote(text)
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file)
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass
# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
"""
Explains why feathers fall slower than stones.
Scene 4: Effect of Net Force.
Uses Chinese narration and subtitles.
"""
def setup(self):
Scene.setup(self)
if final_font:
Text.set_default(font=final_font)
self.current_scene_num_mob = None
self.section_elements = VGroup()
self.camera.background_color = MY_SKY_BLUE
def update_scene_number(self, number_str, color=MY_DARK_TEXT):
new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
if self.current_scene_num_mob is not None:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*animations)
self.current_scene_num_mob = new_scene_num
def clear_section(self):
for mob in self.section_elements:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
self.play_section_04()
self.clear_section()
self.wait(1)
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Section 4: Effect of Net Force ---
def play_section_04(self):
"""Section 4: Show the effect of forces and the fall."""
self.update_scene_number("04")
# Title
title = Text("合力的影响 (Effect of Net Force)", font_size=40, color=MY_DARK_TEXT)
title.to_edge(UP, buff=MED_LARGE_BUFF)
self.section_elements.add(title)
# Ground Line
ground = Line(LEFT * config.frame_width/2, RIGHT * config.frame_width/2, color=MY_GROUND_BROWN, stroke_width=5)
ground.to_edge(DOWN, buff=1.0)
self.section_elements.add(ground)
# Starting Positions (Higher)
start_y = config.frame_height / 2 - 1.5
feather_start_pos = LEFT * 3 + UP * start_y
stone_start_pos = RIGHT * 3 + UP * start_y
# Feather
feather_shape = Polygon(
[-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
[0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
).scale(0.8).rotate(-PI / 12)
feather = VGroup(feather_shape).move_to(feather_start_pos)
self.section_elements.add(feather)
# Stone
stone_shape = Polygon(
[-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
[0.1, 0.9, 0], [-0.7, 0.5, 0],
color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
).scale(1.2)
stone = VGroup(stone_shape).move_to(stone_start_pos)
self.section_elements.add(stone)
# Forces Visualization (Static representation)
gravity_feather = Arrow(feather.get_center(), feather.get_center() + DOWN * 1.0, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
gravity_stone = Arrow(stone.get_center(), stone.get_center() + DOWN * 1.5, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
air_res_feather = Arrow(feather.get_center(), feather.get_center() + UP * 0.9, color=MY_AIR_RESISTANCE_BLUE, buff=0.1, stroke_width=5)
air_res_stone = Arrow(stone.get_center(), stone.get_center() + UP * 0.3, color=MY_AIR_RESISTANCE_BLUE, buff=0.1, stroke_width=5)
net_force_feather = Arrow(feather.get_center()+DOWN*0.1, feather.get_center() + DOWN * 0.2, color=MY_NET_FORCE_GREEN, buff=0.1, stroke_width=6)
net_force_stone = Arrow(stone.get_center()+DOWN*0.1, stone.get_center() + DOWN * 1.3, color=MY_NET_FORCE_GREEN, buff=0.1, stroke_width=6)
label_gravity = Text("重力", font_size=20, color=MY_GRAVITY_RED).next_to(gravity_stone, RIGHT, buff=0.1)
label_air_res = Text("空气阻力", font_size=20, color=MY_AIR_RESISTANCE_BLUE).next_to(air_res_stone, RIGHT, buff=0.1)
label_net_force = Text("合力 (Net Force)", font_size=20, color=MY_NET_FORCE_GREEN).next_to(net_force_stone, RIGHT, buff=0.1)
forces_group = VGroup(
gravity_feather, gravity_stone, air_res_feather, air_res_stone,
net_force_feather, net_force_stone,
label_gravity, label_air_res, label_net_force
)
self.section_elements.add(forces_group)
# Explanation Text
explanation = Text(
"羽毛受到的空气阻力很大,几乎抵消了重力,所以向下的合力很小。\n石头受到的空气阻力远小于其重力,所以向下的合力很大。",
font_size=28, color=MY_DARK_TEXT, line_spacing=1.5, width=config.frame_width - 6
).next_to(title, DOWN, buff=MED_LARGE_BUFF)
self.section_elements.add(explanation)
# Narration 4 (Chinese)
voice_text_04 = "关键就在于这两个力的合力。对于羽毛来说,它的表面积大,重量轻,所以空气阻力非常显著,几乎可以抵消掉大部分重力。这样一来,作用在羽毛上向下的合力就很小。而石头密度大,表面积相对较小,重力远大于空气阻力,因此它受到的向下的合力就大得多。根据牛顿第二定律,合力越大,加速度越大,下落得就越快。"
with custom_voiceover_tts(voice_text_04) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 4 TTS failed.")
subtitle_voice = Text(
voice_text_04,
font_size=32,
color=MY_DARK_TEXT,
width=config.frame_width - 2,
should_center=True
).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(FadeIn(feather), FadeIn(stone), run_time=0.5)
self.play(FadeIn(explanation), Create(forces_group), run_time=3.0)
self.wait(6.0)
self.play(FadeOut(forces_group), FadeOut(explanation), run_time=0.5)
stone_fall_anim = stone.animate.move_to(stone_start_pos + DOWN * (start_y - ground.get_y() - stone.height/2)).set_run_time(1.5).set_rate_func(rate_functions.ease_in_quad)
feather_target_pos = feather_start_pos + DOWN * (start_y - ground.get_y() - feather.height/2) * 0.6 + RIGHT * 0.5
feather_fall_anim = feather.animate.move_to(feather_target_pos).set_run_time(4.0).set_rate_func(rate_functions.smooth)
self.play(stone_fall_anim, feather_fall_anim)
anim_duration = 1.0 + 0.5 + 3.0 + 6.0 + 0.5 + 4.0
remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
if remaining_wait_time > 0:
self.wait(remaining_wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Main execution block ---
if __name__ == "__main__":
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 30
config.output_file = "CombinedScene"
config.disable_caching = True
config.media_dir = r"#(output_path)"
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
运行代码
运行完成返回地址:
/data/hls/500675755279237120/main.m3u8
2.7 第 5 个提示
提示词
omit
推理与输出示例
推理耗时约 30.6 秒,生成的 Scene 5 代码如下(本场景展示真空环境下物体下落的情况):
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone
# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC"
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
MY_NET_FORCE_GREEN = "#4CAF50"
MY_GROUND_BROWN = "#A1887F"
MY_VACUUM_BG = "#37474F"
MY_VACUUM_BORDER = "#B0BEC5"
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text):
"""Generates a unique filename based on the text hash."""
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
pass
else:
try:
input_text_encoded = requests.utils.quote(text)
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file)
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass
# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
"""
Explains why feathers fall slower than stones.
Scene 5: In a Vacuum.
Uses Chinese narration and subtitles.
"""
def setup(self):
Scene.setup(self)
if final_font:
Text.set_default(font=final_font)
self.current_scene_num_mob = None
self.section_elements = VGroup()
# Change background to represent vacuum environment
self.camera.background_color = MY_VACUUM_BG
def update_scene_number(self, number_str, color=MY_WHITE):
new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
if self.current_scene_num_mob is not None:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*animations)
self.current_scene_num_mob = new_scene_num
def clear_section(self):
for mob in self.section_elements:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
self.play_section_05()
self.clear_section()
self.wait(1)
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Section 5: In a Vacuum ---
def play_section_05(self):
"""Section 5: Show the fall in a vacuum."""
self.update_scene_number("05")
# Title
title = Text("在真空中会怎样? (In a Vacuum?)", font_size=40, color=MY_WHITE)
title.to_edge(UP, buff=MED_LARGE_BUFF)
self.section_elements.add(title)
# Vacuum Chamber Representation
chamber_height = config.frame_height - 3.0
chamber_width = 6.0
vacuum_chamber = Rectangle(
height=chamber_height, width=chamber_width,
color=MY_VACUUM_BORDER, stroke_width=3
).move_to(ORIGIN)
chamber_label = Text("真空环境 (Vacuum)", font_size=24, color=MY_WHITE).next_to(vacuum_chamber, DOWN)
chamber_group = VGroup(vacuum_chamber, chamber_label)
self.section_elements.add(chamber_group)
# Starting Positions (Inside chamber)
start_y = vacuum_chamber.get_top()[1] - 1.0
feather_start_pos = vacuum_chamber.get_center() + LEFT * (chamber_width/4) + UP * (chamber_height/2 - 1.0)
stone_start_pos = vacuum_chamber.get_center() + RIGHT * (chamber_width/4) + UP * (chamber_height/2 - 1.0)
# Feather
feather_shape = Polygon(
[-0.5, -1.5, 0], [-0.4, 0, 0], [-0.2, 1, 0], [0, 1.5, 0],
[0.2, 1, 0], [0.4, 0, 0], [0.5, -1.5, 0],
color=MY_FEATHER_WHITE, fill_opacity=1.0, stroke_width=1, stroke_color=MY_DARK_TEXT
).scale(0.6).rotate(-PI / 12)
feather = VGroup(feather_shape).move_to(feather_start_pos)
self.section_elements.add(feather)
# Stone
stone_shape = Polygon(
[-0.8, -0.6, 0], [0.5, -0.8, 0], [0.9, 0.2, 0],
[0.1, 0.9, 0], [-0.7, 0.5, 0],
color=MY_STONE_GRAY, fill_opacity=1.0, stroke_width=0
).scale(0.9)
stone = VGroup(stone_shape).move_to(stone_start_pos)
self.section_elements.add(stone)
# Gravity Arrows Only
gravity_feather = Arrow(feather.get_center(), feather.get_center() + DOWN * 1.0, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
gravity_stone = Arrow(stone.get_center(), stone.get_center() + DOWN * 1.0, color=MY_GRAVITY_RED, buff=0.1, stroke_width=5)
gravity_label = Text("只有重力 (Gravity Only)", font_size=20, color=MY_GRAVITY_RED).next_to(gravity_stone, RIGHT, buff=0.1)
forces_group = VGroup(gravity_feather, gravity_stone, gravity_label)
# Narration 5 (Chinese)
voice_text_05 = "那么,如果在一个没有空气,也就是真空的环境里,情况会怎样呢?在真空中,没有空气阻力。唯一作用在物体上的力就是重力。根据物理学原理,在只受重力作用的情况下,所有物体,无论轻重、形状如何,都会以相同的速度下落。"
with custom_voiceover_tts(voice_text_05) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 5 TTS failed.")
subtitle_voice = Text(
voice_text_05,
font_size=32,
color=MY_WHITE,
width=config.frame_width - 2,
should_center=True
).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(title), FadeIn(chamber_group), FadeIn(subtitle_voice), run_time=1.5)
self.play(FadeIn(feather), FadeIn(stone), run_time=1.0)
self.play(Create(gravity_feather), Create(gravity_stone), FadeIn(gravity_label), run_time=1.5)
target_y = vacuum_chamber.get_bottom()[1] + 0.5
feather_target_pos = feather.get_center() * [1,0,0] + target_y * UP
stone_target_pos = stone.get_center() * [1,0,0] + target_y * UP
fall_duration = 2.5
feather_fall_anim = feather.animate.move_to(feather_target_pos).set_run_time(fall_duration).set_rate_func(rate_functions.ease_in_quad)
stone_fall_anim = stone.animate.move_to(stone_target_pos).set_run_time(fall_duration).set_rate_func(rate_functions.ease_in_quad)
self.play(feather_fall_anim, stone_fall_anim)
anim_duration = 1.5 + 1.0 + 1.5 + 5.0 + 0.5 + fall_duration
remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
if remaining_wait_time > 0:
self.wait(remaining_wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Main execution block ---
if __name__ == "__main__":
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 30
config.output_file = "CombinedScene"
config.disable_caching = True
config.media_dir = r"#(output_path)"
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
运行代码
运行结束后返回地址:
/data/hls/500677626312114176/main.m3u8
2.8 第 6 个提示
提示词
omit
推理与输出示例
推理耗时约 28.08 秒,生成的 Scene 6 代码如下(本场景为总结部分,对全部内容做概括):
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
from manim.utils.color.SVGNAMES import BROWN # Import BROWN for stone
# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC"
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor fallback Chinese fonts found. Using Manim default. Chinese characters may not display correctly.")
# --- Custom Colors ---
MY_SKY_BLUE = "#ADD8E6"
MY_FEATHER_WHITE = "#F5F5F5"
MY_STONE_GRAY = "#808080"
MY_DARK_TEXT = "#1F2937"
MY_WHITE = "#FFFFFF"
MY_GOLD = "#B8860B"
MY_GRAVITY_RED = "#E53935"
MY_AIR_RESISTANCE_BLUE = "#03A9F4"
MY_NET_FORCE_GREEN = "#4CAF50"
MY_GROUND_BROWN = "#A1887F"
MY_VACUUM_BG = "#37474F"
MY_VACUUM_BORDER = "#B0BEC5"
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/audio"
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text):
"""Generates a unique filename based on the text hash."""
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
pass
else:
try:
input_text_encoded = requests.utils.quote(text)
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file)
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass
# -----------------------------
# CombinedScene: Why Feathers Fall Slower Than Stones
# -----------------------------
class CombinedScene(Scene):
"""
Explains why feathers fall slower than stones.
Scene 6: Conclusion/Summary.
Uses Chinese narration and subtitles.
"""
def setup(self):
Scene.setup(self)
if final_font:
Text.set_default(font=final_font)
self.current_scene_num_mob = None
self.section_elements = VGroup()
self.camera.background_color = MY_SKY_BLUE
def update_scene_number(self, number_str, color=MY_DARK_TEXT):
new_scene_num = Text(number_str, font_size=24, color=color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
if self.current_scene_num_mob is not None:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*animations)
self.current_scene_num_mob = new_scene_num
def clear_section(self):
for mob in self.section_elements:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
valid_elements = [elem for elem in self.section_elements if elem is not None]
if valid_elements:
self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
self.play_section_06()
self.clear_section()
self.wait(1)
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Section 6: Conclusion ---
def play_section_06(self):
"""Section 6: Summarize the key points."""
self.update_scene_number("06")
# Title
title = Text("总结 (Conclusion)", font_size=48, color=MY_DARK_TEXT)
title.to_edge(UP, buff=MED_LARGE_BUFF)
self.section_elements.add(title)
# Summary Points
point_size = 32
line_width = config.frame_width - 4
point1 = Text("• 重力对所有物体都起作用。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
point2 = Text("• 空气阻力与物体的形状、表面积和速度有关。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
point3 = Text("• 羽毛受到的空气阻力远大于其重力影响,石头则相反。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
point4 = Text("• 因此,在空气中,羽毛的合力小,下落慢;石头的合力大,下落快。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
point5 = Text("• 在真空中,没有空气阻力,两者只受重力,下落速度相同。", font_size=point_size, color=MY_DARK_TEXT, width=line_width)
summary_group = VGroup(point1, point2, point3, point4, point5).arrange(
DOWN, buff=MED_LARGE_BUFF * 0.8, aligned_edge=LEFT
)
summary_group.next_to(title, DOWN, buff=LARGE_BUFF)
summary_group.align_on_border(LEFT, buff=1.0)
self.section_elements.add(summary_group)
# Narration 6 (Chinese)
voice_text_06 = "总结一下:羽毛下落得比石头慢,主要是因为空气阻力的影响。虽然重力对两者都起作用,但羽毛轻飘飘、表面积大,受到的空气阻力非常显著,大大减缓了它的下落速度。而石头又重又密实,空气阻力对其影响小得多。所以,在空气中,石头会比羽毛下落得快得多。但在真空中,没有了空气阻力,它们就会以相同的速度下落啦!"
with custom_voiceover_tts(voice_text_06) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 6 TTS failed.")
subtitle_voice = Text(
voice_text_06,
font_size=32,
color=MY_DARK_TEXT,
width=config.frame_width - 2,
should_center=True
).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(FadeIn(point1, shift=UP*0.1), run_time=1.0)
self.play(FadeIn(point2, shift=UP*0.1), run_time=1.5)
self.play(FadeIn(point3, shift=UP*0.1), run_time=2.0)
self.play(FadeIn(point4, shift=UP*0.1), run_time=2.0)
self.play(FadeIn(point5, shift=UP*0.1), run_time=2.0)
anim_duration = 1.0 + 1.0 + 1.5 + 2.0 + 2.0 + 2.0
remaining_wait_time = max(0, tracker.duration - anim_duration - 0.5) if tracker.duration > 0 else 1.0
if remaining_wait_time > 0:
self.wait(remaining_wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Main execution block ---
if __name__ == "__main__":
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 30
config.output_file = "CombinedScene"
config.disable_caching = True
config.media_dir = r"#(output_path)"
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
运行代码
运行返回地址:
/data/hls/500678285593788416/main.m3u8
2.9 第 7 个提示
提示词
omit
推理与输出示例
推理耗时约 5 秒,输出结果为:
done
这表明大模型确认没有后续场景,生成结果为 “done”。
3. 整理视频
完成所有场景生成后,整理的视频文件 URL 如下(每个场景的视频已切片生成主 m3u8 文件,并合并成 mp4 文件):
https://manim.collegebot.ai/data/hls/500673260100370432/main.mp4
https://manim.collegebot.ai/data/hls/500674430197940224/main.mp4
https://manim.collegebot.ai/data/hls/500675171104964608/main.mp4
https://manim.collegebot.ai/data/hls/500675755279237120/main.mp4
https://manim.collegebot.ai/data/hls/500677626312114176/main.mp4
https://manim.collegebot.ai/data/hls/500678285593788416/main.mp4
对应的文件名为:
main_1.mp4
main_2.mp4
main_3.mp4
main_4.mp4
main_5.mp4
main_6.mp4
4. 合并视频
最后,通过合并各场景生成的视频切片,得到整体视频文件。下面提供了 Java 代码示例,该代码演示了如何初始化一个持久化 HLS 会话,然后追加所有视频片段,最后合并生成完整的视频。
package com.litongjava.linux.service;
import java.io.File;
import org.junit.Test;
import com.litongjava.media.NativeMedia;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HlsSenceTest {
@Test
public void testSession() {
// 配置测试用路径,请根据实际情况修改
String playlistUrl = "./data/hls/test/main.m3u8";
String tsPattern = "./data/hls/test/segment_video_%03d.ts";
int startNumber = 0;
int segmentDuration = 10; // 每个分段时长(秒)
long sessionPtr = NativeMedia.initPersistentHls(playlistUrl, tsPattern, startNumber, segmentDuration);
System.out.println("Session pointer: " + sessionPtr);
String folderPath = "C:\\Users\\Administrator\\Downloads";
File folderFile = new File(folderPath);
File[] listFiles = folderFile.listFiles();
for (int i = 0; i < listFiles.length; i++) {
log.info("filename:{}", listFiles[i].getName());
if (listFiles[i].getName().endsWith(".mp4")) {
System.out.println(NativeMedia.appendVideoSegmentToHls(sessionPtr, listFiles[i].getAbsolutePath()));
}
}
// 结束会话
System.out.println(NativeMedia.finishPersistentHls(sessionPtr, playlistUrl));
}
}
总结
本文档详细描述了 manim 分场景生成的解决方案,包括:
- 背景和设计思路:通过分场景生成代码,可提高输出速度,并针对运行错误进行自动修复,确保前端能尽早播放第一个场景的视频,同时后续场景逐步追加。
- 交互流程:通过预设提示词,依次生成每个场景的 Python 脚本代码;在生成过程中如果发生错误,则调用大模型修复代码;最后确认无后续场景时输出 “done”。
- 完整代码示例:文档中从场景 1 到场景 7(done)的所有代码均完整展示,并附有详细的注释说明;另外,视频整理和合并视频的 Java 示例代码也完整展示,便于读者快速搭建和扩展系统。
希望本篇文档能够帮助您全面理解 manim 分场景生成的实现方案及具体代码,进而实现高效且健壮的视频自动生成系统。