Skip to content

Commit

Permalink
feat(lib): allow to insert external videos as slides (#526)
Browse files Browse the repository at this point in the history
* feat(lib): allow to insert external videos as slides

See #520

* chore(lib): lint and changelog entry

* chore: fix PR #

fix

* fix: docs
  • Loading branch information
jeertmans authored Jan 29, 2025
1 parent a2bd1ff commit ccbe9d5
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 22 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added `max_duration_before_split_reverse` and `num_processes` class variables.
[#439](https://github.com/jeertmans/manim-slides/pull/439)
- Added `src = ...` filepath argument to allow inserting external
videos as slides.
[#526](https://github.com/jeertmans/manim-slides/pull/526)

(unreleased-changed)=
### Changed
Expand Down
44 changes: 26 additions & 18 deletions manim_slides/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
notes: str = ""
dedent_notes: bool = True
skip_animations: bool = False
src: Optional[FilePath] = None

@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
Expand Down Expand Up @@ -205,14 +206,13 @@ def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807
return _wrapper_

@model_validator(mode="after")
@classmethod
def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig"
self,
) -> "BaseSlideConfig":
if base_slide_config.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes)
if self.dedent_notes:
self.notes = dedent(self.notes)

return base_slide_config
return self


class PreSlideConfig(BaseSlideConfig):
Expand Down Expand Up @@ -242,25 +242,33 @@ def index_is_posint(cls, v: int) -> int:
return v

@model_validator(mode="after")
@classmethod
def start_animation_is_before_end(
cls, pre_slide_config: "PreSlideConfig"
self,
) -> "PreSlideConfig":
if pre_slide_config.start_animation >= pre_slide_config.end_animation:
if pre_slide_config.start_animation == pre_slide_config.end_animation == 0:
raise ValueError(
"You have to play at least one animation (e.g., `self.wait()`) "
"before pausing. If you want to start paused, use the appropriate "
"command-line option when presenting. "
"IMPORTANT: when using ManimGL, `self.wait()` is not considered "
"to be an animation, so prefer to directly use `self.play(...)`."
)

if self.start_animation > self.end_animation:
raise ValueError(
"Start animation index must be strictly lower than end animation index"
)
return self

@model_validator(mode="after")
def has_src_or_more_than_zero_animations(
self,
) -> "PreSlideConfig":
if self.src is not None and self.start_animation != self.end_animation:
raise ValueError(
"A slide cannot have 'src=...' and more than zero animations at the same time."
)
elif self.src is None and self.start_animation == self.end_animation:
raise ValueError(
"You have to play at least one animation (e.g., 'self.wait()') "
"before pausing. If you want to start paused, use the appropriate "
"command-line option when presenting. "
"IMPORTANT: when using ManimGL, 'self.wait()' is not considered "
"to be an animation, so prefer to directly use 'self.play(...)'."
)

return pre_slide_config
return self

@property
def slides_slice(self) -> slice:
Expand Down
26 changes: 23 additions & 3 deletions manim_slides/slide/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def next_slide(
:param skip_animations:
Exclude the next slide from the output.
If `manim` is used, this is also passed to `:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
If `manim` is used, this is also passed to :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
which will avoid rendering the corresponding animations.
.. seealso::
Expand Down Expand Up @@ -348,6 +348,11 @@ def next_slide(
``manim-slides convert --to=pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param pathlib.Path src:
An optional path to a video file to include as next slide.
The video will be copied into the output folder, but no rescaling
is applied.
:param kwargs:
Keyword arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
Expand Down Expand Up @@ -471,6 +476,18 @@ def construct(self):

self._current_slide += 1

if base_slide_config.src is not None:
self._slides.append(
PreSlideConfig.from_base_slide_config_and_animation_indices(
base_slide_config,
self._current_animation,
self._current_animation,
)
)

base_slide_config = BaseSlideConfig() # default
self._current_slide += 1

if self._skip_animations:
base_slide_config.skip_animations = True

Expand All @@ -493,7 +510,7 @@ def _add_last_slide(self) -> None:
)
)

def _save_slides(
def _save_slides( # noqa: C901
self,
use_cache: bool = True,
flush_cache: bool = False,
Expand Down Expand Up @@ -540,7 +557,10 @@ def _save_slides(
):
if pre_slide_config.skip_animations:
continue
slide_files = files[pre_slide_config.slides_slice]
if pre_slide_config.src:
slide_files = [pre_slide_config.src]
else:
slide_files = files[pre_slide_config.slides_slice]

try:
file = merge_basenames(slide_files)
Expand Down
2 changes: 1 addition & 1 deletion manim_slides/slide/manim.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
for slides rendering.
:param args: Positional arguments passed to scene object.
:param output_folder: Where the slide animation files should be written.
:param pathlib.Path output_folder: Where the slide animation files should be written.
:param kwargs: Keyword arguments passed to scene object.
:cvar bool disable_caching: :data:`False`: Whether to disable the use of
cached animation files.
Expand Down
4 changes: 4 additions & 0 deletions manim_slides/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import os
import shutil
import tempfile
from collections.abc import Iterator
from multiprocessing import Pool
Expand All @@ -14,6 +15,9 @@

def concatenate_video_files(files: list[Path], dest: Path) -> None:
"""Concatenate multiple video files into one."""
if len(files) == 1:
shutil.copy(files[0], dest)
return

def _filter(files: list[Path]) -> Iterator[Path]:
"""Patch possibly empty video files."""
Expand Down
45 changes: 45 additions & 0 deletions tests/test_slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,51 @@ def construct(self) -> None:

assert len(config.slides) == 1

def test_next_slide_include_video(self) -> None:
class Foo(CESlide):
def construct(self) -> None:
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
square = Square(color=BLUE)
self.play(GrowFromCenter(square))
self.next_slide()
self.wait(2)

with tmp_cwd() as tmp_dir:
init_slide(Foo).render()

slides_folder = Path(tmp_dir) / "slides"

assert slides_folder.exists()

slide_file = slides_folder / "Foo.json"

config = PresentationConfig.from_file(slide_file)

assert len(config.slides) == 3

class Bar(CESlide):
def construct(self) -> None:
self.next_slide(src=config.slides[0].file)
self.wait(2)
self.next_slide()
self.wait(2)
self.next_slide() # Dummy
self.next_slide(src=config.slides[1].file, loop=True)
self.next_slide() # Dummy
self.wait(2)
self.next_slide(src=config.slides[2].file)

init_slide(Bar).render()

slide_file = slides_folder / "Bar.json"

config = PresentationConfig.from_file(slide_file)

assert len(config.slides) == 6
assert config.slides[-3].loop

def test_canvas(self) -> None:
@assert_constructs
class _(CESlide):
Expand Down

0 comments on commit ccbe9d5

Please sign in to comment.