Skip to content

Commit dde2228

Browse files
huguesdevimeuxHugues Devimeuxbehackl
authored
FIX : 472 / Small refactor of partial-movie-files handling (#489)
* fix #460 by enabling skipping wait statements. * added tests * refactored partialmovie files handling * improved berobosity of hashing * fixed tests * fixed black * fixed file name * change format to f string * fixed comment * fixed comment * Update manim/scene/scene.py Co-authored-by: Benjamin Hackl <[email protected]> * fix test output (caused by 3fff83e) Co-authored-by: Hugues Devimeux <[email protected]> Co-authored-by: Benjamin Hackl <[email protected]>
1 parent 5d4dfcf commit dde2228

File tree

5 files changed

+128
-50
lines changed

5 files changed

+128
-50
lines changed

manim/scene/scene.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def __init__(self, **kwargs):
103103
self,
104104
**file_writer_config,
105105
)
106-
self.play_hashes_list = []
106+
self.animations_hashes = []
107107

108108
self.mobjects = []
109109
self.original_skipping_status = file_writer_config["skip_animations"]
@@ -852,13 +852,16 @@ def wrapper(self, *args, **kwargs):
852852
if file_writer_config["skip_animations"]:
853853
logger.debug(f"Skipping animation {self.num_plays}")
854854
func(self, *args, **kwargs)
855+
# If the animation is skipped, we mark its hash as None.
856+
# When sceneFileWriter will start combining partial movie files, it won't take into account None hashes.
857+
self.animations_hashes.append(None)
858+
self.file_writer.add_partial_movie_file(None)
855859
return
856860
if not file_writer_config["disable_caching"]:
857861
mobjects_on_scene = self.get_mobjects()
858862
hash_play = get_hash_from_play_call(
859863
self, self.camera, animations, mobjects_on_scene
860864
)
861-
self.play_hashes_list.append(hash_play)
862865
if self.file_writer.is_already_cached(hash_play):
863866
logger.info(
864867
f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)",
@@ -867,7 +870,12 @@ def wrapper(self, *args, **kwargs):
867870
file_writer_config["skip_animations"] = True
868871
else:
869872
hash_play = "uncached_{:05}".format(self.num_plays)
870-
self.play_hashes_list.append(hash_play)
873+
self.animations_hashes.append(hash_play)
874+
self.file_writer.add_partial_movie_file(hash_play)
875+
logger.debug(
876+
"List of the first few animation hashes of the scene: %(h)s",
877+
{"h": str(self.animations_hashes[:5])},
878+
)
871879
func(self, *args, **kwargs)
872880

873881
return wrapper
@@ -890,20 +898,28 @@ def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
890898
if file_writer_config["skip_animations"]:
891899
logger.debug(f"Skipping wait {self.num_plays}")
892900
func(self, duration, stop_condition)
901+
# If the animation is skipped, we mark its hash as None.
902+
# When sceneFileWriter will start combining partial movie files, it won't take into account None hashes.
903+
self.animations_hashes.append(None)
904+
self.file_writer.add_partial_movie_file(None)
893905
return
894906
if not file_writer_config["disable_caching"]:
895907
hash_wait = get_hash_from_wait_call(
896908
self, self.camera, duration, stop_condition, self.get_mobjects()
897909
)
898-
self.play_hashes_list.append(hash_wait)
899910
if self.file_writer.is_already_cached(hash_wait):
900911
logger.info(
901912
f"Wait {self.num_plays} : Using cached data (hash : {hash_wait})"
902913
)
903914
file_writer_config["skip_animations"] = True
904915
else:
905916
hash_wait = "uncached_{:05}".format(self.num_plays)
906-
self.play_hashes_list.append(hash_wait)
917+
self.animations_hashes.append(hash_wait)
918+
self.file_writer.add_partial_movie_file(hash_wait)
919+
logger.debug(
920+
"Animations hashes list of the scene : (concatened to 5) %(h)s",
921+
{"h": str(self.animations_hashes[:5])},
922+
)
907923
func(self, duration, stop_condition)
908924

909925
return wrapper

