manim-slides icon indicating copy to clipboard operation
manim-slides copied to clipboard

[BUG] `[Errno 22] Invalid argument` when rendering a large number of animations

Open hcq11419 opened this issue 7 months ago • 14 comments

Terms

Describe the issue

I find that manim-slides is very useful when rendering a small number of animations. However, it will report an error once the number of animations is relatively large. For example, there are many animations in this piece of my code. Manim can render it into a video normally, but an error occurs when using manim-slides render. Who can help me? I really want to solve this problem!

Command

manim-slides render Golden_section_point.py

Issue Type

Visual bug when presenting (manim-slides present)

Python version

Python 3.11

Python environment

Manim bindings:
        manim (version: 0.19.0)
        manimgl not found
Qt API: pyside6 (version: 6.8.2)

What is your platform?

Windows

Other platform

No response

Manim Slides Python code

import numpy as np
from manim import *
from compass_and_ruler_scene_CE import *
from manim_slides import Slide
from ercihanshu_utils import *
import manimforge as mf

mf.setup()

config["max_files_cached"] = 5000


class Golden_section_point(CompassAndRulerScene, Slide):
    Dot_config = {
        "radius": 0.04,
        "color": WHITE,
    }

    CONFIG = {
        "compass_config": {
            "leg_length": 3,
            "stroke_color": RED,  # 圆规腿颜色
            "fill_color": WHITE,  # 圆盘颜色
        },
        "arc_config": {
            "stroke_color": YELLOW_B,
            "stroke_width": 2.5,
        },
        "add_ruler": True,  # add ruler when the scene starts
    }

    def setup(self):
        # 先调用父类的setup方法来初始化ruler等属性
        super().setup()
        # 然后设置TeX模板
        # MathTex.set_default(tex_template=TexTemplateLibrary.ctex)

    def construct(self):

        rate_ease_out_back = {"rate_func": rate_functions.ease_out_back}
        d1, d2 = LEFT * 2, RIGHT * 2
        dA = Dot(LEFT * 2, **self.Dot_config)
        dB = Dot(RIGHT * 2, **self.Dot_config)
        lAB = Line(dA.get_center(), dB.get_center())
        lb_dA, lb_dB = Tex("A").next_to(dA, DOWN, buff=0.1), Tex("B").next_to(
            dB, DOWN, buff=0.1
        )

        self.add(dA, dB, lAB, lb_dA, lb_dB)
        self.bring_to_back(lAB)
        # 延长AB
        l1 = DashedLine(
            dB.get_center(), RIGHT * 5, dash_length=0.1, **self.CONFIG["arc_config"]
        )
        self.set_ruler(dB.get_center(), RIGHT * 5)
        self.wait(0.1)
        self.draw_line_(l1, run_time=0.8)
        self.wait(0.1)
        self.put_aside_ruler(DOWN * 0.7)
        p1, p2 = RIGHT * 0.5, RIGHT * 3.5

        self.set_compass(dB.get_center(), RIGHT * 2.5)
        self.wait(0.1)
        self.set_compass(dB.get_center(), p2)
        self.wait(0.1)
        arc_r = Arc(
            arc_center=dB.get_center(),
            start_angle=-15 * DEGREES,
            angle=30 * DEGREES,
            radius=1.5,
            **self.CONFIG["arc_config"]
        )
        arc_l = Arc(
            arc_center=dB.get_center(),
            start_angle=165 * DEGREES,
            angle=30 * DEGREES,
            radius=1.5,
            **self.CONFIG["arc_config"]
        )
        self.set_compass_to_draw_arc_(arc_r, adjust_angle=-PI, run_time=0.6)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_r, run_time=0.5)
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_l, adjust_angle=PI, run_time=0.6)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_l, run_time=0.5)
        self.next_slide()

        p3 = dB.get_center() + UP * 1.5 * np.sqrt(3)
        p4 = dB.get_center() + DOWN * 1.5 * np.sqrt(3)
        self.set_compass(p1, RIGHT * 1.5)
        self.wait(0.2)
        self.set_compass(p1, p2)
        self.wait(0.2)

        arc_ru = Arc(
            arc_center=p1,
            start_angle=45 * DEGREES,
            angle=30 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )

        arc_lu = Arc(
            arc_center=p2,
            start_angle=105 * DEGREES,
            angle=30 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )

        self.set_compass_to_draw_arc_(arc_ru, adjust_angle=PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_ru, run_time=0.5)
        self.wait(0.1)

        self.set_compass_to_draw_arc_(arc_lu, adjust_angle=PI, run_time=0.4)
        self.wait(0.2)
        self.draw_arc_by_compass(arc_lu, run_time=0.5)
        self.wait(0.1)
        self.put_aside_compass(DR * 0.6)
        self.wait(0.2)
        # 画垂线
        l2 = Line(p3 + UP, d2)
        self.set_ruler(p3, p4)
        self.wait(0.1)
        self.emphasize_dot([p3, dB.get_center()], run_time=0.25)
        self.wait(0.1)
        self.draw_line_(l2)
        self.wait(0.1)
        self.put_aside_ruler(DOWN * 0.7)
        self.wait(0.1)
        self.highlight_on(l2)
        angle_90 = Square(0.22, stroke_width=2, stroke_color=GREY_C).next_to(
            dB.get_center(), UL, buff=0
        )
        self.bring_to_back(angle_90)

        self.play(FadeIn(angle_90), run_time=0.64)
        self.next_slide()
        # 找AB中点
        p5 = lAB.get_center() + UP * np.sqrt(5)
        p6 = lAB.get_center() + DOWN * np.sqrt(5)

        self.set_compass(dA.get_center(), dA.get_center() + RIGHT, run_time=0.75)
        self.wait(0.1)
        arc_r2 = Arc(
            arc_center=dA.get_center(),
            start_angle=-70 * DEGREES,
            angle=140 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )
        arc_l2 = Arc(
            arc_center=dB.get_center(),
            start_angle=110 * DEGREES,
            angle=140 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )
        self.set_compass_to_draw_arc_(arc_r2, adjust_angle=-PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_r2)
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_l2, adjust_angle=PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_l2)
        self.wait(0.1)
        self.put_aside_compass(DOWN * 0.8)
        self.wait(0.1)
        self.emphasize_dot([p5, p6], run_time=0.25)
        self.set_ruler(p5, p6)
        self.wait(0.1)
        l3 = DashedLine(p5, p6, dash_length=0.1, **self.CONFIG["arc_config"]).scale(
            1.25
        )
        self.draw_line_(l3)
        self.wait(0.2)
        self.put_aside_ruler(DOWN * 0.8)
        center = Dot(l3.get_center(), **self.Dot_config)
        self.play(FadeIn(center))
        self.wait(1)

        arc_l3 = Arc(
            arc_center=dB.get_center(),
            start_angle=-180 * DEGREES,
            angle=-90 * DEGREES,
            radius=2,
            **self.CONFIG["arc_config"]
        )
        p7 = dB.get_center() + UP * 2
        self.set_compass(dB.get_center(), dB.get_center() + LEFT * 2, run_time=0.5)
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_l3, adjust_angle=PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_l3)
        self.wait(0.1)
        self.put_aside_compass(DOWN * 0.8)
        dC = Dot(p7, **self.Dot_config)
        lb_dC = Tex("C").next_to(dC, RIGHT, buff=0.1)
        self.play(
            LaggedStart(
                FadeIn(dC),
                FadeIn(lb_dC, shift=RIGHT * 0.5, scale=0.1, **rate_ease_out_back),
                lag_ratio=0.6,
            )
        )
        self.next_slide()
        line_AC = Line(d1, p7, **self.CONFIG["arc_config"])
        self.emphasize_dot([d1, p7], run_time=0.25)
        self.set_ruler(d1, p7)
        self.wait(0.1)
        self.draw_line_(line_AC)
        self.wait(0.2)
        self.put_aside_ruler(DOWN * 0.8)
        self.next_slide()
        l4 = Line(d2, p7, **self.CONFIG["arc_config"])
        self.add(l4)
        out = Group(*self.mobjects).remove(
            dA, dB, dC, lAB, line_AC, lb_dA, lb_dB, lb_dC, l4, dC, angle_90
        )
        self.play(FadeOut(out))
        # out=VGroup(l1,arc_l,arc_l2,arc_l3,arc_lu,arc_r,arc_r2,arc_ru)
        self.next_slide()

        arc_4 = Arc(
            arc_center=p7,
            start_angle=-90 * DEGREES,
            angle=-np.arctan(2) - 10 * DEGREES,
            radius=2,
            stroke_color=YELLOW_B,
            stroke_width=2.5,
        )
        arc_5 = Arc(
            arc_center=d1,
            start_angle=np.arctan(1 / 2),
            angle=-np.arctan(1 / 2) - 10 * DEGREES,
            radius=2 * np.sqrt(5) - 2,
            stroke_color=YELLOW_B,
            stroke_width=2.5,
        )
        dD = Dot(line_AC.point_from_proportion(1 - np.sqrt(5) / 5), **self.Dot_config)
        lb_dD = Tex("D").next_to(dD, UL, buff=0.1)
        dE = Dot(lAB.point_from_proportion((np.sqrt(5) - 1) / 2), **self.Dot_config)
        lb_dE = Tex("E").next_to(dE, DR, buff=0.1)
        super().setup()  # 圆规初始化

        self.set_compass(p7, d2 + UP)
        self.set_compass_to_draw_arc_(arc_4, adjust_angle=-PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_4)
        self.wait(0.1)
        self.put_aside_compass(DOWN * 0.8)
        self.wait(0.1)
        self.emphasize_dot(dD.get_center())
        self.wait(0.1)
        self.play(
            LaggedStart(
                FadeIn(dD),
                FadeIn(lb_dD, shift=UL * 0.5, scale=0.1, **rate_ease_out_back),
                lag_ratio=0.6,
                run_time=1,
            )
        )
        self.next_slide()
        self.set_compass(d1, line_AC.point_from_proportion(0.3))
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_5, adjust_angle=PI, run_time=0.5)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_5, run_time=0.5)
        self.next_slide()
        self.put_aside_compass(DOWN)
        self.next_slide()
        self.emphasize_dot(dE.get_center())
        self.play(
            LaggedStart(
                FadeIn(dE),
                FadeIn(lb_dE, shift=DOWN * 0.5, scale=0.1, **rate_ease_out_back),
                lag_ratio=0.6,
                run_time=1,
            )
        )
        self.highlight_on([dE, lb_dE])
        self.wait()


if __name__ == "__main__":
    from os import system

    system("manim-slides render  {}    ".format(__file__))

Relevant log output

