feat(cli): reverse videos on the fly

This commit is contained in:
Jérome Eertmans
2022-09-07 23:24:54 +02:00
7 changed files with 94 additions and 34 deletions

View File

@ -65,6 +65,7 @@ Default keybindings to control the presentation:
| Right Arrow | Continue/Next Slide | | Right Arrow | Continue/Next Slide |
| Left Arrow | Previous Slide | | Left Arrow | Previous Slide |
| R | Re-Animate Current Slide | | R | Re-Animate Current Slide |
| V | Reverse Current Slide |
| Spacebar | Play/Pause | | Spacebar | Play/Pause |
| Q | Quit | | Q | Quit |
@ -122,10 +123,10 @@ Here are a few things that I implemented (or that I'm planning to implement) on
- [x] Only one cli (to rule them all) - [x] Only one cli (to rule them all)
- [x] User can easily generate dummy config file - [x] User can easily generate dummy config file
- [x] Config file path can be manually set - [x] Config file path can be manually set
- [ ] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9) - [x] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
- [x] Handle 3D scenes out of the box - [x] Handle 3D scenes out of the box
- [ ] Generate docs online - [ ] Generate docs online
- [ ] Fix the quality problem on Windows platforms with `fullscreen` flag - [x] Fix the quality problem on Windows platforms with `fullscreen` flag
## Contributions and license ## Contributions and license

View File

@ -1 +1 @@
__version__ = "3.0.1" __version__ = "3.1.0"

View File

@ -23,6 +23,7 @@ class Config(BaseModel):
QUIT: Key = Key(ids=[ord("q")], name="QUIT") QUIT: Key = Key(ids=[ord("q")], name="QUIT")
CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT") CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT")
BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK") BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK")
REVERSE: Key = Key(ids=[ord("v")], name="REVERSE")
REWIND: Key = Key(ids=[ord("r")], name="REWIND") REWIND: Key = Key(ids=[ord("r")], name="REWIND")
PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE") PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE")
@ -33,7 +34,7 @@ class Config(BaseModel):
for key in values.values(): for key in values.values():
if len(ids.intersection(key.ids)) != 0: if len(ids.intersection(key.ids)) != 0:
raise ValueError( raise ValueError(
f"Two or more keys share a common key code: please make sure each key has distinc key codes" "Two or more keys share a common key code: please make sure each key has distinc key codes"
) )
ids.update(key.ids) ids.update(key.ids)

View File

@ -2,7 +2,7 @@ import click
from click_default_group import DefaultGroup from click_default_group import DefaultGroup
from . import __version__ from . import __version__
from .present import present from .present import list_scenes, present
from .wizard import init, wizard from .wizard import init, wizard
@ -13,6 +13,7 @@ def cli():
pass pass
cli.add_command(list_scenes)
cli.add_command(present) cli.add_command(present)
cli.add_command(wizard) cli.add_command(wizard)
cli.add_command(init) cli.add_command(init)

View File