manim/scene/scene_file_writer.py

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class SceneFileWriter(object):
3737
The PIL image mode to use when outputting PNGs
3838
"movie_file_extension" (str=".mp4")
3939
The file-type extension of the outputted video.
40+
"partial_movie_files"
41+
List of all the partial-movie files.
42+
4043
"""
4144

4245
def __init__(self, scene, **kwargs):
@@ -46,7 +49,7 @@ def __init__(self, scene, **kwargs):
4649
self.init_output_directories()
4750
self.init_audio()
4851
self.frame_count = 0
49-
self.index_partial_movie_file = 0
52+
self.partial_movie_files = []
5053

5154
# Output directories and files
5255
def init_output_directories(self):
@@ -113,6 +116,29 @@ def init_output_directories(self):
113116
)
114117
)
115118

119+
def add_partial_movie_file(self, hash_animation):
120+
"""Adds a new partial movie file path to scene.partial_movie_files from an hash. This method will compute the path from the hash.
121+
122+
Parameters
123+
----------
124+
hash_animation : str
125+
Hash of the animation.
126+
"""
127+
128+
# None has to be added to partial_movie_files to keep the right index with scene.num_plays.
129+
# i.e if an animation is skipped, scene.num_plays is still incremented and we add an element to partial_movie_file be even with num_plays.
130+
if hash_animation is None:
131+
self.partial_movie_files.append(None)
132+
return
133+
new_partial_movie_file = os.path.join(
134+
self.partial_movie_directory,
135+
"{}{}".format(
136+
hash_animation,
137+
file_writer_config["movie_file_extension"],
138+
),
139+
)
140+
self.partial_movie_files.append(new_partial_movie_file)
141+
116142
def get_default_module_directory(self):
117143
"""
118144
This method gets the name of the directory containing
@@ -149,7 +175,7 @@ def get_resolution_directory(self):
149175
This method gets the name of the directory that immediately contains the
150176
video file. This name is ``<height_in_pixels_of_video>p<frame_rate>``.
151177
For example, if you are rendering an 854x480 px animation at 15fps,
152-
the name of the directory that immediately contains the video file
178+
the name of the directory that immediately contains the video, file
153179
will be ``480p15``.
154180
155181
The file structure should look something like::
@@ -186,29 +212,6 @@ def get_image_file_path(self):
186212
"""
187213
return self.image_file_path
188214

189-
def get_next_partial_movie_path(self):
190-
"""
191-
Manim renders each play-like call in a short partial
192-
video file. All such files are then concatenated with
193-
the help of FFMPEG.
194-
195-
This method returns the path of the next partial movie.
196-
197-
Returns
198-
-------
199-
str
200-
The path of the next partial movie.
201-
"""
202-
result = os.path.join(
203-
self.partial_movie_directory,
204-
"{}{}".format(
205-
self.scene.play_hashes_list[self.index_partial_movie_file],
206-
file_writer_config["movie_file_extension"],
207-
),
208-
)
209-
self.index_partial_movie_file += 1
210-
return result
211-
212215
def get_movie_file_path(self):
213216
"""
214217
Returns the final path of the written video file.
@@ -399,7 +402,9 @@ def open_movie_pipe(self):
399402
FFMPEG and begin writing to FFMPEG's input
400403
buffer.
401404
"""
402-
file_path = self.get_next_partial_movie_path()
405+
file_path = self.partial_movie_files[self.scene.num_plays]
406+
407+
# TODO #486 Why does ffmpeg need temp files ?
403408
temp_file_path = (
404409
os.path.splitext(file_path)[0]
405410
+ "_temp"
@@ -497,21 +502,20 @@ def combine_movie_files(self):
497502
# cuts at all the places you might want. But for viewing
498503
# the scene as a whole, one of course wants to see it as a
499504
# single piece.
500-
partial_movie_files = [
501-
os.path.join(
502-
self.partial_movie_directory,
503-
"{}{}".format(hash_play, file_writer_config["movie_file_extension"]),
504-
)
505-
for hash_play in self.scene.play_hashes_list
506-
]
507-
if len(partial_movie_files) == 0:
508-
logger.error("No animations in this scene")
509-
return
505+
partial_movie_files = [el for el in self.partial_movie_files if el is not None]
506+
# NOTE : Here we should do a check and raise an exeption if partial movie file is empty.
507+
# We can't, as a lot of stuff (in particular, in tests) use scene initialization, and this error would be raised as it's just
508+
# an empty scene initialized.
509+
510510
# Write a file partial_file_list.txt containing all
511511
# partial movie files. This is used by FFMPEG.
512512
file_list = os.path.join(
513513
self.partial_movie_directory, "partial_movie_file_list.txt"
514514
)
515+
logger.debug(
516+
f"Partial movie files to combine ({len(partial_movie_files)} files): %(p)s",
517+
{"p": partial_movie_files[:5]},
518+
)
515519
with open(file_list, "w") as fp:
516520
fp.write("# This file is used internally by FFMPEG.\n")
517521
for pf_path in partial_movie_files:

manim/utils/hashing.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,11 @@ def get_hash_from_play_call(
252252
]
253253
t_end = perf_counter()
254254
logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]})
255+
hash_complete = f"{hash_camera}_{hash_animations}_{hash_current_mobjects}"
255256
# This will reset ALREADY_PROCESSED_ID as all the hashing processus is finished.
256257
ALREADY_PROCESSED_ID = {}
257-
return "{}_{}_{}".format(hash_camera, hash_animations, hash_current_mobjects)
258+
logger.debug("Hash generated : %(h)s", {"h": hash_complete})
259+
return hash_complete
258260

259261

260262
def get_hash_from_wait_call(
@@ -299,15 +301,15 @@ def get_hash_from_wait_call(
299301
ALREADY_PROCESSED_ID = {}
300302
t_end = perf_counter()
301303
logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]})
302-
return "{}_{}{}_{}".format(
303-
hash_camera,
304-
str(wait_time).replace(".", "-"),
305-
hash_function,
306-
hash_current_mobjects,
307-
)
304+
hash_complete = f"{hash_camera}_{str(wait_time).replace('.', '-')}{hash_function}_{hash_current_mobjects}"
305+
logger.debug("Hash generated : %(h)s", {"h": hash_complete})
306+
return hash_complete
308307
ALREADY_PROCESSED_ID = {}
309308
t_end = perf_counter()
310309
logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]})
311-
return "{}_{}_{}".format(
312-
hash_camera, str(wait_time).replace(".", "-"), hash_current_mobjects
310+
hash_complete = (
311+
f"{hash_camera}_{str(wait_time).replace('.', '-')}_{hash_current_mobjects}"
313312
)
313+
314+
logger.debug("Hash generated : %(h)s", {"h": hash_complete})
315+
return hash_complete
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{"levelname": "INFO", "module": "config", "message": "Log file will be saved in <>"}
22
{"levelname": "DEBUG", "module": "hashing", "message": "Hashing ..."}
33
{"levelname": "DEBUG", "module": "hashing", "message": "Hashing done in <> s."}
4+
{"levelname": "DEBUG", "module": "hashing", "message": "Hash generated : <>"}
5+
{"levelname": "DEBUG", "module": "scene", "message": "List of the first few animation hashes of the scene: <>"}
46
{"levelname": "INFO", "module": "scene_file_writer", "message": "Animation 0 : Partial movie file written in <>"}
7+
{"levelname": "DEBUG", "module": "scene_file_writer", "message": "Partial movie files to combine (1 files): <>"}
58
{"levelname": "INFO", "module": "scene_file_writer", "message": "\nFile ready at <>\n"}
69
{"levelname": "INFO", "module": "scene", "message": "Rendered SquareToCircle\nPlayed 1 animations"}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import os
2+
import pytest
3+
import subprocess
4+
from manim import file_writer_config
5+
6+
from ..utils.commands import capture
7+
from ..utils.video_tester import *
8+
9+
10+
@video_comparison(
11+
"SceneWithMultipleWaitCallsWithNFlag.json",
12+
"videos/simple_scenes/480p15/SceneWithMultipleWaitCalls.mp4",
13+
)
14+
def test_wait_skip(tmp_path, manim_cfg_file, simple_scenes_path):
15+
# Test for PR #468. Intended to test if wait calls are correctly skipped.
16+
scene_name = "SceneWithMultipleWaitCalls"
17+
command = [
18+
"python",
19+
"-m",
20+
"manim",
21+
simple_scenes_path,
22+
scene_name,
23+
"-l",
24+
"--media_dir",
25+
str(tmp_path),
26+
"-n",
27+
"3",
28+
]
29+
out, err, exit_code = capture(command)
30+
assert exit_code == 0, err
31+
32+
33+
@video_comparison(
34+
"SceneWithMultiplePlayCallsWithNFlag.json",
35+
"videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4",
36+
)
37+
def test_play_skip(tmp_path, manim_cfg_file, simple_scenes_path):
38+
# Intended to test if play calls are correctly skipped.
39+
scene_name = "SceneWithMultipleCalls"
40+
command = [
41+
"python",
42+
"-m",
43+
"manim",
44+
simple_scenes_path,
45+
scene_name,
46+
"-l",
47+
"--media_dir",
48+
str(tmp_path),
49+
"-n",
50+
"3",
51+
]
52+
out, err, exit_code = capture(command)
53+
assert exit_code == 0, err

0 commit comments

Comments
 (0)