INFO     Animation 763 : Using cached data (hash : 2852726489_2655088185_1908071073)      cairo_renderer.py:89
                    INFO     Animation 764 : Using cached data (hash : 2852726489_3052040169_409836217)       cairo_renderer.py:89
                    INFO     Animation 765 : Using cached data (hash : 2852726489_1703042791_1696893326)      cairo_renderer.py:89
                    INFO     Animation 766 : Using cached data (hash : 2852726489_2143140768_2214148004)      cairo_renderer.py:89
                    INFO     Animation 767 : Using cached data (hash : 2852726489_146554378_2349365127)       cairo_renderer.py:89
                    INFO     Animation 768 : Using cached data (hash : 2852726489_2789980886_757578521)       cairo_renderer.py:89
                    INFO     Animation 769 : Using cached data (hash : 2852726489_2198369401_3849849265)      cairo_renderer.py:89
                    INFO     Animation 770 : Using cached data (hash : 2852726489_2834709167_2430403131)      cairo_renderer.py:89
                    INFO     Combining to Movie file.                                                     scene_file_writer.py:739
[04/03/25 10:11:43] INFO                                                                                  scene_file_writer.py:886
                             File ready at
                             'C:\Users\Administrator\Desktop\manimdemo\media\videos\3gold_point\1080p60\G 
                             olden_section_point.mp4'

                    INFO     Rendered Golden_section_point                                                            scene.py:262
                             Played 771 animations
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮                              
│ C:\ProgramData\miniconda3\envs\manim-slides-CE\Lib\site-packages\manim\cli\render\commands.py:12 │
│ 5 in render                                                                                      │
│                                                                                                  │
│   122 │   │   │   try:                                                                           │
│   123 │   │   │   │   with tempconfig({}):                                                       │
│   124 │   │   │   │   │   scene = SceneClass()                                                   │
│ ❱ 125 │   │   │   │   │   scene.render()                                                         │
│   126 │   │   │   except Exception:                                                              │
│   127 │   │   │   │   error_console.print_exception()                                            │
│   128 │   │   │   │   sys.exit(1)                                                                │
│                                                                                                  │
│ C:\ProgramData\miniconda3\envs\manim-slides-CE\Lib\site-packages\manim_slides\slide\manim.py:167 │
│ in render                                                                                        │
│                                                                                                  │
│   164 │   │                                                                                      │
│   165 │   │   config["max_files_cached"] = max_files_cached                                      │
│   166 │   │                                                                                      │
│ ❱ 167 │   │   self._save_slides(                                                                 │
│   168 │   │   │   use_cache=not (config["disable_caching"] or self.disable_caching),             │
│   169 │   │   │   flush_cache=(config["flush_cache"] or self.flush_cache),                       │
│   170 │   │   │   skip_reversing=self.skip_reversing,                                            │
│                                                                                                  │
│ C:\ProgramData\miniconda3\envs\manim-slides-CE\Lib\site-packages\manim_slides\slide\base.py:576  │
│ in _save_slides                                                                                  │
│                                                                                                  │
│   573 │   │   │                                                                                  │
│   574 │   │   │   # We only concat animations if it was not present                              │
│   575 │   │   │   if not use_cache or not dst_file.exists():                                     │
│ ❱ 576 │   │   │   │   concatenate_video_files(slide_files, dst_file)                             │
│   577 │   │   │                                                                                  │
│   578 │   │   │   # We only reverse video if it was not present                                  │
│   579 │   │   │   if not use_cache or not rev_file.exists():                                     │
│                                                                                                  │
│ C:\ProgramData\miniconda3\envs\manim-slides-CE\Lib\site-packages\manim_slides\utils.py:67 in     │
│ concatenate_video_files                                                                          │
│                                                                                                  │
│    64 │   │   │   │   packet.stream = output_audio_stream                                        │
│    65 │   │   │   else:                                                                          │
│    66 │   │   │   │   continue  # We don't support subtitles                                     │
│ ❱  67 │   │   │   output_container.mux(packet)                                                   │
│    68 │                                                                                          │
│    69 │   os.unlink(tmp_file)  # https://stackoverflow.com/a/54768241                            │
│    70                                                                                            │
│                                                                                                  │
│ in av.container.output.OutputContainer.mux:257                                                   │
│                                                                                                  │
│ in av.container.output.OutputContainer.mux_one:278                                               │
│                                                                                                  │
│ in av.container.core.Container.err_check:286                                                     │
│                                                                                                  │
│ in av.error.err_check:326                                                                        │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: [Errno 22] Invalid argument:
'slides\\files\\Golden_section_point\\e7fbccb2fb654008e5d649d0a3f4eea9874dfc3d2901efdd2691bb8d0f387b98.mp4'

Screenshots

Image

Additional information

No response

Recommended fix or suggestions

No response

hcq11419 avatar Apr 03 '25 03:04 hcq11419

Hi @hcq11419, thanks for reporting your bug! Could you please provide a fully working example? Here, I am missing other files if I want to test locally :)

jeertmans avatar Apr 03 '25 10:04 jeertmans

@jeertmans It has nothing to do with the code. I've tested many different versions. As long as there are a large number of animations, for example, more than a few hundred, this error will occur.

from manim import *

# utils
get_angle_ = lambda c: (
    np.angle(-c) + PI if not c / abs(c) == 1 else 0
)  # 利用np.angle求出复数a+bj与实轴直角的夹角,复数中c / abs(c) == 1的话意味着他的虚部为0
# c为复平面中,这里用-c+PI
# 可以调用my_fun中的的get_angle(a,b)代替。
convert_angle = lambda a: a if a >= 0 else a + TAU  # 将负角度转化为正角度


class Compass(VGroup):

    def __init__(
        self,
        stroke_color=RED,  # 圆规颜色
        fill_color=WHITE,
        stroke_width=2,
        leg_length=3,
        leg_width=0.12,
        r=0.2,
        depth_test=True,
        span=2.5,  # 针脚距离,半径
        **kwargs
    ):
        super().__init__()
        self.span = span
        self.leg_length = leg_length
        self.stroke_color = stroke_color
        self.fill_color = fill_color
        self.stroke_width = stroke_width
        self.leg_width = leg_width
        self.r = r
        self.depth_test = depth_test
        self.create_compass()

    def create_compass(self):
        s, l, r, w = self.span, self.leg_length, self.r, self.leg_width
        self.theta = np.arcsin(s / 2 / l)  # theta 为圆规腿与针脚连线的夹角

        self.c = Circle(
            radius=r,
            fill_color=self.fill_color,
            fill_opacity=1,
            stroke_color=self.stroke_color,
            stroke_width=self.stroke_width * 5,
        )
        c2 = Circle(
            radius=r + self.stroke_width * 5 / 100 / 2,
            fill_opacity=0,
            stroke_color=self.fill_color,
            stroke_width=self.stroke_width,
        )
        # leg_1是针尖腿,leg_2是笔尖腿
        self.leg_1 = Polygon(
            ORIGIN,
            l * RIGHT,
            (l - w * np.sqrt(3)) * RIGHT + w * DOWN,
            w * DOWN,
            stroke_width=0,
            stroke_color=self.fill_color,
            fill_color=self.stroke_color,
            fill_opacity=1,
        ).rotate(-PI / 2 - self.theta, about_point=self.c.get_center())
        self.leg_2 = Polygon(
            ORIGIN,
            l * RIGHT,
            (l - w * np.sqrt(3)) * RIGHT + w * UP,
            w * UP,
            stroke_width=0,
            stroke_color=self.fill_color,
            fill_color=self.stroke_color,
            fill_opacity=1,
        ).rotate(-PI / 2 + self.theta, about_point=self.c.get_center())

        # self.leg_1, self.leg_2 = VGroup(leg_01, leg_11),  VGroup(leg_02, leg_12, pen_point)
        h = Line(
            UP * r,
            UP * (r + r * 1.8),
            stroke_color=self.stroke_color,
            stroke_width=self.stroke_width * 6,
        )

        self.head = VGroup(h, self.c, c2)
        self.add(self.leg_1, self.leg_2, self.head)
        self.move_to(ORIGIN)
        return self

    def get_niddle_tip(self):  # 获取针脚
        return self.leg_1.get_vertices()[1]

    def get_pen_tip(self):  # 获取笔尖
        return self.leg_2.get_vertices()[1]

    def move_niddle_tip_to(self, pos):
        self.shift(pos - self.get_niddle_tip())
        return self

    def rotate_about_niddle_tip(self, angle=PI / 2):
        p = Dot(self.get_niddle_tip())  # manim bug?
        self.rotate(angle=angle, about_point=p.get_center())

    def get_span(self):
        # return self.span 如果进行了缩放而self.span没变会有问题
        return get_norm(self.get_pen_tip() - self.get_niddle_tip())

    def set_span(self, s):
        self.span = s
        l, r, w = self.leg_length, self.r, self.leg_width
        theta_new, theta_old = np.arcsin(s / 2 / l), self.theta
        sign = np.sign(
            get_angle_(
                R3_to_complex(
                    self.leg_2.get_vertices()[1] - self.leg_2.get_vertices()[0]
                )
            )
            - get_angle_(
                R3_to_complex(
                    self.leg_1.get_vertices()[1] - self.leg_1.get_vertices()[0]
                )
            )
        )
        # sign=np.sign(get_angle(self.leg_2.get_vertices()[0],self.leg_2.get_vertices()[1])-get_angle(
        #     self.leg_1.get_vertices()[0],self.leg_1.get_vertices()[1])) #调用my_fun

        rotate_angle = 2 * (theta_new - theta_old) * sign
        self.leg_2.rotate(rotate_angle, about_point=self.c.get_center())
        self.theta = theta_new
        self.head.rotate(rotate_angle / 2, about_point=self.c.get_center())
        self.rotate_about_niddle_tip(-rotate_angle / 2)
        return self

    def set_compass(self, center, pen_tip):
        self.move_niddle_tip_to(center)
        self.set_span(get_norm(pen_tip - center))
        self.rotate_about_niddle_tip(
            np.angle(R3_to_complex(pen_tip - center))
            - np.angle(R3_to_complex(self.get_pen_tip() - center))
        )
        return self

    def set_compass_to_draw_arc(self, arc):
        return self.set_compass(arc.arc_center, arc.get_start())

    def reverse_tip(self):
        return self.flip(
            axis=self.head[0].get_end() - self.head[0].get_start(),
            about_point=self.c.get_center(),
        )


