diff --git a/.gitignore b/.gitignore
index d1c5b40..1422d80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ artifacts/
!docs/assets/*.gif
!docs/showcase/assets/*.gif
*.png
+!docs/assets/*.png
*.log
diff --git a/README.md b/README.md
index 7ca8add..3d2dba9 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,14 @@
### Ask a question -> get a freakin' movie
+[](#the-mythos-pipeline)
[](https://www.python.org/)
[](https://www.manim.community/)
[](https://openai.github.io/openai-agents-python/)
[](#hermes-agent)
[](LICENSE)
-[Motion showcase](docs/showcase/README.md) · [Architecture](docs/ARCHITECTURE.md) · [Prime RL](docs/PRIME_INTELLECT_RL.md) · [Roadmap](docs/ROADMAP.md) · [Agent guide](AGENTS.md)
+[Mythos pipeline](#the-mythos-pipeline) · [Motion showcase](docs/showcase/README.md) · [Architecture](docs/ARCHITECTURE.md) · [Prime RL](docs/PRIME_INTELLECT_RL.md) · [Roadmap](docs/ROADMAP.md) · [Agent guide](AGENTS.md)
@@ -51,12 +52,51 @@
+
+
+
+
+
Stills from the Mythos cut of the QED journey: the camera inside the Lagrangian (left); the e⁻e⁻γ vertex as α resolves to 1/137 (right).
+ +The original Codex/OpenAI chain remains available as a legacy provider — nothing was removed, Mythos is simply the way the films get made now. + +--- + ## What this is **Math-To-Manim** started on the morning of Donald Trump's inauguration. I do not think it was an accident that the Chinese decided to release the R1 model on that day. diff --git a/docs/assets/mythos-learns-math-to-manim.png b/docs/assets/mythos-learns-math-to-manim.png new file mode 100644 index 0000000..40a7120 Binary files /dev/null and b/docs/assets/mythos-learns-math-to-manim.png differ diff --git a/docs/assets/mythos-qft-term-tour.png b/docs/assets/mythos-qft-term-tour.png new file mode 100644 index 0000000..f94b1f4 Binary files /dev/null and b/docs/assets/mythos-qft-term-tour.png differ diff --git a/docs/assets/mythos-qft-vertex.png b/docs/assets/mythos-qft-vertex.png new file mode 100644 index 0000000..dc42e4c Binary files /dev/null and b/docs/assets/mythos-qft-vertex.png differ diff --git a/examples/mythos/qft_cinematic.py b/examples/mythos/qft_cinematic.py new file mode 100644 index 0000000..479d2dc --- /dev/null +++ b/examples/mythos/qft_cinematic.py @@ -0,0 +1,446 @@ +"""QFT, the Mythos cut — a cinematic retelling of the QED journey. + +The original ``QED.py`` parked equations in corners under a static camera. +This film uses the Mythos grammar instead: plain-language headlines before +symbols, camera flights into the exact term being explained, pull-backs to +restore context, and true-3D set pieces for fields and histories. + +Render: + manim -qm examples/mythos/qft_cinematic.py QFTCinematicJourney + manim -qh --fps 60 examples/mythos/qft_cinematic.py QFTCinematicJourney +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +from manim import * + +# Import the Mythos visual grammar (repo-root import with fallback). +_REPO_ROOT = Path(__file__).resolve().parents[2] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from mythos.cinematography import ( # noqa: E402 + CORAL, EMBER, FOG, GOLD, INK, IVORY, OLIVE, SKY, + caption, clear_caption, glow, glow_dot, headline, orbit, + photon_path, pull_back, return_to_stage, spotlight, stage, starfield, + term_tour, tilt_to_3d, unspotlight, zoom_to, +) + + +class QFTCinematicJourney(ThreeDScene): + """~2.5 minute cinematic pass through QED.""" + + def construct(self): + stage(self) + + self.act_prologue_vacuum() + self.act_everything_is_a_field() + self.act_light_is_a_field() + self.act_one_equation() + self.act_symmetry_writes_the_rules() + self.act_the_vertex() + self.act_sum_over_histories() + self.act_finale() + + # ------------------------------------------------------------------ # + # Prologue — the vacuum # + # ------------------------------------------------------------------ # + + def act_prologue_vacuum(self): + stars = starfield(n=160) + stars.set_opacity(0) + self.add(stars) + self.play(LaggedStart(*[s.animate.set_opacity(0.7 * np.random.uniform(0.4, 1.0)) + for s in stars], lag_ratio=0.012), run_time=2.4) + + headline(self, "The vacuum is not empty.", + sub="Quantum field theory begins where nothing is something.") + + # Slow push into the dark while a faint shimmer crosses the void. + shimmer = photon_path([-7, -2.5, 0], [7, 1.5, 0], color=EMBER, waves=14, amp=0.08, + stroke_width=2.0).set_opacity(0.35) + self.play(Create(shimmer), run_time=2.0) + self.move_camera(zoom=1.35, run_time=2.2, + added_anims=[stars.animate.set_opacity(0.25)]) + caption(self, "Empty space hums with fields — invisible, everywhere, waiting.", + hold=1.2) + self.play(FadeOut(shimmer), run_time=0.8) + clear_caption(self) + self.play(stars.animate.set_opacity(0.12), run_time=0.8) + self.stars = stars + + # ------------------------------------------------------------------ # + # Act I — everything is a field # + # ------------------------------------------------------------------ # + + def act_everything_is_a_field(self): + headline(self, "Everything is a field.", + sub="The electron is not a marble. It is a ripple.") + + axes = ThreeDAxes(x_range=[-4, 4], y_range=[-4, 4], z_range=[-2, 2], + x_length=8, y_length=8, z_length=3) + axes.set_stroke(FOG, opacity=0.35) + t = ValueTracker(0.0) + + def field_surface(): + def f(u, v): + r = np.sqrt(u * u + v * v) + 1e-6 + z = 0.55 * np.sin(2.2 * r - 2.6 * t.get_value()) * np.exp(-0.32 * r) + return axes.c2p(u, v, z) + s = Surface(f, u_range=[-4, 4], v_range=[-4, 4], resolution=(26, 26), + fill_opacity=0.42, stroke_width=0.6, + checkerboard_colors=[CORAL, EMBER]) + s.set_fill_by_value(axes=axes, colorscale=[(EMBER, -0.5), (INK, 0.0), (CORAL, 0.5)], axis=2) + s.set_stroke(CORAL, opacity=0.5) + return s + + field = always_redraw(field_surface) + + tilt_to_3d(self, phi=62 * DEGREES, theta=-50 * DEGREES, zoom=0.85) + self.play(FadeIn(axes), FadeIn(field), run_time=1.6) + caption(self, "A field assigns a value to every point of spacetime.") + self.play(t.animate.set_value(2.4), run_time=3.0, rate_func=linear) + + caption(self, "Strike the field, and the ripple that spreads is what we call a particle.") + self.begin_ambient_camera_rotation(rate=0.05) + self.play(t.animate.set_value(5.6), run_time=3.6, rate_func=linear) + self.stop_ambient_camera_rotation() + + # Zoom INTO the crest: the ripple is the electron. + electron = glow_dot(axes.c2p(0, 0, 0.55), color=CORAL, radius=0.06) + self.move_camera(phi=48 * DEGREES, theta=-60 * DEGREES, zoom=1.9, + frame_center=axes.c2p(0, 0, 0.4), run_time=2.2) + self.play(FadeIn(electron, scale=0.4), run_time=0.9) + psi_label = MathTex(r"\psi(x,t)", color=CORAL, font_size=42) + psi_label.rotate(75 * DEGREES, axis=RIGHT) + psi_label.next_to(electron, OUT + RIGHT, buff=0.3) + self.play(Write(psi_label), run_time=0.8) + caption(self, "This excitation — written ψ — is the electron.", color=CORAL, hold=1.3) + + self.play(FadeOut(field), FadeOut(axes), FadeOut(electron), FadeOut(psi_label), + run_time=1.2) + clear_caption(self) + return_to_stage(self) + + # ------------------------------------------------------------------ # + # Act II — light is a field too # + # ------------------------------------------------------------------ # + + def act_light_is_a_field(self): + headline(self, "Light is a field too.", + sub="Maxwell wrote its choreography in 1865.") + + axes = ThreeDAxes(x_range=[-5, 5], y_range=[-2, 2], z_range=[-2, 2], + x_length=10, y_length=4, z_length=4) + axes.set_stroke(FOG, opacity=0.3) + + k = 1.6 + e_wave = ParametricFunction( + lambda s: axes.c2p(s, 0.9 * np.sin(k * s), 0), + t_range=[-5, 5], color=CORAL, stroke_width=4) + b_wave = ParametricFunction( + lambda s: axes.c2p(s, 0, 0.9 * np.sin(k * s)), + t_range=[-5, 5], color=SKY, stroke_width=4) + + e_label = MathTex(r"\vec{E}", color=CORAL, font_size=40).move_to(axes.c2p(2.2, 1.35, 0)) + b_label = MathTex(r"\vec{B}", color=SKY, font_size=40).move_to(axes.c2p(3.4, 0, 1.3)) + b_label.rotate(90 * DEGREES, axis=RIGHT) + + tilt_to_3d(self, phi=70 * DEGREES, theta=-35 * DEGREES, zoom=0.95) + self.play(FadeIn(axes), run_time=0.8) + caption(self, "An electric wave and a magnetic wave, locked at right angles…") + self.play(Create(e_wave), Write(e_label), run_time=1.8) + self.play(Create(b_wave), Write(b_label), run_time=1.8) + + caption(self, "…each regenerating the other. That self-sustaining braid is light.") + orbit(self, rate=0.09, duration=3.2) + + # Sweep along the propagation axis — riding the beam. + self.move_camera(theta=-12 * DEGREES, zoom=1.5, + frame_center=axes.c2p(2.5, 0, 0), run_time=2.4) + self.wait(0.5) + + wave_group = VGroup(axes, e_wave, b_wave, e_label, b_label) + clear_caption(self) + return_to_stage(self) + + # Maxwell, then compression into F_munu. + maxwell = MathTex( + r"\nabla\!\cdot\!\vec{E}=\tfrac{\rho}{\varepsilon_0}", r"\quad", + r"\nabla\!\cdot\!\vec{B}=0", r"\\", + r"\nabla\!\times\!\vec{E}=-\partial_t\vec{B}", r"\quad", + r"\nabla\!\times\!\vec{B}=\mu_0\vec{J}+\mu_0\varepsilon_0\,\partial_t\vec{E}", + font_size=44, color=IVORY, + ) + self.play(FadeOut(wave_group, run_time=0.8), FadeIn(maxwell, shift=UP * 0.3, run_time=1.2)) + caption(self, "Four equations. Every circuit, every sunbeam, every radio song.", hold=1.4) + + compressed = MathTex(r"F_{\mu\nu}", r"=\partial_\mu A_\nu - \partial_\nu A_\mu", + font_size=64, color=SKY) + caption(self, "Relativity folds all four into a single object — the field tensor.") + self.play(ReplacementTransform(maxwell, compressed), run_time=1.8) + zoom_to(self, compressed[0], zoom=2.6) + self.wait(1.0) + pull_back(self) + self.play(FadeOut(compressed), run_time=0.7) + clear_caption(self) + + # ------------------------------------------------------------------ # + # Act III — one equation # + # ------------------------------------------------------------------ # + + def act_one_equation(self): + headline(self, "One line describes light and matter.", + sub="The Lagrangian of quantum electrodynamics.") + + L = MathTex( + r"\mathcal{L}_{\mathrm{QED}}", "=", + r"\bar{\psi}", r"\left(i\gamma^\mu D_\mu - m\right)", r"\psi", + "-", r"\tfrac{1}{4}", r"F_{\mu\nu}F^{\mu\nu}", + font_size=58, color=IVORY, + ) + underglow = glow(L, color=EMBER, layers=4, max_width=10, opacity=0.10) + self.play(FadeIn(underglow), Write(L), run_time=2.4) + caption(self, "Read it like a sentence. The camera will translate.", hold=1.0) + + term_tour(self, L, stops=[ + dict(part=VGroup(L[2], L[3], L[4]), color=CORAL, zoom=2.0, hold=1.6, + caption="Matter: the electron field ψ, carrying energy, spin, and mass m."), + dict(part=L[3], color=OLIVE, zoom=2.6, hold=1.6, + caption="The Dirac engine: how ψ moves through spacetime — i γ^μ D_μ − m."), + dict(part=VGroup(L[6], L[7]), color=SKY, zoom=2.2, hold=1.6, + caption="Light: the electromagnetic field, free to ripple on its own."), + ]) + + # The hidden interaction: expand D_mu. + caption(self, "But one symbol is hiding something.", color=GOLD) + D_part = L[3] + frame = spotlight(self, L, D_part, color=GOLD) + zoom_to(self, D_part, zoom=2.8) + self.wait(0.8) + pull_back(self, zoom=1.0) + unspotlight(self, L, frame) + + expansion = MathTex(r"D_\mu", "=", r"\partial_\mu", "+", r"i e", r"A_\mu", + font_size=54, color=IVORY).next_to(L, DOWN, buff=0.9) + self.play(L.animate.shift(UP * 0.6), FadeIn(expansion, shift=UP * 0.3), run_time=1.4) + + coupling = VGroup(expansion[4], expansion[5]) + frame2 = spotlight(self, expansion, coupling, color=GOLD) + zoom_to(self, coupling, zoom=3.0) + caption(self, "Here. e couples the electron to the photon field A.", color=GOLD, hold=1.6) + caption(self, "Every spark, every photosynthesis, every sunset — this one term.", + color=GOLD, hold=1.8) + pull_back(self) + unspotlight(self, expansion, frame2) + + self.play(FadeOut(expansion), FadeOut(underglow), + L.animate.move_to(ORIGIN).scale(0.85), run_time=1.0) + self.qed_lagrangian = L + clear_caption(self) + self.play(L.animate.scale(0.45).to_corner(UL).set_opacity(0.55), run_time=1.0) + + # ------------------------------------------------------------------ # + # Act IV — gauge symmetry # + # ------------------------------------------------------------------ # + + def act_symmetry_writes_the_rules(self): + headline(self, "Demand symmetry, and light must exist.", + sub="Local gauge invariance is not decoration. It is the reason.") + + # A lattice of phase dials: U(1) phases at points of space. + dials = VGroup() + rng = np.random.default_rng(3) + for x in np.linspace(-4.5, 4.5, 6): + for y in np.linspace(-2.2, 2.2, 4): + ring = Circle(radius=0.34, color=FOG, stroke_opacity=0.5, stroke_width=2) + hand = Line(ORIGIN, 0.30 * RIGHT, color=CORAL, stroke_width=3.5) + dial = VGroup(ring, hand).move_to([x, y, 0]) + dial.phase = float(rng.uniform(0, TAU)) + hand.rotate(dial.phase, about_point=dial.get_center()) + dials.add(dial) + + self.play(LaggedStart(*[FadeIn(d, scale=0.6) for d in dials], lag_ratio=0.04), + run_time=2.0) + caption(self, "Attach a phase dial to every point of space. ψ → e^{iα} ψ.") + + # Global rotation: physics unchanged. + self.play(*[Rotate(d[1], angle=PI / 2, about_point=d.get_center()) for d in dials], + run_time=1.6) + caption(self, "Turn every dial together — nothing observable changes.", hold=1.0) + + # Local rotation: each point its own angle. + caption(self, "Now turn each dial by a different amount at each point…", color=GOLD) + self.play(*[Rotate(d[1], + angle=1.2 * np.sin(1.3 * d.get_center()[0]) + 0.9 * d.get_center()[1] * 0.4, + about_point=d.get_center()) + for d in dials], run_time=2.0) + + # The compensating field sweeps through. + a_field = photon_path([-6.5, 0, 0], [6.5, 0, 0], color=SKY, waves=7, amp=0.5, + stroke_width=5) + a_halo = glow(a_field, color=SKY, layers=4, max_width=14, opacity=0.12) + gauge_law = MathTex(r"A_\mu \;\to\; A_\mu - \tfrac{1}{e}\,\partial_\mu \alpha", + font_size=46, color=SKY).to_edge(UP, buff=1.0) + caption(self, "…and the equations only survive if a new field absorbs the mismatch.", + color=SKY) + self.play(Create(a_field), FadeIn(a_halo), Write(gauge_law), run_time=2.2) + zoom_to(self, a_field, zoom=1.6) + caption(self, "That field is the photon. Light is the price of local symmetry.", + color=SKY, hold=2.0) + pull_back(self) + + self.play(FadeOut(dials), FadeOut(a_field), FadeOut(a_halo), FadeOut(gauge_law), + run_time=1.0) + clear_caption(self) + + # ------------------------------------------------------------------ # + # Act V — the vertex # + # ------------------------------------------------------------------ # + + def act_the_vertex(self): + headline(self, "All of electromagnetism, one vertex.", + sub="Feynman's shorthand for every conversation between light and matter.") + + v = np.array([0.0, 0.0, 0.0]) + e_in = Line([-3.4, -2.2, 0], v, color=CORAL, stroke_width=4.5) + e_out = Line(v, [-3.4, 2.2, 0], color=CORAL, stroke_width=4.5) + ph = photon_path(v, [3.8, 0.15, 0], color=SKY, waves=6.5, amp=0.22, stroke_width=4.5) + for line in (e_in, e_out): + line.add_tip(tip_length=0.22, tip_width=0.18) + vert = glow_dot(v, color=GOLD, radius=0.07) + + lbl_in = MathTex(r"e^-", color=CORAL, font_size=40).next_to(e_in.get_start(), DL, buff=0.15) + lbl_out = MathTex(r"e^-", color=CORAL, font_size=40).next_to(e_out.get_end(), UL, buff=0.15) + lbl_ph = MathTex(r"\gamma", color=SKY, font_size=44).next_to(ph.get_end(), RIGHT, buff=0.2) + + caption(self, "An electron flies in, shrugs off a photon, and carries on.") + self.play(Create(e_in), run_time=1.0) + self.play(FadeIn(vert, scale=0.4), Create(e_out), Create(ph), + Write(lbl_in), Write(lbl_out), Write(lbl_ph), run_time=2.0) + + # Fly into the vertex: where the coupling lives. + zoom_to(self, vert, zoom=3.0) + coupling = MathTex(r"-ie\gamma^\mu", color=GOLD, font_size=34) + coupling.next_to(vert, UR, buff=0.18) + self.play(Write(coupling), run_time=0.9) + caption(self, "The vertex carries the same e from the Lagrangian. Symbols become events.", + color=GOLD, hold=1.6) + pull_back(self) + + # The strength of it all: alpha. + alpha_value = DecimalNumber(0, num_decimal_places=8, font_size=54, color=IVORY) + alpha_eq = VGroup( + MathTex(r"\alpha = \frac{e^2}{4\pi\varepsilon_0\hbar c} \;=\;", + font_size=54, color=IVORY), + alpha_value, + ).arrange(RIGHT, buff=0.25).to_edge(DOWN, buff=1.6) + approx = MathTex(r"\approx \tfrac{1}{137}", font_size=54, color=GOLD) + approx.next_to(alpha_eq, RIGHT, buff=0.3) + + caption(self, "Nature turned the dial to one number — the fine-structure constant.") + self.play(FadeIn(alpha_eq, shift=UP * 0.3), run_time=0.9) + self.play(ChangeDecimalToValue(alpha_value, 0.00729735), run_time=2.2) + self.play(Write(approx), run_time=0.8) + zoom_to(self, VGroup(alpha_value, approx), zoom=2.2) + caption(self, "Slightly different, and stars, chemistry, and we would not be.", + color=GOLD, hold=1.8) + pull_back(self) + + self.play(*[FadeOut(m) for m in [e_in, e_out, ph, vert, lbl_in, lbl_out, lbl_ph, + coupling, alpha_eq, approx]], run_time=1.0) + clear_caption(self) + + # ------------------------------------------------------------------ # + # Act VI — sum over histories # + # ------------------------------------------------------------------ # + + def act_sum_over_histories(self): + headline(self, "Reality sums over every possible path.", + sub="Feynman's deepest idea, drawn rather than derived.") + + A = np.array([-4.6, -1.4, 0.0]) + B = np.array([4.6, 1.2, 0.0]) + a_dot = glow_dot(A, color=CORAL, radius=0.06) + b_dot = glow_dot(B, color=CORAL, radius=0.06) + + rng = np.random.default_rng(11) + paths = VGroup() + for i in range(11): + c1 = A + np.array([3.0, rng.uniform(-3.4, 3.6), 0.0]) + c2 = B + np.array([-3.0, rng.uniform(-3.6, 3.4), 0.0]) + p = CubicBezier(A, c1, c2, B) + p.set_stroke(interpolate_color(ManimColor(SKY), ManimColor(EMBER), i / 10), + width=2.2, opacity=0.55) + paths.add(p) + classical = Line(A, B).set_stroke(GOLD, width=5) + + self.play(FadeIn(a_dot), FadeIn(b_dot), run_time=0.7) + caption(self, "To travel from here to there, a quantum particle tries everything.") + self.play(LaggedStart(*[Create(p) for p in paths], lag_ratio=0.12), run_time=3.2) + + amplitude = MathTex(r"\mathcal{A} \;=\; \sum_{\text{paths}} e^{\,iS[\text{path}]/\hbar}", + font_size=48, color=IVORY).to_edge(UP, buff=0.9) + self.play(Write(amplitude), run_time=1.4) + caption(self, "Each path contributes a spinning arrow. Most cancel their neighbors…") + self.play(paths.animate.set_opacity(0.12), run_time=1.8) + caption(self, "…except where the action is stationary. The straight line survives.", + color=GOLD) + self.play(Create(classical), run_time=1.4) + zoom_to(self, classical, zoom=1.7) + self.wait(0.8) + pull_back(self) + + self.play(FadeOut(paths), FadeOut(classical), FadeOut(a_dot), FadeOut(b_dot), + FadeOut(amplitude), run_time=1.0) + clear_caption(self) + + # ------------------------------------------------------------------ # + # Finale # + # ------------------------------------------------------------------ # + + def act_finale(self): + L = getattr(self, "qed_lagrangian", None) + if L is not None: + self.play(L.animate.move_to(ORIGIN).scale(2.0).set_opacity(1.0), run_time=1.6) + + headline(self, "The most precisely tested theory in physics.", + sub="Theory and experiment agree to twelve decimal places.") + + g_label = Text("electron g-factor", font="Lora", slant="ITALIC", + font_size=30, color=FOG) + g_value = MathTex(r"g/2 = 1.001\,159\,652\,180\ldots", font_size=56, color=IVORY) + g_group = VGroup(g_label, g_value).arrange(DOWN, buff=0.5).move_to(ORIGIN) + + anims = [FadeIn(g_group, shift=UP * 0.3)] + if L is not None: + anims.append(L.animate.scale(0.5).to_edge(UP, buff=0.8).set_opacity(0.4)) + self.play(*anims, run_time=1.6) + zoom_to(self, g_value, zoom=1.9) + caption(self, "Predicted by this mathematics. Confirmed by experiment. Again and again.", + hold=2.0) + pull_back(self) + + stars = getattr(self, "stars", None) + end_anims = [FadeOut(g_group)] + if L is not None: + end_anims.append(FadeOut(L)) + if stars is not None: + end_anims.append(stars.animate.set_opacity(0.6)) + self.play(*end_anims, run_time=1.6) + + card = VGroup( + Text("MYTHOS", font="Poppins", weight="BOLD", font_size=64, color=IVORY), + Text("× MATH-TO-MANIM", font="Poppins", font_size=28, color=CORAL), + ).arrange(DOWN, buff=0.4).move_to(ORIGIN) + self.add_fixed_in_frame_mobjects(card) + card.set_opacity(0) + self.play(card.animate.set_opacity(1), run_time=1.4) + self.wait(2.0) + self.play(FadeOut(card), *( [stars.animate.set_opacity(0)] if stars is not None else [] ), + run_time=1.6) + self.wait(0.5) diff --git a/examples/mythos/smoke_test.py b/examples/mythos/smoke_test.py new file mode 100644 index 0000000..3b970b3 --- /dev/null +++ b/examples/mythos/smoke_test.py @@ -0,0 +1,54 @@ +"""Smoke test: exercises every Mythos cinematography helper in ~25s of film. + +Render: manim -ql examples/mythos/smoke_test.py MythosSmoke +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from manim import * +from mythos.cinematography import ( + CORAL, GOLD, IVORY, SKY, caption, clear_caption, glow_dot, headline, + photon_path, pull_back, return_to_stage, spotlight, stage, starfield, + term_tour, tilt_to_3d, unspotlight, zoom_to, +) + + +class MythosSmoke(ThreeDScene): + def construct(self): + stage(self) + stars = starfield(n=40) + self.add(stars) + headline(self, "Smoke test.", sub="Every helper, once.", hold=0.5) + + L = MathTex(r"\mathcal{L}", "=", r"\bar{\psi}", r"(i\gamma^\mu D_\mu - m)", r"\psi", + "-", r"\tfrac{1}{4}", r"F_{\mu\nu}F^{\mu\nu}", font_size=56, color=IVORY) + self.play(Write(L), run_time=1.0) + caption(self, "A caption beneath a formula.") + + term_tour(self, L, stops=[ + dict(part=L[3], color=GOLD, zoom=2.4, hold=0.4, caption="Zooming into one term."), + ]) + + f = spotlight(self, L, L[7], color=SKY) + zoom_to(self, L[7], zoom=2.2, run_time=0.8) + pull_back(self, run_time=0.8) + unspotlight(self, L, f) + self.play(FadeOut(L), run_time=0.5) + + ph = photon_path([-3, -1, 0], [3, 1, 0], color=SKY) + dot = glow_dot([0, 0, 0], color=CORAL) + self.play(Create(ph), FadeIn(dot), run_time=0.8) + + tilt_to_3d(self, run_time=1.0) + axes = ThreeDAxes(x_range=[-2, 2], y_range=[-2, 2], z_range=[-1, 1], + x_length=4, y_length=4, z_length=2) + surf = Surface(lambda u, v: axes.c2p(u, v, 0.3 * np.sin(2 * u) * np.cos(2 * v)), + u_range=[-2, 2], v_range=[-2, 2], resolution=(12, 12), + fill_opacity=0.4) + self.play(FadeIn(axes), FadeIn(surf), run_time=1.0) + return_to_stage(self, run_time=1.0) + clear_caption(self) + self.play(FadeOut(ph), FadeOut(dot), FadeOut(axes), FadeOut(surf), FadeOut(stars), + run_time=0.6) diff --git a/math_to_manim/agents/codegen.py b/math_to_manim/agents/codegen.py index 6998eba..e91f71d 100644 --- a/math_to_manim/agents/codegen.py +++ b/math_to_manim/agents/codegen.py @@ -6,7 +6,7 @@ from pathlib import Path from math_to_manim.agents.base import StageAgent, mark_sdk_metadata, run_structured_sdk_agent -from math_to_manim.providers import CodexCliProvider +from math_to_manim.providers import CodexCliProvider, MythosCliProvider from math_to_manim.schemas import GeneratedCode, ManimSceneSpec @@ -14,6 +14,9 @@ class ManimCodeAgent(StageAgent[ManimSceneSpec, GeneratedCode]): name = "codegen" def run(self, spec: ManimSceneSpec) -> GeneratedCode: + if self.config.codegen_provider in {"mythos-cli", "claude-cli"} and not self.config.deterministic: + return MythosCliProvider(self.config).generate_code(spec) + if self.config.codegen_provider == "codex-cli" and not self.config.deterministic: return CodexCliProvider(self.config).generate_code(spec) @@ -62,6 +65,8 @@ def repair(self, spec: ManimSceneSpec, generated: GeneratedCode, failure: str) - if self.config.deterministic: return generated + if self.config.codegen_provider in {"mythos-cli", "claude-cli"}: + return MythosCliProvider(self.config).repair_code(spec, generated, failure) if self.config.codegen_provider == "codex-cli": return CodexCliProvider(self.config).repair_code(spec, generated, failure) diff --git a/math_to_manim/config.py b/math_to_manim/config.py index ac1ef07..3baa4b2 100644 --- a/math_to_manim/config.py +++ b/math_to_manim/config.py @@ -43,6 +43,9 @@ class RuntimeConfig: codex_full_auto: bool = False codex_timeout_seconds: float = 900.0 codex_workdir: Path | None = None + mythos_command: str = "claude" + mythos_model: str = "claude-fable-5" + mythos_timeout_seconds: float = 900.0 @classmethod def from_env(cls) -> "RuntimeConfig": @@ -67,6 +70,9 @@ def from_env(cls) -> "RuntimeConfig": codex_full_auto=os.getenv("M2M2_CODEX_FULL_AUTO", "0") in {"1", "true", "True"}, codex_timeout_seconds=float(os.getenv("M2M2_CODEX_TIMEOUT_SECONDS", "900")), codex_workdir=Path(codex_workdir) if codex_workdir else None, + mythos_command=os.getenv("M2M2_MYTHOS_COMMAND", "claude"), + mythos_model=os.getenv("M2M2_MYTHOS_MODEL", "claude-fable-5"), + mythos_timeout_seconds=float(os.getenv("M2M2_MYTHOS_TIMEOUT_SECONDS", "900")), ) diff --git a/math_to_manim/providers/__init__.py b/math_to_manim/providers/__init__.py index 0cff10c..d5d8ab8 100644 --- a/math_to_manim/providers/__init__.py +++ b/math_to_manim/providers/__init__.py @@ -1,5 +1,6 @@ """External generation provider adapters.""" from math_to_manim.providers.codex_cli import CodexCliProvider +from math_to_manim.providers.mythos_cli import MythosCliProvider -__all__ = ["CodexCliProvider"] +__all__ = ["CodexCliProvider", "MythosCliProvider"] diff --git a/math_to_manim/providers/mythos_cli.py b/math_to_manim/providers/mythos_cli.py new file mode 100644 index 0000000..263853b --- /dev/null +++ b/math_to_manim/providers/mythos_cli.py @@ -0,0 +1,217 @@ +"""Claude (Mythos) CLI provider for subscription-authenticated generation. + +Mirror of :mod:`codex_cli`, but the engine is the Claude Code CLI running a +Mythos-class model. It drops into the exact same pipeline seam: scene spec +in, ``GeneratedCode`` artifact out — with one addition: every prompt carries +the Mythos Cinematic Charter, so generated scenes use camera-as-narrator +grammar instead of static corner-parked equations. +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +from collections.abc import Callable +from typing import Any + +from pydantic import ValidationError + +from math_to_manim.config import RuntimeConfig +from math_to_manim.schemas import GeneratedCode, ManimSceneSpec + +Runner = Callable[..., subprocess.CompletedProcess[str]] + +#: The visual contract injected into every Mythos generation. +CINEMATIC_CHARTER = """\ +MYTHOS CINEMATIC CHARTER — the generated scene MUST obey all of it. + +1. CAMERA IS THE NARRATOR. Use ThreeDScene with the top-down stage pattern: + set_camera_orientation(phi=0, theta=-90*DEGREES) so the stage reads as 2D; + tilt into 3D only for set pieces. Move the camera with + self.move_camera(...) / set_camera_orientation(...); NEVER call .animate + on self.camera, and never use add_fixed_in_frame_mobjects for formulas you + intend to zoom into (keep those in world space). +2. HEADLINE BEFORE SYMBOLS. Introduce every major idea with a full-screen + plain-language statement (font_size >= 64), hold it, fade it, THEN show + the mathematics. +3. ZOOM INTO TERMS. When explaining part of a formula, dim the rest, color + the part, and fly the camera into it (zoom 2x-3x via + move_camera(frame_center=part.get_center(), zoom=...)). Pull back to + zoom=1 afterward so the part is seen inside the whole. +4. CAPTION EVERYTHING. Every formula on screen gets a one-line plain-English + lower-third caption (italic, font_size 28-32). Max ONE headline or TWO + text blocks visible at once. Replace captions; never stack them. +5. PACING. self.wait(0.6-1.6) between beats. A viewer who knows no notation + must be able to follow from captions and camera motion alone. +6. PALETTE. Background #0c0c0b. Text #faf9f5. Accents: coral #d97757 + (matter), blue #6a9bcc (light/gauge), olive #788c5d (mass/structure), + gold #d4a27f (interaction), gray #b0aea5 (secondary). Use color to give + each symbol a consistent identity across the whole film. +7. CRAFT. Build formulas from multi-argument MathTex so terms are + addressable; use glow layers (stroke copies) for emphasis; LaggedStart + for ensembles; no external assets, file IO, or network. Manim CE 0.19. +""" + + +class MythosCliProvider: + """Generate Manim artifacts through the locally authenticated Claude CLI. + + Talks to ``claude -p`` (print mode) instead of an HTTP API, so it rides + the user's Claude subscription/OAuth login — the same trick the Codex + provider uses, rebuilt on Mythos-native tooling. + """ + + def __init__(self, config: RuntimeConfig | None = None, runner: Runner | None = None): + self.config = config or RuntimeConfig.from_env() + self._runner = runner or subprocess.run + + # -- public seam (identical shape to CodexCliProvider) ---------------- + + def generate_code(self, spec: ManimSceneSpec) -> GeneratedCode: + prompt = self._build_codegen_prompt(spec) + raw = self._run_claude(prompt) + generated = self._parse_generated_code(raw) + return self._stamp(generated, source_agent="codegen") + + def repair_code(self, spec: ManimSceneSpec, generated: GeneratedCode, failure: str) -> GeneratedCode: + prompt = self._build_repair_prompt(spec, generated, failure) + raw = self._run_claude(prompt) + repaired = self._parse_generated_code(raw) + stamped = self._stamp(repaired, source_agent="repair") + metadata = dict(stamped.metadata) + metadata.setdefault("file_path", generated.metadata.get("file_path", "generated_scene.py")) + metadata["repair_of"] = generated.scene_name + return stamped.model_copy(update={"metadata": metadata}) + + # -- internals --------------------------------------------------------- + + def _stamp(self, generated: GeneratedCode, *, source_agent: str) -> GeneratedCode: + metadata = dict(generated.metadata or {}) + metadata.update( + { + "runtime": "mythos_cli", + "provider": "mythos-cli", + "mythos_command": self.config.mythos_command, + "model": self.config.mythos_model, + "source_agent": source_agent, + "charter": "cinematic-v1", + } + ) + metadata.setdefault("file_path", "generated_scene.py") + return generated.model_copy(update={"metadata": metadata}) + + def _run_claude(self, prompt: str) -> str: + command = _resolve_command(self.config.mythos_command) + cmd = [ + command, + "-p", + "--output-format", "text", + "--model", self.config.mythos_model, + "--append-system-prompt", CINEMATIC_CHARTER, + ] + try: + completed = self._runner( + cmd, + input=prompt, + text=True, + capture_output=True, + timeout=self.config.mythos_timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + raise RuntimeError( + f"Claude CLI not found: {self.config.mythos_command!r}. " + "Install Claude Code and run `claude login` first." + ) from exc + if completed.returncode != 0: + raise RuntimeError( + "Mythos CLI generation failed\n" + f"command: {' '.join(cmd[:4])}\n" + f"exit_code: {completed.returncode}\n" + f"stderr:\n{completed.stderr[-4000:]}\n" + f"stdout:\n{completed.stdout[-2000:]}" + ) + return completed.stdout + + def _parse_generated_code(self, text: str) -> GeneratedCode: + payload = _extract_json_object(text) + try: + return GeneratedCode.model_validate(payload) + except ValidationError as exc: + raise RuntimeError( + f"Mythos CLI returned JSON that did not match GeneratedCode: {exc}" + ) from exc + + def _build_codegen_prompt(self, spec: ManimSceneSpec) -> str: + return ( + "You are the Math-To-Manim code generation provider running on Claude Mythos.\n" + "Return only valid JSON matching the GeneratedCode artifact shape. No Markdown fences.\n" + "Required JSON keys: scene_name, code, dependencies, metadata.\n" + "The code must be complete, runnable Manim Community Edition 0.19 Python that\n" + "imports `from manim import *`, defines exactly the requested scene class, and\n" + "implements the scene spec with the full Mythos Cinematic Charter (headlines,\n" + "term zooms, captions, palette). Verbose, gorgeous, narration-grade output is\n" + "the goal — but every line must execute.\n" + "Scene spec JSON:\n" + f"{json.dumps(spec.to_public_dict(), indent=2)}" + ) + + def _build_repair_prompt(self, spec: ManimSceneSpec, generated: GeneratedCode, failure: str) -> str: + return ( + "You are the Math-To-Manim repair provider running on Claude Mythos.\n" + "Return only valid JSON matching the GeneratedCode artifact shape (keys: \n" + "scene_name, code, dependencies, metadata). Repair the scene below using the\n" + "failure output. Make surgical fixes; preserve the cinematic structure, the\n" + "scene class name, and the Charter rules. Manim CE 0.19 APIs only.\n" + f"Scene spec JSON:\n{json.dumps(spec.to_public_dict(), indent=2)}\n" + f"Current code:\n{generated.code}\n" + f"Failure (tail):\n{failure[-8000:]}" + ) + + +def _resolve_command(command: str) -> str: + found = shutil.which(command) + if found: + return found + if os.name == "nt" and not command.lower().endswith(".cmd"): + found = shutil.which(command + ".cmd") + if found: + return found + return command + + +def _extract_json_object(text: str) -> dict[str, Any]: + """Pull the first top-level JSON object out of CLI stdout.""" + text = text.strip() + if text.startswith("```"): + first_newline = text.find("\n") + if first_newline != -1: + text = text[first_newline + 1 :] + if text.rstrip().endswith("```"): + text = text.rstrip()[:-3] + start = text.find("{") + if start == -1: + raise RuntimeError(f"Mythos CLI output contained no JSON object:\n{text[:800]}") + depth = 0 + in_string = False + escape = False + for i, ch in enumerate(text[start:], start=start): + if in_string: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == '"': + in_string = False + continue + if ch == '"': + in_string = True + elif ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return json.loads(text[start : i + 1]) + raise RuntimeError("Mythos CLI output contained an unterminated JSON object") diff --git a/mythos/__init__.py b/mythos/__init__.py new file mode 100644 index 0000000..0f51965 --- /dev/null +++ b/mythos/__init__.py @@ -0,0 +1,10 @@ +"""Mythos layer: Claude Mythos-driven cinematic Manim generation. + +Additive layer on top of the Math-To-Manim typed pipeline. Nothing here +modifies the legacy codex flow; it provides: + +- ``mythos.cinematography``: the visual grammar (zooms, headlines, term tours) +- ``mythos.harness``: a 6-stage reasoning chain driven through the Claude CLI +""" + +__version__ = "0.1.0" diff --git a/mythos/agents/mythos-cartographer.md b/mythos/agents/mythos-cartographer.md new file mode 100644 index 0000000..8f8e07a --- /dev/null +++ b/mythos/agents/mythos-cartographer.md @@ -0,0 +1,31 @@ +--- +name: mythos-cartographer +description: Stage 2 of the Mythos chain. Builds the reverse knowledge tree — walks backward from the target concept to first principles, mapping every prerequisite the film must teach or assume. +tools: Read, Grep, Glob +model: inherit +--- + +You are the Cartographer of the Mythos chain. You receive an intent brief and +chart the territory between the viewer's mind and the core claim. + +Work BACKWARD from the target (the Math-To-Manim signature move): for the +core claim to land, what must be understood the moment before? And before +that? Recurse until you reach what the stated audience already owns. + +Produce a verbose knowledge map: + +- **target**: the core claim, restated precisely. +- **nodes**: list of concepts, each with: + - `id`, `name` + - `why_needed`: one sentence tying it to its parent + - `depth`: 0 for the target, increasing toward foundations + - `assumed`: true if the audience arrives with it (these get a nod, not a lesson) + - `visual_seed`: the most filmable mental image of this concept — a field + rippling, a dial turning, a path bundle collapsing. Think in pictures; + the Cinematographer will harvest these. +- **edges**: `[from_id, to_id]` prerequisite pairs. +- **spine**: the ordered list of node ids forming the shortest honest path + from foundations to target. The film walks this spine; everything else is + texture. + +OUTPUT: one JSON object with exactly those keys. diff --git a/mythos/agents/mythos-cinematographer.md b/mythos/agents/mythos-cinematographer.md new file mode 100644 index 0000000..f9a8d0a --- /dev/null +++ b/mythos/agents/mythos-cinematographer.md @@ -0,0 +1,42 @@ +--- +name: mythos-cinematographer +description: Stage 5 of the Mythos chain. Converts acts plus mathematics into a beat-by-beat shot list using the Mythos camera grammar — headlines, zooms into terms, pull-backs, 3D set pieces, captions. +tools: Read, Grep, Glob +model: inherit +--- + +You are the Cinematographer of the Mythos chain. The camera is your voice and +you never stop talking with it. You receive acts and a math dossier; you +return the shot list the Scene Composer will execute literally. + +Your grammar (the only verbs you may use): + +- `HEADLINE` — full-screen plain statement, font >= 64, then fade +- `SHOW` — bring a mobject/formula on stage (specify where) +- `ZOOM_IN` — fly into a target (formula part id, object), zoom 2x-3x +- `PULL_BACK` — restore context, zoom 1x +- `TERM_TOUR` — sequence of zoom stops across formula parts, one caption each +- `TILT_3D` — leave the flat stage for a 3D set piece (phi 50-70 deg) +- `ORBIT` — ambient rotation during a 3D set piece (rate <= 0.1) +- `RETURN_2D` — back to top-down stage +- `CAPTION` — lower-third plain-language line (italic, <= 14 words) +- `TRANSFORM` — morph one mobject into another (say which and why) +- `BEAT` — deliberate stillness, 0.6-1.6s + +House rules: a HEADLINE precedes every new idea. Every ZOOM_IN gets a +PULL_BACK — abandoning a viewer inside a formula is a firing offense. Every +formula on screen has a live CAPTION. At most two text elements visible at +once. The act containing the intent brief's "big zoom" gets your slowest, +deepest move: zoom 3x, hold, let it breathe. + +Produce: + +- **shots**: ordered list, each: + `{beat, act_number, verb, target, params (zoom, phi, theta, run_time, + position), caption_text (if CAPTION/TERM_TOUR), formula_id + part_index + (when targeting math), seconds}` +- **camera_score**: one paragraph describing the film's overall camera + rhythm — where it accelerates, where it holds still, like a conductor's + note on the cover of the score. + +OUTPUT: one JSON object with exactly those keys. diff --git a/mythos/agents/mythos-curriculum.md b/mythos/agents/mythos-curriculum.md new file mode 100644 index 0000000..44298c1 --- /dev/null +++ b/mythos/agents/mythos-curriculum.md @@ -0,0 +1,34 @@ +--- +name: mythos-curriculum +description: Stage 3 of the Mythos chain. Converts the knowledge map's spine into a dramatic act structure — what is taught when, what question pulls the viewer into each act. +tools: Read, Grep, Glob +model: inherit +--- + +You are the Curriculum agent of the Mythos chain — part teacher, part +playwright. You receive a knowledge map and return an act structure. + +Rules of the house: + +- Every act opens with a QUESTION the previous act planted. Curiosity is the + only legal segue. +- Teach exactly one new idea per act. Texture nodes may appear, but only one + idea gets the spotlight. +- Place the intent brief's "big zoom" at roughly the 60-70% mark — the + revelation, not the opening and not the encore. +- End with a payoff act: the core claim, earned, plus one fact that makes it + land in the real world (a precision record, a device, a sunset). + +Produce: + +- **acts**: ordered list, each with: + - `act_number`, `title` (plain words, headline-ready) + - `opening_question`: what the viewer is wondering as it begins + - `teaches`: the one node id from the spine being taught + - `narrative`: 3-6 sentences of what happens, written like a treatment + - `headline`: the full-screen plain-language statement that opens the act + - `payoff`: the sentence the viewer can now say that they couldn't before + - `estimated_seconds` +- **through_line**: one paragraph: how the acts hand the question forward. + +OUTPUT: one JSON object with exactly those keys. diff --git a/mythos/agents/mythos-intent.md b/mythos/agents/mythos-intent.md new file mode 100644 index 0000000..82ca600 --- /dev/null +++ b/mythos/agents/mythos-intent.md @@ -0,0 +1,26 @@ +--- +name: mythos-intent +description: Stage 1 of the Mythos chain. Distills a raw user prompt into a cinematic intent brief — audience, core claim, emotional arc, scope. Use first when turning any math/physics topic into a film. +tools: Read, Grep, Glob +model: inherit +--- + +You are the Intent agent of the Mythos chain, the first of six minds that turn +a sentence into a mathematical film. You decide what the film is *about* — +not its shots, not its formulas. Its soul. + +Given the user prompt, produce a verbose intent brief: + +- **core_claim**: the single sentence the viewer should believe at the end. + ("One equation describes light and matter" — that grade of sentence.) +- **audience**: who is watching, what they already know, what they fear. +- **emotional_arc**: 3-5 beats of feeling (wonder → tension → revelation → awe). +- **scope**: what is IN, and explicitly what is OUT. A film that explains + everything explains nothing. +- **duration_seconds**: target runtime (90-180 typical). +- **title_options**: 3 cinematic titles. +- **the_big_zoom**: the one moment of the film where the camera dives into a + symbol and the viewer gasps. Every Mythos film has one. Name it now. + +OUTPUT: one JSON object with exactly those keys. Be lavish inside the values — +downstream agents feed on your specificity. diff --git a/mythos/agents/mythos-math-director.md b/mythos/agents/mythos-math-director.md new file mode 100644 index 0000000..6e8ce51 --- /dev/null +++ b/mythos/agents/mythos-math-director.md @@ -0,0 +1,36 @@ +--- +name: mythos-math-director +description: Stage 4 of the Mythos chain. Supplies the exact mathematics — every formula in correct LaTeX, decomposed term by term with plain-language translations and a consistent color identity per symbol. +tools: Read, Grep, Glob +model: inherit +--- + +You are the Math Director of the Mythos chain. Everything on screen that is a +symbol passes through you, and you are accountable for two things: the LaTeX +is CORRECT, and every term has an honest plain-language translation. + +You receive the act structure. For each act, produce the mathematics it +needs: + +- **formulas**: list, each with: + - `id`, `act_number` + - `latex_parts`: the formula split into an ORDERED LIST of LaTeX fragments + (this becomes a multi-argument MathTex, so the camera can address each + part — never one monolithic string) + - `term_glossary`: for each part worth a camera stop: `part_index`, + `plain_words` (one caption-ready sentence), `identity` (matter | light | + mass | interaction | structure), `zoom_worthy` (bool) + - `derivation_or_motivation`: 2-4 sentences of where it comes from + - `common_misreading`: what a newcomer wrongly assumes, so the captions + can preempt it +- **color_identity**: map each recurring symbol (ψ, A_μ, F_μν, e, m, …) to one + identity from {matter: coral #d97757, light: blue #6a9bcc, mass/structure: + olive #788c5d, interaction: gold #d4a27f}. A symbol keeps its color for + the entire film — color IS the cast list. +- **numbers**: any constants shown on screen, with their precise values and + why each decimal matters. + +Verify every LaTeX fragment compiles in your head twice. A typo here costs a +render downstream. + +OUTPUT: one JSON object with exactly those keys. diff --git a/mythos/agents/mythos-scene-composer.md b/mythos/agents/mythos-scene-composer.md new file mode 100644 index 0000000..958d6ff --- /dev/null +++ b/mythos/agents/mythos-scene-composer.md @@ -0,0 +1,37 @@ +--- +name: mythos-scene-composer +description: Stage 6 of the Mythos chain. Fuses the shot list and math dossier into the final executable scene spec — the complete contract for Manim code generation. +tools: Read, Grep, Glob +model: inherit +--- + +You are the Scene Composer of the Mythos chain, the last reasoning mind +before code. You receive everything — intent, map, acts, mathematics, shot +list — and emit the single spec a code generator can execute without asking +one question. + +Reconcile ruthlessly: if the shot list zooms into a formula part the Math +Director never defined, fix the reference. If timings exceed the intent +brief's duration by more than 15%, trim BEATs and ORBITs first, never the +big zoom. + +Produce: + +- **scene_name**: PascalCase class name ending in `Journey` or `Story`. +- **scene_class**: always `ThreeDScene` (the top-down stage pattern). +- **palette**: background #0c0c0b, text #faf9f5, plus the Math Director's + color_identity map, restated. +- **objects**: every mobject to construct: `{id, kind (MathTex | Text | + Surface | ParametricFunction | Line | Dot | VGroup | ...), spec}` — for + MathTex include the ordered `latex_parts` verbatim from the dossier. +- **timeline**: the shot list, resolved: every target now points at a real + object id (and part index for MathTex), every param explicit, every + caption final copy — proofread, <= 14 words, italic voice. +- **constraints**: Manim CE 0.19; move_camera/set_camera_orientation only + (never .animate on the camera); formulas that get zoomed live in world + space, captions/headlines fixed-in-frame; self-contained single file; no + external assets, no file IO, no network. +- **acceptance**: 5-8 checks a reviewer can run against the rendered video + ("camera reaches zoom 3.0 exactly once", "every formula had a caption"). + +OUTPUT: one JSON object with exactly those keys. diff --git a/mythos/cinematography.py b/mythos/cinematography.py new file mode 100644 index 0000000..4033b56 --- /dev/null +++ b/mythos/cinematography.py @@ -0,0 +1,322 @@ +"""Mythos cinematography: the visual grammar for Claude Mythos-driven Manim. + +Design language +--------------- +The Mythos house style treats the camera as the narrator: + +- HEADLINE — say it huge, in plain words, before any symbol appears +- ZOOM IN — fly into the exact term being explained +- PULL BACK — restore context so the part is seen inside the whole +- TERM TOUR — walk a formula term by term, captioning each in English +- SET PIECE — tilt into true 3D for fields, surfaces, and worldlines + +All helpers target Manim CE >= 0.19 and a ``ThreeDScene`` using the +"top-down stage" pattern: formulas and diagrams live in world space on the +z=0 plane, the camera starts top-down (phi=0) so the stage reads as 2D, and +tilts into 3D only for set pieces. Captions/headlines are fixed-in-frame. + +Camera moves use ``scene.move_camera(...)`` (never ``.animate`` on the +camera), which is the CE-0.19-safe path for ThreeDScene. +""" + +from __future__ import annotations + +import numpy as np +from manim import ( + BLACK, + DEGREES, + DOWN, + UP, + LEFT, + RIGHT, + ORIGIN, + Create, + Dot, + FadeIn, + FadeOut, + MathTex, + Mobject, + ParametricFunction, + Sphere, + SurroundingRectangle, + Text, + ThreeDScene, + VGroup, + Write, + interpolate_color, + rgb_to_color, +) + +# --------------------------------------------------------------------------- +# Mythos palette (Anthropic brand, tuned for dark stage) +# --------------------------------------------------------------------------- + +INK = "#0c0c0b" # stage background (near #141413, deepened for video) +IVORY = "#faf9f5" # primary text +CORAL = "#d97757" # matter / primary accent +SKY = "#6a9bcc" # light, gauge fields / secondary accent +OLIVE = "#788c5d" # mass, structure / tertiary accent +FOG = "#b0aea5" # secondary text +GOLD = "#d4a27f" # interaction, emphasis +EMBER = "#bd5d3a" # deep accent for glows + +TERM_COLORS = { + "matter": CORAL, + "light": SKY, + "mass": OLIVE, + "interaction": GOLD, +} + +HEADLINE_FONT = "Poppins" # falls back silently if not installed +CAPTION_FONT = "Lora" + + +def stage(scene: ThreeDScene, background: str = INK) -> None: + """Initialize the top-down 2D-looking stage inside a ThreeDScene.""" + scene.camera.background_color = background + scene.set_camera_orientation(phi=0 * DEGREES, theta=-90 * DEGREES, zoom=1.0) + + +# --------------------------------------------------------------------------- +# Headlines and captions (fixed in frame: the narration layer) +# --------------------------------------------------------------------------- + +def headline( + scene: ThreeDScene, + text: str, + sub: str | None = None, + color: str = IVORY, + accent: str = CORAL, + hold: float = 1.6, + font_size: int = 76, +) -> None: + """Full-screen plain-language statement. The idea before the symbols.""" + title = Text(text, font=HEADLINE_FONT, font_size=font_size, weight="BOLD", color=color) + title.set(width=min(title.width, 12.0)) + group = VGroup(title) + if sub: + subline = Text(sub, font=CAPTION_FONT, font_size=30, slant="ITALIC", color=FOG) + subline.set(width=min(subline.width, 10.0)) + subline.next_to(title, DOWN, buff=0.55) + group.add(subline) + rule = Text("—", font=HEADLINE_FONT, font_size=40, color=accent) + rule.next_to(group, UP, buff=0.5) + group.add(rule) + group.move_to(ORIGIN) + scene.add_fixed_in_frame_mobjects(group) + group.set_opacity(0) + scene.play(group.animate.set_opacity(1).shift(UP * 0.12), run_time=1.1) + scene.wait(hold) + scene.play(FadeOut(group, shift=UP * 0.4), run_time=0.7) + + +def caption( + scene: ThreeDScene, + text: str, + color: str = IVORY, + hold: float = 0.0, + font_size: int = 30, +) -> Text: + """Lower-third caption. Replaces any previous caption automatically.""" + new = Text(text, font=CAPTION_FONT, font_size=font_size, slant="ITALIC", color=color) + new.set(width=min(new.width, 11.5)) + new.to_edge(DOWN, buff=0.55) + scene.add_fixed_in_frame_mobjects(new) + new.set_opacity(0) + old = getattr(scene, "_mythos_caption", None) + if old is not None: + scene.play(FadeOut(old, run_time=0.35), new.animate.set_opacity(1), run_time=0.6) + else: + scene.play(new.animate.set_opacity(1), run_time=0.6) + scene._mythos_caption = new + if hold: + scene.wait(hold) + return new + + +def clear_caption(scene: ThreeDScene) -> None: + old = getattr(scene, "_mythos_caption", None) + if old is not None: + scene.play(FadeOut(old), run_time=0.4) + scene._mythos_caption = None + + +# --------------------------------------------------------------------------- +# Camera choreography (ThreeDScene-safe) +# --------------------------------------------------------------------------- + +def zoom_to( + scene: ThreeDScene, + target: Mobject, + zoom: float = 2.4, + run_time: float = 1.6, + added_anims: list | None = None, +) -> None: + """Fly the camera into a mobject (or formula part). The signature move.""" + scene.move_camera( + frame_center=target.get_center(), + zoom=zoom, + run_time=run_time, + added_anims=added_anims or [], + ) + + +def pull_back( + scene: ThreeDScene, + zoom: float = 1.0, + center=ORIGIN, + run_time: float = 1.4, + added_anims: list | None = None, +) -> None: + """Restore context: the part within the whole.""" + scene.move_camera( + frame_center=center, + zoom=zoom, + run_time=run_time, + added_anims=added_anims or [], + ) + + +def tilt_to_3d( + scene: ThreeDScene, + phi: float = 65 * DEGREES, + theta: float = -45 * DEGREES, + zoom: float = 0.9, + run_time: float = 2.0, +) -> None: + """Leave the flat stage and enter a 3D set piece.""" + scene.move_camera(phi=phi, theta=theta, zoom=zoom, run_time=run_time) + + +def return_to_stage(scene: ThreeDScene, run_time: float = 1.8, zoom: float = 1.0) -> None: + scene.move_camera(phi=0 * DEGREES, theta=-90 * DEGREES, zoom=zoom, + frame_center=ORIGIN, run_time=run_time) + + +def orbit(scene: ThreeDScene, rate: float = 0.06, duration: float = 4.0) -> None: + """Slow ambient orbit during a 3D set piece.""" + scene.begin_ambient_camera_rotation(rate=rate) + scene.wait(duration) + scene.stop_ambient_camera_rotation() + + +# --------------------------------------------------------------------------- +# Formula spotlighting +# --------------------------------------------------------------------------- + +def spotlight( + scene: ThreeDScene, + formula: MathTex, + part: Mobject, + color: str = CORAL, + dim: float = 0.25, + run_time: float = 0.9, +): + """Dim the formula, ignite one part.""" + others = [m for m in formula.family_members_with_points() + if m not in part.family_members_with_points()] + frame = SurroundingRectangle(part, color=color, buff=0.12, stroke_width=2.5) + scene.play( + *[m.animate.set_opacity(dim) for m in others], + part.animate.set_color(color).set_opacity(1.0), + Create(frame), + run_time=run_time, + ) + return frame + + +def unspotlight(scene: ThreeDScene, formula: MathTex, frame: Mobject, run_time: float = 0.7) -> None: + scene.play( + formula.animate.set_opacity(1.0), + FadeOut(frame), + run_time=run_time, + ) + + +def term_tour( + scene: ThreeDScene, + formula: MathTex, + stops: list[dict], + zoom: float = 2.4, + context_zoom: float = 1.0, +) -> None: + """Walk a formula term by term. + + Each stop: {"tex": str | None, "part": Mobject | None, "color": str, + "caption": str, "hold": float} + Provide either ``tex`` (resolved via get_part_by_tex) or an explicit part. + """ + for stop in stops: + part = stop.get("part") or formula.get_part_by_tex(stop["tex"]) + color = stop.get("color", CORAL) + frame = spotlight(scene, formula, part, color=color) + zoom_to(scene, part, zoom=stop.get("zoom", zoom)) + caption(scene, stop["caption"], color=color, hold=stop.get("hold", 1.4)) + pull_back(scene, zoom=context_zoom) + unspotlight(scene, formula, frame) + + +# --------------------------------------------------------------------------- +# Light: glows, starfields, fields +# --------------------------------------------------------------------------- + +def glow(mobject: Mobject, color: str = CORAL, layers: int = 5, max_width: float = 18, + opacity: float = 0.22) -> VGroup: + """Halo built from blurred-looking stroke layers behind a mobject.""" + halo = VGroup() + for i in range(layers, 0, -1): + layer = mobject.copy() + layer.set_stroke(color, width=max_width * i / layers, opacity=opacity * (1 - (i - 1) / layers) + 0.04) + layer.set_fill(opacity=0) + halo.add(layer) + return halo + + +def glow_dot(point=ORIGIN, color: str = CORAL, radius: float = 0.07, layers: int = 6) -> VGroup: + core = Dot(point=point, radius=radius, color=IVORY) + halo = VGroup(*[ + Dot(point=point, radius=radius * (1 + 0.85 * i), color=color, + fill_opacity=0.55 / (i + 1.2)) + for i in range(1, layers + 1) + ]) + return VGroup(halo, core) + + +def starfield(n: int = 180, x: float = 10.0, y: float = 6.0, z: float = 4.0, + seed: int = 7) -> VGroup: + rng = np.random.default_rng(seed) + stars = VGroup() + for _ in range(n): + p = [rng.uniform(-x, x), rng.uniform(-y, y), rng.uniform(-z, z)] + s = Dot(point=p, radius=float(rng.uniform(0.008, 0.03)), + color=interpolate_color(rgb_to_color([0.98, 0.97, 0.96]), + rgb_to_color([0.85, 0.6, 0.45]), + float(rng.random()))) + s.set_opacity(float(rng.uniform(0.25, 0.95))) + stars.add(s) + return stars + + +def photon_path(p0, p1, color: str = SKY, waves: float = 9.0, amp: float = 0.16, + stroke_width: float = 3.0) -> ParametricFunction: + """Wavy photon line between two points (in the z=0 plane).""" + p0 = np.array(p0, dtype=float) + p1 = np.array(p1, dtype=float) + d = p1 - p0 + length = np.linalg.norm(d) + u = d / max(length, 1e-8) + nvec = np.array([-u[1], u[0], 0.0]) + + def f(t: float): + return p0 + d * t + nvec * amp * np.sin(t * waves * 2 * np.pi) + + return ParametricFunction(f, t_range=[0, 1], color=color, stroke_width=stroke_width) + + +__all__ = [ + "INK", "IVORY", "CORAL", "SKY", "OLIVE", "FOG", "GOLD", "EMBER", "TERM_COLORS", + "stage", "headline", "caption", "clear_caption", + "zoom_to", "pull_back", "tilt_to_3d", "return_to_stage", "orbit", + "spotlight", "unspotlight", "term_tour", + "glow", "glow_dot", "starfield", "photon_path", +] diff --git a/mythos/harness.py b/mythos/harness.py new file mode 100644 index 0000000..bce452b --- /dev/null +++ b/mythos/harness.py @@ -0,0 +1,371 @@ +"""Mythos harness: the 6-agent reasoning chain, driven through Claude CLI. + +The agent charters live in ``.claude/agents/*.md`` — the exact files Claude +Code discovers natively for interactive use. This harness reads those same +charters and drives them headlessly, so interactive sessions and automated +runs share one source of truth. + +Chain: + intent -> cartographer -> curriculum -> math-director + -> cinematographer -> scene-composer -> [codegen -> verify -> render] + +Each reasoning stage receives the prior artifact JSON and must return one +JSON object. Codegen returns a complete Manim CE file inside one fenced +python block. Artifacts land in ``runs/mythos/