Finished SceneFileWriter refactor

This commit is contained in:
Grant Sanderson 2019-01-24 22:24:01 -08:00
parent 8ae0556394
commit e5e1fa908b
14 changed files with 94 additions and 111 deletions

View file

@ -60,7 +60,7 @@ Set MEDIA_DIR environment variable to determine where image and animation files
Look through the old_projects folder to see the code for previous 3b1b videos. Note, however, that developments are often made to the library without considering backwards compatibility on those old_projects. To run them with a guarantee that they will work, you will have to go back to the commit which complete that project.
While developing a scene, the `-s` flag is helpful to just see what things look like at the end without having to generate the full animation. It can also be helpful to use the `-n` flag to skip over some number of animations.
While developing a scene, the `-sp` flags are helpful to just see what things look like at the end without having to generate the full animation. It can also be helpful to use the `-n` flag to skip over some number of animations.
### Documentation
Documentation is in progress at [manim.readthedocs.io](https://manim.readthedocs.io).

View file

@ -1,4 +0,0 @@
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00000.mp4'
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00001.mp4'
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00002.mp4'
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00003.mp4'

View file

@ -7,8 +7,9 @@ from big_ol_pile_of_manim_imports import *
#
# Use the flat -l for a faster rendering at a lower
# quality.
# Use -s to skip to the end and just show the final frame
# Use the -p to have the animation pop up once done.
# Use -s to skip to the end and just save the final frame
# Use the -p to have the animation (or image, if -s was
# used) pop up once done.
# Use -n <number> to skip ahead to the n'th animation of a scene.

View file

@ -25,7 +25,7 @@ def parse_cli():
parser.add_argument(
"-p", "--preview",
action="store_true",
help="Automatically open movie file once its done",
help="Automatically open the saved file once its done",
),
parser.add_argument(
"-w", "--write_to_movie",
@ -35,7 +35,7 @@ def parse_cli():
parser.add_argument(
"-s", "--save_last_frame",
action="store_true",
help="Save the last frame and open the image file",
help="Save the last frame",
),
parser.add_argument(
"-l", "--low_quality",

View file

@ -33,7 +33,6 @@ class Scene(Container):
}
def __init__(self, **kwargs):
# Perhaps allow passing in a non-empty *mobjects parameter?
Container.__init__(self, **kwargs)
self.camera = self.camera_class(**self.camera_config)
self.file_writer = SceneFileWriter(
@ -42,6 +41,7 @@ class Scene(Container):
self.mobjects = []
self.continual_animations = []
# TODO, remove need for foreground mobjects
self.foreground_mobjects = []
self.num_plays = 0
self.time = 0
@ -59,16 +59,6 @@ class Scene(Container):
self.file_writer.finish()
self.print_end_message()
def handle_play_like_call(func):
def wrapper(self, *args, **kwargs):
self.handle_animation_skipping()
allow_write = not self.skip_animations
self.file_writer.begin_animation(allow_write)
func(self, *args, **kwargs)
self.file_writer.end_animation(allow_write)
self.num_plays += 1
return wrapper
def setup(self):
"""
This is meant to be implement by any scenes which
@ -111,8 +101,6 @@ class Scene(Container):
def get_attrs(self, *keys):
return [getattr(self, key) for key in keys]
# TODO, Scene file writer now handles sound
# Only these methods should touch the camera
def set_camera(self, camera):
self.camera = camera
@ -449,7 +437,7 @@ class Scene(Container):
compile_method(state)
return animations
def handle_animation_skipping(self):
def update_skipping_status(self):
if self.start_at_animation_number:
if self.num_plays == self.start_at_animation_number:
self.skip_animations = False
@ -458,6 +446,16 @@ class Scene(Container):
self.skip_animations = True
raise EndSceneEarlyException()
def handle_play_like_call(func):
def wrapper(self, *args, **kwargs):
self.update_skipping_status()
allow_write = not self.skip_animations
self.file_writer.begin_animation(allow_write)
func(self, *args, **kwargs)
self.file_writer.end_animation(allow_write)
self.num_plays += 1
return wrapper
@handle_play_like_call
def play(self, *args, **kwargs):
if len(args) == 0:
@ -496,7 +494,6 @@ class Scene(Container):
return self
# TODO
def idle_stream(self):
self.file_writer.idle_stream()
@ -569,6 +566,10 @@ class Scene(Container):
for frame in frames:
self.file_writer.write_frame(frame)
def add_sound(self, sound_file, time_offset=0):
time = self.get_time() + time_offset
self.file_writer.add_sound(sound_file, time)
def show_frame(self):
self.update_frame(ignore_skipping=True)
self.get_image().show()

View file

@ -16,6 +16,7 @@ from manimlib.utils.config_ops import digest_config
from manimlib.utils.file_ops import guarantee_existance
from manimlib.utils.file_ops import add_extension_if_not_present
from manimlib.utils.file_ops import get_sorted_integer_files
from manimlib.utils.sounds import get_full_sound_file_path
class SceneFileWriter(object):
@ -38,12 +39,11 @@ class SceneFileWriter(object):
def __init__(self, scene, **kwargs):
digest_config(self, kwargs)
self.scene = scene
self.init_audio()
self.init_output_directories()
self.stream_lock = False
self.init_output_directories()
self.init_audio()
# Output directories and files
def init_output_directories(self):
output_directory = self.output_directory or self.get_default_output_directory()
file_name = self.file_name or self.get_default_file_name()
@ -59,6 +59,7 @@ class SceneFileWriter(object):
)
if self.write_to_movie:
movie_dir = guarantee_existance(os.path.join(
VIDEO_DIR,
output_directory,
self.get_movie_directory(),
))
@ -94,39 +95,6 @@ class SceneFileWriter(object):
def get_partial_movie_directory(self):
return "partial_movie_directory"
# Sound
# TODO, make work with Scene
def init_audio(self):
self.includes_sound = False
def create_audio_segment(self):
self.audio_segment = AudioSegment.silent()
def add_audio_segment(self, new_segment, time_offset=0):
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
segment = self.audio_segment
overly_time = self.get_time() + time_offset
if overly_time < 0:
raise Exception("Adding sound at timestamp < 0")
curr_end = segment.duration_seconds
new_end = overly_time + new_segment.duration_seconds
diff = new_end - curr_end
if diff > 0:
segment = segment.append(
AudioSegment.silent(int(np.ceil(diff * 1000))),
crossfade=0,
)
self.audio_segment = segment.overlay(
new_segment, position=int(1000 * overly_time)
)
def add_sound(self, sound_file, time_offset=0):
new_segment = AudioSegment.from_file(sound_file)
self.add_audio_segment(new_segment, 0)
# Directory getters
def get_image_file_path(self):
return self.image_file_path
@ -144,16 +112,41 @@ class SceneFileWriter(object):
def get_movie_file_path(self):
return self.movie_file_path
# Sound
def init_audio(self):
self.includes_sound = False
def create_audio_segment(self):
self.audio_segment = AudioSegment.silent()
def add_audio_segment(self, new_segment, time=None):
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
segment = self.audio_segment
curr_end = segment.duration_seconds
if time is None:
time = curr_end
if time < 0:
raise Exception("Adding sound at timestamp < 0")
new_end = time + new_segment.duration_seconds
diff = new_end - curr_end
if diff > 0:
segment = segment.append(
AudioSegment.silent(int(np.ceil(diff * 1000))),
crossfade=0,
)
self.audio_segment = segment.overlay(
new_segment, position=int(1000 * time)
)
def add_sound(self, sound_file, time):
file_path = get_full_sound_file_path(sound_file)
new_segment = AudioSegment.from_file(file_path)
self.add_audio_segment(new_segment, time)
# Writers
def write_frame(self, frame):
if self.write_to_movie:
self.writing_process.stdin.write(frame.tostring())
def save_image(self, image):
file_path = self.get_image_file_path()
image.save(file_path)
self.print_file_ready_message(file_path)
def begin_animation(self, allow_write=False):
if self.write_to_movie and allow_write:
self.open_movie_pipe()
@ -167,6 +160,15 @@ class SceneFileWriter(object):
self.stream_lock = True
thread.start_new_thread(self.idle_stream, ())
def write_frame(self, frame):
if self.write_to_movie:
self.writing_process.stdin.write(frame.tostring())
def save_image(self, image):
file_path = self.get_image_file_path()
image.save(file_path)
self.print_file_ready_message(file_path)
def idle_stream(self):
while self.stream_lock:
a = datetime.datetime.now()

View file

@ -16,38 +16,16 @@ def guarantee_existance(path):
return os.path.abspath(path)
# def get_scene_output_directory(scene_class):
# return guarantee_existance(os.path.join(
# VIDEO_DIR,
# scene_class.__module__.replace(".", os.path.sep)
# ))
# def get_movie_output_directory(scene_class, camera_config, frame_duration):
# directory = get_scene_output_directory(scene_class)
# sub_dir = "%dp%d" % (
# camera_config["pixel_height"],
# int(1.0 / frame_duration)
# )
# return guarantee_existance(os.path.join(directory, sub_dir))
# def get_partial_movie_output_directory(scene, camera_config, frame_duration):
# directory = get_movie_output_directory(
# scene.__class__, camera_config, frame_duration
# )
# return guarantee_existance(
# os.path.join(
# directory,
# "partial_movie_files",
# scene.get_output_file_name(),
# )
# )
# def get_image_output_directory(scene_class, sub_dir="images"):
# directory = get_scene_output_directory(scene_class)
# return guarantee_existance(os.path.join(directory, sub_dir))
def seek_full_path_from_defaults(file_name, default_dir, extensions):
possible_paths = [file_name]
possible_paths += [
os.path.join(default_dir, file_name + extension)
for extension in ["", *extensions]
]
for path in possible_paths:
if os.path.exists(path):
return path
raise IOError("File {} not Found".format(file_name))
def get_sorted_integer_files(directory,

View file

@ -4,20 +4,15 @@ import os
from PIL import Image
from manimlib.constants import RASTER_IMAGE_DIR
from manimlib.utils.file_ops import seek_full_path_from_defaults
def get_full_raster_image_path(image_file_name):
possible_paths = [
return seek_full_path_from_defaults(
image_file_name,
os.path.join(RASTER_IMAGE_DIR, image_file_name),
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".jpg"),
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".png"),
os.path.join(RASTER_IMAGE_DIR, image_file_name + ".gif"),
]
for path in possible_paths:
if os.path.exists(path):
return path
raise IOError("File %s not Found" % image_file_name)
default_dir=RASTER_IMAGE_DIR,
extensions=[".jpg", ".png", ".gif"]
)
def drag_pixels(frames):

View file

@ -1,4 +1,6 @@
import os
from manimlib.constants import SOUND_DIR
from manimlib.utils.file_ops import seek_full_path_from_defaults
def play_chord(*nums):
@ -28,3 +30,11 @@ def play_error_sound():
def play_finish_sound():
play_chord(12, 9, 5, 2)
def get_full_sound_file_path(sound_file_name):
return seek_full_path_from_defaults(
sound_file_name,
default_dir=SOUND_DIR,
extensions=[".wav", ".mp3"]
)