class DrawingScene(Scene):

    def __init__(
        self,
        compass_config={
            "stroke_color": RED,  # 圆规颜色
            "fill_color": WHITE,
            "stroke_width": 2,
            "leg_length": 3,
            "leg_width": 0.12,
            "r": 0.2,
            "depth_test": True,
        },  # to define size and style of the compass
        ruler_config={
            "width": 10,
            "height": 0.8,
            "stroke_width": 8,
            "stroke_color": GREY_E,
            "stroke_opacity": 0.4,
            "fill_color": WHITE,
            "fill_opacity": 0.5,
        },  # to define size and style of the ruler
        dot_config={
            "radius": 0.06,
            "color": GREY_E,
        },  # to define size and color of the dot (e.g., dot in arc/circle's center)
        line_config={
            "stroke_color": YELLOW_E,
            "stroke_width": 2.5,
        },  # the default line style drawing by ruler (not the defualt arc style drawing by compass)
        brace_config={
            "fill_color": YELLOW_E,
            "buff": 0.025,
        },
        text_config={
            "font_size": 20,  # 5 times than the actual size here and
            # will be sacle down to the actual size later
            # in 'get_length_label' methods.
            # 'font': 'Cambria Math',
            "color": YELLOW_E,
        },
        add_ruler=True,
    ):
        super().__init__()
        self.compass_config = compass_config
        self.ruler_config = ruler_config
        self.dot_config = dot_config
        self.line_config = line_config
        self.brace_config = brace_config
        self.text_config = text_config
        self.add_ruler = add_ruler

    def setup(self):
        self.cp = Compass(**self.compass_config)
        self.ruler = VGroup(
            Rectangle(**self.ruler_config)
            .stretch_to_fit_height(
                self.ruler_config["height"]
                - self.ruler_config["stroke_width"] / 2 / 100
            )
            .round_corners(self.ruler_config["height"] / 8),
            Rectangle(**self.ruler_config).set_opacity(0),
        )
        self.dot = Dot(**self.dot_config)

        self.cp.move_to(UP * 10)
        if self.add_ruler:
            self.ruler.move_to(DOWN * 10)
            self.add(self.ruler)
        self.add(self.cp)

        self.temp_points = []

    def construct(self):

        self.add(self.cp)
        self.play(self.cp.animate.move_niddle_tip_to(ORIGIN), run_time=1)
        # self.wait(0.3)
        self.play(Wait(0.3))
        self.set_span(3.6, run_time=1, rate_func=smooth)
        # self.wait(0.5)
        self.play(Wait(0.5))

        self.set_compass(DL * 0.5, UR * 0.5, run_time=1, rate_func=there_and_back)
        arc = Arc(color=GREY_E)
        self.set_compass_to_draw_arc(arc)
        self.draw_arc_by_compass(arc)

        # self.wait()
        self.play(Wait(1))

    def set_span(self, s, run_time=1, rate_func=smooth):
        """设置针尖"""
        s_old = self.cp.get_span()
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        # s_series = s_old + rate_func(t_series) * (s - s_old)
        s_series = [s_old + rate_func(t_series[i]) * (s - s_old) for i in range(n)]
        for i in range(n):
            self.cp.set_span(s_series[i])
            self.play(Wait(dt))
            # self.wait(dt)

    def set_compass_direction(self, start, end, run_time=1, rate_func=smooth):
        vect = end - start
        a = np.angle(R3_to_complex(vect))
        c_old, p_old = self.cp.get_niddle_tip(), self.cp.get_pen_tip()
        a_old = np.angle(R3_to_complex(p_old - c_old))
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        c_series = [c_old + rate_func(t_series[i]) * (start - c_old) for i in range(n)]
        delta_a = (a - a_old) / n
        for i in range(n):
            self.bring_to_front(self.cp)
            self.cp.move_niddle_tip_to(c_series[i])
            self.cp.rotate_about_niddle_tip(delta_a)
            # self.wait(dt)
            self.play(Wait(dt))

    def set_compass(
        self, center, pen_tip, run_time=1, rate_func=smooth, emphasize_dot=False
    ):
        if emphasize_dot:
            run_time -= 0.15
        c_old, p_old = self.cp.get_niddle_tip(), self.cp.get_pen_tip()
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        # s_series = s_old + rate_func(t_series) * (s - s_old)
        c_series = [c_old + rate_func(t_series[i]) * (center - c_old) for i in range(n)]
        p_series = [
            p_old + rate_func(t_series[i]) * (pen_tip - p_old) for i in range(n)
        ]

        for i in range(n):
            self.bring_to_front(self.cp)
            self.cp.set_compass(c_series[i], p_series[i])
            # self.wait(dt)
            self.play(Wait(dt))
        if emphasize_dot:
            self.emphasize_dot([center, pen_tip], run_time=0.15)

    def set_compass_(
        self,
        center,
        pen_tip,
        adjust_angle=0,
        run_time=1,
        rate_func=smooth,
        emphasize_dot=False,
    ):

        vect = center - pen_tip
        a = np.angle(R3_to_complex(vect)) + adjust_angle
        s = get_norm(vect)
        c_old, p_old, s_old = (
            self.cp.get_niddle_tip(),
            self.cp.get_pen_tip(),
            self.cp.get_span(),
        )
        a_old = np.angle(R3_to_complex(p_old - c_old))
        if emphasize_dot:
            run_time -= 0.15
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        c_series = [c_old + rate_func(t_series[i]) * (center - c_old) for i in range(n)]
        delta_a = (a - a_old) / n
        s_series = [s_old + rate_func(t_series[i]) * (s - s_old) for i in range(n)]

        for i in range(n):
            self.bring_to_front(self.cp)
            self.cp.move_niddle_tip_to(c_series[i])
            self.cp.rotate_about_niddle_tip(delta_a)
            self.cp.set_span(s_series[i])
            # self.wait(dt)
            self.play(Wait(dt))
        if emphasize_dot:
            self.emphasize_dot([center, pen_tip], run_time=0.15)

    def set_compass_to_draw_arc(self, arc, **kwargs):
        self.set_compass(arc.arc_center, arc.get_start(), **kwargs)

    def set_compass_to_draw_arc_(self, arc, **kwargs):
        self.set_compass_(arc.arc_center, arc.get_start(), **kwargs)

    def draw_arc_by_compass(
        self,
        arc,
        is_prepared=True,
        run_time=1,
        rate_func=smooth,
        reverse=False,
        add_center=False,
        **kwargs
    ):
        self.bring_to_front(self.cp)
        if not is_prepared:
            self.set_compass_to_draw_arc(arc, run_time=0.5)
        theta = arc.angle if not reverse else -1 * arc.angle
        self.play(
            Rotating(self.cp, radians=theta, about_point=self.cp.get_niddle_tip()),
            Create(arc),
            rate_func=rate_func,
            run_time=run_time,
        )
        if add_center:
            d = Dot(self.cp.get_niddle_tip(), **self.dot_config).scale(0.5)
            self.temp_points.append(d)
            self.add(d)

    def emphasize_dot(self, pos, add_dot=False, size=1.2, run_time=0.2, **kwargs):
        if type(pos) == list:
            d = VGroup(
                *[
                    Dot(
                        pos[i], radius=size / 2, color=YELLOW_C, fill_opacity=0.25
                    ).scale(0.25)
                    for i in range(len(pos))
                ]
            )
        else:
            d = Dot(pos, radius=size / 2, color=YELLOW_C, fill_opacity=0.25).scale(0.25)
        self.add(d)
        if type(pos) == list:
            self.play(
                d[0].animate.scale(4),
                d[1].animate.scale(4),
                rate_func=linear,
                run_time=run_time,
            )

        else:
            self.play(d.animate.scale(4), rate_func=linear, run_time=run_time)

        self.remove(d)
        if add_dot:
            if type(pos) == list:
                dot = VGroup(*[Dot(pos[i], **kwargs) for i in range(len(pos))])
            else:
                dot = Dot(pos, **kwargs)
            self.add(dot)
            return dot

    def set_ruler(self, pos1, pos2, run_time=1, rate_func=smooth):
        p1, p2 = self.ruler[-1].get_vertices()[1], self.ruler[-1].get_vertices()[0]
        c12 = (p1 + p2) / 2
        center = (pos1 + pos2) / 2
        self.bring_to_front(self.ruler)
        self.play(
            self.ruler.animate.shift(center - c12),
            run_time=run_time / 2,
            rate_func=rate_func,
        )
        self.play(
            Rotating(
                self.ruler,
                radians=np.angle(R3_to_complex(pos2 - pos1))
                - np.angle(R3_to_complex(p2 - p1)),
                about_point=center,
            ),
            run_time=run_time / 2,
            rate_func=rate_func,
        )

    def draw_line(
        self, pos1, pos2, is_prepared=True, run_time=1.2, rate_func=smooth, pre_time=0.8
    ):
        if not is_prepared:
            self.set_ruler(pos1, pos2, run_time=pre_time)
        self.dot.move_to(pos1)
        self.emphasize_dot(pos1, run_time=0.15)
        self.add(self.dot)
        l = Line(pos1, pos2, **self.line_config)
        self.play(
            Create(l),
            self.dot.animate.move_to(pos2),
            run_time=run_time - 0.3,
            rate_func=rate_func,
        )
        self.emphasize_dot(pos2, run_time=0.15)
        self.remove(self.dot)
        return l

    def draw_line_(self, l, is_prepared=True, run_time=1.2, rate_func=smooth):
        pos1, pos2 = l.get_start(), l.get_end()
        if not is_prepared:
            self.set_ruler(pos1, pos2, run_time=0.5)
        self.dot.move_to(pos1)
        self.emphasize_dot(pos1, run_time=0.15)
        self.add(self.dot)
        # l = Line(pos1, pos2, **self.line_config)
        self.play(
            Create(l),
            self.dot.animate.move_to(pos2),
            run_time=run_time - 0.3,
            rate_func=rate_func,
        )
        self.emphasize_dot(pos2, run_time=0.15)
        self.remove(self.dot)
        return l

    def put_aside_ruler(self, direction=DOWN, run_time=0.5):
        self.bring_to_front(self.ruler)
        self.play(self.ruler.animate.move_to(direction * 15), run_time=run_time)

    def put_aside_compass(self, direction=DOWN, run_time=0.5):
        self.bring_to_front(self.cp)
        self.play(self.cp.animate.move_to(direction * 15), run_time=run_time)

    def get_length_label(
        self, p1, p2, text="", reverse_label=False, add_bg=False, bg_color=WHITE
    ):
        l = Line(p1, p2)
        b = Brace(
            l,
            direction=complex_to_R3(
                np.exp(1j * (l.get_angle() + PI / 2 * (1 - 2 * float(reverse_label))))
            ),
            **self.brace_config
        )
        t = b.get_text(text)
        # t = Text(text, **self.text_config).scale(0.2)
        if add_bg:
            bg = SurroundingRectangle(
                t, fill_color=bg_color, fill_opacity=0.6, stroke_opacity=0
            ).stretch_to_fit_height(t.get_height() + 0.05)
            b.put_at_tip(bg, buff=0.0)
            b.put_at_tip(t, buff=0.05)
            return b, bg, t
        else:
            b.put_at_tip(t, buff=0.05)
            return b, t

    def set_compass_and_show_span(
        self,
        p1,
        p2,
        run_time=1,
        show_span_time=[0.4, 0.3, 0.9, 0.4],
        text="",
        reverse_label=False,
        add_bg=False,
        **kwargs
    ):
        self.set_compass(p1, p2, run_time=run_time, **kwargs)
        bt = self.get_length_label(
            p1, p2, text=text, reverse_label=reverse_label, add_bg=add_bg
        )
        b, t = bt[0], bt[-1]
        st = show_span_time
        self.play(Create(b), run_time=st[0])
        if add_bg:
            self.add(bt[1])
            self.play(FadeIn(t), run_time=st[1])
        else:
            self.play(FadeIn(t), run_time=st[1])
        # self.wait(st[2])
        self.play(Wait(st[2]))

        self.play(FadeOut(VGroup(*bt)), run_time=st[3])
        return bt

    def set_compass_and_show_span_(
        self,
        p1,
        p2,
        run_time=1,
        show_span_time=[0.4, 0.3, 0.9, 0.4],
        text="",
        reverse_label=False,
        add_bg=True,
        **kwargs
    ):
        self.set_compass_(p1, p2, run_time=run_time, **kwargs)
        bt = self.get_length_label(p1, p2, text=text, reverse_label=reverse_label)
        b, t = bt[0], bt[-1]
        st = show_span_time
        self.play(Create(b), run_time=st[0])
        if add_bg:
            self.add(bt[1])
            self.play(FadeIn(t), run_time=st[1])
        else:
            self.play(FadeIn(t), run_time=st[1])
        # self.wait(st[2])
        self.play(Wait(st[2]))
        self.play(FadeOut(VGroup(*bt)), run_time=st[3])
        return bt

    def highlight_on(self, *mobjects, to_front=True, run_time=1, **kwargs):
        self.highlight = VGroup(*mobjects)
        self.play(
            self.highlight.animate.set_stroke(color="#66CCFF", width=4),
            run_time=run_time,
            **kwargs
        )
        if to_front:
            self.bring_to_front(self.highlight)
            self.bring_to_front(self.cp, self.ruler)

    def highlight_off(self, *mobjects):

        pass

    def show_arc_info(self, arc, time_list=[0.5, 0.2, 0.3]):

        c, r, s, a, ps, pe = (
            arc.arc_center,
            arc.radius,
            arc.start_angle,
            arc.angle,
            arc.get_start(),
            arc.get_end(),
        )
        d_center = Dot(c, radius=0.08, color=PINK)
        r1, r2 = DashedLine(c, ps, stroke_width=3.5, stroke_color=PINK), DashedLine(
            c, pe, stroke_width=3.5, stroke_color=PINK
        )
        arc_new = Arc(
            arc_center=c,
            radius=r,
            start_angle=s,
            angle=a,
            stroke_width=8,
            stroke_color=RED,
        )
        self.play(Create(arc_new), run_time=time_list[0])
        self.play(FadeIn(arc_new), run_time=time_list[1])
        self.play(Create(r1), Create(r2), run_time=time_list[2])


