mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
* Allow python 3.13
* Specify scipy more strictly
* try using 3rd-party audioop package
* debug: wrap context generation in try-block
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* remove audioop-lts, update dependencies
* add wrapt pre-release (wheels!)
* update lockfile
* CI, windows: try installing all tinytex packages at once
* tmp: verbosity of pytest
* tmp: pass -n 0 for testing
* tmp: disable pytest-xdist
* remove deprecated poetry installer option
* try running tests via uv-provided env
* tmp: run -> run --no-project
* try --full-trace
* try running tests via python -v ...
* tmp: test import of suspicious modules
* test click/cloup
* ci: back to poetry
* ci: disable pytest plugins altogether
* update lockfile
* ci: upgrade pytest
* remove all flags from pytest call
* ci: move conftest, temporarily disable test header
* fix test fixture
* try with more recent version of pytest-xdist
* loadfile -> loadscope
* explicitly test pytest for non-manim module
* some proper tests
* only keep fixtures in new conftest.py
* remove all non-necessary fixtures
* try a literal try/except in fixture file
* simplify tests further
* try and explicitly trigger non-silent errors
* Revert "try and explicitly trigger non-silent errors"
This reverts commit 35b862d483.
* more pytest debug output
* test whether manim can be imported correctly
* test with upgraded numpy
* revert all sorts of debug changes, attempt clean pytest run
* fix whitespace
* pre-commit hooks
* apply config fixture to render tests
* let config fixture ensure renderer is set to cairo
* update doctests for numpy>=2.0
* missed some
* upgrade required numpy version, revert separate treatment of numpy in ci
* upgraded moderngl-window to latest
* more delicate versioning of numpy dependency
---------
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
187 lines
5.7 KiB
Python
187 lines
5.7 KiB
Python
import sys
|
|
from fractions import Fraction
|
|
from pathlib import Path
|
|
|
|
import av
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from manim import DR, Circle, Create, Scene, Star, tempconfig
|
|
from manim.scene.scene_file_writer import to_av_frame_rate
|
|
from manim.utils.commands import capture, get_video_metadata
|
|
|
|
|
|
class StarScene(Scene):
|
|
def construct(self):
|
|
circle = Circle(fill_opacity=1, color="#ff0000")
|
|
circle.to_corner(DR).shift(DR)
|
|
self.add(circle)
|
|
star = Star()
|
|
self.play(Create(star))
|
|
click_path = (
|
|
Path(__file__).parent.parent.parent
|
|
/ "docs"
|
|
/ "source"
|
|
/ "_static"
|
|
/ "click.wav"
|
|
)
|
|
self.add_sound(click_path)
|
|
self.wait()
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.parametrize(
|
|
"transparent",
|
|
[False, True],
|
|
)
|
|
def test_gif_writing(config, tmp_path, transparent):
|
|
output_filename = f"gif_{'transparent' if transparent else 'opaque'}"
|
|
with tempconfig(
|
|
{
|
|
"media_dir": tmp_path,
|
|
"quality": "low_quality",
|
|
"format": "gif",
|
|
"transparent": transparent,
|
|
"output_file": output_filename,
|
|
}
|
|
):
|
|
StarScene().render()
|
|
|
|
video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.gif"
|
|
assert video_path.exists()
|
|
metadata = get_video_metadata(video_path)
|
|
# reported duration + avg_frame_rate is slightly off for gifs
|
|
del metadata["duration"], metadata["avg_frame_rate"]
|
|
target_metadata = {
|
|
"width": 854,
|
|
"height": 480,
|
|
"nb_frames": "30",
|
|
"codec_name": "gif",
|
|
"pix_fmt": "bgra",
|
|
}
|
|
assert metadata == target_metadata
|
|
|
|
with av.open(video_path) as container:
|
|
first_frame = next(container.decode(video=0))
|
|
frame_format = "argb" if transparent else "rgb24"
|
|
first_frame = first_frame.to_ndarray(format=frame_format)
|
|
|
|
target_rgba_corner = (
|
|
np.array([0, 255, 255, 255], dtype=np.uint8)
|
|
if transparent
|
|
else np.array([0, 0, 0], dtype=np.uint8)
|
|
)
|
|
np.testing.assert_array_equal(first_frame[0, 0], target_rgba_corner)
|
|
|
|
target_rgba_center = (
|
|
np.array([255, 255, 0, 0]) # components (A, R, G, B)
|
|
if transparent
|
|
else np.array([255, 0, 0], dtype=np.uint8)
|
|
)
|
|
np.testing.assert_allclose(first_frame[-1, -1], target_rgba_center, atol=5)
|
|
|
|
|
|
@pytest.mark.slow
|
|
@pytest.mark.parametrize(
|
|
("format", "transparent", "codec", "pixel_format"),
|
|
[
|
|
("mp4", False, "h264", "yuv420p"),
|
|
("mov", False, "h264", "yuv420p"),
|
|
("mov", True, "qtrle", "argb"),
|
|
("webm", False, "vp9", "yuv420p"),
|
|
("webm", True, "vp9", "yuv420p"),
|
|
],
|
|
)
|
|
def test_codecs(config, tmp_path, format, transparent, codec, pixel_format):
|
|
output_filename = f"codec_{format}_{'transparent' if transparent else 'opaque'}"
|
|
with tempconfig(
|
|
{
|
|
"media_dir": tmp_path,
|
|
"quality": "low_quality",
|
|
"format": format,
|
|
"transparent": transparent,
|
|
"output_file": output_filename,
|
|
}
|
|
):
|
|
StarScene().render()
|
|
|
|
video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.{format}"
|
|
assert video_path.exists()
|
|
metadata = get_video_metadata(video_path)
|
|
target_metadata = {
|
|
"width": 854,
|
|
"height": 480,
|
|
"nb_frames": "30",
|
|
"duration": "2.000000",
|
|
"avg_frame_rate": "15/1",
|
|
"codec_name": codec,
|
|
"pix_fmt": pixel_format,
|
|
}
|
|
assert metadata == target_metadata
|
|
|
|
with av.open(video_path) as container:
|
|
if transparent and format == "webm":
|
|
from av.codec.context import CodecContext
|
|
|
|
context = CodecContext.create("libvpx-vp9", "r")
|
|
packet = next(container.demux(video=0))
|
|
first_frame = context.decode(packet)[0].to_ndarray(format="argb")
|
|
else:
|
|
first_frame = next(container.decode(video=0)).to_ndarray()
|
|
|
|
has_samples = [
|
|
np.any(frame.to_ndarray()) for frame in container.decode(audio=0)
|
|
]
|
|
assert any(has_samples), "All audio samples are zero, this is not intended"
|
|
|
|
target_rgba_corner = (
|
|
np.array([0, 0, 0, 0]) if transparent else np.array(16, dtype=np.uint8)
|
|
)
|
|
np.testing.assert_array_equal(first_frame[0, 0], target_rgba_corner)
|
|
|
|
target_rgba_center = (
|
|
np.array([255, 255, 0, 0]) # components (A, R, G, B)
|
|
if transparent
|
|
else np.array(240, dtype=np.uint8)
|
|
)
|
|
np.testing.assert_allclose(first_frame[-1, -1], target_rgba_center, atol=5)
|
|
|
|
|
|
def test_scene_with_non_raw_or_wav_audio(config, manim_caplog):
|
|
class SceneWithMP3(Scene):
|
|
def construct(self):
|
|
file_path = Path(__file__).parent / "click.mp3"
|
|
self.add_sound(file_path)
|
|
self.wait()
|
|
|
|
SceneWithMP3().render()
|
|
assert "click.mp3 to .wav" in manim_caplog.text
|
|
|
|
|
|
@pytest.mark.slow
|
|
def test_unicode_partial_movie(config, tmpdir, simple_scenes_path):
|
|
# Characters that failed for a user on Windows
|
|
# due to its weird default encoding.
|
|
unicode_str = "三角函数"
|
|
|
|
scene_name = "SquareToCircle"
|
|
command = [
|
|
sys.executable,
|
|
"-m",
|
|
"manim",
|
|
"--media_dir",
|
|
str(tmpdir / unicode_str),
|
|
str(simple_scenes_path),
|
|
scene_name,
|
|
]
|
|
|
|
_, err, exit_code = capture(command)
|
|
assert exit_code == 0, err
|
|
|
|
|
|
def test_frame_rates():
|
|
assert to_av_frame_rate(25) == Fraction(25, 1)
|
|
assert to_av_frame_rate(24.0) == Fraction(24, 1)
|
|
assert to_av_frame_rate(23.976) == Fraction(24 * 1000, 1001)
|
|
assert to_av_frame_rate(23.98) == Fraction(24 * 1000, 1001)
|
|
assert to_av_frame_rate(59.94) == Fraction(60 * 1000, 1001)
|