Skip to content

Commit 2949710

Browse files
Added basic support for adding subcaptions via :meth:.Scene.add_subcaption (#2314)
* added new depependency for subcaption parsing library * added implementation for subcaptions * added test for subcaptions * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix typo * added comment to explain offset * added remark about start time of subcaption * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * clarify documentation entry Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a4c1c4f commit 2949710

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed

manim/scene/scene.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__all__ = ["Scene"]
44

55
import copy
6+
import datetime
67
import inspect
78
import platform
89
import random
@@ -12,6 +13,8 @@
1213
from queue import Queue
1314
from typing import List, Optional
1415

16+
import srt
17+
1518
from manim.scene.section import DefaultSectionType
1619

1720
try:
@@ -899,8 +902,50 @@ def get_run_time(self, animations):
899902
else:
900903
return np.max([animation.run_time for animation in animations])
901904

902-
def play(self, *args, **kwargs):
905+
def play(
906+
self,
907+
*args,
908+
subcaption=None,
909+
subcaption_duration=None,
910+
subcaption_offset=0,
911+
**kwargs,
912+
):
913+
r"""Plays an animation in this scene.
914+
915+
Parameters
916+
----------
917+
918+
args
919+
Animations to be played.
920+
subcaption
921+
The content of the external subcaption that should
922+
be added during the animation.
923+
subcaption_duration
924+
The duration for which the specified subcaption is
925+
added. If ``None`` (the default), the run time of the
926+
animation is taken.
927+
subcaption_offset
928+
An offset (in seconds) for the start time of the
929+
added subcaption.
930+
kwargs
931+
All other keywords are passed to the renderer.
932+
933+
"""
934+
start_time = self.renderer.time
903935
self.renderer.play(self, *args, **kwargs)
936+
run_time = self.renderer.time - start_time
937+
if subcaption:
938+
if subcaption_duration is None:
939+
subcaption_duration = run_time
940+
# The start of the subcaption needs to be offset by the
941+
# run_time of the animation because it is added after
942+
# the animation has already been played (and Scene.renderer.time
943+
# has already been updated).
944+
self.add_subcaption(
945+
content=subcaption,
946+
duration=subcaption_duration,
947+
offset=-run_time + subcaption_offset,
948+
)
904949

905950
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
906951
self.play(Wait(run_time=duration, stop_condition=stop_condition))
@@ -1242,6 +1287,55 @@ def update_to_time(self, t):
12421287
self.update_meshes(dt)
12431288
self.update_self(dt)
12441289

1290+
def add_subcaption(
1291+
self, content: str, duration: float = 1, offset: float = 0
1292+
) -> None:
1293+
r"""Adds an entry in the corresponding subcaption file
1294+
at the current time stamp.
1295+
1296+
The current time stamp is obtained from ``Scene.renderer.time``.
1297+
1298+
Parameters
1299+
----------
1300+
1301+
content
1302+
The subcaption content.
1303+
duration
1304+
The duration (in seconds) for which the subcaption is shown.
1305+
offset
1306+
This offset (in seconds) is added to the starting time stamp
1307+
of the subcaption.
1308+
1309+
Examples
1310+
--------
1311+
1312+
This example illustrates both possibilities for adding
1313+
subcaptions to Manimations::
1314+
1315+
class SubcaptionExample(Scene):
1316+
def construct(self):
1317+
square = Square()
1318+
circle = Circle()
1319+
1320+
# first option: via the add_subcaption method
1321+
self.add_subcaption("Hello square!", duration=1)
1322+
self.play(Create(square))
1323+
1324+
# second option: within the call to Scene.play
1325+
self.play(
1326+
Transform(square, circle),
1327+
subcaption="The square transforms."
1328+
)
1329+
1330+
"""
1331+
subtitle = srt.Subtitle(
1332+
index=len(self.renderer.file_writer.subcaptions),
1333+
content=content,
1334+
start=datetime.timedelta(seconds=self.renderer.time + offset),
1335+
end=datetime.timedelta(seconds=self.renderer.time + offset + duration),
1336+
)
1337+
self.renderer.file_writer.subcaptions.append(subtitle)
1338+
12451339
def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
12461340
"""
12471341
This method is used to add a sound to the animation.

manim/scene/scene_file_writer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Any, Dict, List, Optional
1313

1414
import numpy as np
15+
import srt
1516
from PIL import Image
1617
from pydub import AudioSegment
1718

@@ -71,6 +72,7 @@ def __init__(self, renderer, scene_name, **kwargs):
7172
self.init_audio()
7273
self.frame_count = 0
7374
self.partial_movie_files: List[str] = []
75+
self.subcaptions: List[srt.Subtitle] = []
7476
self.sections: List[Section] = []
7577
# first section gets automatically created for convenience
7678
# if you need the first section to be skipped, add a first section by hand, it will replace this one
@@ -449,6 +451,8 @@ def finish(self):
449451
elif is_png_format() and not config["dry_run"]:
450452
target_dir, _ = os.path.splitext(self.image_file_path)
451453
logger.info("\n%i images ready at %s\n", self.frame_count, target_dir)
454+
if self.subcaptions:
455+
self.write_subcaption_file()
452456

453457
def open_movie_pipe(self, file_path=None):
454458
"""
@@ -712,6 +716,13 @@ def flush_cache_directory(self):
712716
{"par_dir": self.partial_movie_directory},
713717
)
714718