class CompassAndRulerScene(DrawingScene):
    # just rename `DrawingScene`
    pass

hcq11419 avatar Apr 04 '25 01:04 hcq11419

Hi @hcq11419, my apologies for the delayed reply, I was very busy last week.

Does your issue occur when you clear the cache? So cleanup file insides the media/ folder created by Manim (and also slides/ folder, to be sure).

Unfortunately, I am still missing some files to run your code (please provide a Git repository if it contains multiple files, and reduce their size to the minimum if possible), and I could not reproduce your bug locally by generating many slides.

Here I created 1000 slides, with no issue:

import random
from math import pi

from manim import *
from manim_slides import Slide


class Main(Slide):
    def construct(self):
        dot = Dot().move_to([0.0, 1.0, 0.0])
        start_angle = pi

        for _ in range(1000):
            self.next_slide()
            angle = random.uniform(-2*pi, +2*pi)
            arc = Arc(start_angle=start_angle, angle=angle)
            start_angle = (start_angle + angle) % (2 * pi)
            self.play(MoveAlongPath(dot, arc))

FYI, I am using manim-slides[manim]>=5.5.1.

jeertmans avatar Apr 10 '25 08:04 jeertmans

Hi @jeertmans I've reorganized the code. Please test it again. I've tested it on both Mac and Windows, and the errors reported are the same.

from manim import *
from manim_slides import Slide

# utils
get_angle_ = lambda c: (
    np.angle(-c) + PI if not c / abs(c) == 1 else 0
)
convert_angle = lambda a: a if a >= 0 else a + TAU  


class Compass(VGroup):

    def __init__(
        self,
        stroke_color=RED,  # 圆规颜色
        fill_color=WHITE,
        stroke_width=2,
        leg_length=3,
        leg_width=0.12,
        r=0.2,
        depth_test=True,
        span=2.5,  # 针脚距离,半径
        **kwargs
    ):
        super().__init__()
        self.span = span
        self.leg_length = leg_length
        self.stroke_color = stroke_color
        self.fill_color = fill_color
        self.stroke_width = stroke_width
        self.leg_width = leg_width
        self.r = r
        self.depth_test = depth_test
        self.create_compass()

    def create_compass(self):
        s, l, r, w = self.span, self.leg_length, self.r, self.leg_width
        self.theta = np.arcsin(s / 2 / l)  # theta 为圆规腿与针脚连线的夹角

        self.c = Circle(
            radius=r,
            fill_color=self.fill_color,
            fill_opacity=1,
            stroke_color=self.stroke_color,
            stroke_width=self.stroke_width * 5,
        )
        c2 = Circle(
            radius=r + self.stroke_width * 5 / 100 / 2,
            fill_opacity=0,
            stroke_color=self.fill_color,
            stroke_width=self.stroke_width,
        )
        # leg_1是针尖腿,leg_2是笔尖腿
        self.leg_1 = Polygon(
            ORIGIN,
            l * RIGHT,
            (l - w * np.sqrt(3)) * RIGHT + w * DOWN,
            w * DOWN,
            stroke_width=0,
            stroke_color=self.fill_color,
            fill_color=self.stroke_color,
            fill_opacity=1,
        ).rotate(-PI / 2 - self.theta, about_point=self.c.get_center())
        self.leg_2 = Polygon(
            ORIGIN,
            l * RIGHT,
            (l - w * np.sqrt(3)) * RIGHT + w * UP,
            w * UP,
            stroke_width=0,
            stroke_color=self.fill_color,
            fill_color=self.stroke_color,
            fill_opacity=1,
        ).rotate(-PI / 2 + self.theta, about_point=self.c.get_center())

        # self.leg_1, self.leg_2 = VGroup(leg_01, leg_11),  VGroup(leg_02, leg_12, pen_point)
        h = Line(
            UP * r,
            UP * (r + r * 1.8),
            stroke_color=self.stroke_color,
            stroke_width=self.stroke_width * 6,
        )

        self.head = VGroup(h, self.c, c2)
        self.add(self.leg_1, self.leg_2, self.head)
        self.move_to(ORIGIN)
        return self

    def get_niddle_tip(self):  # 获取针脚
        return self.leg_1.get_vertices()[1]

    def get_pen_tip(self):  # 获取笔尖
        return self.leg_2.get_vertices()[1]

    def move_niddle_tip_to(self, pos):
        self.shift(pos - self.get_niddle_tip())
        return self

    def rotate_about_niddle_tip(self, angle=PI / 2):
        p = Dot(self.get_niddle_tip())  # manim bug?
        self.rotate(angle=angle, about_point=p.get_center())

    def get_span(self):
        # return self.span 如果进行了缩放而self.span没变会有问题
        return get_norm(self.get_pen_tip() - self.get_niddle_tip())

    def set_span(self, s):
        self.span = s
        l, r, w = self.leg_length, self.r, self.leg_width
        theta_new, theta_old = np.arcsin(s / 2 / l), self.theta
        sign = np.sign(
            get_angle_(
                R3_to_complex(
                    self.leg_2.get_vertices()[1] - self.leg_2.get_vertices()[0]
                )
            )
            - get_angle_(
                R3_to_complex(
                    self.leg_1.get_vertices()[1] - self.leg_1.get_vertices()[0]
                )
            )
        )
        # sign=np.sign(get_angle(self.leg_2.get_vertices()[0],self.leg_2.get_vertices()[1])-get_angle(
        #     self.leg_1.get_vertices()[0],self.leg_1.get_vertices()[1])) #调用my_fun

        rotate_angle = 2 * (theta_new - theta_old) * sign
        self.leg_2.rotate(rotate_angle, about_point=self.c.get_center())
        self.theta = theta_new
        self.head.rotate(rotate_angle / 2, about_point=self.c.get_center())
        self.rotate_about_niddle_tip(-rotate_angle / 2)
        return self

    def set_compass(self, center, pen_tip):
        self.move_niddle_tip_to(center)
        self.set_span(get_norm(pen_tip - center))
        self.rotate_about_niddle_tip(
            np.angle(R3_to_complex(pen_tip - center))
            - np.angle(R3_to_complex(self.get_pen_tip() - center))
        )
        return self

    def set_compass_to_draw_arc(self, arc):
        return self.set_compass(arc.arc_center, arc.get_start())

    def reverse_tip(self):
        return self.flip(
            axis=self.head[0].get_end() - self.head[0].get_start(),
            about_point=self.c.get_center(),
        )


