manim 0.17.0__py3-none-any.whl → 0.19.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- manim/__init__.py +11 -6
- manim/__main__.py +62 -19
- manim/_config/__init__.py +10 -9
- manim/_config/cli_colors.py +26 -9
- manim/_config/default.cfg +1 -3
- manim/_config/logger_utils.py +23 -13
- manim/_config/utils.py +662 -468
- manim/animation/animation.py +164 -18
- manim/animation/changing.py +34 -23
- manim/animation/composition.py +265 -67
- manim/animation/creation.py +208 -26
- manim/animation/fading.py +16 -18
- manim/animation/growing.py +35 -15
- manim/animation/indication.py +150 -76
- manim/animation/movement.py +56 -22
- manim/animation/numbers.py +64 -6
- manim/animation/rotation.py +78 -7
- manim/animation/specialized.py +6 -7
- manim/animation/speedmodifier.py +13 -10
- manim/animation/transform.py +14 -11
- manim/animation/transform_matching_parts.py +3 -4
- manim/animation/updaters/mobject_update_utils.py +152 -30
- manim/animation/updaters/update.py +10 -7
- manim/camera/camera.py +182 -118
- manim/camera/mapping_camera.py +34 -3
- manim/camera/moving_camera.py +95 -74
- manim/camera/multi_camera.py +23 -15
- manim/camera/three_d_camera.py +70 -52
- manim/cli/__init__.py +17 -0
- manim/cli/cfg/group.py +76 -44
- manim/cli/checkhealth/checks.py +192 -0
- manim/cli/checkhealth/commands.py +90 -0
- manim/cli/default_group.py +158 -25
- manim/cli/init/commands.py +33 -25
- manim/cli/plugins/commands.py +16 -3
- manim/cli/render/commands.py +72 -60
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +59 -17
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +98 -33
- manim/constants.py +109 -59
- manim/data_structures.py +31 -0
- manim/mobject/frame.py +8 -5
- manim/mobject/geometry/__init__.py +1 -0
- manim/mobject/geometry/arc.py +277 -135
- manim/mobject/geometry/boolean_ops.py +32 -31
- manim/mobject/geometry/labeled.py +376 -0
- manim/mobject/geometry/line.py +192 -87
- manim/mobject/geometry/polygram.py +224 -58
- manim/mobject/geometry/shape_matchers.py +61 -25
- manim/mobject/geometry/tips.py +122 -48
- manim/mobject/graph.py +1027 -419
- manim/mobject/graphing/coordinate_systems.py +533 -278
- manim/mobject/graphing/functions.py +53 -32
- manim/mobject/graphing/number_line.py +123 -65
- manim/mobject/graphing/probability.py +88 -62
- manim/mobject/graphing/scale.py +33 -19
- manim/mobject/logo.py +118 -28
- manim/mobject/matrix.py +87 -83
- manim/mobject/mobject.py +912 -442
- manim/mobject/opengl/dot_cloud.py +16 -5
- manim/mobject/opengl/opengl_compatibility.py +4 -2
- manim/mobject/opengl/opengl_geometry.py +254 -153
- manim/mobject/opengl/opengl_image_mobject.py +3 -1
- manim/mobject/opengl/opengl_mobject.py +779 -482
- manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
- manim/mobject/opengl/opengl_surface.py +14 -92
- manim/mobject/opengl/opengl_three_dimensions.py +12 -8
- manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
- manim/mobject/svg/brace.py +173 -41
- manim/mobject/svg/svg_mobject.py +139 -53
- manim/mobject/table.py +61 -68
- manim/mobject/text/code_mobject.py +193 -539
- manim/mobject/text/numbers.py +81 -34
- manim/mobject/text/tex_mobject.py +130 -78
- manim/mobject/text/text_mobject.py +288 -164
- manim/mobject/three_d/polyhedra.py +111 -13
- manim/mobject/three_d/three_d_utils.py +17 -8
- manim/mobject/three_d/three_dimensions.py +239 -106
- manim/mobject/types/image_mobject.py +50 -30
- manim/mobject/types/point_cloud_mobject.py +120 -75
- manim/mobject/types/vectorized_mobject.py +841 -408
- manim/mobject/value_tracker.py +105 -38
- manim/mobject/vector_field.py +50 -31
- manim/opengl/__init__.py +3 -3
- manim/plugins/__init__.py +14 -1
- manim/plugins/plugins_flags.py +10 -14
- manim/renderer/cairo_renderer.py +65 -50
- manim/renderer/opengl_renderer.py +89 -69
- manim/renderer/opengl_renderer_window.py +39 -18
- manim/renderer/shader.py +123 -87
- manim/renderer/shader_wrapper.py +44 -28
- manim/renderer/vectorized_mobject_rendering.py +38 -10
- manim/scene/moving_camera_scene.py +32 -3
- manim/scene/scene.py +507 -242
- manim/scene/scene_file_writer.py +371 -220
- manim/scene/section.py +20 -16
- manim/scene/three_d_scene.py +14 -22
- manim/scene/vector_space_scene.py +223 -129
- manim/scene/zoomed_scene.py +46 -41
- manim/typing.py +990 -0
- manim/utils/bezier.py +1823 -371
- manim/utils/caching.py +12 -5
- manim/utils/color/AS2700.py +236 -0
- manim/utils/color/BS381.py +318 -0
- manim/utils/color/DVIPSNAMES.py +96 -0
- manim/utils/color/SVGNAMES.py +179 -0
- manim/utils/color/X11.py +533 -0
- manim/utils/color/XKCD.py +952 -0
- manim/utils/color/__init__.py +61 -0
- manim/utils/color/core.py +1667 -0
- manim/utils/color/manim_colors.py +218 -0
- manim/utils/commands.py +48 -20
- manim/utils/config_ops.py +39 -19
- manim/utils/debug.py +8 -7
- manim/utils/deprecation.py +86 -39
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +236 -0
- manim/utils/docbuild/autocolor_directive.py +99 -0
- manim/utils/docbuild/manim_directive.py +94 -41
- manim/utils/docbuild/module_parsing.py +245 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +5 -3
- manim/utils/family_ops.py +17 -4
- manim/utils/file_ops.py +27 -17
- manim/utils/hashing.py +55 -45
- manim/utils/images.py +13 -7
- manim/utils/ipython_magic.py +13 -7
- manim/utils/iterables.py +163 -120
- manim/utils/module_ops.py +66 -24
- manim/utils/opengl.py +77 -24
- manim/utils/parameter_parsing.py +32 -0
- manim/utils/paths.py +30 -33
- manim/utils/polylabel.py +235 -0
- manim/utils/qhull.py +218 -0
- manim/utils/rate_functions.py +98 -32
- manim/utils/simple_functions.py +25 -33
- manim/utils/sounds.py +7 -1
- manim/utils/space_ops.py +188 -115
- manim/utils/testing/__init__.py +17 -0
- manim/utils/testing/_frames_testers.py +13 -8
- manim/utils/testing/_show_diff.py +5 -3
- manim/utils/testing/_test_class_makers.py +34 -18
- manim/utils/testing/frames_comparison.py +37 -19
- manim/utils/tex.py +130 -198
- manim/utils/tex_file_writing.py +77 -47
- manim/utils/tex_templates.py +2 -1
- manim/utils/unit.py +6 -5
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
- manim-0.19.1.dist-info/RECORD +220 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
- manim-0.19.1.dist-info/entry_points.txt +3 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
- manim/cli/new/group.py +0 -189
- manim/communitycolors.py +0 -9
- manim/gui/__init__.py +0 -0
- manim/gui/gui.py +0 -82
- manim/plugins/import_plugins.py +0 -43
- manim/utils/color.py +0 -552
- manim-0.17.0.dist-info/RECORD +0 -206
- manim-0.17.0.dist-info/entry_points.txt +0 -4
- /manim/cli/{new → checkhealth}/__init__.py +0 -0
- {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
manim/scene/scene_file_writer.py
CHANGED
|
@@ -5,12 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
__all__ = ["SceneFileWriter"]
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
import os
|
|
9
8
|
import shutil
|
|
10
|
-
import
|
|
9
|
+
from fractions import Fraction
|
|
11
10
|
from pathlib import Path
|
|
11
|
+
from queue import Queue
|
|
12
|
+
from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
|
|
13
|
+
from threading import Thread
|
|
12
14
|
from typing import TYPE_CHECKING, Any
|
|
13
15
|
|
|
16
|
+
import av
|
|
14
17
|
import numpy as np
|
|
15
18
|
import srt
|
|
16
19
|
from PIL import Image
|
|
@@ -24,11 +27,9 @@ from ..constants import RendererType
|
|
|
24
27
|
from ..utils.file_ops import (
|
|
25
28
|
add_extension_if_not_present,
|
|
26
29
|
add_version_before_extension,
|
|
27
|
-
ensure_executable,
|
|
28
30
|
guarantee_existence,
|
|
29
31
|
is_gif_format,
|
|
30
32
|
is_png_format,
|
|
31
|
-
is_webm_format,
|
|
32
33
|
modify_atime,
|
|
33
34
|
write_to_movie,
|
|
34
35
|
)
|
|
@@ -36,12 +37,50 @@ from ..utils.sounds import get_full_sound_file_path
|
|
|
36
37
|
from .section import DefaultSectionType, Section
|
|
37
38
|
|
|
38
39
|
if TYPE_CHECKING:
|
|
40
|
+
from av.container.output import OutputContainer
|
|
41
|
+
from av.stream import Stream
|
|
42
|
+
|
|
43
|
+
from manim.renderer.cairo_renderer import CairoRenderer
|
|
39
44
|
from manim.renderer.opengl_renderer import OpenGLRenderer
|
|
45
|
+
from manim.typing import PixelArray, StrPath
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_av_frame_rate(fps: float) -> Fraction:
|
|
49
|
+
epsilon1 = 1e-4
|
|
50
|
+
epsilon2 = 0.02
|
|
51
|
+
|
|
52
|
+
if isinstance(fps, int):
|
|
53
|
+
(num, denom) = (fps, 1)
|
|
54
|
+
elif abs(fps - round(fps)) < epsilon1:
|
|
55
|
+
(num, denom) = (round(fps), 1)
|
|
56
|
+
else:
|
|
57
|
+
denom = 1001
|
|
58
|
+
num = round(fps * denom / 1000) * 1000
|
|
59
|
+
if abs(fps - num / denom) >= epsilon2:
|
|
60
|
+
raise ValueError("invalid frame rate")
|
|
61
|
+
|
|
62
|
+
return Fraction(num, denom)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def convert_audio(
|
|
66
|
+
input_path: Path, output_path: Path | _TemporaryFileWrapper[bytes], codec_name: str
|
|
67
|
+
) -> None:
|
|
68
|
+
with (
|
|
69
|
+
av.open(input_path) as input_audio,
|
|
70
|
+
av.open(output_path, "w") as output_audio,
|
|
71
|
+
):
|
|
72
|
+
input_audio_stream = input_audio.streams.audio[0]
|
|
73
|
+
output_audio_stream = output_audio.add_stream(codec_name)
|
|
74
|
+
for frame in input_audio.decode(input_audio_stream):
|
|
75
|
+
for packet in output_audio_stream.encode(frame):
|
|
76
|
+
output_audio.mux(packet)
|
|
77
|
+
|
|
78
|
+
for packet in output_audio_stream.encode():
|
|
79
|
+
output_audio.mux(packet)
|
|
40
80
|
|
|
41
81
|
|
|
42
82
|
class SceneFileWriter:
|
|
43
|
-
"""
|
|
44
|
-
SceneFileWriter is the object that actually writes the animations
|
|
83
|
+
"""SceneFileWriter is the object that actually writes the animations
|
|
45
84
|
played, into video files, using FFMPEG.
|
|
46
85
|
This is mostly for Manim's internal use. You will rarely, if ever,
|
|
47
86
|
have to use the methods for this class, unless tinkering with the very
|
|
@@ -70,29 +109,26 @@ class SceneFileWriter:
|
|
|
70
109
|
|
|
71
110
|
force_output_as_scene_name = False
|
|
72
111
|
|
|
73
|
-
def __init__(
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
renderer: CairoRenderer | OpenGLRenderer,
|
|
115
|
+
scene_name: str,
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> None:
|
|
74
118
|
self.renderer = renderer
|
|
75
119
|
self.init_output_directories(scene_name)
|
|
76
120
|
self.init_audio()
|
|
77
121
|
self.frame_count = 0
|
|
78
|
-
self.partial_movie_files: list[str] = []
|
|
122
|
+
self.partial_movie_files: list[str | None] = []
|
|
79
123
|
self.subcaptions: list[srt.Subtitle] = []
|
|
80
124
|
self.sections: list[Section] = []
|
|
81
125
|
# first section gets automatically created for convenience
|
|
82
126
|
# if you need the first section to be skipped, add a first section by hand, it will replace this one
|
|
83
127
|
self.next_section(
|
|
84
|
-
name="autocreated",
|
|
128
|
+
name="autocreated", type_=DefaultSectionType.NORMAL, skip_animations=False
|
|
85
129
|
)
|
|
86
|
-
# fail fast if ffmpeg is not found
|
|
87
|
-
if not ensure_executable(Path(config.ffmpeg_executable)):
|
|
88
|
-
raise RuntimeError(
|
|
89
|
-
"Manim could not find ffmpeg, which is required for generating video output.\n"
|
|
90
|
-
"For installing ffmpeg please consult https://docs.manim.community/en/stable/installation.html\n"
|
|
91
|
-
"Make sure to either add ffmpeg to the PATH environment variable\n"
|
|
92
|
-
"or set path to the ffmpeg executable under the ffmpeg header in Manim's configuration."
|
|
93
|
-
)
|
|
94
130
|
|
|
95
|
-
def init_output_directories(self, scene_name):
|
|
131
|
+
def init_output_directories(self, scene_name: str) -> None:
|
|
96
132
|
"""Initialise output directories.
|
|
97
133
|
|
|
98
134
|
Notes
|
|
@@ -105,10 +141,7 @@ class SceneFileWriter:
|
|
|
105
141
|
if config["dry_run"]: # in dry-run mode there is no output
|
|
106
142
|
return
|
|
107
143
|
|
|
108
|
-
if config["input_file"]
|
|
109
|
-
module_name = config.get_dir("input_file").stem
|
|
110
|
-
else:
|
|
111
|
-
module_name = ""
|
|
144
|
+
module_name = config.get_dir("input_file").stem if config["input_file"] else ""
|
|
112
145
|
|
|
113
146
|
if SceneFileWriter.force_output_as_scene_name:
|
|
114
147
|
self.output_name = Path(scene_name)
|
|
@@ -137,7 +170,7 @@ class SceneFileWriter:
|
|
|
137
170
|
self.output_name, config["movie_file_extension"]
|
|
138
171
|
)
|
|
139
172
|
|
|
140
|
-
# TODO: /dev/null would be good in case sections_output_dir is used without
|
|
173
|
+
# TODO: /dev/null would be good in case sections_output_dir is used without being set (doesn't work on Windows), everyone likes defensive programming, right?
|
|
141
174
|
self.sections_output_dir = Path("")
|
|
142
175
|
if config.save_sections:
|
|
143
176
|
self.sections_output_dir = guarantee_existence(
|
|
@@ -177,7 +210,7 @@ class SceneFileWriter:
|
|
|
177
210
|
if len(self.sections) and self.sections[-1].is_empty():
|
|
178
211
|
self.sections.pop()
|
|
179
212
|
|
|
180
|
-
def next_section(self, name: str,
|
|
213
|
+
def next_section(self, name: str, type_: str, skip_animations: bool) -> None:
|
|
181
214
|
"""Create segmentation cut here."""
|
|
182
215
|
self.finish_last_section()
|
|
183
216
|
|
|
@@ -191,20 +224,23 @@ class SceneFileWriter:
|
|
|
191
224
|
and not skip_animations
|
|
192
225
|
):
|
|
193
226
|
# relative to index file
|
|
194
|
-
section_video = f"{self.output_name}_{len(self.sections):04}{config.movie_file_extension}"
|
|
227
|
+
section_video = f"{self.output_name}_{len(self.sections):04}_{name}{config.movie_file_extension}"
|
|
195
228
|
|
|
196
229
|
self.sections.append(
|
|
197
230
|
Section(
|
|
198
|
-
|
|
231
|
+
type_,
|
|
199
232
|
section_video,
|
|
200
233
|
name,
|
|
201
234
|
skip_animations,
|
|
202
235
|
),
|
|
203
236
|
)
|
|
204
237
|
|
|
205
|
-
def add_partial_movie_file(self, hash_animation: str):
|
|
206
|
-
"""Adds a new partial movie file path to
|
|
207
|
-
|
|
238
|
+
def add_partial_movie_file(self, hash_animation: str | None) -> None:
|
|
239
|
+
"""Adds a new partial movie file path to ``scene.partial_movie_files``
|
|
240
|
+
and current section from a hash.
|
|
241
|
+
|
|
242
|
+
This method will compute the path from the hash. In addition to that it
|
|
243
|
+
adds the new animation to the current section.
|
|
208
244
|
|
|
209
245
|
Parameters
|
|
210
246
|
----------
|
|
@@ -227,7 +263,7 @@ class SceneFileWriter:
|
|
|
227
263
|
self.partial_movie_files.append(new_partial_movie_file)
|
|
228
264
|
self.sections[-1].partial_movie_files.append(new_partial_movie_file)
|
|
229
265
|
|
|
230
|
-
def get_resolution_directory(self):
|
|
266
|
+
def get_resolution_directory(self) -> str:
|
|
231
267
|
"""Get the name of the resolution directory directly containing
|
|
232
268
|
the video file.
|
|
233
269
|
|
|
@@ -243,9 +279,11 @@ class SceneFileWriter:
|
|
|
243
279
|
|--Tex
|
|
244
280
|
|--texts
|
|
245
281
|
|--videos
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
282
|
+
|--<name_of_file_containing_scene>
|
|
283
|
+
|--<height_in_pixels_of_video>p<frame_rate>
|
|
284
|
+
|--partial_movie_files
|
|
285
|
+
|--<scene_name>.mp4
|
|
286
|
+
|--<scene_name>.srt
|
|
249
287
|
|
|
250
288
|
Returns
|
|
251
289
|
-------
|
|
@@ -257,16 +295,12 @@ class SceneFileWriter:
|
|
|
257
295
|
return f"{pixel_height}p{frame_rate}"
|
|
258
296
|
|
|
259
297
|
# Sound
|
|
260
|
-
def init_audio(self):
|
|
261
|
-
"""
|
|
262
|
-
Preps the writer for adding audio to the movie.
|
|
263
|
-
"""
|
|
298
|
+
def init_audio(self) -> None:
|
|
299
|
+
"""Preps the writer for adding audio to the movie."""
|
|
264
300
|
self.includes_sound = False
|
|
265
301
|
|
|
266
|
-
def create_audio_segment(self):
|
|
267
|
-
"""
|
|
268
|
-
Creates an empty, silent, Audio Segment.
|
|
269
|
-
"""
|
|
302
|
+
def create_audio_segment(self) -> None:
|
|
303
|
+
"""Creates an empty, silent, Audio Segment."""
|
|
270
304
|
self.audio_segment = AudioSegment.silent()
|
|
271
305
|
|
|
272
306
|
def add_audio_segment(
|
|
@@ -274,10 +308,9 @@ class SceneFileWriter:
|
|
|
274
308
|
new_segment: AudioSegment,
|
|
275
309
|
time: float | None = None,
|
|
276
310
|
gain_to_background: float | None = None,
|
|
277
|
-
):
|
|
278
|
-
"""
|
|
279
|
-
|
|
280
|
-
AudioSegment type object and suitable parameters.
|
|
311
|
+
) -> None:
|
|
312
|
+
"""This method adds an audio segment from an AudioSegment type object
|
|
313
|
+
and suitable parameters.
|
|
281
314
|
|
|
282
315
|
Parameters
|
|
283
316
|
----------
|
|
@@ -285,8 +318,7 @@ class SceneFileWriter:
|
|
|
285
318
|
The audio segment to add
|
|
286
319
|
|
|
287
320
|
time
|
|
288
|
-
the timestamp at which the
|
|
289
|
-
sound should be added.
|
|
321
|
+
the timestamp at which the sound should be added.
|
|
290
322
|
|
|
291
323
|
gain_to_background
|
|
292
324
|
The gain of the segment from the background.
|
|
@@ -316,13 +348,12 @@ class SceneFileWriter:
|
|
|
316
348
|
|
|
317
349
|
def add_sound(
|
|
318
350
|
self,
|
|
319
|
-
sound_file:
|
|
351
|
+
sound_file: StrPath,
|
|
320
352
|
time: float | None = None,
|
|
321
353
|
gain: float | None = None,
|
|
322
|
-
**kwargs,
|
|
323
|
-
):
|
|
324
|
-
"""
|
|
325
|
-
This method adds an audio segment from a sound file.
|
|
354
|
+
**kwargs: Any,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""This method adds an audio segment from a sound file.
|
|
326
357
|
|
|
327
358
|
Parameters
|
|
328
359
|
----------
|
|
@@ -341,15 +372,28 @@ class SceneFileWriter:
|
|
|
341
372
|
|
|
342
373
|
"""
|
|
343
374
|
file_path = get_full_sound_file_path(sound_file)
|
|
344
|
-
|
|
375
|
+
# we assume files with .wav / .raw suffix are actually
|
|
376
|
+
# .wav and .raw files, respectively.
|
|
377
|
+
if file_path.suffix not in (".wav", ".raw"):
|
|
378
|
+
# we need to pass delete=False to work on Windows
|
|
379
|
+
# TODO: figure out a way to cache the wav file generated (benchmark needed)
|
|
380
|
+
with NamedTemporaryFile(suffix=".wav", delete=False) as wav_file_path:
|
|
381
|
+
convert_audio(file_path, wav_file_path, "pcm_s16le")
|
|
382
|
+
new_segment = AudioSegment.from_file(wav_file_path.name)
|
|
383
|
+
logger.info(f"Automatically converted {file_path} to .wav")
|
|
384
|
+
Path(wav_file_path.name).unlink()
|
|
385
|
+
else:
|
|
386
|
+
new_segment = AudioSegment.from_file(file_path)
|
|
387
|
+
|
|
345
388
|
if gain:
|
|
346
389
|
new_segment = new_segment.apply_gain(gain)
|
|
347
390
|
self.add_audio_segment(new_segment, time, **kwargs)
|
|
348
391
|
|
|
349
392
|
# Writers
|
|
350
|
-
def begin_animation(
|
|
351
|
-
|
|
352
|
-
|
|
393
|
+
def begin_animation(
|
|
394
|
+
self, allow_write: bool = False, file_path: StrPath | None = None
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Used internally by manim to stream the animation to FFMPEG for
|
|
353
397
|
displaying or writing to a file.
|
|
354
398
|
|
|
355
399
|
Parameters
|
|
@@ -358,12 +402,10 @@ class SceneFileWriter:
|
|
|
358
402
|
Whether or not to write to a video file.
|
|
359
403
|
"""
|
|
360
404
|
if write_to_movie() and allow_write:
|
|
361
|
-
self.
|
|
405
|
+
self.open_partial_movie_stream(file_path=file_path)
|
|
362
406
|
|
|
363
|
-
def end_animation(self, allow_write: bool = False):
|
|
364
|
-
"""
|
|
365
|
-
Internally used by Manim to stop streaming to
|
|
366
|
-
FFMPEG gracefully.
|
|
407
|
+
def end_animation(self, allow_write: bool = False) -> None:
|
|
408
|
+
"""Internally used by Manim to stop streaming to FFMPEG gracefully.
|
|
367
409
|
|
|
368
410
|
Parameters
|
|
369
411
|
----------
|
|
@@ -371,63 +413,86 @@ class SceneFileWriter:
|
|
|
371
413
|
Whether or not to write to a video file.
|
|
372
414
|
"""
|
|
373
415
|
if write_to_movie() and allow_write:
|
|
374
|
-
self.
|
|
416
|
+
self.close_partial_movie_stream()
|
|
375
417
|
|
|
376
|
-
def
|
|
418
|
+
def listen_and_write(self) -> None:
|
|
419
|
+
"""For internal use only: blocks until new frame is available on the queue."""
|
|
420
|
+
while True:
|
|
421
|
+
num_frames, frame_data = self.queue.get()
|
|
422
|
+
if frame_data is None:
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
self.encode_and_write_frame(frame_data, num_frames)
|
|
426
|
+
|
|
427
|
+
def encode_and_write_frame(self, frame: PixelArray, num_frames: int) -> None:
|
|
428
|
+
"""For internal use only: takes a given frame in ``np.ndarray`` format and
|
|
429
|
+
writes it to the stream
|
|
377
430
|
"""
|
|
378
|
-
|
|
379
|
-
|
|
431
|
+
for _ in range(num_frames):
|
|
432
|
+
# Notes: precomputing reusing packets does not work!
|
|
433
|
+
# I.e., you cannot do `packets = encode(...)`
|
|
434
|
+
# and reuse it, as it seems that `mux(...)`
|
|
435
|
+
# consumes the packet.
|
|
436
|
+
# The same issue applies for `av_frame`,
|
|
437
|
+
# reusing it renders weird-looking frames.
|
|
438
|
+
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
|
|
439
|
+
for packet in self.video_stream.encode(av_frame):
|
|
440
|
+
self.video_container.mux(packet)
|
|
441
|
+
|
|
442
|
+
def write_frame(
|
|
443
|
+
self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Used internally by Manim to write a frame to the FFMPEG input buffer.
|
|
380
446
|
|
|
381
447
|
Parameters
|
|
382
448
|
----------
|
|
383
449
|
frame_or_renderer
|
|
384
450
|
Pixel array of the frame.
|
|
451
|
+
num_frames
|
|
452
|
+
The number of times to write frame.
|
|
385
453
|
"""
|
|
386
|
-
if config.renderer == RendererType.OPENGL:
|
|
387
|
-
self.write_opengl_frame(frame_or_renderer)
|
|
388
|
-
elif config.renderer == RendererType.CAIRO:
|
|
389
|
-
frame = frame_or_renderer
|
|
390
|
-
if write_to_movie():
|
|
391
|
-
self.writing_process.stdin.write(frame.tobytes())
|
|
392
|
-
if is_png_format() and not config["dry_run"]:
|
|
393
|
-
self.output_image_from_array(frame)
|
|
394
|
-
|
|
395
|
-
def write_opengl_frame(self, renderer: OpenGLRenderer):
|
|
396
454
|
if write_to_movie():
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
455
|
+
if isinstance(frame_or_renderer, np.ndarray):
|
|
456
|
+
frame = frame_or_renderer
|
|
457
|
+
else:
|
|
458
|
+
frame = (
|
|
459
|
+
frame_or_renderer.get_frame()
|
|
460
|
+
if config.renderer == RendererType.OPENGL
|
|
461
|
+
else frame_or_renderer
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
msg = (num_frames, frame)
|
|
465
|
+
self.queue.put(msg)
|
|
466
|
+
|
|
467
|
+
if is_png_format() and not config["dry_run"]:
|
|
468
|
+
if isinstance(frame_or_renderer, np.ndarray):
|
|
469
|
+
image = Image.fromarray(frame_or_renderer)
|
|
470
|
+
else:
|
|
471
|
+
image = (
|
|
472
|
+
frame_or_renderer.get_image()
|
|
473
|
+
if config.renderer == RendererType.OPENGL
|
|
474
|
+
else Image.fromarray(frame_or_renderer)
|
|
475
|
+
)
|
|
401
476
|
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
|
402
477
|
extension = self.image_file_path.suffix
|
|
403
478
|
self.output_image(
|
|
404
|
-
|
|
479
|
+
image,
|
|
405
480
|
target_dir,
|
|
406
481
|
extension,
|
|
407
482
|
config["zero_pad"],
|
|
408
483
|
)
|
|
409
484
|
|
|
410
|
-
def
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
self.output_image(
|
|
414
|
-
Image.fromarray(frame_data),
|
|
415
|
-
target_dir,
|
|
416
|
-
extension,
|
|
417
|
-
config["zero_pad"],
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
|
|
485
|
+
def output_image(
|
|
486
|
+
self, image: Image.Image, target_dir: StrPath, ext: str, zero_pad: int
|
|
487
|
+
) -> None:
|
|
421
488
|
if zero_pad:
|
|
422
489
|
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
|
|
423
490
|
else:
|
|
424
491
|
image.save(f"{target_dir}{self.frame_count}{ext}")
|
|
425
492
|
self.frame_count += 1
|
|
426
493
|
|
|
427
|
-
def
|
|
428
|
-
"""
|
|
429
|
-
The name is a misnomer. This method saves the image
|
|
430
|
-
passed to it as an in the default image directory.
|
|
494
|
+
def save_image(self, image: Image.Image) -> None:
|
|
495
|
+
"""This method saves the image passed to it in the default image directory.
|
|
431
496
|
|
|
432
497
|
Parameters
|
|
433
498
|
----------
|
|
@@ -442,18 +507,12 @@ class SceneFileWriter:
|
|
|
442
507
|
image.save(self.image_file_path)
|
|
443
508
|
self.print_file_ready_message(self.image_file_path)
|
|
444
509
|
|
|
445
|
-
def finish(self):
|
|
446
|
-
"""
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
Combines the partial movie files into the
|
|
450
|
-
whole scene.
|
|
451
|
-
If save_last_frame is True, saves the last
|
|
452
|
-
frame in the default image directory.
|
|
510
|
+
def finish(self) -> None:
|
|
511
|
+
"""Finishes writing to the FFMPEG buffer or writing images to output directory.
|
|
512
|
+
Combines the partial movie files into the whole scene.
|
|
513
|
+
If save_last_frame is True, saves the last frame in the default image directory.
|
|
453
514
|
"""
|
|
454
515
|
if write_to_movie():
|
|
455
|
-
if hasattr(self, "writing_process"):
|
|
456
|
-
self.writing_process.terminate()
|
|
457
516
|
self.combine_to_movie()
|
|
458
517
|
if config.save_sections:
|
|
459
518
|
self.combine_to_section_videos()
|
|
@@ -467,69 +526,73 @@ class SceneFileWriter:
|
|
|
467
526
|
if self.subcaptions:
|
|
468
527
|
self.write_subcaption_file()
|
|
469
528
|
|
|
470
|
-
def
|
|
471
|
-
"""
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
529
|
+
def open_partial_movie_stream(self, file_path: StrPath | None = None) -> None:
|
|
530
|
+
"""Open a container holding a video stream.
|
|
531
|
+
|
|
532
|
+
This is used internally by Manim initialize the container holding
|
|
533
|
+
the video stream of a partial movie file.
|
|
475
534
|
"""
|
|
476
535
|
if file_path is None:
|
|
477
536
|
file_path = self.partial_movie_files[self.renderer.num_plays]
|
|
478
537
|
self.partial_movie_file_path = file_path
|
|
479
538
|
|
|
480
|
-
fps = config
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"
|
|
497
|
-
"
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if config.renderer == RendererType.OPENGL:
|
|
509
|
-
command += ["-vf", "vflip"]
|
|
510
|
-
if is_webm_format():
|
|
511
|
-
command += ["-vcodec", "libvpx-vp9", "-auto-alt-ref", "0"]
|
|
512
|
-
# .mov format
|
|
513
|
-
elif config["transparent"]:
|
|
514
|
-
command += ["-vcodec", "qtrle"]
|
|
515
|
-
else:
|
|
516
|
-
command += ["-vcodec", "libx264", "-pix_fmt", "yuv420p"]
|
|
517
|
-
command += [file_path]
|
|
518
|
-
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
|
539
|
+
fps = to_av_frame_rate(config.frame_rate)
|
|
540
|
+
|
|
541
|
+
partial_movie_file_codec = "libx264"
|
|
542
|
+
partial_movie_file_pix_fmt = "yuv420p"
|
|
543
|
+
av_options = {
|
|
544
|
+
"an": "1", # ffmpeg: -an, no audio
|
|
545
|
+
"crf": "23", # ffmpeg: -crf, constant rate factor (improved bitrate)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if config.movie_file_extension == ".webm":
|
|
549
|
+
partial_movie_file_codec = "libvpx-vp9"
|
|
550
|
+
av_options["-auto-alt-ref"] = "1"
|
|
551
|
+
if config.transparent:
|
|
552
|
+
partial_movie_file_pix_fmt = "yuva420p"
|
|
553
|
+
|
|
554
|
+
elif config.transparent:
|
|
555
|
+
partial_movie_file_codec = "qtrle"
|
|
556
|
+
partial_movie_file_pix_fmt = "argb"
|
|
557
|
+
|
|
558
|
+
with av.open(file_path, mode="w") as video_container:
|
|
559
|
+
stream = video_container.add_stream(
|
|
560
|
+
partial_movie_file_codec,
|
|
561
|
+
rate=fps,
|
|
562
|
+
options=av_options,
|
|
563
|
+
)
|
|
564
|
+
stream.pix_fmt = partial_movie_file_pix_fmt
|
|
565
|
+
stream.width = config.pixel_width
|
|
566
|
+
stream.height = config.pixel_height
|
|
519
567
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
568
|
+
self.video_container: OutputContainer = video_container
|
|
569
|
+
self.video_stream: Stream = stream
|
|
570
|
+
|
|
571
|
+
self.queue: Queue[tuple[int, PixelArray | None]] = Queue()
|
|
572
|
+
self.writer_thread = Thread(target=self.listen_and_write, args=())
|
|
573
|
+
self.writer_thread.start()
|
|
574
|
+
|
|
575
|
+
def close_partial_movie_stream(self) -> None:
|
|
576
|
+
"""Close the currently opened video container.
|
|
577
|
+
|
|
578
|
+
Used internally by Manim to first flush the remaining packages
|
|
579
|
+
in the video stream holding a partial file, and then close
|
|
580
|
+
the corresponding container.
|
|
523
581
|
"""
|
|
524
|
-
self.
|
|
525
|
-
self.
|
|
582
|
+
self.queue.put((-1, None))
|
|
583
|
+
self.writer_thread.join()
|
|
584
|
+
|
|
585
|
+
for packet in self.video_stream.encode():
|
|
586
|
+
self.video_container.mux(packet)
|
|
587
|
+
|
|
588
|
+
self.video_container.close()
|
|
526
589
|
|
|
527
590
|
logger.info(
|
|
528
591
|
f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s",
|
|
529
592
|
{"path": f"'{self.partial_movie_file_path}'"},
|
|
530
593
|
)
|
|
531
594
|
|
|
532
|
-
def is_already_cached(self, hash_invocation: str):
|
|
595
|
+
def is_already_cached(self, hash_invocation: str) -> bool:
|
|
533
596
|
"""Will check if a file named with `hash_invocation` exists.
|
|
534
597
|
|
|
535
598
|
Parameters
|
|
@@ -554,9 +617,9 @@ class SceneFileWriter:
|
|
|
554
617
|
self,
|
|
555
618
|
input_files: list[str],
|
|
556
619
|
output_file: Path,
|
|
557
|
-
create_gif=False,
|
|
558
|
-
includes_sound=False,
|
|
559
|
-
):
|
|
620
|
+
create_gif: bool = False,
|
|
621
|
+
includes_sound: bool = False,
|
|
622
|
+
) -> None:
|
|
560
623
|
file_list = self.partial_movie_directory / "partial_movie_file_list.txt"
|
|
561
624
|
logger.debug(
|
|
562
625
|
f"Partial movie files to combine ({len(input_files)} files): %(p)s",
|
|
@@ -567,39 +630,94 @@ class SceneFileWriter:
|
|
|
567
630
|
for pf_path in input_files:
|
|
568
631
|
pf_path = Path(pf_path).as_posix()
|
|
569
632
|
fp.write(f"file 'file:{pf_path}'\n")
|
|
570
|
-
commands = [
|
|
571
|
-
config.ffmpeg_executable,
|
|
572
|
-
"-y", # overwrite output file if it exists
|
|
573
|
-
"-f",
|
|
574
|
-
"concat",
|
|
575
|
-
"-safe",
|
|
576
|
-
"0",
|
|
577
|
-
"-i",
|
|
578
|
-
str(file_list),
|
|
579
|
-
"-loglevel",
|
|
580
|
-
config.ffmpeg_loglevel.lower(),
|
|
581
|
-
"-metadata",
|
|
582
|
-
f"comment=Rendered with Manim Community v{__version__}",
|
|
583
|
-
"-nostdin",
|
|
584
|
-
]
|
|
585
633
|
|
|
634
|
+
av_options = {
|
|
635
|
+
"safe": "0", # needed to read files
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if not includes_sound:
|
|
639
|
+
av_options["an"] = "1"
|
|
640
|
+
|
|
641
|
+
partial_movies_input = av.open(
|
|
642
|
+
str(file_list), options=av_options, format="concat"
|
|
643
|
+
)
|
|
644
|
+
partial_movies_stream = partial_movies_input.streams.video[0]
|
|
645
|
+
output_container = av.open(str(output_file), mode="w")
|
|
646
|
+
output_container.metadata["comment"] = (
|
|
647
|
+
f"Rendered with Manim Community v{__version__}"
|
|
648
|
+
)
|
|
649
|
+
output_stream = output_container.add_stream(
|
|
650
|
+
codec_name="gif" if create_gif else None,
|
|
651
|
+
template=partial_movies_stream if not create_gif else None,
|
|
652
|
+
)
|
|
653
|
+
if config.transparent and config.movie_file_extension == ".webm":
|
|
654
|
+
output_stream.pix_fmt = "yuva420p"
|
|
586
655
|
if create_gif:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
656
|
+
"""The following solution was largely inspired from this comment
|
|
657
|
+
https://github.com/imageio/imageio/issues/995#issuecomment-1580533018,
|
|
658
|
+
and the following code
|
|
659
|
+
https://github.com/imageio/imageio/blob/65d79140018bb7c64c0692ea72cb4093e8d632a0/imageio/plugins/pyav.py#L927-L996.
|
|
660
|
+
"""
|
|
661
|
+
output_stream.pix_fmt = "rgb8"
|
|
662
|
+
if config.transparent:
|
|
663
|
+
output_stream.pix_fmt = "pal8"
|
|
664
|
+
output_stream.width = config.pixel_width
|
|
665
|
+
output_stream.height = config.pixel_height
|
|
666
|
+
output_stream.rate = to_av_frame_rate(config.frame_rate)
|
|
667
|
+
graph = av.filter.Graph()
|
|
668
|
+
input_buffer = graph.add_buffer(template=partial_movies_stream)
|
|
669
|
+
split = graph.add("split")
|
|
670
|
+
palettegen = graph.add("palettegen", "stats_mode=diff")
|
|
671
|
+
paletteuse = graph.add(
|
|
672
|
+
"paletteuse", "dither=bayer:bayer_scale=5:diff_mode=rectangle"
|
|
673
|
+
)
|
|
674
|
+
output_sink = graph.add("buffersink")
|
|
675
|
+
|
|
676
|
+
input_buffer.link_to(split)
|
|
677
|
+
split.link_to(palettegen, 0, 0) # 1st input of split -> input of palettegen
|
|
678
|
+
split.link_to(paletteuse, 1, 0) # 2nd output of split -> 1st input
|
|
679
|
+
palettegen.link_to(paletteuse, 0, 1) # output of palettegen -> 2nd input
|
|
680
|
+
paletteuse.link_to(output_sink)
|
|
681
|
+
|
|
682
|
+
graph.configure()
|
|
683
|
+
|
|
684
|
+
for frame in partial_movies_input.decode(video=0):
|
|
685
|
+
graph.push(frame)
|
|
686
|
+
|
|
687
|
+
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
|
688
|
+
|
|
689
|
+
frames_written = 0
|
|
690
|
+
while True:
|
|
691
|
+
try:
|
|
692
|
+
frame = graph.pull()
|
|
693
|
+
if output_stream.codec_context.time_base is not None:
|
|
694
|
+
frame.time_base = output_stream.codec_context.time_base
|
|
695
|
+
frame.pts = frames_written
|
|
696
|
+
frames_written += 1
|
|
697
|
+
output_container.mux(output_stream.encode(frame))
|
|
698
|
+
except av.error.EOFError:
|
|
699
|
+
break
|
|
700
|
+
|
|
701
|
+
for packet in output_stream.encode():
|
|
702
|
+
output_container.mux(packet)
|
|
703
|
+
|
|
591
704
|
else:
|
|
592
|
-
|
|
705
|
+
for packet in partial_movies_input.demux(partial_movies_stream):
|
|
706
|
+
# We need to skip the "flushing" packets that `demux` generates.
|
|
707
|
+
if packet.dts is None:
|
|
708
|
+
continue
|
|
593
709
|
|
|
594
|
-
|
|
595
|
-
|
|
710
|
+
packet.dts = None # This seems to be needed, as dts from consecutive
|
|
711
|
+
# files may not be monotically increasing, so we let libav compute it.
|
|
596
712
|
|
|
597
|
-
|
|
713
|
+
# We need to assign the packet to the new stream.
|
|
714
|
+
packet.stream = output_stream
|
|
715
|
+
output_container.mux(packet)
|
|
598
716
|
|
|
599
|
-
|
|
600
|
-
|
|
717
|
+
partial_movies_input.close()
|
|
718
|
+
output_container.close()
|
|
601
719
|
|
|
602
|
-
def combine_to_movie(self):
|
|
720
|
+
def combine_to_movie(self) -> None:
|
|
603
721
|
"""Used internally by Manim to combine the separate
|
|
604
722
|
partial movie files that make up a Scene into a single
|
|
605
723
|
video file for that Scene.
|
|
@@ -614,6 +732,11 @@ class SceneFileWriter:
|
|
|
614
732
|
movie_file_path = self.movie_file_path
|
|
615
733
|
if is_gif_format():
|
|
616
734
|
movie_file_path = self.gif_file_path
|
|
735
|
+
|
|
736
|
+
if len(partial_movie_files) == 0: # Prevent calling concat on empty list
|
|
737
|
+
logger.info("No animations are contained in this scene.")
|
|
738
|
+
return
|
|
739
|
+
|
|
617
740
|
logger.info("Combining to Movie file.")
|
|
618
741
|
self.combine_files(
|
|
619
742
|
partial_movie_files,
|
|
@@ -623,44 +746,72 @@ class SceneFileWriter:
|
|
|
623
746
|
)
|
|
624
747
|
|
|
625
748
|
# handle sound
|
|
626
|
-
if self.includes_sound:
|
|
749
|
+
if self.includes_sound and config.format != "gif":
|
|
627
750
|
sound_file_path = movie_file_path.with_suffix(".wav")
|
|
628
751
|
# Makes sure sound file length will match video file
|
|
629
752
|
self.add_audio_segment(AudioSegment.silent(0))
|
|
630
753
|
self.audio_segment.export(
|
|
631
754
|
sound_file_path,
|
|
755
|
+
format="wav",
|
|
632
756
|
bitrate="312k",
|
|
633
757
|
)
|
|
758
|
+
# Audio added to a VP9 encoded (webm) video file needs
|
|
759
|
+
# to be encoded as vorbis or opus. Directly exporting
|
|
760
|
+
# self.audio_segment with such a codec works in principle,
|
|
761
|
+
# but tries to call ffmpeg via its CLI -- which we want
|
|
762
|
+
# to avoid. This is why we need to do the conversion
|
|
763
|
+
# manually.
|
|
764
|
+
if config.movie_file_extension == ".webm":
|
|
765
|
+
ogg_sound_file_path = sound_file_path.with_suffix(".ogg")
|
|
766
|
+
convert_audio(sound_file_path, ogg_sound_file_path, "libvorbis")
|
|
767
|
+
sound_file_path = ogg_sound_file_path
|
|
768
|
+
elif config.movie_file_extension == ".mp4":
|
|
769
|
+
# Similarly, pyav may reject wav audio in an .mp4 file;
|
|
770
|
+
# convert to AAC.
|
|
771
|
+
aac_sound_file_path = sound_file_path.with_suffix(".aac")
|
|
772
|
+
convert_audio(sound_file_path, aac_sound_file_path, "aac")
|
|
773
|
+
sound_file_path = aac_sound_file_path
|
|
774
|
+
|
|
634
775
|
temp_file_path = movie_file_path.with_name(
|
|
635
776
|
f"{movie_file_path.stem}_temp{movie_file_path.suffix}"
|
|
636
777
|
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
"
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
778
|
+
av_options = {
|
|
779
|
+
"shortest": "1",
|
|
780
|
+
"metadata": f"comment=Rendered with Manim Community v{__version__}",
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
with (
|
|
784
|
+
av.open(movie_file_path) as video_input,
|
|
785
|
+
av.open(sound_file_path) as audio_input,
|
|
786
|
+
):
|
|
787
|
+
video_stream = video_input.streams.video[0]
|
|
788
|
+
audio_stream = audio_input.streams.audio[0]
|
|
789
|
+
output_container = av.open(
|
|
790
|
+
str(temp_file_path), mode="w", options=av_options
|
|
791
|
+
)
|
|
792
|
+
output_video_stream = output_container.add_stream(template=video_stream)
|
|
793
|
+
output_audio_stream = output_container.add_stream(template=audio_stream)
|
|
794
|
+
|
|
795
|
+
for packet in video_input.demux(video_stream):
|
|
796
|
+
# We need to skip the "flushing" packets that `demux` generates.
|
|
797
|
+
if packet.dts is None:
|
|
798
|
+
continue
|
|
799
|
+
|
|
800
|
+
# We need to assign the packet to the new stream.
|
|
801
|
+
packet.stream = output_video_stream
|
|
802
|
+
output_container.mux(packet)
|
|
803
|
+
|
|
804
|
+
for packet in audio_input.demux(audio_stream):
|
|
805
|
+
# We need to skip the "flushing" packets that `demux` generates.
|
|
806
|
+
if packet.dts is None:
|
|
807
|
+
continue
|
|
808
|
+
|
|
809
|
+
# We need to assign the packet to the new stream.
|
|
810
|
+
packet.stream = output_audio_stream
|
|
811
|
+
output_container.mux(packet)
|
|
812
|
+
|
|
813
|
+
output_container.close()
|
|
814
|
+
|
|
664
815
|
shutil.move(str(temp_file_path), str(movie_file_path))
|
|
665
816
|
sound_file_path.unlink()
|
|
666
817
|
|
|
@@ -672,7 +823,6 @@ class SceneFileWriter:
|
|
|
672
823
|
|
|
673
824
|
def combine_to_section_videos(self) -> None:
|
|
674
825
|
"""Concatenate partial movie files for each section."""
|
|
675
|
-
|
|
676
826
|
self.finish_last_section()
|
|
677
827
|
sections_index: list[dict[str, Any]] = []
|
|
678
828
|
for section in self.sections:
|
|
@@ -687,7 +837,7 @@ class SceneFileWriter:
|
|
|
687
837
|
with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file:
|
|
688
838
|
json.dump(sections_index, file, indent=4)
|
|
689
839
|
|
|
690
|
-
def clean_cache(self):
|
|
840
|
+
def clean_cache(self) -> None:
|
|
691
841
|
"""Will clean the cache by removing the oldest partial_movie_files."""
|
|
692
842
|
cached_partial_movies = [
|
|
693
843
|
(self.partial_movie_directory / file_name)
|
|
@@ -700,9 +850,8 @@ class SceneFileWriter:
|
|
|
700
850
|
)
|
|
701
851
|
oldest_files_to_delete = sorted(
|
|
702
852
|
cached_partial_movies,
|
|
703
|
-
key=
|
|
853
|
+
key=lambda path: path.stat().st_atime,
|
|
704
854
|
)[:number_files_to_delete]
|
|
705
|
-
# oldest_file_path = min(cached_partial_movies, key=os.path.getatime)
|
|
706
855
|
for file_to_delete in oldest_files_to_delete:
|
|
707
856
|
file_to_delete.unlink()
|
|
708
857
|
logger.info(
|
|
@@ -710,7 +859,7 @@ class SceneFileWriter:
|
|
|
710
859
|
" You can change this behaviour by changing max_files_cached in config.",
|
|
711
860
|
)
|
|
712
861
|
|
|
713
|
-
def flush_cache_directory(self):
|
|
862
|
+
def flush_cache_directory(self) -> None:
|
|
714
863
|
"""Delete all the cached partial movie files"""
|
|
715
864
|
cached_partial_movies = [
|
|
716
865
|
self.partial_movie_directory / file_name
|
|
@@ -724,13 +873,15 @@ class SceneFileWriter:
|
|
|
724
873
|
{"par_dir": self.partial_movie_directory},
|
|
725
874
|
)
|
|
726
875
|
|
|
727
|
-
def write_subcaption_file(self):
|
|
876
|
+
def write_subcaption_file(self) -> None:
|
|
728
877
|
"""Writes the subcaption file."""
|
|
878
|
+
if config.output_file is None:
|
|
879
|
+
return
|
|
729
880
|
subcaption_file = Path(config.output_file).with_suffix(".srt")
|
|
730
|
-
subcaption_file.write_text(srt.compose(self.subcaptions))
|
|
881
|
+
subcaption_file.write_text(srt.compose(self.subcaptions), encoding="utf-8")
|
|
731
882
|
logger.info(f"Subcaption file has been written as {subcaption_file}")
|
|
732
883
|
|
|
733
|
-
def print_file_ready_message(self, file_path):
|
|
884
|
+
def print_file_ready_message(self, file_path: StrPath) -> None:
|
|
734
885
|
"""Prints the "File Ready" message to STDOUT."""
|
|
735
886
|
config["output_file"] = file_path
|
|
736
887
|
logger.info("\nFile ready at %(file_path)s\n", {"file_path": f"'{file_path}'"})
|