@ -42,7 +42,9 @@ class Presentation:
def __init__(self, config, last_frame_next: bool = False): def __init__(self, config, last_frame_next: bool = False):
self.last_frame_next = last_frame_next self.last_frame_next = last_frame_next
self.slides = config["slides"] self.slides = config["slides"]
self.files = [reverse_video_path(path) for path in config["files"]] self.files = [path for path in config["files"]]
self.reverse = False
self.reversed_slide = -1
self.lastframe = [] self.lastframe = []
@ -80,22 +82,34 @@ class Presentation:
self.current_slide_i = max(0, self.current_slide_i - 1) self.current_slide_i = max(0, self.current_slide_i - 1)
self.rewind_slide() self.rewind_slide()
def reserve_slide(self): def reverse_slide(self):
pass self.rewind_slide(reverse=True)
def rewind_slide(self): def rewind_slide(self, reverse: bool = False):
self.reverse = reverse
self.current_animation = self.current_slide["start_animation"] self.current_animation = self.current_slide["start_animation"]
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
def load_this_cap(self, cap_number): def load_this_cap(self, cap_number: int):
if self.caps[cap_number] == None: if (
self.caps[cap_number] is None
or (self.reverse and self.reversed_slide != cap_number)
or (not self.reverse and self.reversed_slide == cap_number)
):
# unload other caps # unload other caps
for i in range(len(self.caps)): for i in range(len(self.caps)):
if self.caps[i] != None: if not self.caps[i] is None:
self.caps[i].release() self.caps[i].release()
self.caps[i] = None self.caps[i] = None
# load this cap # load this cap
self.caps[cap_number] = cv2.VideoCapture(self.files[cap_number]) file = self.files[cap_number]
if self.reverse:
self.reversed_slide = cap_number
file = "{}_reversed{}".format(*os.path.splitext(file))
else:
self.reversed_slide = -1
self.caps[cap_number] = cv2.VideoCapture(file)
@property @property
def current_slide(self): def current_slide(self):
@ -174,7 +188,9 @@ class Display:
if platform.system() == "Windows": if platform.system() == "Windows":
user32 = ctypes.windll.user32 user32 = ctypes.windll.user32
self.screen_width, self.screen_height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) self.screen_width, self.screen_height = user32.GetSystemMetrics(
0
), user32.GetSystemMetrics(1)
if fullscreen: if fullscreen:
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN) cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
@ -287,6 +303,9 @@ class Display:
else: else:
self.current_presentation.prev() self.current_presentation.prev()
self.state = State.PLAYING self.state = State.PLAYING
elif self.config.REVERSE.match(key):
self.current_presentation.reverse_slide()
self.state = State.PLAYING
elif self.config.REWIND.match(key): elif self.config.REWIND.match(key):
self.current_presentation.rewind_slide() self.current_presentation.rewind_slide()
self.state = State.PLAYING self.state = State.PLAYING
@ -296,7 +315,6 @@ class Display:
sys.exit() sys.exit()
"""
@click.command() @click.command()
@click.option( @click.option(
"--folder", "--folder",
@ -304,20 +322,26 @@ class Display:
type=click.Path(exists=True, file_okay=False), type=click.Path(exists=True, file_okay=False),
help="Set slides folder.", help="Set slides folder.",
) )
"""
@click.help_option("-h", "--help") @click.help_option("-h", "--help")
def list_scenes(folder): def list_scenes(folder):
"""List available scenes."""
for i, scene in enumerate(_list_scenes(folder), start=1):
click.secho(f"{i}: {scene}", fg="green")
def _list_scenes(folder):
scenes = [] scenes = []
for file in os.listdir(folder): for file in os.listdir(folder):
if file.endswith(".json"): if file.endswith(".json"):
scenes.append(os.path.basename(file)[:-4]) scenes.append(os.path.basename(file)[:-5])
return scenes return scenes
@click.command() @click.command()
@click.option("--scenes", nargs=-1, prompt=True) @click.argument("scenes", nargs=-1)
@config_path_option @config_path_option
@click.option( @click.option(
"--folder", "--folder",
@ -337,19 +361,36 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
"""Present the different scenes.""" """Present the different scenes."""
if len(scenes) == 0: if len(scenes) == 0:
print("ICI") scene_choices = _list_scenes(folder)
scene_choices = list_scenes(folder)
scene_choices = dict(enumerate(scene_choices, start=1)) scene_choices = dict(enumerate(scene_choices, start=1))
choices = [str(i) for i in scene_choices.keys()]
for i, scene in scene_choices.items():
click.secho(f"{i}: {scene}", fg="green")
click.echo()
click.echo("Choose number corresponding to desired scene/arguments.")
click.echo("(Use comma separated list for multiple entries)")
def value_proc(value: str): def value_proc(value: str):
raise ValueError("Value:") indices = list(map(int, value.strip().replace(" ", "").split(",")))
print(scene_choices) if not all(map(lambda i: 0 < i <= len(scene_choices), indices)):
raise ValueError("Please only enter numbers displayed on the screen.")
scenes = click.prompt("Choose a scene", value_proc=value_proc)
return [scene_choices[i] for i in indices]
if len(scene_choices) == 0:
raise ValueError("No scenes were found, are you in the correct directory?")
while True:
try:
scenes = click.prompt("Choice(s)", value_proc=value_proc)
break
except ValueError as e:
click.secho(e, fg="red")
presentations = list() presentations = list()
for scene in scenes: for scene in scenes:
config_file = os.path.join(folder, f"{scene}.json") config_file = os.path.join(folder, f"{scene}.json")

View File

@ -91,11 +91,12 @@ class Slide(Scene):
scene_name = type(self).__name__ scene_name = type(self).__name__
scene_files_folder = os.path.join(files_folder, scene_name) scene_files_folder = os.path.join(files_folder, scene_name)
if os.path.exists(scene_files_folder): old_animation_files = set()
shutil.rmtree(scene_files_folder)
if not os.path.exists(scene_files_folder): if not os.path.exists(scene_files_folder):
os.mkdir(scene_files_folder) os.mkdir(scene_files_folder)
else:
old_animation_files.update(os.listdir(scene_files_folder))
files = list() files = list()
for src_file in tqdm( for src_file in tqdm(
@ -105,10 +106,25 @@ class Slide(Scene):
ascii=True if platform.system() == "Windows" else None, ascii=True if platform.system() == "Windows" else None,
disable=config["progress_bar"] == "none", disable=config["progress_bar"] == "none",
): ):
dst_file = os.path.join(scene_files_folder, os.path.basename(src_file)) filename = os.path.basename(src_file)
shutil.copyfile(src_file, dst_file) _hash, ext = os.path.splitext(filename)
rev_file = reverse_video_path(dst_file)
reverse_video_file(src_file, rev_file) rev_filename = f"{_hash}_reversed{ext}"
dst_file = os.path.join(scene_files_folder, filename)
# We only copy animation if it was not present
if filename in old_animation_files:
old_animation_files.remove(filename)
else:
shutil.copyfile(src_file, dst_file)
# We only reverse video if it was not present
if rev_filename in old_animation_files:
old_animation_files.remove(rev_filename)
else:
rev_file = os.path.join(scene_files_folder, rev_filename)
reverse_video_file(src_file, rev_file)
files.append(dst_file) files.append(dst_file)
logger.info( logger.info(

View File

@ -2,7 +2,7 @@ import sys
import setuptools import setuptools
from .__version__ import __version__ as version from manim_slides import __version__ as version
if sys.version_info < (3, 7): if sys.version_info < (3, 7):
raise RuntimeError("This package requires Python 3.7+") raise RuntimeError("This package requires Python 3.7+")