mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Make manim into a module and refactor configuration
This commit is contained in:
parent
465951ab88
commit
27f677e45f
6 changed files with 290 additions and 257 deletions
142
config.py
Normal file
142
config.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import argparse
|
||||||
|
import colour
|
||||||
|
import constants
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cli():
|
||||||
|
try:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
module_location = parser.add_mutually_exclusive_group()
|
||||||
|
module_location.add_argument(
|
||||||
|
"file",
|
||||||
|
nargs="?",
|
||||||
|
help="path to file holding the python code for the scene",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"scene_name",
|
||||||
|
nargs="?",
|
||||||
|
help="Name of the Scene class you want to see",
|
||||||
|
)
|
||||||
|
module_location.add_argument("--livestream", action="store_true")
|
||||||
|
parser.add_argument("--to-twitch", action="store_true")
|
||||||
|
optional_args = [
|
||||||
|
("-p", "--preview"),
|
||||||
|
("-w", "--write_to_movie"),
|
||||||
|
("-s", "--show_last_frame"),
|
||||||
|
("-l", "--low_quality"),
|
||||||
|
("-m", "--medium_quality"),
|
||||||
|
("-g", "--save_pngs"),
|
||||||
|
("-f", "--show_file_in_finder"),
|
||||||
|
("-t", "--transparent"),
|
||||||
|
("-q", "--quiet"),
|
||||||
|
("-a", "--write_all")
|
||||||
|
]
|
||||||
|
for short_arg, long_arg in optional_args:
|
||||||
|
parser.add_argument(short_arg, long_arg, action="store_true")
|
||||||
|
parser.add_argument("-o", "--output_name")
|
||||||
|
parser.add_argument("-n", "--start_at_animation_number")
|
||||||
|
parser.add_argument("-r", "--resolution")
|
||||||
|
parser.add_argument("-c", "--color")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.file is None and not args.livestream:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(2)
|
||||||
|
else:
|
||||||
|
return args
|
||||||
|
except argparse.ArgumentError as err:
|
||||||
|
print(str(err))
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_configuration(args):
|
||||||
|
if args.output_name is not None:
|
||||||
|
output_name_root, output_name_ext = os.path.splitext(
|
||||||
|
args.output_name)
|
||||||
|
expected_ext = '.png' if args.show_last_frame else '.mp4'
|
||||||
|
if output_name_ext not in ['', expected_ext]:
|
||||||
|
print("WARNING: The output will be to (doubly-dotted) %s%s" %
|
||||||
|
output_name_root % expected_ext)
|
||||||
|
output_name = args.output_name
|
||||||
|
else:
|
||||||
|
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
|
||||||
|
output_name = output_name_root
|
||||||
|
else:
|
||||||
|
output_name = args.output_name
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"file": args.file,
|
||||||
|
"scene_name": args.scene_name,
|
||||||
|
"open_video_upon_completion": args.preview,
|
||||||
|
"show_file_in_finder": args.show_file_in_finder,
|
||||||
|
# By default, write to file
|
||||||
|
"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,
|
||||||
|
"ignore_waits": args.preview,
|
||||||
|
"write_all": args.write_all,
|
||||||
|
"output_name": output_name,
|
||||||
|
"start_at_animation_number": args.start_at_animation_number,
|
||||||
|
"end_at_animation_number": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Camera configuration
|
||||||
|
config["camera_config"] = {}
|
||||||
|
if args.low_quality:
|
||||||
|
config["camera_config"].update(constants.LOW_QUALITY_CAMERA_CONFIG)
|
||||||
|
config["frame_duration"] = constants.LOW_QUALITY_FRAME_DURATION
|
||||||
|
elif args.medium_quality:
|
||||||
|
config["camera_config"].update(constants.MEDIUM_QUALITY_CAMERA_CONFIG)
|
||||||
|
config["frame_duration"] = constants.MEDIUM_QUALITY_FRAME_DURATION
|
||||||
|
else:
|
||||||
|
config["camera_config"].update(constants.PRODUCTION_QUALITY_CAMERA_CONFIG)
|
||||||
|
config["frame_duration"] = constants.PRODUCTION_QUALITY_FRAME_DURATION
|
||||||
|
|
||||||
|
# If the resolution was passed in via -r
|
||||||
|
if args.resolution:
|
||||||
|
if "," in args.resolution:
|
||||||
|
height_str, width_str = args.resolution.split(",")
|
||||||
|
height = int(height_str)
|
||||||
|
width = int(width_str)
|
||||||
|
else:
|
||||||
|
height = int(args.resolution)
|
||||||
|
width = int(16 * height / 9)
|
||||||
|
config["camera_config"].update({
|
||||||
|
"pixel_height": height,
|
||||||
|
"pixel_width": width,
|
||||||
|
})
|
||||||
|
|
||||||
|
if args.color:
|
||||||
|
try:
|
||||||
|
config["camera_config"]["background_color"] = colour.Color(args.color)
|
||||||
|
except AttributeError as err:
|
||||||
|
print("Please use a valid color")
|
||||||
|
print(err)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# If rendering a transparent image/move, make sure the
|
||||||
|
# scene has a background opacity of 0
|
||||||
|
if args.transparent:
|
||||||
|
config["camera_config"]["background_opacity"] = 0
|
||||||
|
|
||||||
|
# Arguments related to skipping
|
||||||
|
stan = config["start_at_animation_number"]
|
||||||
|
if stan is not None:
|
||||||
|
if "," in stan:
|
||||||
|
start, end = stan.split(",")
|
||||||
|
config["start_at_animation_number"] = int(start)
|
||||||
|
config["end_at_animation_number"] = int(end)
|
||||||
|
else:
|
||||||
|
config["start_at_animation_number"] = int(stan)
|
||||||
|
|
||||||
|
config["skip_animations"] = any([
|
||||||
|
config["show_last_frame"] and not config["write_to_movie"],
|
||||||
|
config["start_at_animation_number"],
|
||||||
|
])
|
||||||
|
return config
|
108
constants.py
108
constants.py
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
# Initialize directories
|
||||||
env_MEDIA_DIR = os.getenv("MEDIA_DIR")
|
env_MEDIA_DIR = os.getenv("MEDIA_DIR")
|
||||||
if env_MEDIA_DIR:
|
if env_MEDIA_DIR:
|
||||||
MEDIA_DIR = env_MEDIA_DIR
|
MEDIA_DIR = env_MEDIA_DIR
|
||||||
|
@ -12,17 +13,78 @@ else:
|
||||||
os.path.expanduser('~'),
|
os.path.expanduser('~'),
|
||||||
"Dropbox (3Blue1Brown)/3Blue1Brown Team Folder"
|
"Dropbox (3Blue1Brown)/3Blue1Brown Team Folder"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.isdir(MEDIA_DIR):
|
if not os.path.isdir(MEDIA_DIR):
|
||||||
MEDIA_DIR = "media"
|
MEDIA_DIR = "media"
|
||||||
print(
|
print(
|
||||||
f"Media will be stored in {MEDIA_DIR + os.sep}. You can change " + \
|
f"Media will be stored in {MEDIA_DIR + os.sep}. You can change "
|
||||||
"this behavior by writing a different directory to media_dir.txt."
|
"this behavior by writing a different directory to media_dir.txt."
|
||||||
)
|
)
|
||||||
|
|
||||||
with open("media_dir.txt", 'w') as media_file:
|
with open("media_dir.txt", 'w') as media_file:
|
||||||
media_file.write(MEDIA_DIR)
|
media_file.write(MEDIA_DIR)
|
||||||
|
|
||||||
|
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
||||||
|
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
||||||
|
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
||||||
|
# TODO, staged scenes should really go into a subdirectory of a given scenes directory
|
||||||
|
STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes")
|
||||||
|
###
|
||||||
|
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
FILE_DIR = os.path.join(THIS_DIR, "files")
|
||||||
|
TEX_DIR = os.path.join(FILE_DIR, "Tex")
|
||||||
|
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
|
||||||
|
# These two may be depricated now.
|
||||||
|
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
|
||||||
|
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
|
||||||
|
|
||||||
|
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR,
|
||||||
|
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR,
|
||||||
|
STAGED_SCENES_DIR]:
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
os.makedirs(folder)
|
||||||
|
|
||||||
|
TEX_USE_CTEX = False
|
||||||
|
TEX_TEXT_TO_REPLACE = "YourTextHere"
|
||||||
|
TEMPLATE_TEX_FILE = os.path.join(
|
||||||
|
THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX
|
||||||
|
else "ctex_template.tex"
|
||||||
|
)
|
||||||
|
with open(TEMPLATE_TEX_FILE, "r") as infile:
|
||||||
|
TEMPLATE_TEXT_FILE_BODY = infile.read()
|
||||||
|
TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace(
|
||||||
|
TEX_TEXT_TO_REPLACE,
|
||||||
|
"\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}",
|
||||||
|
)
|
||||||
|
|
||||||
|
HELP_MESSAGE = """
|
||||||
|
Usage:
|
||||||
|
python extract_scene.py <module> [<scene name>]
|
||||||
|
-p preview in low quality
|
||||||
|
-s show and save picture of last frame
|
||||||
|
-w write result to file [this is default if nothing else is stated]
|
||||||
|
-o <file_name> write to a different file_name
|
||||||
|
-l use low quality
|
||||||
|
-m use medium quality
|
||||||
|
-a run and save every scene in the script, or all args for the given scene
|
||||||
|
-q don't print progress
|
||||||
|
-f when writing to a movie file, export the frames in png sequence
|
||||||
|
-t use transperency when exporting images
|
||||||
|
-n specify the number of the animation to start from
|
||||||
|
-r specify a resolution
|
||||||
|
-c specify a background color
|
||||||
|
"""
|
||||||
|
SCENE_NOT_FOUND_MESSAGE = """
|
||||||
|
That scene is not in the script
|
||||||
|
"""
|
||||||
|
CHOOSE_NUMBER_MESSAGE = """
|
||||||
|
Choose number corresponding to desired scene/arguments.
|
||||||
|
(Use comma separated list for multiple entries)
|
||||||
|
Choice(s): """
|
||||||
|
INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit"
|
||||||
|
|
||||||
|
NO_SCENE_MESSAGE = """
|
||||||
|
There are no scenes inside that module
|
||||||
|
"""
|
||||||
|
|
||||||
LOW_QUALITY_FRAME_DURATION = 1. / 15
|
LOW_QUALITY_FRAME_DURATION = 1. / 15
|
||||||
MEDIUM_QUALITY_FRAME_DURATION = 1. / 30
|
MEDIUM_QUALITY_FRAME_DURATION = 1. / 30
|
||||||
PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60
|
PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60
|
||||||
|
@ -102,44 +164,9 @@ PI = np.pi
|
||||||
TAU = 2 * PI
|
TAU = 2 * PI
|
||||||
DEGREES = TAU / 360
|
DEGREES = TAU / 360
|
||||||
|
|
||||||
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
|
|
||||||
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
|
|
||||||
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
|
|
||||||
# TODO, staged scenes should really go into a subdirectory of a given scenes directory
|
|
||||||
STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes")
|
|
||||||
###
|
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
FILE_DIR = os.path.join(THIS_DIR, "files")
|
|
||||||
TEX_DIR = os.path.join(FILE_DIR, "Tex")
|
|
||||||
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
|
|
||||||
# These two may be depricated now.
|
|
||||||
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
|
|
||||||
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
|
|
||||||
|
|
||||||
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR,
|
|
||||||
TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR,
|
|
||||||
STAGED_SCENES_DIR]:
|
|
||||||
if not os.path.exists(folder):
|
|
||||||
os.makedirs(folder)
|
|
||||||
|
|
||||||
TEX_USE_CTEX = False
|
|
||||||
TEX_TEXT_TO_REPLACE = "YourTextHere"
|
|
||||||
TEMPLATE_TEX_FILE = os.path.join(
|
|
||||||
THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX
|
|
||||||
else "ctex_template.tex"
|
|
||||||
)
|
|
||||||
with open(TEMPLATE_TEX_FILE, "r") as infile:
|
|
||||||
TEMPLATE_TEXT_FILE_BODY = infile.read()
|
|
||||||
TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace(
|
|
||||||
TEX_TEXT_TO_REPLACE,
|
|
||||||
"\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}",
|
|
||||||
)
|
|
||||||
|
|
||||||
FFMPEG_BIN = "ffmpeg"
|
FFMPEG_BIN = "ffmpeg"
|
||||||
|
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
|
|
||||||
COLOR_MAP = {
|
COLOR_MAP = {
|
||||||
"DARK_BLUE": "#236B8E",
|
"DARK_BLUE": "#236B8E",
|
||||||
"DARK_BROWN": "#8B4513",
|
"DARK_BROWN": "#8B4513",
|
||||||
|
@ -202,12 +229,11 @@ locals().update(COLOR_MAP)
|
||||||
for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]:
|
for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]:
|
||||||
locals()[name.replace("_C", "")] = locals()[name]
|
locals()[name.replace("_C", "")] = locals()[name]
|
||||||
|
|
||||||
# Streaming related configurations
|
# Streaming related configuration
|
||||||
IS_LIVE_STREAMING = False
|
|
||||||
LIVE_STREAM_NAME = "LiveStream"
|
LIVE_STREAM_NAME = "LiveStream"
|
||||||
IS_STREAMING_TO_TWITCH = False
|
|
||||||
TWITCH_STREAM_KEY = "YOUR_STREAM_KEY"
|
TWITCH_STREAM_KEY = "YOUR_STREAM_KEY"
|
||||||
STREAMING_PROTOCOL = "tcp"
|
STREAMING_PROTOCOL = "tcp"
|
||||||
STREAMING_IP = "127.0.0.1"
|
STREAMING_IP = "127.0.0.1"
|
||||||
STREAMING_PORT = "2000"
|
STREAMING_PORT = "2000"
|
||||||
STREAMING_CLIENT = "ffplay"
|
STREAMING_CLIENT = "ffplay"
|
||||||
|
STREAMING_URL = f"{STREAMING_PROTOCOL}://{STREAMING_IP}:{STREAMING_PORT}?listen"
|
||||||
|
|
186
extract_scene.py
186
extract_scene.py
|
@ -1,173 +1,18 @@
|
||||||
# !/usr/bin/env python2
|
# !/usr/bin/env python
|
||||||
|
|
||||||
import sys
|
import constants
|
||||||
import argparse
|
|
||||||
# import imp
|
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import itertools as it
|
import itertools as it
|
||||||
import os
|
import os
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from constants import *
|
|
||||||
|
|
||||||
from scene.scene import Scene
|
from scene.scene import Scene
|
||||||
from utils.sounds import play_error_sound
|
from utils.sounds import play_error_sound
|
||||||
from utils.sounds import play_finish_sound
|
from utils.sounds import play_finish_sound
|
||||||
|
|
||||||
from colour import Color
|
|
||||||
|
|
||||||
HELP_MESSAGE = """
|
|
||||||
Usage:
|
|
||||||
python extract_scene.py <module> [<scene name>]
|
|
||||||
-p preview in low quality
|
|
||||||
-s show and save picture of last frame
|
|
||||||
-w write result to file [this is default if nothing else is stated]
|
|
||||||
-o <file_name> write to a different file_name
|
|
||||||
-l use low quality
|
|
||||||
-m use medium quality
|
|
||||||
-a run and save every scene in the script, or all args for the given scene
|
|
||||||
-q don't print progress
|
|
||||||
-f when writing to a movie file, export the frames in png sequence
|
|
||||||
-t use transperency when exporting images
|
|
||||||
-n specify the number of the animation to start from
|
|
||||||
-r specify a resolution
|
|
||||||
-c specify a background color
|
|
||||||
"""
|
|
||||||
SCENE_NOT_FOUND_MESSAGE = """
|
|
||||||
That scene is not in the script
|
|
||||||
"""
|
|
||||||
CHOOSE_NUMBER_MESSAGE = """
|
|
||||||
Choose number corresponding to desired scene/arguments.
|
|
||||||
(Use comma separated list for multiple entries)
|
|
||||||
Choice(s): """
|
|
||||||
INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit"
|
|
||||||
|
|
||||||
NO_SCENE_MESSAGE = """
|
|
||||||
There are no scenes inside that module
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_configuration():
|
|
||||||
try:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument(
|
|
||||||
"file", help="path to file holding the python code for the scene"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"scene_name", help="Name of the Scene class you want to see"
|
|
||||||
)
|
|
||||||
optional_args = [
|
|
||||||
("-p", "--preview"),
|
|
||||||
("-w", "--write_to_movie"),
|
|
||||||
("-s", "--show_last_frame"),
|
|
||||||
("-l", "--low_quality"),
|
|
||||||
("-m", "--medium_quality"),
|
|
||||||
("-g", "--save_pngs"),
|
|
||||||
("-f", "--show_file_in_finder"),
|
|
||||||
("-t", "--transparent"),
|
|
||||||
("-q", "--quiet"),
|
|
||||||
("-a", "--write_all")
|
|
||||||
]
|
|
||||||
for short_arg, long_arg in optional_args:
|
|
||||||
parser.add_argument(short_arg, long_arg, action="store_true")
|
|
||||||
parser.add_argument("-o", "--output_name")
|
|
||||||
parser.add_argument("-n", "--start_at_animation_number")
|
|
||||||
parser.add_argument("-r", "--resolution")
|
|
||||||
parser.add_argument("-c", "--color")
|
|
||||||
args = parser.parse_args()
|
|
||||||
if args.output_name is not None:
|
|
||||||
output_name_root, output_name_ext = os.path.splitext(
|
|
||||||
args.output_name)
|
|
||||||
expected_ext = '.png' if args.show_last_frame else '.mp4'
|
|
||||||
if output_name_ext not in ['', expected_ext]:
|
|
||||||
print("WARNING: The output will be to (doubly-dotted) %s%s" %
|
|
||||||
output_name_root % expected_ext)
|
|
||||||
output_name = args.output_name
|
|
||||||
else:
|
|
||||||
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
|
|
||||||
output_name = output_name_root
|
|
||||||
else:
|
|
||||||
output_name = args.output_name
|
|
||||||
except argparse.ArgumentError as err:
|
|
||||||
print(str(err))
|
|
||||||
sys.exit(2)
|
|
||||||
config = {
|
|
||||||
"file": args.file,
|
|
||||||
"scene_name": args.scene_name,
|
|
||||||
"open_video_upon_completion": args.preview,
|
|
||||||
"show_file_in_finder": args.show_file_in_finder,
|
|
||||||
# By default, write to file
|
|
||||||
"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,
|
|
||||||
"ignore_waits": args.preview,
|
|
||||||
"write_all": args.write_all,
|
|
||||||
"output_name": output_name,
|
|
||||||
"start_at_animation_number": args.start_at_animation_number,
|
|
||||||
"end_at_animation_number": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Camera configuration
|
|
||||||
config["camera_config"] = {}
|
|
||||||
if args.low_quality:
|
|
||||||
config["camera_config"].update(LOW_QUALITY_CAMERA_CONFIG)
|
|
||||||
config["frame_duration"] = LOW_QUALITY_FRAME_DURATION
|
|
||||||
elif args.medium_quality:
|
|
||||||
config["camera_config"].update(MEDIUM_QUALITY_CAMERA_CONFIG)
|
|
||||||
config["frame_duration"] = MEDIUM_QUALITY_FRAME_DURATION
|
|
||||||
else:
|
|
||||||
config["camera_config"].update(PRODUCTION_QUALITY_CAMERA_CONFIG)
|
|
||||||
config["frame_duration"] = PRODUCTION_QUALITY_FRAME_DURATION
|
|
||||||
|
|
||||||
# If the resolution was passed in via -r
|
|
||||||
if args.resolution:
|
|
||||||
if "," in args.resolution:
|
|
||||||
height_str, width_str = args.resolution.split(",")
|
|
||||||
height = int(height_str)
|
|
||||||
width = int(width_str)
|
|
||||||
else:
|
|
||||||
height = int(args.resolution)
|
|
||||||
width = int(16 * height / 9)
|
|
||||||
config["camera_config"].update({
|
|
||||||
"pixel_height": height,
|
|
||||||
"pixel_width": width,
|
|
||||||
})
|
|
||||||
|
|
||||||
if args.color:
|
|
||||||
try:
|
|
||||||
config["camera_config"]["background_color"] = Color(args.color)
|
|
||||||
except AttributeError as err:
|
|
||||||
print("Please use a valid color")
|
|
||||||
print(err)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
# If rendering a transparent image/move, make sure the
|
|
||||||
# scene has a background opacity of 0
|
|
||||||
if args.transparent:
|
|
||||||
config["camera_config"]["background_opacity"] = 0
|
|
||||||
|
|
||||||
# Arguments related to skipping
|
|
||||||
stan = config["start_at_animation_number"]
|
|
||||||
if stan is not None:
|
|
||||||
if "," in stan:
|
|
||||||
start, end = stan.split(",")
|
|
||||||
config["start_at_animation_number"] = int(start)
|
|
||||||
config["end_at_animation_number"] = int(end)
|
|
||||||
else:
|
|
||||||
config["start_at_animation_number"] = int(stan)
|
|
||||||
|
|
||||||
config["skip_animations"] = any([
|
|
||||||
config["show_last_frame"] and not config["write_to_movie"],
|
|
||||||
config["start_at_animation_number"],
|
|
||||||
])
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def handle_scene(scene, **config):
|
def handle_scene(scene, **config):
|
||||||
import platform
|
import platform
|
||||||
|
@ -223,26 +68,31 @@ def prompt_user_for_choice(name_to_obj):
|
||||||
print("%d: %s" % (count, name))
|
print("%d: %s" % (count, name))
|
||||||
num_to_name[count] = name
|
num_to_name[count] = name
|
||||||
try:
|
try:
|
||||||
user_input = input(CHOOSE_NUMBER_MESSAGE)
|
user_input = input(constants.CHOOSE_NUMBER_MESSAGE)
|
||||||
return [
|
return [
|
||||||
name_to_obj[num_to_name[int(num_str)]]
|
name_to_obj[num_to_name[int(num_str)]]
|
||||||
for num_str in user_input.split(",")
|
for num_str in user_input.split(",")
|
||||||
]
|
]
|
||||||
except:
|
except KeyError:
|
||||||
print(INVALID_NUMBER_MESSAGE)
|
print(constants.INVALID_NUMBER_MESSAGE)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
user_input = input(constants.CHOOSE_NUMBER_MESSAGE)
|
||||||
|
return [
|
||||||
|
name_to_obj[num_to_name[int(num_str)]]
|
||||||
|
for num_str in user_input.split(",")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_scene_classes(scene_names_to_classes, config):
|
def get_scene_classes(scene_names_to_classes, config):
|
||||||
if len(scene_names_to_classes) == 0:
|
if len(scene_names_to_classes) == 0:
|
||||||
print(NO_SCENE_MESSAGE)
|
print(constants.NO_SCENE_MESSAGE)
|
||||||
return []
|
return []
|
||||||
if len(scene_names_to_classes) == 1:
|
if len(scene_names_to_classes) == 1:
|
||||||
return list(scene_names_to_classes.values())
|
return list(scene_names_to_classes.values())
|
||||||
if config["scene_name"] in scene_names_to_classes:
|
if config["scene_name"] in scene_names_to_classes:
|
||||||
return [scene_names_to_classes[config["scene_name"]]]
|
return [scene_names_to_classes[config["scene_name"]]]
|
||||||
if config["scene_name"] != "":
|
if config["scene_name"] != "":
|
||||||
print(SCENE_NOT_FOUND_MESSAGE)
|
print(constants.SCENE_NOT_FOUND_MESSAGE)
|
||||||
return []
|
return []
|
||||||
if config["write_all"]:
|
if config["write_all"]:
|
||||||
return list(scene_names_to_classes.values())
|
return list(scene_names_to_classes.values())
|
||||||
|
@ -254,16 +104,10 @@ def get_module(file_name):
|
||||||
return importlib.import_module(module_name)
|
return importlib.import_module(module_name)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(config):
|
||||||
config = get_configuration()
|
|
||||||
module = get_module(config["file"])
|
module = get_module(config["file"])
|
||||||
scene_names_to_classes = dict(inspect.getmembers(module, is_scene))
|
scene_names_to_classes = dict(inspect.getmembers(module, is_scene))
|
||||||
|
|
||||||
# config["output_directory"] = os.path.join(
|
|
||||||
# VIDEO_DIR,
|
|
||||||
# config["file"].replace(".py", "")
|
|
||||||
# )
|
|
||||||
|
|
||||||
scene_kwargs = dict([
|
scene_kwargs = dict([
|
||||||
(key, config[key])
|
(key, config[key])
|
||||||
for key in [
|
for key in [
|
||||||
|
@ -288,7 +132,7 @@ def main():
|
||||||
try:
|
try:
|
||||||
handle_scene(SceneClass(**scene_kwargs), **config)
|
handle_scene(SceneClass(**scene_kwargs), **config)
|
||||||
play_finish_sound()
|
play_finish_sound()
|
||||||
except:
|
except Exception:
|
||||||
print("\n\n")
|
print("\n\n")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print("\n\n")
|
print("\n\n")
|
||||||
|
|
40
manim.py
40
manim.py
|
@ -1,31 +1,11 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/env python
|
||||||
|
import config
|
||||||
|
import extract_scene
|
||||||
|
import stream_starter
|
||||||
|
|
||||||
from constants import *
|
args = config.parse_cli()
|
||||||
from scene.scene import Scene
|
if not args.livestream:
|
||||||
|
config = config.get_configuration(args)
|
||||||
|
extract_scene.main(config)
|
||||||
class Manim():
|
else:
|
||||||
|
stream_starter.start_livestream(args.to_twitch)
|
||||||
def __new__(cls):
|
|
||||||
kwargs = {
|
|
||||||
"scene_name": LIVE_STREAM_NAME,
|
|
||||||
"open_video_upon_completion": False,
|
|
||||||
"show_file_in_finder": False,
|
|
||||||
# By default, write to file
|
|
||||||
"write_to_movie": True,
|
|
||||||
"show_last_frame": False,
|
|
||||||
"save_pngs": False,
|
|
||||||
# If -t is passed in (for transparent), this will be RGBA
|
|
||||||
"saved_image_mode": "RGB",
|
|
||||||
"movie_file_extension": ".mp4",
|
|
||||||
"quiet": True,
|
|
||||||
"ignore_waits": False,
|
|
||||||
"write_all": False,
|
|
||||||
"name": LIVE_STREAM_NAME,
|
|
||||||
"start_at_animation_number": 0,
|
|
||||||
"end_at_animation_number": None,
|
|
||||||
"skip_animations": False,
|
|
||||||
"camera_config": HIGH_QUALITY_CAMERA_CONFIG,
|
|
||||||
"frame_duration": MEDIUM_QUALITY_FRAME_DURATION,
|
|
||||||
}
|
|
||||||
return Scene(**kwargs)
|
|
||||||
|
|
|
@ -51,6 +51,8 @@ class Scene(Container):
|
||||||
"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,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -76,7 +78,7 @@ class Scene(Container):
|
||||||
self.setup()
|
self.setup()
|
||||||
if self.write_to_movie:
|
if self.write_to_movie:
|
||||||
self.open_movie_pipe()
|
self.open_movie_pipe()
|
||||||
if IS_LIVE_STREAMING:
|
if self.livestreaming:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
self.construct(*self.construct_args)
|
self.construct(*self.construct_args)
|
||||||
|
@ -464,7 +466,7 @@ class Scene(Container):
|
||||||
raise EndSceneEarlyException()
|
raise EndSceneEarlyException()
|
||||||
|
|
||||||
def play(self, *args, **kwargs):
|
def play(self, *args, **kwargs):
|
||||||
if IS_LIVE_STREAMING:
|
if self.livestreaming:
|
||||||
self.stream_lock = False
|
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")
|
||||||
|
@ -503,7 +505,7 @@ class Scene(Container):
|
||||||
self.continual_update(0)
|
self.continual_update(0)
|
||||||
self.num_plays += 1
|
self.num_plays += 1
|
||||||
|
|
||||||
if IS_LIVE_STREAMING:
|
if self.livestreaming:
|
||||||
self.stream_lock = True
|
self.stream_lock = True
|
||||||
thread.start_new_thread(self.idle_stream, ())
|
thread.start_new_thread(self.idle_stream, ())
|
||||||
|
|
||||||
|
@ -653,8 +655,8 @@ class Scene(Container):
|
||||||
'-vcodec', 'libx264',
|
'-vcodec', 'libx264',
|
||||||
'-pix_fmt', 'yuv420p',
|
'-pix_fmt', 'yuv420p',
|
||||||
]
|
]
|
||||||
if IS_LIVE_STREAMING:
|
if self.livestreaming:
|
||||||
if IS_STREAMING_TO_TWITCH:
|
if self.to_twitch:
|
||||||
command += ['-f', 'flv']
|
command += ['-f', 'flv']
|
||||||
command += ['rtmp://live.twitch.tv/app/' + TWITCH_STREAM_KEY]
|
command += ['rtmp://live.twitch.tv/app/' + TWITCH_STREAM_KEY]
|
||||||
else:
|
else:
|
||||||
|
@ -668,7 +670,7 @@ class Scene(Container):
|
||||||
def close_movie_pipe(self):
|
def close_movie_pipe(self):
|
||||||
self.writing_process.stdin.close()
|
self.writing_process.stdin.close()
|
||||||
self.writing_process.wait()
|
self.writing_process.wait()
|
||||||
if IS_LIVE_STREAMING:
|
if self.livestreaming:
|
||||||
return True
|
return True
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
shutil.move(*self.args_to_rename_file)
|
shutil.move(*self.args_to_rename_file)
|
||||||
|
|
|
@ -1,13 +1,52 @@
|
||||||
from big_ol_pile_of_manim_imports import *
|
from scene.scene import Scene
|
||||||
import subprocess
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from manim import Manim
|
import code
|
||||||
|
import constants
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
if not IS_STREAMING_TO_TWITCH:
|
|
||||||
|
def start_livestream(to_twitch=False):
|
||||||
|
class Manim():
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
kwargs = {
|
||||||
|
"scene_name": constants.LIVE_STREAM_NAME,
|
||||||
|
"open_video_upon_completion": False,
|
||||||
|
"show_file_in_finder": False,
|
||||||
|
# By default, write to file
|
||||||
|
"write_to_movie": True,
|
||||||
|
"show_last_frame": False,
|
||||||
|
"save_pngs": False,
|
||||||
|
# If -t is passed in (for transparent), this will be RGBA
|
||||||
|
"saved_image_mode": "RGB",
|
||||||
|
"movie_file_extension": ".mp4",
|
||||||
|
"quiet": True,
|
||||||
|
"ignore_waits": False,
|
||||||
|
"write_all": False,
|
||||||
|
"name": constants.LIVE_STREAM_NAME,
|
||||||
|
"start_at_animation_number": 0,
|
||||||
|
"end_at_animation_number": None,
|
||||||
|
"skip_animations": False,
|
||||||
|
"camera_config": constants.HIGH_QUALITY_CAMERA_CONFIG,
|
||||||
|
"frame_duration": constants.MEDIUM_QUALITY_FRAME_DURATION,
|
||||||
|
"livestreaming": True,
|
||||||
|
"to_twitch": to_twitch,
|
||||||
|
}
|
||||||
|
return Scene(**kwargs)
|
||||||
|
|
||||||
|
if not to_twitch:
|
||||||
FNULL = open(os.devnull, 'w')
|
FNULL = open(os.devnull, 'w')
|
||||||
subprocess.Popen([STREAMING_CLIENT, STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT + '?listen'], stdout=FNULL, stderr=FNULL)
|
subprocess.Popen(
|
||||||
|
[constants.STREAMING_CLIENT, constants.STREAMING_URL],
|
||||||
|
stdout=FNULL,
|
||||||
|
stderr=FNULL)
|
||||||
sleep(3)
|
sleep(3)
|
||||||
|
|
||||||
manim = Manim()
|
manim = Manim()
|
||||||
|
print("YOUR STREAM IS READY!")
|
||||||
print("YOUR STREAM IS READY!")
|
variables = globals().copy()
|
||||||
|
variables.update(locals())
|
||||||
|
shell = code.InteractiveConsole(variables)
|
||||||
|
shell.push("from big_ol_pile_of_manim_imports import *")
|
||||||
|
shell.interact()
|
||||||
|
|
Loading…
Add table
Reference in a new issue