mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-06-23 23:44:00 +08:00
feat(lib): allow to insert external videos as slides (#526)
* feat(lib): allow to insert external videos as slides See https://github.com/jeertmans/manim-slides/discussions/520 * chore(lib): lint and changelog entry * chore: fix PR # fix * fix: docs
This commit is contained in:
@ -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
|
||||
|
@ -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]:
|
||||
@ -205,14 +206,13 @@ class BaseSlideConfig(BaseModel): # type: ignore
|
||||
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):
|
||||
@ -242,25 +242,33 @@ class PreSlideConfig(BaseSlideConfig):
|
||||
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
|
||||
|
||||
return pre_slide_config
|
||||
@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 self
|
||||
|
||||
@property
|
||||
def slides_slice(self) -> slice:
|
||||
|
@ -305,7 +305,7 @@ class BaseSlide:
|
||||
: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::
|
||||
@ -348,6 +348,11 @@ class BaseSlide:
|
||||
``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>`,
|
||||
@ -471,6 +476,18 @@ class BaseSlide:
|
||||
|
||||
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
|
||||
|
||||
@ -493,7 +510,7 @@ class BaseSlide:
|
||||
)
|
||||
)
|
||||
|
||||
def _save_slides(
|
||||
def _save_slides( # noqa: C901
|
||||
self,
|
||||
use_cache: bool = True,
|
||||
flush_cache: bool = False,
|
||||
@ -540,6 +557,9 @@ class BaseSlide:
|
||||
):
|
||||
if pre_slide_config.skip_animations:
|
||||
continue
|
||||
if pre_slide_config.src:
|
||||
slide_files = [pre_slide_config.src]
|
||||
else:
|
||||
slide_files = files[pre_slide_config.slides_slice]
|
||||
|
||||
try:
|
||||
|
@ -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.
|
||||
|
@ -1,5 +1,6 @@
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from multiprocessing import Pool
|
||||
@ -14,6 +15,9 @@ from .logger import logger
|
||||
|
||||
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."""
|
||||
|
@ -589,6 +589,51 @@ class TestSlide:
|
||||
|
||||
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):
|
||||
|
Reference in New Issue
Block a user