719+
def write_subcaption_file(self):
720+
"""Writes the subcaption file."""
721+
subcaption_file = Path(config.output_file).with_suffix(".srt")
722+
with open(subcaption_file, "w") as f:
723+
f.write(srt.compose(self.subcaptions))
724+
logger.info(f"Subcaption file has been written as {subcaption_file}")
725+
715726
def print_file_ready_message(self, file_path):
716727
"""Prints the "File Ready" message to STDOUT."""
717728
config["output_file"] = file_path

poetry.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dearpygui = { version = "^0.8", optional = true }
5555
screeninfo = "^0.6.7"
5656
skia-pathops = "^0.7.0"
5757
isosurfaces = "0.1.0"
58+
srt = "^3.5.0"
5859

5960
[tool.poetry.extras]
6061
webgl_renderer = ["grpcio","grpcio-tools"]

tests/test_scene.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import datetime
2+
13
import pytest
24

35
from manim import Circle, FadeIn, Mobject, Scene, Square, config, tempconfig
6+
from manim.animation.animation import Wait
47

58

69
def test_scene_add_remove():
@@ -46,3 +49,25 @@ def test_scene_time():
4649
scene.renderer._original_skipping_status = True
4750
scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped.
4851
assert pytest.approx(scene.renderer.time) == 7.5
52+
53+
54+
def test_subcaption():
55+
with tempconfig({"dry_run": True}):
56+
scene = Scene()
57+
scene.add_subcaption("Testing add_subcaption", duration=1, offset=0)
58+
scene.wait()
59+
scene.play(
60+
Wait(),
61+
run_time=2,
62+
subcaption="Testing Scene.play subcaption interface",
63+
subcaption_duration=1.5,
64+
subcaption_offset=0.5,
65+
)
66+
subcaptions = scene.renderer.file_writer.subcaptions
67+
assert len(subcaptions) == 2
68+
assert subcaptions[0].start == datetime.timedelta(seconds=0)
69+
assert subcaptions[0].end == datetime.timedelta(seconds=1)
70+
assert subcaptions[0].content == "Testing add_subcaption"
71+
assert subcaptions[1].start == datetime.timedelta(seconds=1.5)
72+
assert subcaptions[1].end == datetime.timedelta(seconds=3)
73+
assert subcaptions[1].content == "Testing Scene.play subcaption interface"

0 commit comments

Comments
 (0)