mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
First pass at SceneFileWriter refactor
This commit is contained in:
parent
77f0ca1ad7
commit
8ae0556394
18 changed files with 432 additions and 384 deletions
8
active_projects/clacks/all_questions_scenes.py
Normal file
8
active_projects/clacks/all_questions_scenes.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from active_projects import clacks
|
||||||
|
|
||||||
|
output_directory = "clacks_question"
|
||||||
|
all_scenes = [
|
||||||
|
clacks.NameIntro,
|
||||||
|
clacks.MathAndPhysicsConspiring,
|
||||||
|
clacks.LightBouncing,
|
||||||
|
]
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from big_ol_pile_of_manim_imports import *
|
from big_ol_pile_of_manim_imports import *
|
||||||
|
|
||||||
from active_projects.clacks import BlocksAndWallExample
|
from active_projects.clacks.question import BlocksAndWallExample
|
||||||
|
|
||||||
|
|
||||||
class NameBump(BlocksAndWallExample):
|
class NameBump(BlocksAndWallExample):
|
||||||
|
@ -72,7 +72,6 @@ class NameBump(BlocksAndWallExample):
|
||||||
block.label.set_fill(YELLOW, opacity=1)
|
block.label.set_fill(YELLOW, opacity=1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# for name in names:
|
# for name in names:
|
||||||
# file_name = name.replace(".", "")
|
# file_name = name.replace(".", "")
|
||||||
# file_name += " Name Bump"
|
# file_name += " Name Bump"
|
|
@ -346,10 +346,7 @@ class BlocksAndWallScene(Scene):
|
||||||
self.counter_mob.set_value(n_clacks)
|
self.counter_mob.set_value(n_clacks)
|
||||||
|
|
||||||
def create_sound_file(self, clack_data):
|
def create_sound_file(self, clack_data):
|
||||||
directory = get_scene_output_directory(BlocksAndWallScene)
|
clack_file = os.path.join(SOUND_DIR, self.collision_sound)
|
||||||
clack_file = os.path.join(
|
|
||||||
directory, 'sounds', self.collision_sound,
|
|
||||||
)
|
|
||||||
output_file = self.get_movie_file_path(extension='.wav')
|
output_file = self.get_movie_file_path(extension='.wav')
|
||||||
times = [
|
times = [
|
||||||
time
|
time
|
BIN
active_projects/clacks/question/480p15/NameIntro.mp4
Normal file
BIN
active_projects/clacks/question/480p15/NameIntro.mp4
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,4 @@
|
||||||
|
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'
|
|
@ -1,5 +1,5 @@
|
||||||
from big_ol_pile_of_manim_imports import *
|
from big_ol_pile_of_manim_imports import *
|
||||||
from active_projects.clacks import *
|
from active_projects.clacks.question import *
|
||||||
from old_projects.div_curl import ShowTwoPopulations
|
from old_projects.div_curl import ShowTwoPopulations
|
||||||
|
|
||||||
|
|
||||||
|
@ -206,10 +206,7 @@ class AskAboutFindingNewVelocities(Scene):
|
||||||
self.show_value_on_equations()
|
self.show_value_on_equations()
|
||||||
|
|
||||||
def add_clack_sound_file(self):
|
def add_clack_sound_file(self):
|
||||||
self.clack_file = os.path.join(
|
self.clack_file = os.path.join(SOUND_DIR, "clack.wav")
|
||||||
VIDEO_DIR, "active_projects",
|
|
||||||
"clacks", "sounds", "clack.wav"
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_floor(self):
|
def add_floor(self):
|
||||||
floor = self.floor = Line(
|
floor = self.floor = Line(
|
|
@ -88,7 +88,7 @@ from manimlib.utils.color import *
|
||||||
from manimlib.utils.config_ops import *
|
from manimlib.utils.config_ops import *
|
||||||
from manimlib.utils.images import *
|
from manimlib.utils.images import *
|
||||||
from manimlib.utils.iterables import *
|
from manimlib.utils.iterables import *
|
||||||
from manimlib.utils.output_directory_getters import *
|
from manimlib.utils.file_ops import *
|
||||||
from manimlib.utils.paths import *
|
from manimlib.utils.paths import *
|
||||||
from manimlib.utils.rate_functions import *
|
from manimlib.utils.rate_functions import *
|
||||||
from manimlib.utils.simple_functions import *
|
from manimlib.utils.simple_functions import *
|
||||||
|
|
|
@ -33,7 +33,7 @@ def parse_cli():
|
||||||
help="Render the scene as a movie file",
|
help="Render the scene as a movie file",
|
||||||
),
|
),
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s", "--show_last_frame",
|
"-s", "--save_last_frame",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Save the last frame and open the image file",
|
help="Save the last frame and open the image file",
|
||||||
),
|
),
|
||||||
|
@ -73,7 +73,7 @@ def parse_cli():
|
||||||
help="Write all the scenes from a file",
|
help="Write all the scenes from a file",
|
||||||
),
|
),
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o", "--output_file_name",
|
"-o", "--file_name",
|
||||||
help="Specify the name of the output file, if"
|
help="Specify the name of the output file, if"
|
||||||
"it should be different from the scene class name",
|
"it should be different from the scene class name",
|
||||||
)
|
)
|
||||||
|
@ -155,36 +155,25 @@ def get_module(file_name):
|
||||||
|
|
||||||
|
|
||||||
def get_configuration(args):
|
def get_configuration(args):
|
||||||
if args.output_file_name is not None:
|
file_writer_config = {
|
||||||
output_file_name_root, output_file_name_ext = os.path.splitext(
|
# By default, write to file
|
||||||
args.output_file_name)
|
"write_to_movie": args.write_to_movie or not args.save_last_frame,
|
||||||
expected_ext = '.png' if args.show_last_frame else '.mp4'
|
"save_last_frame": args.save_last_frame,
|
||||||
if output_file_name_ext not in ['', expected_ext]:
|
"save_pngs": args.save_pngs,
|
||||||
print("WARNING: The output will be to (doubly-dotted) %s%s" %
|
# If -t is passed in (for transparent), this will be RGBA
|
||||||
output_file_name_root % expected_ext)
|
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||||
output_file_name = args.output_file_name
|
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
||||||
else:
|
"file_name": args.file_name,
|
||||||
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
|
}
|
||||||
output_file_name = output_file_name_root
|
|
||||||
else:
|
|
||||||
output_file_name = args.output_file_name
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"module": get_module(args.file),
|
"module": get_module(args.file),
|
||||||
"scene_names": args.scene_names,
|
"scene_names": args.scene_names,
|
||||||
"open_video_upon_completion": args.preview,
|
"open_video_upon_completion": args.preview,
|
||||||
"show_file_in_finder": args.show_file_in_finder,
|
"show_file_in_finder": args.show_file_in_finder,
|
||||||
# By default, write to file
|
"file_writer_config": file_writer_config,
|
||||||
"write_to_movie": args.write_to_movie or not args.show_last_frame,
|
|
||||||
"show_last_frame": args.show_last_frame,
|
|
||||||
"save_pngs": args.save_pngs,
|
|
||||||
# If -t is passed in (for transparent), this will be RGBA
|
|
||||||
"saved_image_mode": "RGBA" if args.transparent else "RGB",
|
|
||||||
"movie_file_extension": ".mov" if args.transparent else ".mp4",
|
|
||||||
"quiet": args.quiet or args.write_all,
|
"quiet": args.quiet or args.write_all,
|
||||||
"ignore_waits": args.preview,
|
"ignore_waits": args.preview,
|
||||||
"write_all": args.write_all,
|
"write_all": args.write_all,
|
||||||
"output_file_name": output_file_name,
|
|
||||||
"start_at_animation_number": args.start_at_animation_number,
|
"start_at_animation_number": args.start_at_animation_number,
|
||||||
"end_at_animation_number": None,
|
"end_at_animation_number": None,
|
||||||
"sound": args.sound,
|
"sound": args.sound,
|
||||||
|
@ -241,7 +230,7 @@ def get_configuration(args):
|
||||||
config["start_at_animation_number"] = int(stan)
|
config["start_at_animation_number"] = int(stan)
|
||||||
|
|
||||||
config["skip_animations"] = any([
|
config["skip_animations"] = any([
|
||||||
config["show_last_frame"] and not config["write_to_movie"],
|
file_writer_config["save_last_frame"],
|
||||||
config["start_at_animation_number"],
|
config["start_at_animation_number"],
|
||||||
])
|
])
|
||||||
return config
|
return config
|
||||||
|
|
|
@ -25,6 +25,7 @@ with open("media_dir.txt", 'w') as media_file:
|
||||||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||||
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
||||||
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
||||||
|
SOUND_DIR = os.path.join(MEDIA_DIR, "designs", "sounds")
|
||||||
###
|
###
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
FILE_DIR = os.path.join(THIS_DIR, "files")
|
FILE_DIR = os.path.join(THIS_DIR, "files")
|
||||||
|
|
|
@ -12,46 +12,43 @@ from manimlib.utils.sounds import play_finish_sound
|
||||||
import manimlib.constants
|
import manimlib.constants
|
||||||
|
|
||||||
|
|
||||||
def handle_scene(scene, **config):
|
def open_file_if_needed(file_writer, **config):
|
||||||
if config["quiet"]:
|
if config["quiet"]:
|
||||||
curr_stdout = sys.stdout
|
curr_stdout = sys.stdout
|
||||||
sys.stdout = open(os.devnull, "w")
|
sys.stdout = open(os.devnull, "w")
|
||||||
|
|
||||||
if config["show_last_frame"]:
|
|
||||||
scene.save_image(mode=config["saved_image_mode"])
|
|
||||||
open_file = any([
|
open_file = any([
|
||||||
config["show_last_frame"],
|
|
||||||
config["open_video_upon_completion"],
|
config["open_video_upon_completion"],
|
||||||
config["show_file_in_finder"]
|
config["show_file_in_finder"]
|
||||||
])
|
])
|
||||||
if open_file:
|
if open_file:
|
||||||
current_os = platform.system()
|
current_os = platform.system()
|
||||||
file_path = None
|
file_paths = []
|
||||||
|
|
||||||
if config["show_last_frame"]:
|
if config["file_writer_config"]["save_last_frame"]:
|
||||||
file_path = scene.get_image_file_path()
|
file_paths.append(file_writer.get_image_file_path())
|
||||||
else:
|
if config["file_writer_config"]["write_to_movie"]:
|
||||||
file_path = scene.get_movie_file_path()
|
file_paths.append(file_writer.get_movie_file_path())
|
||||||
|
|
||||||
if current_os == "Windows":
|
for file_path in file_paths:
|
||||||
os.startfile(file_path)
|
if current_os == "Windows":
|
||||||
else:
|
os.startfile(file_path)
|
||||||
commands = []
|
else:
|
||||||
|
commands = []
|
||||||
|
if (current_os == "Linux"):
|
||||||
|
commands.append("xdg-open")
|
||||||
|
else: # Assume macOS
|
||||||
|
commands.append("open")
|
||||||
|
|
||||||
if (current_os == "Linux"):
|
if config["show_file_in_finder"]:
|
||||||
commands.append("xdg-open")
|
commands.append("-R")
|
||||||
else: # Assume macOS
|
|
||||||
commands.append("open")
|
|
||||||
|
|
||||||
if config["show_file_in_finder"]:
|
commands.append(file_path)
|
||||||
commands.append("-R")
|
|
||||||
|
|
||||||
commands.append(file_path)
|
# commands.append("-g")
|
||||||
|
FNULL = open(os.devnull, 'w')
|
||||||
# commands.append("-g")
|
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
||||||
FNULL = open(os.devnull, 'w')
|
FNULL.close()
|
||||||
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
|
|
||||||
FNULL.close()
|
|
||||||
|
|
||||||
if config["quiet"]:
|
if config["quiet"]:
|
||||||
sys.stdout.close()
|
sys.stdout.close()
|
||||||
|
@ -128,23 +125,18 @@ def main(config):
|
||||||
"camera_config",
|
"camera_config",
|
||||||
"frame_duration",
|
"frame_duration",
|
||||||
"skip_animations",
|
"skip_animations",
|
||||||
"write_to_movie",
|
"file_writer_config",
|
||||||
"save_pngs",
|
|
||||||
"movie_file_extension",
|
|
||||||
"start_at_animation_number",
|
"start_at_animation_number",
|
||||||
"end_at_animation_number",
|
"end_at_animation_number",
|
||||||
"output_file_name",
|
|
||||||
"leave_progress_bars",
|
"leave_progress_bars",
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
if config["save_pngs"]:
|
|
||||||
print("We are going to save a PNG sequence as well...")
|
|
||||||
scene_kwargs["save_pngs"] = True
|
|
||||||
scene_kwargs["pngs_mode"] = config["saved_image_mode"]
|
|
||||||
|
|
||||||
for SceneClass in get_scene_classes(scene_names_to_classes, config):
|
for SceneClass in get_scene_classes(scene_names_to_classes, config):
|
||||||
try:
|
try:
|
||||||
handle_scene(SceneClass(**scene_kwargs), **config)
|
# By invoking, this renders the full scene
|
||||||
|
scene = SceneClass(**scene_kwargs)
|
||||||
|
open_file_if_needed(scene.file_writer, **config)
|
||||||
if config["sound"]:
|
if config["sound"]:
|
||||||
play_finish_sound()
|
play_finish_sound()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
from time import sleep
|
|
||||||
import _thread as thread
|
|
||||||
import datetime
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from tqdm import tqdm as ProgressDisplay
|
from tqdm import tqdm as ProgressDisplay
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydub import AudioSegment
|
|
||||||
|
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
from manimlib.animation.creation import Write
|
from manimlib.animation.creation import Write
|
||||||
|
@ -21,12 +14,8 @@ from manimlib.container.container import Container
|
||||||
from manimlib.continual_animation.continual_animation import ContinualAnimation
|
from manimlib.continual_animation.continual_animation import ContinualAnimation
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.mobject.svg.tex_mobject import TextMobject
|
from manimlib.mobject.svg.tex_mobject import TextMobject
|
||||||
|
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||||
from manimlib.utils.iterables import list_update
|
from manimlib.utils.iterables import list_update
|
||||||
from manimlib.utils.output_directory_getters import add_extension_if_not_present
|
|
||||||
from manimlib.utils.output_directory_getters import get_image_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_movie_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_partial_movie_output_directory
|
|
||||||
from manimlib.utils.output_directory_getters import get_sorted_integer_files
|
|
||||||
|
|
||||||
|
|
||||||
class Scene(Container):
|
class Scene(Container):
|
||||||
|
@ -34,20 +23,12 @@ class Scene(Container):
|
||||||
"camera_class": Camera,
|
"camera_class": Camera,
|
||||||
"camera_config": {},
|
"camera_config": {},
|
||||||
"frame_duration": LOW_QUALITY_FRAME_DURATION,
|
"frame_duration": LOW_QUALITY_FRAME_DURATION,
|
||||||
"construct_args": [],
|
"file_writer_config": {},
|
||||||
"skip_animations": False,
|
"skip_animations": False,
|
||||||
"write_to_movie": False,
|
|
||||||
"save_pngs": False,
|
|
||||||
"pngs_mode": "RGBA",
|
|
||||||
"movie_file_extension": ".mp4",
|
|
||||||
"always_continually_update": False,
|
"always_continually_update": False,
|
||||||
"random_seed": 0,
|
"random_seed": 0,
|
||||||
"start_at_animation_number": None,
|
"start_at_animation_number": None,
|
||||||
"end_at_animation_number": None,
|
"end_at_animation_number": None,
|
||||||
"livestreaming": False,
|
|
||||||
"to_twitch": False,
|
|
||||||
"twitch_key": None,
|
|
||||||
"output_file_name": None,
|
|
||||||
"leave_progress_bars": False,
|
"leave_progress_bars": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,43 +36,36 @@ class Scene(Container):
|
||||||
# Perhaps allow passing in a non-empty *mobjects parameter?
|
# Perhaps allow passing in a non-empty *mobjects parameter?
|
||||||
Container.__init__(self, **kwargs)
|
Container.__init__(self, **kwargs)
|
||||||
self.camera = self.camera_class(**self.camera_config)
|
self.camera = self.camera_class(**self.camera_config)
|
||||||
|
self.file_writer = SceneFileWriter(
|
||||||
|
self, **self.file_writer_config,
|
||||||
|
)
|
||||||
|
|
||||||
self.mobjects = []
|
self.mobjects = []
|
||||||
self.continual_animations = []
|
self.continual_animations = []
|
||||||
self.foreground_mobjects = []
|
self.foreground_mobjects = []
|
||||||
self.num_plays = 0
|
self.num_plays = 0
|
||||||
self.frame_num = 0
|
|
||||||
self.time = 0
|
self.time = 0
|
||||||
self.original_skipping_status = self.skip_animations
|
self.original_skipping_status = self.skip_animations
|
||||||
self.stream_lock = False
|
|
||||||
if self.random_seed is not None:
|
if self.random_seed is not None:
|
||||||
random.seed(self.random_seed)
|
random.seed(self.random_seed)
|
||||||
np.random.seed(self.random_seed)
|
np.random.seed(self.random_seed)
|
||||||
|
|
||||||
self.init_audio()
|
|
||||||
self.setup()
|
self.setup()
|
||||||
if self.livestreaming:
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
self.construct(*self.construct_args)
|
self.construct()
|
||||||
except EndSceneEarlyException:
|
except EndSceneEarlyException:
|
||||||
if hasattr(self, "writing_process"):
|
pass
|
||||||
self.writing_process.terminate()
|
|
||||||
self.tear_down()
|
self.tear_down()
|
||||||
|
self.file_writer.finish()
|
||||||
if self.write_to_movie:
|
|
||||||
self.combine_movie_files()
|
|
||||||
self.print_end_message()
|
self.print_end_message()
|
||||||
|
|
||||||
def handle_play_like_call(func):
|
def handle_play_like_call(func):
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
self.handle_animation_skipping()
|
self.handle_animation_skipping()
|
||||||
should_write = self.write_to_movie and not self.skip_animations
|
allow_write = not self.skip_animations
|
||||||
if should_write:
|
self.file_writer.begin_animation(allow_write)
|
||||||
self.open_movie_pipe()
|
func(self, *args, **kwargs)
|
||||||
func(self, *args, **kwargs)
|
self.file_writer.end_animation(allow_write)
|
||||||
self.close_movie_pipe()
|
|
||||||
else:
|
|
||||||
func(self, *args, **kwargs)
|
|
||||||
self.num_plays += 1
|
self.num_plays += 1
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
@ -116,11 +90,6 @@ class Scene(Container):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def get_output_file_name(self):
|
|
||||||
if self.output_file_name is not None:
|
|
||||||
return self.output_file_name
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
def print_end_message(self):
|
def print_end_message(self):
|
||||||
print("Played {} animations".format(self.num_plays))
|
print("Played {} animations".format(self.num_plays))
|
||||||
|
|
||||||
|
@ -142,40 +111,9 @@ class Scene(Container):
|
||||||
def get_attrs(self, *keys):
|
def get_attrs(self, *keys):
|
||||||
return [getattr(self, key) for key in keys]
|
return [getattr(self, key) for key in keys]
|
||||||
|
|
||||||
# Sound
|
# TODO, Scene file writer now handles 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_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)
|
|
||||||
|
|
||||||
# Only these methods should touch the camera
|
# Only these methods should touch the camera
|
||||||
|
|
||||||
def set_camera(self, camera):
|
def set_camera(self, camera):
|
||||||
self.camera = camera
|
self.camera = camera
|
||||||
|
|
||||||
|
@ -202,9 +140,9 @@ class Scene(Container):
|
||||||
mobjects=None,
|
mobjects=None,
|
||||||
background=None,
|
background=None,
|
||||||
include_submobjects=True,
|
include_submobjects=True,
|
||||||
dont_update_when_skipping=True,
|
ignore_skipping=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
if self.skip_animations and dont_update_when_skipping:
|
if self.skip_animations and not ignore_skipping:
|
||||||
return
|
return
|
||||||
if mobjects is None:
|
if mobjects is None:
|
||||||
mobjects = list_update(
|
mobjects = list_update(
|
||||||
|
@ -522,8 +460,6 @@ class Scene(Container):
|
||||||
|
|
||||||
@handle_play_like_call
|
@handle_play_like_call
|
||||||
def play(self, *args, **kwargs):
|
def play(self, *args, **kwargs):
|
||||||
if self.livestreaming:
|
|
||||||
self.stream_lock = False
|
|
||||||
if len(args) == 0:
|
if len(args) == 0:
|
||||||
warnings.warn("Called Scene.play with no animations")
|
warnings.warn("Called Scene.play with no animations")
|
||||||
return
|
return
|
||||||
|
@ -558,22 +494,11 @@ class Scene(Container):
|
||||||
else:
|
else:
|
||||||
self.continual_update(0)
|
self.continual_update(0)
|
||||||
|
|
||||||
if self.livestreaming:
|
|
||||||
self.stream_lock = True
|
|
||||||
thread.start_new_thread(self.idle_stream, ())
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# TODO
|
||||||
def idle_stream(self):
|
def idle_stream(self):
|
||||||
while(self.stream_lock):
|
self.file_writer.idle_stream()
|
||||||
a = datetime.datetime.now()
|
|
||||||
self.update_frame()
|
|
||||||
n_frames = 1
|
|
||||||
frame = self.get_frame()
|
|
||||||
self.add_frames(*[frame] * n_frames)
|
|
||||||
b = datetime.datetime.now()
|
|
||||||
time_diff = (b - a).total_seconds()
|
|
||||||
if time_diff < self.frame_duration:
|
|
||||||
sleep(self.frame_duration - time_diff)
|
|
||||||
|
|
||||||
def clean_up_animations(self, *animations):
|
def clean_up_animations(self, *animations):
|
||||||
for animation in animations:
|
for animation in animations:
|
||||||
|
@ -638,202 +563,16 @@ class Scene(Container):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_frames(self, *frames):
|
def add_frames(self, *frames):
|
||||||
|
self.increment_time(len(frames) * self.frame_duration)
|
||||||
if self.skip_animations:
|
if self.skip_animations:
|
||||||
return
|
return
|
||||||
self.increment_time(len(frames) * self.frame_duration)
|
for frame in frames:
|
||||||
if self.write_to_movie:
|
self.file_writer.write_frame(frame)
|
||||||
for frame in frames:
|
|
||||||
if self.save_pngs:
|
|
||||||
self.save_image(
|
|
||||||
"frame" + str(self.frame_num), self.pngs_mode, True
|
|
||||||
)
|
|
||||||
self.frame_num = self.frame_num + 1
|
|
||||||
self.writing_process.stdin.write(frame.tostring())
|
|
||||||
|
|
||||||
# Display methods
|
|
||||||
|
|
||||||
def show_frame(self):
|
def show_frame(self):
|
||||||
self.update_frame(dont_update_when_skipping=False)
|
self.update_frame(ignore_skipping=True)
|
||||||
self.get_image().show()
|
self.get_image().show()
|
||||||
|
|
||||||
def get_image_file_path(self, name=None, dont_update=False):
|
|
||||||
sub_dir = "images"
|
|
||||||
output_file_name = self.get_output_file_name()
|
|
||||||
if dont_update:
|
|
||||||
sub_dir = output_file_name
|
|
||||||
path = get_image_output_directory(self.__class__, sub_dir)
|
|
||||||
file_name = add_extension_if_not_present(
|
|
||||||
name or output_file_name, ".png"
|
|
||||||
)
|
|
||||||
return os.path.join(path, file_name)
|
|
||||||
|
|
||||||
def save_image(self, name=None, mode="RGB", dont_update=False):
|
|
||||||
path = self.get_image_file_path(name, dont_update)
|
|
||||||
if not dont_update:
|
|
||||||
self.update_frame(dont_update_when_skipping=False)
|
|
||||||
image = self.get_image()
|
|
||||||
image = image.convert(mode)
|
|
||||||
image.save(path)
|
|
||||||
|
|
||||||
def get_movie_file_path(self, name=None, extension=None):
|
|
||||||
directory = get_movie_output_directory(
|
|
||||||
self.__class__, self.camera_config, self.frame_duration
|
|
||||||
)
|
|
||||||
if extension is None:
|
|
||||||
extension = self.movie_file_extension
|
|
||||||
if name is None:
|
|
||||||
name = self.get_output_file_name()
|
|
||||||
file_path = os.path.join(directory, name)
|
|
||||||
if not file_path.endswith(extension):
|
|
||||||
file_path += extension
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def get_partial_movie_directory(self):
|
|
||||||
return get_partial_movie_output_directory(
|
|
||||||
self, self.camera_config, self.frame_duration
|
|
||||||
)
|
|
||||||
|
|
||||||
def open_movie_pipe(self):
|
|
||||||
directory = self.get_partial_movie_directory()
|
|
||||||
file_path = os.path.join(
|
|
||||||
directory, "{}{}".format(
|
|
||||||
self.num_plays,
|
|
||||||
self.movie_file_extension,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
temp_file_path = file_path.replace(".", "_temp.")
|
|
||||||
|
|
||||||
self.movie_file_path = file_path
|
|
||||||
self.temp_movie_file_path = temp_file_path
|
|
||||||
|
|
||||||
fps = int(1 / self.frame_duration)
|
|
||||||
height = self.camera.get_pixel_height()
|
|
||||||
width = self.camera.get_pixel_width()
|
|
||||||
|
|
||||||
command = [
|
|
||||||
FFMPEG_BIN,
|
|
||||||
'-y', # overwrite output file if it exists
|
|
||||||
'-f', 'rawvideo',
|
|
||||||
'-s', '%dx%d' % (width, height), # size of one frame
|
|
||||||
'-pix_fmt', 'rgba',
|
|
||||||
'-r', str(fps), # frames per second
|
|
||||||
'-i', '-', # The imput comes from a pipe
|
|
||||||
'-c:v', 'h264_nvenc',
|
|
||||||
'-an', # Tells FFMPEG not to expect any audio
|
|
||||||
'-loglevel', 'error',
|
|
||||||
]
|
|
||||||
if self.movie_file_extension == ".mov":
|
|
||||||
# This is if the background of the exported video
|
|
||||||
# should be transparent.
|
|
||||||
command += [
|
|
||||||
'-vcodec', 'qtrle',
|
|
||||||
# '-vcodec', 'png',
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
command += [
|
|
||||||
'-vcodec', 'libx264',
|
|
||||||
'-pix_fmt', 'yuv420p',
|
|
||||||
]
|
|
||||||
if self.livestreaming:
|
|
||||||
if self.to_twitch:
|
|
||||||
command += ['-f', 'flv']
|
|
||||||
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
|
|
||||||
else:
|
|
||||||
command += ['-f', 'mpegts']
|
|
||||||
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
|
||||||
else:
|
|
||||||
command += [temp_file_path]
|
|
||||||
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
|
||||||
|
|
||||||
def close_movie_pipe(self):
|
|
||||||
self.writing_process.stdin.close()
|
|
||||||
self.writing_process.wait()
|
|
||||||
if self.livestreaming:
|
|
||||||
return True
|
|
||||||
shutil.move(
|
|
||||||
self.temp_movie_file_path,
|
|
||||||
self.movie_file_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
def combine_movie_files(self):
|
|
||||||
# Manim renders the scene as many smaller movie files
|
|
||||||
# which are then concatenated to a larger one. The reason
|
|
||||||
# for this is that sometimes video-editing is made easier when
|
|
||||||
# one works with the broken up scene, which effectively has
|
|
||||||
# cuts at all the places you might want. But for viewing
|
|
||||||
# the scene as a whole, one of course wants to see it as a
|
|
||||||
# single piece.
|
|
||||||
partial_movie_file_directory = self.get_partial_movie_directory()
|
|
||||||
kwargs = {
|
|
||||||
"remove_non_integer_files": True,
|
|
||||||
"extension": self.movie_file_extension,
|
|
||||||
}
|
|
||||||
if self.start_at_animation_number is not None:
|
|
||||||
kwargs["min_index"] = self.start_at_animation_number
|
|
||||||
if self.end_at_animation_number is not None:
|
|
||||||
kwargs["max_index"] = self.end_at_animation_number
|
|
||||||
else:
|
|
||||||
kwargs["remove_indices_greater_than"] = self.num_plays - 1
|
|
||||||
partial_movie_files = get_sorted_integer_files(
|
|
||||||
partial_movie_file_directory,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
# Write a file partial_file_list.txt containing all
|
|
||||||
# partial movie files
|
|
||||||
file_list = os.path.join(
|
|
||||||
partial_movie_file_directory,
|
|
||||||
"partial_movie_file_list.txt"
|
|
||||||
)
|
|
||||||
with open(file_list, 'w') as fp:
|
|
||||||
for pf_path in partial_movie_files:
|
|
||||||
if os.name == 'nt':
|
|
||||||
pf_path = pf_path.replace('\\', '/')
|
|
||||||
fp.write("file \'{}\'\n".format(pf_path))
|
|
||||||
|
|
||||||
movie_file_path = self.get_movie_file_path()
|
|
||||||
commands = [
|
|
||||||
FFMPEG_BIN,
|
|
||||||
'-y', # overwrite output file if it exists
|
|
||||||
'-f', 'concat',
|
|
||||||
'-safe', '0',
|
|
||||||
'-i', file_list,
|
|
||||||
'-c', 'copy',
|
|
||||||
'-loglevel', 'error',
|
|
||||||
movie_file_path
|
|
||||||
]
|
|
||||||
if not self.includes_sound:
|
|
||||||
commands.insert(-1, '-an')
|
|
||||||
|
|
||||||
combine_process = subprocess.Popen(commands)
|
|
||||||
combine_process.wait()
|
|
||||||
# os.remove(file_list)
|
|
||||||
|
|
||||||
if self.includes_sound:
|
|
||||||
sound_file_path = movie_file_path.replace(
|
|
||||||
self.movie_file_extension, ".wav"
|
|
||||||
)
|
|
||||||
# Makes sure sound file length will match video file
|
|
||||||
self.add_audio_segment(AudioSegment.silent(0))
|
|
||||||
self.audio_segment.export(sound_file_path)
|
|
||||||
temp_file_path = movie_file_path.replace(".", "_temp.")
|
|
||||||
commands = commands = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-i", movie_file_path,
|
|
||||||
"-i", sound_file_path,
|
|
||||||
'-y', # overwrite output file if it exists
|
|
||||||
"-c:v", "copy", "-c:a", "aac",
|
|
||||||
'-loglevel', 'error',
|
|
||||||
"-shortest",
|
|
||||||
"-strict", "experimental",
|
|
||||||
temp_file_path,
|
|
||||||
]
|
|
||||||
subprocess.call(commands)
|
|
||||||
shutil.move(temp_file_path, movie_file_path)
|
|
||||||
# subprocess.call(["rm", self.temp_movie_file_path])
|
|
||||||
subprocess.call(["rm", sound_file_path])
|
|
||||||
|
|
||||||
print("\nAnimation ready at {}\n".format(movie_file_path))
|
|
||||||
|
|
||||||
# TODO, this doesn't belong in Scene, but should be
|
# TODO, this doesn't belong in Scene, but should be
|
||||||
# part of some more specialized subclass optimized
|
# part of some more specialized subclass optimized
|
||||||
# for livestreaming
|
# for livestreaming
|
||||||
|
|
325
manimlib/scene/scene_file_writer.py
Normal file
325
manimlib/scene/scene_file_writer.py
Normal file
|
@ -0,0 +1,325 @@
|
||||||
|
import numpy as np
|
||||||
|
from pydub import AudioSegment
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import _thread as thread
|
||||||
|
from time import sleep
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from manimlib.constants import FFMPEG_BIN
|
||||||
|
from manimlib.constants import STREAMING_IP
|
||||||
|
from manimlib.constants import STREAMING_PORT
|
||||||
|
from manimlib.constants import STREAMING_PROTOCOL
|
||||||
|
from manimlib.constants import VIDEO_DIR
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class SceneFileWriter(object):
|
||||||
|
CONFIG = {
|
||||||
|
"write_to_movie": False,
|
||||||
|
# TODO, save_pngs is doing nothing
|
||||||
|
"save_pngs": False,
|
||||||
|
"png_mode": "RGBA",
|
||||||
|
"save_last_frame": False,
|
||||||
|
"movie_file_extension": ".mp4",
|
||||||
|
"livestreaming": False,
|
||||||
|
"to_twitch": False,
|
||||||
|
"twitch_key": None,
|
||||||
|
# Previous output_file_name
|
||||||
|
# TODO, address this in extract_scene et. al.
|
||||||
|
"file_name": None,
|
||||||
|
"output_directory": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, scene, **kwargs):
|
||||||
|
digest_config(self, kwargs)
|
||||||
|
self.scene = scene
|
||||||
|
self.init_audio()
|
||||||
|
self.init_output_directories()
|
||||||
|
self.stream_lock = False
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
if self.save_last_frame:
|
||||||
|
image_dir = guarantee_existance(os.path.join(
|
||||||
|
VIDEO_DIR,
|
||||||
|
output_directory,
|
||||||
|
self.get_image_directory(),
|
||||||
|
))
|
||||||
|
self.image_file_path = os.path.join(
|
||||||
|
image_dir,
|
||||||
|
add_extension_if_not_present(file_name, ".png")
|
||||||
|
)
|
||||||
|
if self.write_to_movie:
|
||||||
|
movie_dir = guarantee_existance(os.path.join(
|
||||||
|
output_directory,
|
||||||
|
self.get_movie_directory(),
|
||||||
|
))
|
||||||
|
self.movie_file_path = os.path.join(
|
||||||
|
movie_dir,
|
||||||
|
add_extension_if_not_present(
|
||||||
|
file_name, self.movie_file_extension
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.partial_movie_directory = guarantee_existance(os.path.join(
|
||||||
|
movie_dir,
|
||||||
|
self.get_partial_movie_directory(),
|
||||||
|
file_name,
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_default_output_directory(self):
|
||||||
|
scene_module = self.scene.__class__.__module__
|
||||||
|
return scene_module.replace(".", os.path.sep)
|
||||||
|
|
||||||
|
def get_default_file_name(self):
|
||||||
|
return self.scene.__class__.__name__
|
||||||
|
|
||||||
|
def get_movie_directory(self):
|
||||||
|
pixel_height = self.scene.camera.pixel_height
|
||||||
|
frame_duration = self.scene.frame_duration
|
||||||
|
return "{}p{}".format(
|
||||||
|
pixel_height, int(1.0 / frame_duration)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_image_directory(self):
|
||||||
|
return "images"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_next_partial_movie_path(self):
|
||||||
|
result = os.path.join(
|
||||||
|
self.partial_movie_directory,
|
||||||
|
"{:05}{}".format(
|
||||||
|
self.scene.num_plays,
|
||||||
|
self.movie_file_extension,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_movie_file_path(self):
|
||||||
|
return self.movie_file_path
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
if self.livestreaming:
|
||||||
|
self.stream_lock = False
|
||||||
|
|
||||||
|
def end_animation(self, allow_write=False):
|
||||||
|
if self.write_to_movie and allow_write:
|
||||||
|
self.close_movie_pipe()
|
||||||
|
if self.livestreaming:
|
||||||
|
self.stream_lock = True
|
||||||
|
thread.start_new_thread(self.idle_stream, ())
|
||||||
|
|
||||||
|
def idle_stream(self):
|
||||||
|
while self.stream_lock:
|
||||||
|
a = datetime.datetime.now()
|
||||||
|
self.update_frame()
|
||||||
|
n_frames = 1
|
||||||
|
frame = self.get_frame()
|
||||||
|
self.add_frames(*[frame] * n_frames)
|
||||||
|
b = datetime.datetime.now()
|
||||||
|
time_diff = (b - a).total_seconds()
|
||||||
|
if time_diff < self.frame_duration:
|
||||||
|
sleep(self.frame_duration - time_diff)
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
if self.write_to_movie:
|
||||||
|
if hasattr(self, "writing_process"):
|
||||||
|
self.writing_process.terminate()
|
||||||
|
self.combine_movie_files()
|
||||||
|
if self.save_last_frame:
|
||||||
|
self.scene.update_frame(ignore_skipping=True)
|
||||||
|
self.save_image(self.scene.get_image())
|
||||||
|
|
||||||
|
def open_movie_pipe(self):
|
||||||
|
file_path = self.get_next_partial_movie_path()
|
||||||
|
temp_file_path = file_path.replace(".", "_temp.")
|
||||||
|
|
||||||
|
self.partial_movie_file_path = file_path
|
||||||
|
self.temp_partial_movie_file_path = temp_file_path
|
||||||
|
|
||||||
|
fps = int(1 / self.scene.frame_duration)
|
||||||
|
height = self.scene.camera.get_pixel_height()
|
||||||
|
width = self.scene.camera.get_pixel_width()
|
||||||
|
|
||||||
|
command = [
|
||||||
|
FFMPEG_BIN,
|
||||||
|
'-y', # overwrite output file if it exists
|
||||||
|
'-f', 'rawvideo',
|
||||||
|
'-s', '%dx%d' % (width, height), # size of one frame
|
||||||
|
'-pix_fmt', 'rgba',
|
||||||
|
'-r', str(fps), # frames per second
|
||||||
|
'-i', '-', # The imput comes from a pipe
|
||||||
|
'-c:v', 'h264_nvenc',
|
||||||
|
'-an', # Tells FFMPEG not to expect any audio
|
||||||
|
'-loglevel', 'error',
|
||||||
|
]
|
||||||
|
if self.movie_file_extension == ".mov":
|
||||||
|
# This is if the background of the exported video
|
||||||
|
# should be transparent.
|
||||||
|
command += [
|
||||||
|
'-vcodec', 'qtrle',
|
||||||
|
# '-vcodec', 'png',
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
command += [
|
||||||
|
'-vcodec', 'libx264',
|
||||||
|
'-pix_fmt', 'yuv420p',
|
||||||
|
]
|
||||||
|
if self.livestreaming:
|
||||||
|
if self.to_twitch:
|
||||||
|
command += ['-f', 'flv']
|
||||||
|
command += ['rtmp://live.twitch.tv/app/' + self.twitch_key]
|
||||||
|
else:
|
||||||
|
command += ['-f', 'mpegts']
|
||||||
|
command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT]
|
||||||
|
else:
|
||||||
|
command += [temp_file_path]
|
||||||
|
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
||||||
|
|
||||||
|
def close_movie_pipe(self):
|
||||||
|
self.writing_process.stdin.close()
|
||||||
|
self.writing_process.wait()
|
||||||
|
if self.livestreaming:
|
||||||
|
return True
|
||||||
|
shutil.move(
|
||||||
|
self.temp_partial_movie_file_path,
|
||||||
|
self.partial_movie_file_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
def combine_movie_files(self):
|
||||||
|
# Manim renders the scene as many smaller movie files
|
||||||
|
# which are then concatenated to a larger one. The reason
|
||||||
|
# for this is that sometimes video-editing is made easier when
|
||||||
|
# one works with the broken up scene, which effectively has
|
||||||
|
# cuts at all the places you might want. But for viewing
|
||||||
|
# the scene as a whole, one of course wants to see it as a
|
||||||
|
# single piece.
|
||||||
|
kwargs = {
|
||||||
|
"remove_non_integer_files": True,
|
||||||
|
"extension": self.movie_file_extension,
|
||||||
|
}
|
||||||
|
if self.scene.start_at_animation_number is not None:
|
||||||
|
kwargs["min_index"] = self.start_at_animation_number
|
||||||
|
if self.scene.end_at_animation_number is not None:
|
||||||
|
kwargs["max_index"] = self.end_at_animation_number
|
||||||
|
else:
|
||||||
|
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
|
||||||
|
partial_movie_files = get_sorted_integer_files(
|
||||||
|
self.partial_movie_directory,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
# Write a file partial_file_list.txt containing all
|
||||||
|
# partial movie files
|
||||||
|
file_list = os.path.join(
|
||||||
|
self.partial_movie_directory,
|
||||||
|
"partial_movie_file_list.txt"
|
||||||
|
)
|
||||||
|
with open(file_list, 'w') as fp:
|
||||||
|
for pf_path in partial_movie_files:
|
||||||
|
if os.name == 'nt':
|
||||||
|
pf_path = pf_path.replace('\\', '/')
|
||||||
|
fp.write("file \'{}\'\n".format(pf_path))
|
||||||
|
|
||||||
|
movie_file_path = self.get_movie_file_path()
|
||||||
|
commands = [
|
||||||
|
FFMPEG_BIN,
|
||||||
|
'-y', # overwrite output file if it exists
|
||||||
|
'-f', 'concat',
|
||||||
|
'-safe', '0',
|
||||||
|
'-i', file_list,
|
||||||
|
'-c', 'copy',
|
||||||
|
'-loglevel', 'error',
|
||||||
|
movie_file_path
|
||||||
|
]
|
||||||
|
if not self.includes_sound:
|
||||||
|
commands.insert(-1, '-an')
|
||||||
|
|
||||||
|
combine_process = subprocess.Popen(commands)
|
||||||
|
combine_process.wait()
|
||||||
|
# os.remove(file_list)
|
||||||
|
|
||||||
|
if self.includes_sound:
|
||||||
|
sound_file_path = movie_file_path.replace(
|
||||||
|
self.movie_file_extension, ".wav"
|
||||||
|
)
|
||||||
|
# Makes sure sound file length will match video file
|
||||||
|
self.add_audio_segment(AudioSegment.silent(0))
|
||||||
|
self.audio_segment.export(sound_file_path)
|
||||||
|
temp_file_path = movie_file_path.replace(".", "_temp.")
|
||||||
|
commands = commands = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-i", movie_file_path,
|
||||||
|
"-i", sound_file_path,
|
||||||
|
'-y', # overwrite output file if it exists
|
||||||
|
"-c:v", "copy", "-c:a", "aac",
|
||||||
|
'-loglevel', 'error',
|
||||||
|
"-shortest",
|
||||||
|
"-strict", "experimental",
|
||||||
|
temp_file_path,
|
||||||
|
]
|
||||||
|
subprocess.call(commands)
|
||||||
|
shutil.move(temp_file_path, movie_file_path)
|
||||||
|
subprocess.call(["rm", sound_file_path])
|
||||||
|
|
||||||
|
self.print_file_ready_message(movie_file_path)
|
||||||
|
|
||||||
|
def print_file_ready_message(self, file_path):
|
||||||
|
print("\nFile ready at {}\n".format(file_path))
|
|
@ -1,8 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.constants import VIDEO_DIR
|
|
||||||
|
|
||||||
|
|
||||||
def add_extension_if_not_present(file_name, extension):
|
def add_extension_if_not_present(file_name, extension):
|
||||||
# This could conceivably be smarter about handling existing differing extensions
|
# This could conceivably be smarter about handling existing differing extensions
|
||||||
|
@ -18,38 +16,38 @@ def guarantee_existance(path):
|
||||||
return os.path.abspath(path)
|
return os.path.abspath(path)
|
||||||
|
|
||||||
|
|
||||||
def get_scene_output_directory(scene_class):
|
# def get_scene_output_directory(scene_class):
|
||||||
return guarantee_existance(os.path.join(
|
# return guarantee_existance(os.path.join(
|
||||||
VIDEO_DIR,
|
# VIDEO_DIR,
|
||||||
scene_class.__module__.replace(".", os.path.sep)
|
# scene_class.__module__.replace(".", os.path.sep)
|
||||||
))
|
# ))
|
||||||
|
|
||||||
|
|
||||||
def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
# def get_movie_output_directory(scene_class, camera_config, frame_duration):
|
||||||
directory = get_scene_output_directory(scene_class)
|
# directory = get_scene_output_directory(scene_class)
|
||||||
sub_dir = "%dp%d" % (
|
# sub_dir = "%dp%d" % (
|
||||||
camera_config["pixel_height"],
|
# camera_config["pixel_height"],
|
||||||
int(1.0 / frame_duration)
|
# int(1.0 / frame_duration)
|
||||||
)
|
# )
|
||||||
return guarantee_existance(os.path.join(directory, sub_dir))
|
# return guarantee_existance(os.path.join(directory, sub_dir))
|
||||||
|
|
||||||
|
|
||||||
def get_partial_movie_output_directory(scene, camera_config, frame_duration):
|
# def get_partial_movie_output_directory(scene, camera_config, frame_duration):
|
||||||
directory = get_movie_output_directory(
|
# directory = get_movie_output_directory(
|
||||||
scene.__class__, camera_config, frame_duration
|
# scene.__class__, camera_config, frame_duration
|
||||||
)
|
# )
|
||||||
return guarantee_existance(
|
# return guarantee_existance(
|
||||||
os.path.join(
|
# os.path.join(
|
||||||
directory,
|
# directory,
|
||||||
"partial_movie_files",
|
# "partial_movie_files",
|
||||||
scene.get_output_file_name(),
|
# scene.get_output_file_name(),
|
||||||
)
|
# )
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def get_image_output_directory(scene_class, sub_dir="images"):
|
# def get_image_output_directory(scene_class, sub_dir="images"):
|
||||||
directory = get_scene_output_directory(scene_class)
|
# directory = get_scene_output_directory(scene_class)
|
||||||
return guarantee_existance(os.path.join(directory, sub_dir))
|
# return guarantee_existance(os.path.join(directory, sub_dir))
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_integer_files(directory,
|
def get_sorted_integer_files(directory,
|
|
@ -8,8 +8,7 @@ from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG
|
||||||
from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
|
from manimlib.constants import PRODUCTION_QUALITY_FRAME_DURATION
|
||||||
from manimlib.config import get_module
|
from manimlib.config import get_module
|
||||||
from manimlib.extract_scene import is_child_scene
|
from manimlib.extract_scene import is_child_scene
|
||||||
from manimlib.utils.output_directory_getters import get_movie_output_directory
|
from manimlib.utils.file_ops import get_movie_output_directory
|
||||||
from manimlib.utils.output_directory_getters import get_sorted_integer_files
|
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_scene_classes(module_name):
|
def get_sorted_scene_classes(module_name):
|
||||||
|
|
Loading…
Add table
Reference in a new issue