class DrawingScene(Scene):

    def __init__(
        self,
        compass_config={
            "stroke_color": RED,  # 圆规颜色
            "fill_color": WHITE,
            "stroke_width": 2,
            "leg_length": 3,
            "leg_width": 0.12,
            "r": 0.2,
            "depth_test": True,
        },  # to define size and style of the compass
        ruler_config={
            "width": 10,
            "height": 0.8,
            "stroke_width": 8,
            "stroke_color": GREY_E,
            "stroke_opacity": 0.4,
            "fill_color": WHITE,
            "fill_opacity": 0.5,
        },  # to define size and style of the ruler
        dot_config={
            "radius": 0.06,
            "color": GREY_E,
        },  # to define size and color of the dot (e.g., dot in arc/circle's center)
        line_config={
            "stroke_color": YELLOW_E,
            "stroke_width": 2.5,
        },  # the default line style drawing by ruler (not the defualt arc style drawing by compass)
        brace_config={
            "fill_color": YELLOW_E,
            "buff": 0.025,
        },
        text_config={
            "font_size": 20,  # 5 times than the actual size here and
            # will be sacle down to the actual size later
            # in 'get_length_label' methods.
            # 'font': 'Cambria Math',
            "color": YELLOW_E,
        },
        add_ruler=True,
    ):
        super().__init__()
        self.compass_config = compass_config
        self.ruler_config = ruler_config
        self.dot_config = dot_config
        self.line_config = line_config
        self.brace_config = brace_config
        self.text_config = text_config
        self.add_ruler = add_ruler

    def setup(self):
        self.cp = Compass(**self.compass_config)
        self.ruler = VGroup(
            Rectangle(**self.ruler_config)
            .stretch_to_fit_height(
                self.ruler_config["height"]
                - self.ruler_config["stroke_width"] / 2 / 100
            )
            .round_corners(self.ruler_config["height"] / 8),
            Rectangle(**self.ruler_config).set_opacity(0),
        )
        self.dot = Dot(**self.dot_config)

        self.cp.move_to(UP * 10)
        if self.add_ruler:
            self.ruler.move_to(DOWN * 10)
            self.add(self.ruler)
        self.add(self.cp)

        self.temp_points = []

    def construct(self):

        self.add(self.cp)
        self.play(self.cp.animate.move_niddle_tip_to(ORIGIN), run_time=1)
        # self.wait(0.3)
        self.play(Wait(0.3))
        self.set_span(3.6, run_time=1, rate_func=smooth)
        # self.wait(0.5)
        self.play(Wait(0.5))

        self.set_compass(DL * 0.5, UR * 0.5, run_time=1, rate_func=there_and_back)
        arc = Arc(color=GREY_E)
        self.set_compass_to_draw_arc(arc)
        self.draw_arc_by_compass(arc)

        # self.wait()
        self.play(Wait(1))

    def set_span(self, s, run_time=1, rate_func=smooth):
        """设置针尖"""
        s_old = self.cp.get_span()
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        # s_series = s_old + rate_func(t_series) * (s - s_old)
        s_series = [s_old + rate_func(t_series[i]) * (s - s_old) for i in range(n)]
        for i in range(n):
            self.cp.set_span(s_series[i])
            self.play(Wait(dt))
            # self.wait(dt)

    def set_compass_direction(self, start, end, run_time=1, rate_func=smooth):
        vect = end - start
        a = np.angle(R3_to_complex(vect))
        c_old, p_old = self.cp.get_niddle_tip(), self.cp.get_pen_tip()
        a_old = np.angle(R3_to_complex(p_old - c_old))
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        c_series = [c_old + rate_func(t_series[i]) * (start - c_old) for i in range(n)]
        delta_a = (a - a_old) / n
        for i in range(n):
            self.bring_to_front(self.cp)
            self.cp.move_niddle_tip_to(c_series[i])
            self.cp.rotate_about_niddle_tip(delta_a)
            # self.wait(dt)
            self.play(Wait(dt))

    def set_compass(
        self, center, pen_tip, run_time=1, rate_func=smooth, emphasize_dot=False
    ):
        if emphasize_dot:
            run_time -= 0.15
        c_old, p_old = self.cp.get_niddle_tip(), self.cp.get_pen_tip()
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        # s_series = s_old + rate_func(t_series) * (s - s_old)
        c_series = [c_old + rate_func(t_series[i]) * (center - c_old) for i in range(n)]
        p_series = [
            p_old + rate_func(t_series[i]) * (pen_tip - p_old) for i in range(n)
        ]

        for i in range(n):
            self.bring_to_front(self.cp)
            self.cp.set_compass(c_series[i], p_series[i])
            # self.wait(dt)
            self.play(Wait(dt))
        if emphasize_dot:
            self.emphasize_dot([center, pen_tip], run_time=0.15)

    def set_compass_(
        self,
        center,
        pen_tip,
        adjust_angle=0,
        run_time=1,
        rate_func=smooth,
        emphasize_dot=False,
    ):

        vect = center - pen_tip
        a = np.angle(R3_to_complex(vect)) + adjust_angle
        s = get_norm(vect)
        c_old, p_old, s_old = (
            self.cp.get_niddle_tip(),
            self.cp.get_pen_tip(),
            self.cp.get_span(),
        )
        a_old = np.angle(R3_to_complex(p_old - c_old))
        if emphasize_dot:
            run_time -= 0.15
        n = int(run_time * self.camera.frame_rate)
        dt = 1 / self.camera.frame_rate
        t_series = np.linspace(1, n, n) / n
        c_series = [c_old + rate_func(t_series[i]) * (center - c_old) for i in range(n)]
        delta_a = (a - a_old) / n
        s_series = [s_old + rate_func(t_series[i]) * (s - s_old) for i in range(n)]

        for i in range(n):
            self.bring_to_front(self.cp)
            self.cp.move_niddle_tip_to(c_series[i])
            self.cp.rotate_about_niddle_tip(delta_a)
            self.cp.set_span(s_series[i])
            # self.wait(dt)
            self.play(Wait(dt))
        if emphasize_dot:
            self.emphasize_dot([center, pen_tip], run_time=0.15)

    def set_compass_to_draw_arc(self, arc, **kwargs):
        self.set_compass(arc.arc_center, arc.get_start(), **kwargs)

    def set_compass_to_draw_arc_(self, arc, **kwargs):
        self.set_compass_(arc.arc_center, arc.get_start(), **kwargs)

    def draw_arc_by_compass(
        self,
        arc,
        is_prepared=True,
        run_time=1,
        rate_func=smooth,
        reverse=False,
        add_center=False,
        **kwargs
    ):
        self.bring_to_front(self.cp)
        if not is_prepared:
            self.set_compass_to_draw_arc(arc, run_time=0.5)
        theta = arc.angle if not reverse else -1 * arc.angle
        self.play(
            Rotating(self.cp, radians=theta, about_point=self.cp.get_niddle_tip()),
            Create(arc),
            rate_func=rate_func,
            run_time=run_time,
        )
        if add_center:
            d = Dot(self.cp.get_niddle_tip(), **self.dot_config).scale(0.5)
            self.temp_points.append(d)
            self.add(d)

    def emphasize_dot(self, pos, add_dot=False, size=1.2, run_time=0.2, **kwargs):
        if type(pos) == list:
            d = VGroup(
                *[
                    Dot(
                        pos[i], radius=size / 2, color=YELLOW_C, fill_opacity=0.25
                    ).scale(0.25)
                    for i in range(len(pos))
                ]
            )
        else:
            d = Dot(pos, radius=size / 2, color=YELLOW_C, fill_opacity=0.25).scale(0.25)
        self.add(d)
        if type(pos) == list:
            self.play(
                d[0].animate.scale(4),
                d[1].animate.scale(4),
                rate_func=linear,
                run_time=run_time,
            )

        else:
            self.play(d.animate.scale(4), rate_func=linear, run_time=run_time)

        self.remove(d)
        if add_dot:
            if type(pos) == list:
                dot = VGroup(*[Dot(pos[i], **kwargs) for i in range(len(pos))])
            else:
                dot = Dot(pos, **kwargs)
            self.add(dot)
            return dot

    def set_ruler(self, pos1, pos2, run_time=1, rate_func=smooth):
        p1, p2 = self.ruler[-1].get_vertices()[1], self.ruler[-1].get_vertices()[0]
        c12 = (p1 + p2) / 2
        center = (pos1 + pos2) / 2
        self.bring_to_front(self.ruler)
        self.play(
            self.ruler.animate.shift(center - c12),
            run_time=run_time / 2,
            rate_func=rate_func,
        )
        self.play(
            Rotating(
                self.ruler,
                radians=np.angle(R3_to_complex(pos2 - pos1))
                - np.angle(R3_to_complex(p2 - p1)),
                about_point=center,
            ),
            run_time=run_time / 2,
            rate_func=rate_func,
        )

    def draw_line(
        self, pos1, pos2, is_prepared=True, run_time=1.2, rate_func=smooth, pre_time=0.8
    ):
        if not is_prepared:
            self.set_ruler(pos1, pos2, run_time=pre_time)
        self.dot.move_to(pos1)
        self.emphasize_dot(pos1, run_time=0.15)
        self.add(self.dot)
        l = Line(pos1, pos2, **self.line_config)
        self.play(
            Create(l),
            self.dot.animate.move_to(pos2),
            run_time=run_time - 0.3,
            rate_func=rate_func,
        )
        self.emphasize_dot(pos2, run_time=0.15)
        self.remove(self.dot)
        return l

    def draw_line_(self, l, is_prepared=True, run_time=1.2, rate_func=smooth):
        pos1, pos2 = l.get_start(), l.get_end()
        if not is_prepared:
            self.set_ruler(pos1, pos2, run_time=0.5)
        self.dot.move_to(pos1)
        self.emphasize_dot(pos1, run_time=0.15)
        self.add(self.dot)
        # l = Line(pos1, pos2, **self.line_config)
        self.play(
            Create(l),
            self.dot.animate.move_to(pos2),
            run_time=run_time - 0.3,
            rate_func=rate_func,
        )
        self.emphasize_dot(pos2, run_time=0.15)
        self.remove(self.dot)
        return l

    def put_aside_ruler(self, direction=DOWN, run_time=0.5):
        self.bring_to_front(self.ruler)
        self.play(self.ruler.animate.move_to(direction * 15), run_time=run_time)

    def put_aside_compass(self, direction=DOWN, run_time=0.5):
        self.bring_to_front(self.cp)
        self.play(self.cp.animate.move_to(direction * 15), run_time=run_time)

    def get_length_label(
        self, p1, p2, text="", reverse_label=False, add_bg=False, bg_color=WHITE
    ):
        l = Line(p1, p2)
        b = Brace(
            l,
            direction=complex_to_R3(
                np.exp(1j * (l.get_angle() + PI / 2 * (1 - 2 * float(reverse_label))))
            ),
            **self.brace_config
        )
        t = b.get_text(text)
        # t = Text(text, **self.text_config).scale(0.2)
        if add_bg:
            bg = SurroundingRectangle(
                t, fill_color=bg_color, fill_opacity=0.6, stroke_opacity=0
            ).stretch_to_fit_height(t.get_height() + 0.05)
            b.put_at_tip(bg, buff=0.0)
            b.put_at_tip(t, buff=0.05)
            return b, bg, t
        else:
            b.put_at_tip(t, buff=0.05)
            return b, t

    def set_compass_and_show_span(
        self,
        p1,
        p2,
        run_time=1,
        show_span_time=[0.4, 0.3, 0.9, 0.4],
        text="",
        reverse_label=False,
        add_bg=False,
        **kwargs
    ):
        self.set_compass(p1, p2, run_time=run_time, **kwargs)
        bt = self.get_length_label(
            p1, p2, text=text, reverse_label=reverse_label, add_bg=add_bg
        )
        b, t = bt[0], bt[-1]
        st = show_span_time
        self.play(Create(b), run_time=st[0])
        if add_bg:
            self.add(bt[1])
            self.play(FadeIn(t), run_time=st[1])
        else:
            self.play(FadeIn(t), run_time=st[1])
        # self.wait(st[2])
        self.play(Wait(st[2]))

        self.play(FadeOut(VGroup(*bt)), run_time=st[3])
        return bt

    def set_compass_and_show_span_(
        self,
        p1,
        p2,
        run_time=1,
        show_span_time=[0.4, 0.3, 0.9, 0.4],
        text="",
        reverse_label=False,
        add_bg=True,
        **kwargs
    ):
        self.set_compass_(p1, p2, run_time=run_time, **kwargs)
        bt = self.get_length_label(p1, p2, text=text, reverse_label=reverse_label)
        b, t = bt[0], bt[-1]
        st = show_span_time
        self.play(Create(b), run_time=st[0])
        if add_bg:
            self.add(bt[1])
            self.play(FadeIn(t), run_time=st[1])
        else:
            self.play(FadeIn(t), run_time=st[1])
        # self.wait(st[2])
        self.play(Wait(st[2]))
        self.play(FadeOut(VGroup(*bt)), run_time=st[3])
        return bt

    def highlight_on(self, *mobjects, to_front=True, run_time=1, **kwargs):
        self.highlight = VGroup(*mobjects)
        self.play(
            self.highlight.animate.set_stroke(color="#66CCFF", width=4),
            run_time=run_time,
            **kwargs
        )
        if to_front:
            self.bring_to_front(self.highlight)
            self.bring_to_front(self.cp, self.ruler)

    def highlight_off(self, *mobjects):

        pass

    def show_arc_info(self, arc, time_list=[0.5, 0.2, 0.3]):

        c, r, s, a, ps, pe = (
            arc.arc_center,
            arc.radius,
            arc.start_angle,
            arc.angle,
            arc.get_start(),
            arc.get_end(),
        )
        d_center = Dot(c, radius=0.08, color=PINK)
        r1, r2 = DashedLine(c, ps, stroke_width=3.5, stroke_color=PINK), DashedLine(
            c, pe, stroke_width=3.5, stroke_color=PINK
        )
        arc_new = Arc(
            arc_center=c,
            radius=r,
            start_angle=s,
            angle=a,
            stroke_width=8,
            stroke_color=RED,
        )
        self.play(Create(arc_new), run_time=time_list[0])
        self.play(FadeIn(arc_new), run_time=time_list[1])
        self.play(Create(r1), Create(r2), run_time=time_list[2])


class CompassAndRulerScene(DrawingScene):
    # just rename `DrawingScene`
    pass



# Scene
class Golden_section_point(CompassAndRulerScene, Slide):
    Dot_config = {
        "radius": 0.04,
        "color": WHITE,
    }

    CONFIG = {
        "compass_config": {
            "leg_length": 3,
            "stroke_color": RED,  # 圆规腿颜色
            "fill_color": WHITE,  # 圆盘颜色
        },
        "arc_config": {
            "stroke_color": YELLOW_B,
            "stroke_width": 2.5,
        },
        "add_ruler": True,  # add ruler when the scene starts
    }

    def setup(self):
        # 先调用父类的setup方法来初始化ruler等属性
        super().setup()
        # 然后设置TeX模板
        # MathTex.set_default(tex_template=TexTemplateLibrary.ctex)

    def construct(self):

        rate_ease_out_back = {"rate_func": rate_functions.ease_out_back}
        d1, d2 = LEFT * 2, RIGHT * 2
        dA = Dot(LEFT * 2, **self.Dot_config)
        dB = Dot(RIGHT * 2, **self.Dot_config)
        lAB = Line(dA.get_center(), dB.get_center())
        lb_dA, lb_dB = Tex("A").next_to(dA, DOWN, buff=0.1), Tex("B").next_to(
            dB, DOWN, buff=0.1
        )

        self.add(dA, dB, lAB, lb_dA, lb_dB)
        self.bring_to_back(lAB)
        # 延长AB
        l1 = DashedLine(
            dB.get_center(), RIGHT * 5, dash_length=0.1, **self.CONFIG["arc_config"]
        )
        self.set_ruler(dB.get_center(), RIGHT * 5)
        self.wait(0.1)
        self.draw_line_(l1, run_time=0.8)
        self.wait(0.1)
        self.put_aside_ruler(DOWN * 0.7)
        p1, p2 = RIGHT * 0.5, RIGHT * 3.5

        self.set_compass(dB.get_center(), RIGHT * 2.5)
        self.wait(0.1)
        self.set_compass(dB.get_center(), p2)
        self.wait(0.1)
        arc_r = Arc(
            arc_center=dB.get_center(),
            start_angle=-15 * DEGREES,
            angle=30 * DEGREES,
            radius=1.5,
            **self.CONFIG["arc_config"]
        )
        arc_l = Arc(
            arc_center=dB.get_center(),
            start_angle=165 * DEGREES,
            angle=30 * DEGREES,
            radius=1.5,
            **self.CONFIG["arc_config"]
        )
        self.set_compass_to_draw_arc_(arc_r, adjust_angle=-PI, run_time=0.6)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_r, run_time=0.5)
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_l, adjust_angle=PI, run_time=0.6)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_l, run_time=0.5)
        self.next_slide()

        p3 = dB.get_center() + UP * 1.5 * np.sqrt(3)
        p4 = dB.get_center() + DOWN * 1.5 * np.sqrt(3)
        self.set_compass(p1, RIGHT * 1.5)
        self.wait(0.2)
        self.set_compass(p1, p2)
        self.wait(0.2)

        arc_ru = Arc(
            arc_center=p1,
            start_angle=45 * DEGREES,
            angle=30 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )

        arc_lu = Arc(
            arc_center=p2,
            start_angle=105 * DEGREES,
            angle=30 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )

        self.set_compass_to_draw_arc_(arc_ru, adjust_angle=PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_ru, run_time=0.5)
        self.wait(0.1)

        self.set_compass_to_draw_arc_(arc_lu, adjust_angle=PI, run_time=0.4)
        self.wait(0.2)
        self.draw_arc_by_compass(arc_lu, run_time=0.5)
        self.wait(0.1)
        self.put_aside_compass(DR * 0.6)
        self.wait(0.2)
        # 画垂线
        l2 = Line(p3 + UP, d2)
        self.set_ruler(p3, p4)
        self.wait(0.1)
        self.emphasize_dot([p3, dB.get_center()], run_time=0.25)
        self.wait(0.1)
        self.draw_line_(l2)
        self.wait(0.1)
        self.put_aside_ruler(DOWN * 0.7)
        self.wait(0.1)
        self.highlight_on(l2)
        angle_90 = Square(0.22, stroke_width=2, stroke_color=GREY_C).next_to(
            dB.get_center(), UL, buff=0
        )
        self.bring_to_back(angle_90)

        self.play(FadeIn(angle_90), run_time=0.64)
        self.next_slide()
        # 找AB中点
        p5 = lAB.get_center() + UP * np.sqrt(5)
        p6 = lAB.get_center() + DOWN * np.sqrt(5)

        self.set_compass(dA.get_center(), dA.get_center() + RIGHT, run_time=0.75)
        self.wait(0.1)
        arc_r2 = Arc(
            arc_center=dA.get_center(),
            start_angle=-70 * DEGREES,
            angle=140 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )
        arc_l2 = Arc(
            arc_center=dB.get_center(),
            start_angle=110 * DEGREES,
            angle=140 * DEGREES,
            radius=3,
            **self.CONFIG["arc_config"]
        )
        self.set_compass_to_draw_arc_(arc_r2, adjust_angle=-PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_r2)
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_l2, adjust_angle=PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_l2)
        self.wait(0.1)
        self.put_aside_compass(DOWN * 0.8)
        self.wait(0.1)
        self.emphasize_dot([p5, p6], run_time=0.25)
        self.set_ruler(p5, p6)
        self.wait(0.1)
        l3 = DashedLine(p5, p6, dash_length=0.1, **self.CONFIG["arc_config"]).scale(
            1.25
        )
        self.draw_line_(l3)
        self.wait(0.2)
        self.put_aside_ruler(DOWN * 0.8)
        center = Dot(l3.get_center(), **self.Dot_config)
        self.play(FadeIn(center))
        self.wait(1)

        arc_l3 = Arc(
            arc_center=dB.get_center(),
            start_angle=-180 * DEGREES,
            angle=-90 * DEGREES,
            radius=2,
            **self.CONFIG["arc_config"]
        )
        p7 = dB.get_center() + UP * 2
        self.set_compass(dB.get_center(), dB.get_center() + LEFT * 2, run_time=0.5)
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_l3, adjust_angle=PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_l3)
        self.wait(0.1)
        self.put_aside_compass(DOWN * 0.8)
        dC = Dot(p7, **self.Dot_config)
        lb_dC = Tex("C").next_to(dC, RIGHT, buff=0.1)
        self.play(
            LaggedStart(
                FadeIn(dC),
                FadeIn(lb_dC, shift=RIGHT * 0.5, scale=0.1, **rate_ease_out_back),
                lag_ratio=0.6,
            )
        )
        self.next_slide()
        line_AC = Line(d1, p7, **self.CONFIG["arc_config"])
        self.emphasize_dot([d1, p7], run_time=0.25)
        self.set_ruler(d1, p7)
        self.wait(0.1)
        self.draw_line_(line_AC)
        self.wait(0.2)
        self.put_aside_ruler(DOWN * 0.8)
        self.next_slide()
        l4 = Line(d2, p7, **self.CONFIG["arc_config"])
        self.add(l4)
        out = Group(*self.mobjects).remove(
            dA, dB, dC, lAB, line_AC, lb_dA, lb_dB, lb_dC, l4, dC, angle_90
        )
        self.play(FadeOut(out))
        # out=VGroup(l1,arc_l,arc_l2,arc_l3,arc_lu,arc_r,arc_r2,arc_ru)
        self.next_slide()

        arc_4 = Arc(
            arc_center=p7,
            start_angle=-90 * DEGREES,
            angle=-np.arctan(2) - 10 * DEGREES,
            radius=2,
            stroke_color=YELLOW_B,
            stroke_width=2.5,
        )
        arc_5 = Arc(
            arc_center=d1,
            start_angle=np.arctan(1 / 2),
            angle=-np.arctan(1 / 2) - 10 * DEGREES,
            radius=2 * np.sqrt(5) - 2,
            stroke_color=YELLOW_B,
            stroke_width=2.5,
        )
        dD = Dot(line_AC.point_from_proportion(1 - np.sqrt(5) / 5), **self.Dot_config)
        lb_dD = Tex("D").next_to(dD, UL, buff=0.1)
        dE = Dot(lAB.point_from_proportion((np.sqrt(5) - 1) / 2), **self.Dot_config)
        lb_dE = Tex("E").next_to(dE, DR, buff=0.1)
        super().setup()  # 圆规初始化

        self.set_compass(p7, d2 + UP)
        self.set_compass_to_draw_arc_(arc_4, adjust_angle=-PI, run_time=0.4)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_4)
        self.wait(0.1)
        self.put_aside_compass(DOWN * 0.8)
        self.wait(0.1)
        self.emphasize_dot(dD.get_center())
        self.wait(0.1)
        self.play(
            LaggedStart(
                FadeIn(dD),
                FadeIn(lb_dD, shift=UL * 0.5, scale=0.1, **rate_ease_out_back),
                lag_ratio=0.6,
                run_time=1,
            )
        )
        self.next_slide()
        self.set_compass(d1, line_AC.point_from_proportion(0.3))
        self.wait(0.1)
        self.set_compass_to_draw_arc_(arc_5, adjust_angle=PI, run_time=0.5)
        self.wait(0.1)
        self.draw_arc_by_compass(arc_5, run_time=0.5)
        self.next_slide()
        self.put_aside_compass(DOWN)
        self.next_slide()
        self.emphasize_dot(dE.get_center())
        self.play(
            LaggedStart(
                FadeIn(dE),
                FadeIn(lb_dE, shift=DOWN * 0.5, scale=0.1, **rate_ease_out_back),
                lag_ratio=0.6,
                run_time=1,
            )
        )
        self.highlight_on([dE, lb_dE])
        self.wait()


if __name__ == "__main__":
    from os import system

    system("manim-slides render Golden_section_point {}    ".format(__file__))

hcq11419 avatar Apr 16 '25 11:04 hcq11419

@jeertmans I also tested the code you provided, and I was able to run it normally. However, when I switched to my specific example, this error occurred. I've tried many different scenarios, including changing platforms, switching between ManimCE and ManimGL, using different computers, and trying different code, etc. I'm really frustrated! The error occurs in the last step when using manim - slides to process the previously generated video (there is no problem with Manim rendering). The following is the error I encountered when running the above code with manimgl 1.7.2 on my Mac.

ManimGL v1.7.2
Traceback (most recent call last):                                                                                     
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/opt/miniconda3/envs/manim-gl-172/lib/python3.11/site-packages/manimlib/__main__.py", line 65, in <module>
    main()
  File "/opt/miniconda3/envs/manim-gl-172/lib/python3.11/site-packages/manimlib/__main__.py", line 61, in main
    run_scenes()
  File "/opt/miniconda3/envs/manim-gl-172/lib/python3.11/site-packages/manimlib/__main__.py", line 39, in run_scenes
    scene.run()
  File "/opt/miniconda3/envs/manim-gl-172/lib/python3.11/site-packages/manim_slides/slide/manimlib.py", line 62, in run
    self._save_slides(
  File "/opt/miniconda3/envs/manim-gl-172/lib/python3.11/site-packages/manim_slides/slide/base.py", line 576, in _save_slides
    concatenate_video_files(slide_files, dst_file)
  File "/opt/miniconda3/envs/manim-gl-172/lib/python3.11/site-packages/manim_slides/utils.py", line 67, in concatenate_video_files
    output_container.mux(packet)
  File "av/container/output.pyx", line 257, in av.container.output.OutputContainer.mux
  File "av/container/output.pyx", line 278, in av.container.output.OutputContainer.mux_one
  File "av/container/core.pyx", line 286, in av.container.core.Container.err_check
  File "av/error.pyx", line 326, in av.error.err_check
av.error.ValueError: [Errno 22] Invalid argument: 'slides/files/Golden_section_point/3a409fe872180224acd65b5617c99400fc0dbc63901ec6789908eb6fd0733520.mp4'

hcq11419 avatar Apr 16 '25 12:04 hcq11419

Hi @hcq11419, so I could reproduce your bug, by adding get_norm = np.linalg.norm at the top of the file.

I spent a few hours on the problem, and I'm sorry but can't figure out what is the root cause of it. When I try to reproduce the bug with a smaller code, similar to https://github.com/jeertmans/manim-slides/issues/540#issuecomment-2792016477, but with more animations per slide, I can't reproduce the bug. I hacked into the Manim Slides package to replace concatenate_files (see code below) with Manim's combine_files, and it works... but not always!

I try to clear the cache each time, to avoid caching effects, and most of the time the bug occurs as expected, but sometimes it doesn't and everything just works fine.

https://github.com/jeertmans/manim-slides/blob/a5412a8df2f4e86e949066db3a707cdafeb97007/manim_slides/utils.py#L16-L69

I know my implementation differs from the one written in Manim, but when I try to match both implementation, mine fails and the one from Manim works.

If you could provide a more concise non-working example, it would be great. Especially an example when we can observe the transition from it works to it doesn't work, e.g., with a variable number of animations.

jeertmans avatar Apr 17 '25 15:04 jeertmans

Thank you very, very much for your attention. I'm very sorry for taking up so much of your time. Actually, I've also encountered this problem when rendering other relatively complex code. Currently, the method I'm using is to skip 'manim - slides render', customize the generation of the slides file, and then use 'manim - slides convert'. I'm providing this to you for reference. First, generate the time_data.json file.

from manim import *
import time
from typing import TYPE_CHECKING,Callable
from manim.mobject.mobject import _AnimationBuilder
import threading

Scene.pause_time=0 #1 
Scene.time_list=[Scene.pause_time]
Scene.mark_time_list=[]#1 
Scene.time_mark_notes={}#1 


def play(
        self,
        *args: Animation | Mobject | _AnimationBuilder,
        subcaption=None,
        subcaption_duration=None,
        subcaption_offset=0,
        **kwargs,
    ):
        if (
            self.interactive_mode
            and config.renderer == RendererType.OPENGL
            and threading.current_thread().name != "MainThread"
        ):
            kwargs.update(
                {
                    "subcaption": subcaption,
                    "subcaption_duration": subcaption_duration,
                    "subcaption_offset": subcaption_offset,
                }
            )
            self.queue.put(
                (
                    "play",
                    args,
                    kwargs,
                )
            )
            return

        start_time = self.time
        self.renderer.play(self, *args, **kwargs)
        run_time = self.time - start_time
        self.pause_time+=run_time#1
        if subcaption:
            if subcaption_duration is None:
                subcaption_duration = run_time
            self.add_subcaption(
                content=subcaption,
                duration=subcaption_duration,
                offset=-run_time + subcaption_offset,
            )
def wait(
    self,
    duration: float = DEFAULT_WAIT_TIME,
    stop_condition: Callable[[], bool] | None = None,
    frozen_frame: bool | None = None,
    time_mark_notes:str|None=None,#1
    only_mark_time_without_loop:bool=False,#1
):
    self.play(
        Wait(
            run_time=duration,
            stop_condition=stop_condition,
            frozen_frame=frozen_frame,
        )
    )

    if time_mark_notes:
        if not only_mark_time_without_loop:#1 如果不是只记录时间,则把当前时间加入到循环数组中
            self.mark_time_list.append(self.pause_time)
        self.time_mark_notes[time_mark_notes]=self.pause_time#1
        
    self.time_list.append(round(self.pause_time,4))#1 
Scene.play=play#1
Scene.wait=wait#1

def save_combined_time_data_json(time_list, mark_time_list, time_mark_notes, render_time=None, path="./time_data.json"):
    """
    参数:
    time_list - 全部时间点数组
    mark_time_list - 标记的时间点数组
    time_mark_notes - 时间标记笔记字典
    render_time - 渲染总耗时(秒)
    path - 输出JSON文件路径
    """
    import json
    import time
    
    # 将数组中的值四舍五入到3位小数
    time_list_rounded = np.round(time_list, 3).tolist()
    mark_time_list_rounded = np.round(mark_time_list, 3).tolist()
    
    # 处理time_mark_notes字典,将值四舍五入
    notes_dict = {}
    if time_mark_notes and isinstance(next(iter(time_mark_notes.values())), (int, float)):
        for k, v in time_mark_notes.items():
            notes_dict[k] = round(v, 3) if isinstance(v, (int, float)) else v
    else:
        for k, v in time_mark_notes.items():
            if isinstance(k, (int, float)):
                key = f"{round(k, 3)}"
            else:
                key = str(k)
            notes_dict[key] = v
    
    # 计算分钟和秒
    if render_time is not None:
        minutes = int(render_time // 60)
        seconds = render_time % 60
        render_time_formatted = f"{minutes}分 {seconds:.2f}秒"
    else:
        render_time_formatted = "未记录"
    
    # 手动格式化输出,保持原有风格
    with open(path, "w", encoding='utf-8') as file:
        file.write('{\n')
        
        # 写入渲染总时间
        file.write(f'    "total_render_time": "{render_time_formatted}",\n')
        
        # 写入time_list数组,每10个元素换行
        file.write('    "time_list": [\n        ')
        for i, value in enumerate(time_list_rounded):
            if i > 0 and i % 10 == 0:
                file.write(',\n        ')
            elif i > 0:
                file.write(', ')
            file.write(f"{value}")
        file.write('\n    ],\n')
        
        # 写入time_list_mark数组,每10个元素换行
        file.write('    "time_list_mark": [\n        ')
        for i, value in enumerate(mark_time_list_rounded):
            if i > 0 and i % 10 == 0:
                file.write(',\n        ')
            elif i > 0:
                file.write(', ')
            file.write(f"{value}")
        file.write('\n    ],\n')
        
        # 写入time_mark_notes字典
        file.write('    "time_list_mark_notes": ')
        json_str = json.dumps(notes_dict, ensure_ascii=False, indent=4)
        # 缩进处理,确保格式一致
        json_str = json_str.replace('\n', '\n    ')
        file.write(json_str)
        # 关闭最外层花括号
        file.write('\n}')



class Demo2(Scene):
    def setup(self):
        self.default_wait_time=.5
        self.render_start_time = time.time()  # 记录开始时间
    def tear_down(self):
        render_time = time.time() - self.render_start_time  # 计算渲染时间
        save_combined_time_data_json(
            self.time_list,
            self.mark_time_list,
            self.time_mark_notes,
            render_time=render_time,  # 传递渲染时间
            path="./time_data.json"
        )


    def construct(self):
        r=Circle(fill_opacity=1,fill_color=BLUE_A)
        self.play(Create(r,run_time=.25))
        self.wait(time_mark_notes="section 1",only_mark_time_without_loop=True)
        self.play(FadeOut(r,run_time=.5))
        self.wait(time_mark_notes="section 2")

        self.play(Create(r))
        self.wait(time_mark_notes="section 3")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 4")

        self.play(FadeIn(r,scale=.1))
        self.wait(time_mark_notes="section 5")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 6")
        
        self.play(FadeIn(r,shift=DOWN))
        self.wait(time_mark_notes="section 7")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 8")

        self.play(FadeIn(r))
        self.wait(time_mark_notes="section 9")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 10")

        self.play(FadeIn(r))
        self.wait(time_mark_notes="section 11")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 12")

        self.play(FadeIn(r,scale=.1,rate_func=rate_functions.ease_out_back))
        self.wait(time_mark_notes="section 13")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 14")

        self.play(FadeIn(r,scale=3,rate_func=rate_functions.ease_out_back))
        self.wait(time_mark_notes="section1 5")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 16")

        self.play(FadeIn(r,shift=RIGHT*2,rate_func=rate_functions.ease_out_back))
        self.wait(time_mark_notes="section 17")
        self.play(FadeOut(r))
        self.wait(time_mark_notes="section 18")


        square = Square().next_to(r,DOWN)
        self.play(Create(square))
        self.wait()

if __name__ == "__main__":
    from os import system
    system("manim -pqh {} ".format(__file__))

Next, convert the previously generated data_time.json into individual files in the slides.

import os
import json
import subprocess
import shutil

# Video file path
video_file = "media\\Demo.mp4" # Modify this to the video directory

# Read time data from time_data.json
try:
    with open("time_data.json", "r", encoding="utf-8") as f:
        time_data = json.load(f)
        time_list = time_data["time_list"]
        loop_marks = time_data["time_list_mark"]
except FileNotFoundError:
    print("time_data.json file not found, using default time points")
    time_list = []
    loop_marks = []

# Set output directory
output_dir = "slides\\files\\Demo"
os.makedirs(output_dir, exist_ok=True)

# Whether to create reversed videos
CREATE_REVERSED = True  # Set to False to skip creating reversed videos

# Store paths of split video files
video_files = []

# Split video
for i in range(len(time_list) - 1):
    start_time = time_list[i]
    end_time = time_list[i + 1]
    duration = end_time - start_time - 0.3

    # Check if the current segment should loop
    should_loop = end_time in loop_marks
    if should_loop:
        print(f"Segment {i+1} (from {start_time} to {end_time}) will be set to loop")

    # Use simple sequential naming
    output_file = os.path.join(output_dir, f"part_{i+1}.mp4")
    output_reversed_file = os.path.join(output_dir, f"part_{i+1}_reversed.mp4")

    # Use FFmpeg to split the video
    print(f"Splitting video: {start_time} to {end_time}")

    # All video segments use re-encoding to ensure correct cutting
    if i == 0 and abs(start_time) < 0.001:  # If it's the first segment and start time is close to 0
        ffmpeg_cmd = [
            "ffmpeg",
            "-y",
            "-i",
            video_file,
            "-t",  # Specify only the duration, not the start time
            str(duration),
            "-c:v",  # Use re-encoding to ensure accuracy
            "libx264",
            "-preset",
            "medium",
            "-crf",
            "23",
            output_file,
        ]
    else:
        ffmpeg_cmd = [
            "ffmpeg",
            "-y",
            "-i",
            video_file,
            "-ss",
            str(start_time),
            "-t",
            str(duration),
            "-c:v",  # Use re-encoding instead of stream copy
            "libx264",
            "-preset",
            "medium",
            "-crf",
            "23",
            output_file,
        ]

    result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)

    if result.returncode != 0:
        print(f"FFmpeg command failed! Error message: {result.stderr}")
        continue  # Skip the current video segment
    else:
        print(f"Successfully created file: {output_file}")

    reversed_success = False
    if CREATE_REVERSED:
        # Create reversed video
        print(f"Creating reversed video: {output_reversed_file}")
        reversed_cmd = [
            "ffmpeg",
            "-y",
            "-i",
            output_file,
            "-vf",
            "reverse",
            "-af",
            "areverse",
            output_reversed_file,
        ]
        rev_result = subprocess.run(reversed_cmd, capture_output=True, text=True)

        if rev_result.returncode != 0:
            print(f"Failed to create reversed video! Error message: {rev_result.stderr}")
            # If reversed video creation fails, use the original video as the reversed video
            shutil.copy(output_file, output_reversed_file)
            print(f"Copied original video as reversed video: {output_reversed_file}")
        else:
            print(f"Successfully created reversed video: {output_reversed_file}")
            reversed_success = True

    # Add to file list
    video_files.append(
        {
            "file": output_file,
            "rev_file": output_reversed_file,
            "reversed_success": reversed_success if CREATE_REVERSED else False,
            "should_loop": should_loop,  # Record whether it should loop
        }
    )

# Create JSON file
slides_data = []
for i, video in enumerate(video_files):
    # Get the start and end time of the current segment
    start_time = time_list[i]
    end_time = time_list[i + 1]

    # Correct file path to ensure consistency with output_dir
    # Use os.path.join to ensure cross-platform compatibility
    file_path = os.path.join("slides", "files", "Demo", os.path.basename(video["file"]))
    rev_file_path = (
        file_path
        if not CREATE_REVERSED
        else os.path.join(
            "slides", "files", "Demo", os.path.basename(video["rev_file"])
        )
    )

    slides_data.append(
        {
            "loop": video["should_loop"],  # Set loop attribute based on mark
            "auto_next": False,
            "playback_rate": 1.0,
            "reversed_playback_rate": 1.0,
            "notes": "",
            "dedent_notes": True,
            "skip_animations": False,
            "src": None,
            "file": file_path,
            "rev_file": rev_file_path,
            "time_range": f"{start_time:.3f} - {end_time:.3f}",  # Add time range identifier
        }
    )

# Create a structure similar to Demo.json
json_data = {
    "slides": slides_data,
    "resolution": [1920, 1080],
    "background_color": "black",
}

# Save JSON file
with open("slides/Demo.json", "w") as f:
    json.dump(json_data, f, indent=2)

print(f"Video splitting completed, a total of {len(video_files)} segments generated")
print(f"JSON file saved to slides/Demo.json")
print(f"Set {sum(1 for v in video_files if v['should_loop'])} segments to loop")

I'm a newbie. I really admire you. Thank you again!

hcq11419 avatar Apr 18 '25 01:04 hcq11419

Thanks for sharing @hcq11419! I'll leave this issue open, as we didn't find a good fix yet. If you ever find a simpler example showcasing your bug, please share it here :-)

jeertmans avatar Apr 18 '25 10:04 jeertmans

@jeertmans Okay, thank you for your attention!

hcq11419 avatar Apr 21 '25 03:04 hcq11419

I thought I was hitting the same problem. After some minimizing I can trigger it with very few animations.

# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "manim-slides[manim]==5.5.1",
# ]
# ///
from manim import *
from manim_slides import ThreeDSlide


class T2vert(ThreeDSlide):
    def construct(self):
        k = ValueTracker(0)

        def slice_function(x, y):
            return x + y

        def z_slice(num):
            k.set_value(num)
            return ImplicitFunction(
                slice_function, color=YELLOW, x_range=((-5, 5)), y_range=((-5, 5))
            )

        ref_dot2 = always_redraw(
            lambda: Dot3D(
                point=interpolate(
                    DOWN,
                    UP,
                    k.get_value(),
                ),
            )
        )

        slice = z_slice(1)
        self.play(
            FadeIn(slice),
            run_time=0.025,
        )

        self.play(FadeOut(ref_dot2))


if __name__ == "__main__":
    import subprocess
    import sys

    subprocess.run([sys.executable, "-m", "manim_slides", "render", __file__])

If either of the last two animations are omitted the bug is not triggered

[06/03/25 21:05:52] INFO     Animation 1 : Partial movie file written in '/home/ethan/manim-repo/media/videos/T2vert/1440p60/partial_movie_files/T2vert/1616758632_239749663_3857932860.mp4'             scene_file_writer.py:588
                    INFO     Combining to Movie file.                                                                                                                                                    scene_file_writer.py:739
                    INFO                                                                                                                                                                                 scene_file_writer.py:886
                             File ready at '/home/ethan/manim-repo/media/videos/T2vert/1440p60/T2vert.mp4'                                                                                                                       
                                                                                                                                                                                                                                 
                    INFO     Rendered T2vert                                                                                                                                                                         scene.py:255
                             Played 2 animations                                                                                                                                                                                 

Concatenating animation files to 'slides/files/T2vert' and generating reversed animations:   0%|          | 0/1 [00:00<?, ?it/s]
                                                                                                                                
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /nix/store/pq0jl3sl9m5vabhnrqpir3w3dlxmfcpb-python3-3.12.9-env/lib/python3.12/site-packages/mani │
│ m/cli/render/commands.py:125 in render                                                           │
│                                                                                                  │
│   122 │   │   │   try:                                                                           │
│   123 │   │   │   │   with tempconfig({}):                                                       │
│   124 │   │   │   │   │   scene = SceneClass()                                                   │
│ ❱ 125 │   │   │   │   │   scene.render()                                                         │
│   126 │   │   │   except Exception:                                                              │
│   127 │   │   │   │   error_console.print_exception()                                            │
│   128 │   │   │   │   sys.exit(1)                                                                │
│                                                                                                  │
│ /nix/store/pq0jl3sl9m5vabhnrqpir3w3dlxmfcpb-python3-3.12.9-env/lib/python3.12/site-packages/mani │
│ m_slides/slide/manim.py:156 in render                                                            │
│                                                                                                  │
│   153 │   │                                                                                      │
│   154 │   │   config["max_files_cached"] = max_files_cached                                      │
│   155 │   │                                                                                      │
│ ❱ 156 │   │   self._save_slides(                                                                 │
│   157 │   │   │   use_cache=not (config["disable_caching"] or self.disable_caching),             │
│   158 │   │   │   flush_cache=(config["flush_cache"] or self.flush_cache),                       │
│   159 │   │   │   skip_reversing=self.skip_reversing,                                            │
│                                                                                                  │
│ /nix/store/pq0jl3sl9m5vabhnrqpir3w3dlxmfcpb-python3-3.12.9-env/lib/python3.12/site-packages/mani │
│ m_slides/slide/base.py:553 in _save_slides                                                       │
│                                                                                                  │
│   550 │   │   │                                                                                  │
│   551 │   │   │   # We only concat animations if it was not present                              │
│   552 │   │   │   if not use_cache or not dst_file.exists():                                     │
│ ❱ 553 │   │   │   │   concatenate_video_files(slide_files, dst_file)                             │
│   554 │   │   │                                                                                  │
│   555 │   │   │   # We only reverse video if it was not present                                  │
│   556 │   │   │   if not use_cache or not rev_file.exists():                                     │
│                                                                                                  │
│ /nix/store/pq0jl3sl9m5vabhnrqpir3w3dlxmfcpb-python3-3.12.9-env/lib/python3.12/site-packages/mani │
│ m_slides/utils.py:60 in concatenate_video_files                                                  │
│                                                                                                  │
│    57 │   │   │   │   packet.stream = output_audio_stream                                        │
│    58 │   │   │   else:                                                                          │
│    59 │   │   │   │   continue  # We don't support subtitles                                     │
│ ❱  60 │   │   │   output_container.mux(packet)                                                   │
│    61 │                                                                                          │
│    62 │   os.unlink(tmp_file)  # https://stackoverflow.com/a/54768241                            │
│    63                                                                                            │
│                                                                                                  │
│ in av.container.output.OutputContainer.mux:257                                                   │
│                                                                                                  │
│ in av.container.output.OutputContainer.mux_one:278                                               │
│                                                                                                  │
│ in av.container.core.Container.err_check:286                                                     │
│                                                                                                  │
│ in av.error.err_check:326                                                                        │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: [Errno 22] Invalid argument: 'slides/files/T2vert/f138f54039f0736b833e15b6fe59b38d26e90ec90e497f9544604db7955b195c.mp4'

System info: manimce: 0.19.0 manim-slides: 5.5.1

erooke avatar Jun 04 '25 02:06 erooke

Thanks @erooke, I have reformatted it as a runnable script (I edited your comment) with uv: uv run file.py.

jeertmans avatar Jun 04 '25 15:06 jeertmans

any updates on this? I am running into the same problem. @jeertmans

ufuk-cakir avatar Jun 18 '25 20:06 ufuk-cakir

wild guess: I think the problem happens if the run_time and the fps values are too short so a video gets rendered which is bascially an empty frame. A run_time of 0.025 is less than two frames at 60fps and only one frame at 30fps.

I managed to render my slides by changing the fps value, I guess equivalently one could increase the run_time of the animation

ufuk-cakir avatar Jun 18 '25 20:06 ufuk-cakir

Hi @ufuk-cakir, thanks for investigating this! I already had some issues when the video was too short to first one at least one frame, but I don't know if we can provide a good fix to this. This might be worth asking the team behind PyAV...

jeertmans avatar Jun 19 '25 09:06 jeertmans