manim-math-video-studio 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Path of Knowledge contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ include VERSION
2
+ include LICENSE
3
+ include README.md
4
+ include README-pypi.md
5
+ include install.py
6
+ prune dist
7
+ prune release-dist
8
+ prune tests
9
+ prune docs
10
+ prune manim-math-video
11
+ prune platforms
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: manim-math-video-studio
3
+ Version: 0.4.0
4
+ Summary: Installer for the manim-math-video Agent Skill
5
+ Author: kappa77
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kappa77/manim-math-video-studio
8
+ Project-URL: Repository, https://github.com/kappa77/manim-math-video-studio
9
+ Project-URL: Documentation, https://github.com/kappa77/manim-math-video-studio/blob/main/INSTALL.md
10
+ Project-URL: Changelog, https://github.com/kappa77/manim-math-video-studio/blob/main/CHANGELOG.md
11
+ Keywords: agent-skills,manim,video,claude-code,codex,copilot,pi
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Video
22
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ # Manim Math Video Studio
29
+
30
+ Installer for the [manim-math-video Agent Skill](https://github.com/kappa77/manim-math-video-studio) — a portable skill that turns a topic into a storyboarded, narrated Manim video with subtitles.
31
+
32
+ This package is the **thin installer**: it detects the agent harness, installs the prebuilt skill tree, and prepares one shared Python environment with Manim. The skill files themselves are downloaded from the matching GitHub release tag.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ uvx manim-math-video-studio --platform claude
38
+ ```
39
+
40
+ or with `pipx`:
41
+
42
+ ```bash
43
+ pipx run --spec manim-math-video-studio manim-math-video-studio --platform claude
44
+ ```
45
+
46
+ Replace `claude` with `codex`, `copilot`, or `pi`. Use `--platform all` to install every harness explicitly.
47
+
48
+ Supported harnesses:
49
+
50
+ | Harness | Invocation |
51
+ |---|---|
52
+ | Claude Code | `/manim-math-video` |
53
+ | Codex | `$manim-math-video` |
54
+ | GitHub Copilot | `/manim-math-video` |
55
+ | Pi | `/skill:manim-math-video` |
56
+
57
+ ## Requirements
58
+
59
+ Python 3.10+. LaTeX and `dvisvgm` are optional and only needed for equation beats. The installer prints the platform-specific install commands when either is missing.
60
+
61
+ ## Links
62
+
63
+ - [Install guide](https://github.com/kappa77/manim-math-video-studio/blob/main/INSTALL.md)
64
+ - [Usage](https://github.com/kappa77/manim-math-video-studio/blob/main/USAGE.md)
65
+ - [Source](https://github.com/kappa77/manim-math-video-studio)
66
+ - [Changelog](https://github.com/kappa77/manim-math-video-studio/blob/main/CHANGELOG.md)
67
+
68
+ MIT licensed.
@@ -0,0 +1,41 @@
1
+ # Manim Math Video Studio
2
+
3
+ Installer for the [manim-math-video Agent Skill](https://github.com/kappa77/manim-math-video-studio) — a portable skill that turns a topic into a storyboarded, narrated Manim video with subtitles.
4
+
5
+ This package is the **thin installer**: it detects the agent harness, installs the prebuilt skill tree, and prepares one shared Python environment with Manim. The skill files themselves are downloaded from the matching GitHub release tag.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ uvx manim-math-video-studio --platform claude
11
+ ```
12
+
13
+ or with `pipx`:
14
+
15
+ ```bash
16
+ pipx run --spec manim-math-video-studio manim-math-video-studio --platform claude
17
+ ```
18
+
19
+ Replace `claude` with `codex`, `copilot`, or `pi`. Use `--platform all` to install every harness explicitly.
20
+
21
+ Supported harnesses:
22
+
23
+ | Harness | Invocation |
24
+ |---|---|
25
+ | Claude Code | `/manim-math-video` |
26
+ | Codex | `$manim-math-video` |
27
+ | GitHub Copilot | `/manim-math-video` |
28
+ | Pi | `/skill:manim-math-video` |
29
+
30
+ ## Requirements
31
+
32
+ Python 3.10+. LaTeX and `dvisvgm` are optional and only needed for equation beats. The installer prints the platform-specific install commands when either is missing.
33
+
34
+ ## Links
35
+
36
+ - [Install guide](https://github.com/kappa77/manim-math-video-studio/blob/main/INSTALL.md)
37
+ - [Usage](https://github.com/kappa77/manim-math-video-studio/blob/main/USAGE.md)
38
+ - [Source](https://github.com/kappa77/manim-math-video-studio)
39
+ - [Changelog](https://github.com/kappa77/manim-math-video-studio/blob/main/CHANGELOG.md)
40
+
41
+ MIT licensed.
@@ -0,0 +1,64 @@
1
+ # Manim Math Video Studio
2
+
3
+ [![release](https://img.shields.io/github/v/release/kappa77/manim-math-video-studio?color=blue)](https://github.com/kappa77/manim-math-video-studio/releases/latest)
4
+ [![license](https://img.shields.io/github/license/kappa77/manim-math-video-studio)](LICENSE)
5
+ [![python](https://img.shields.io/badge/python-3.10%2B-yellow)](INSTALL.md)
6
+ [![agents](https://img.shields.io/badge/agents-Claude%20Code%20%7C%20Codex%20%7C%20Copilot%20%7C%20Pi-blueviolet)](INSTALL.md)
7
+
8
+ A portable [Agent Skill](https://agentskills.io/specification) that turns a topic into a storyboarded, narrated Manim video — and into a consistent series. Prebuilt for Claude Code, Codex, GitHub Copilot, and Pi.
9
+
10
+ ```bash
11
+ python -c "import urllib.request as u;exec(u.urlopen('https://raw.githubusercontent.com/kappa77/manim-math-video-studio/main/install.py').read().decode())"
12
+ ```
13
+
14
+ It finds the agents on your machine, installs the skill for each, prepares one shared environment at `~/.manim-math-video/venv` (with `uv` when available), and verifies it. Same command on macOS, Linux, and Windows; needs Python 3.10+.
15
+
16
+ Standard Python package (v0.4.0+): `uvx manim-math-video-studio --platform claude` — one agent at a time, no clone.
17
+
18
+ Other routes — `curl -fsSL …/install.sh | bash`, `irm …/install.ps1 | iex`, or `git clone` + `python install.py` — are in [INSTALL.md](INSTALL.md).
19
+
20
+ ![The birthday-problem episode: the probability curve for a shared birthday rising through 23 people at 50.7%](docs/img/demo.gif)
21
+
22
+ *"Why 23 people are enough for a shared birthday" — 2½ minutes, produced end to end by the skill: brief → storyboard → narration → render → QA. [Watch the full episode](https://github.com/kappa77/manim-math-video-studio/releases/download/v0.2.0/manim-math-video-demo-23-people-1080p.mp4) (1080p, subtitles included).*
23
+
24
+ ## What it does
25
+
26
+ - **Interview first.** It asks about the content — central question, audience, crux, visual approach, takeaway — then locks quality, voice, language, and template. A theme is not a brief.
27
+ - **Narration drives the clock.** Each beat gets a measured TTS duration and the animation follows it, so nothing is cut off mid-sentence and subtitles stay in sync.
28
+ - **Beats and visuals.** `title`, `section`, `bullets`, `compare`, `quote`, `image`, `recap`, `outro`, plus custom beats, graphs, equations, camera, and 3D.
29
+ - **Built for series.** A series registry keeps template, palette, typography, and narration identity stable, so episode 12 still looks like episode 1.
30
+ - **Verified, not vibes.** JSON-schema validation for brief and storyboard, environment checks, contact-sheet QA, and a self-test.
31
+ - **Any subject.** History, science, technology, literature, philosophy, art, tutorials — mathematics is one supported domain, not the whole product.
32
+
33
+ ## Documentation
34
+
35
+ | Doc | Covers |
36
+ |---|---|
37
+ | [INSTALL.md](INSTALL.md) | one-command install, options, LaTeX, manual install, runtime resolution, update, uninstall, troubleshooting |
38
+ | [USAGE.md](USAGE.md) | quick start, command reference, pipeline, beats, templates, graphs, narration, series, validation |
39
+ | [CONTRIBUTING.md](CONTRIBUTING.md) | repository layout, source of truth vs `dist/`, adding beats and templates, versioning |
40
+ | [tests/README.md](tests/README.md) | Docker-based install verification for every supported agent harness |
41
+ | [CHANGELOG.md](CHANGELOG.md) | what changed, release by release |
42
+ | [manim-math-video/SKILL.md](manim-math-video/SKILL.md) | the workflow the agent follows; the detailed manual is `manim-math-video/references/` |
43
+ | [Landing page](https://kappa77.github.io/manim-math-video-studio/) | one-page overview with the copy-paste install |
44
+
45
+ ## Invoke
46
+
47
+ | Agent | Command |
48
+ |---|---|
49
+ | Claude Code | `/manim-math-video` |
50
+ | Codex | `$manim-math-video` |
51
+ | GitHub Copilot | `/manim-math-video` |
52
+ | Pi | `/skill:manim-math-video` |
53
+
54
+ Then ask for a video, for example: *"Make a 90-second video on why the derivative of x² is 2x."*
55
+
56
+ ## Requirements
57
+
58
+ Python 3.10+, Manim Community 0.19+ (0.22+ recommended), `edge-tts`, PyAV, Pillow, and NumPy. The installer provides all of them. LaTeX plus `dvisvgm` is needed only for equation beats (`--with-latex` automates MiKTeX on Windows and BasicTeX on macOS).
59
+
60
+ ## License and notes
61
+
62
+ - MIT — see [LICENSE](LICENSE).
63
+ - Not affiliated with Manim Community or Microsoft `edge-tts`. The `dark-board` template is an original visual style, not a reproduction of any channel's branding.
64
+ - Verify the mathematics, and the rights to any images, music, fonts, and quotations you use.
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+ """One-command installer for the manim-math-video agent skill.
3
+
4
+ Installs the skill tree for the agents found on this machine and prepares one
5
+ shared Python environment at ~/.manim-math-video/venv. Standard library only:
6
+ this file must run before any dependency exists.
7
+
8
+ Typical use (see install.sh / install.ps1 for the piped one-liners):
9
+
10
+ python install.py # auto-detect agents, install deps
11
+ python install.py --platform claude # one agent only
12
+ python install.py --with-latex # also install a LaTeX engine
13
+ python install.py --uninstall # remove skill trees and the venv
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import platform as platform_module
22
+ import shlex
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ import tarfile
27
+ import tempfile
28
+ import urllib.request
29
+ import zipfile
30
+ from datetime import datetime, timezone
31
+ from pathlib import Path
32
+
33
+ ARCHIVE_URL = os.environ.get("MMS_ARCHIVE_URL", "").strip() # optional mirror or file:// URL
34
+ REPO = os.environ.get("MMS_REPO", "kappa77/manim-math-video-studio")
35
+ PACKAGE_NAME = "manim-math-video-studio"
36
+ SKILL_NAME = "manim-math-video"
37
+ PLATFORMS = ("claude", "codex", "copilot", "pi")
38
+ MIN_PYTHON = (3, 10)
39
+ STATE_DIR = Path.home() / ".manim-math-video"
40
+ DEFAULT_VENV = STATE_DIR / "venv"
41
+ STATE_FILE = STATE_DIR / "install.json"
42
+
43
+ INVOKE = {
44
+ "claude": "/manim-math-video",
45
+ "codex": "$manim-math-video",
46
+ "copilot": "/manim-math-video",
47
+ "pi": "/skill:manim-math-video",
48
+ }
49
+ CLI = {"claude": "claude", "codex": "codex", "copilot": "copilot", "pi": "pi"}
50
+
51
+ IS_WINDOWS = os.name == "nt"
52
+ ECHO = True # set by --quiet, controls command echoing
53
+ VERBOSE = False # set by --verbose, controls pip output
54
+
55
+
56
+ # --------------------------------------------------------------------------- #
57
+ # output helpers
58
+ # --------------------------------------------------------------------------- #
59
+
60
+
61
+ def say(message: str = "") -> None:
62
+ print(message, flush=True)
63
+
64
+
65
+ def step(message: str) -> None:
66
+ say(f"\n==> {message}")
67
+
68
+
69
+ def note(message: str) -> None:
70
+ say(f" {message}")
71
+
72
+
73
+ def warn(message: str) -> None:
74
+ say(f" ! {message}")
75
+
76
+
77
+ def die(message: str, code: int = 1):
78
+ print(f"\nERROR: {message}", file=sys.stderr, flush=True)
79
+ raise SystemExit(code)
80
+
81
+
82
+ # --------------------------------------------------------------------------- #
83
+ # source tree: local checkout or download
84
+ # --------------------------------------------------------------------------- #
85
+
86
+
87
+ def looks_like_source(root: Path) -> bool:
88
+ return (root / "platforms" / "manifest.json").is_file() and (root / "dist").is_dir()
89
+
90
+
91
+ def _package_version() -> str | None:
92
+ """Return the installed package version, when this is a wheel/sdist install."""
93
+ try:
94
+ from importlib.metadata import version
95
+
96
+ return version(PACKAGE_NAME)
97
+ except Exception: # noqa: BLE001 - running standalone from a URL or checkout
98
+ return None
99
+
100
+
101
+ def default_ref() -> str:
102
+ """Prefer the release tag matching the installed package, then main."""
103
+ override = os.environ.get("MMS_REF", "").strip()
104
+ if override:
105
+ return override
106
+ package_version = _package_version()
107
+ if package_version and not any(
108
+ marker in package_version for marker in ("dev", "a", "b", "rc")
109
+ ):
110
+ return f"v{package_version}"
111
+ return "main"
112
+
113
+
114
+ def installer_version() -> str:
115
+ """Version to show in --version; works from a checkout and from a wheel."""
116
+ if "__file__" in globals():
117
+ local_version = Path(globals()["__file__"]).resolve().parent / "VERSION"
118
+ if local_version.is_file():
119
+ return local_version.read_text(encoding="utf-8").strip()
120
+ return _package_version() or "development"
121
+
122
+
123
+ DEFAULT_REF = default_ref()
124
+
125
+
126
+ def download_source(ref: str, destination: Path) -> Path:
127
+ """Download the repository archive for `ref` and return the extracted root."""
128
+ if ARCHIVE_URL:
129
+ url = ARCHIVE_URL
130
+ say(f" fetching {url}")
131
+ else:
132
+ kind = "tags" if ref.startswith("v") else "heads"
133
+ url = f"https://codeload.github.com/{REPO}/tar.gz/refs/{kind}/{ref}"
134
+ say(f" fetching {REPO}@{ref}")
135
+ archive = destination / "repo.archive"
136
+ try:
137
+ with urllib.request.urlopen(url, timeout=120) as response, archive.open("wb") as handle:
138
+ shutil.copyfileobj(response, handle)
139
+ except Exception as exc: # noqa: BLE001 - surfaced to the user as a hint
140
+ die(
141
+ f"could not download {url}: {exc}\n"
142
+ " Check your network, or clone the repository and run install.py from it."
143
+ )
144
+
145
+ extract = destination / "src"
146
+ extract.mkdir()
147
+ try:
148
+ if zipfile.is_zipfile(archive):
149
+ with zipfile.ZipFile(archive) as bundle:
150
+ bundle.extractall(extract)
151
+ else:
152
+ with tarfile.open(archive) as bundle:
153
+ try:
154
+ bundle.extractall(extract, filter="data")
155
+ except TypeError: # Python < 3.12 has no filter argument
156
+ bundle.extractall(extract)
157
+ except Exception as exc: # noqa: BLE001
158
+ die(f"could not unpack the archive: {exc}")
159
+
160
+ entries = [path for path in extract.iterdir() if path.is_dir()]
161
+ if len(entries) != 1:
162
+ die("unexpected archive layout; please report this issue")
163
+ return entries[0]
164
+
165
+
166
+ def resolve_source(explicit: str | None, ref: str = DEFAULT_REF) -> tuple[Path, "tempfile.TemporaryDirectory | None"]:
167
+ if explicit:
168
+ root = Path(explicit).expanduser().resolve()
169
+ if not looks_like_source(root):
170
+ die(f"--source {root} is not a checkout of this repository")
171
+ return root, None
172
+
173
+ # When exec'd from a URL (`python -c "...exec(urlopen(...))"`) there is no
174
+ # __file__, so fall through to the download path.
175
+ here = Path(globals()["__file__"]).resolve().parent if "__file__" in globals() else None
176
+ if here is not None and looks_like_source(here):
177
+ return here, None
178
+
179
+ holder = tempfile.TemporaryDirectory(prefix="mms-install-")
180
+ return download_source(ref, Path(holder.name)), holder
181
+
182
+
183
+ # --------------------------------------------------------------------------- #
184
+ # platform detection
185
+ # --------------------------------------------------------------------------- #
186
+
187
+
188
+ def load_manifest(root: Path) -> dict:
189
+ try:
190
+ return json.loads((root / "platforms" / "manifest.json").read_text(encoding="utf-8"))
191
+ except Exception as exc: # noqa: BLE001
192
+ die(f"could not read platforms/manifest.json: {exc}")
193
+
194
+
195
+ def signals_for(platform: str) -> list[Path | None]:
196
+ home = Path.home()
197
+ found: list[Path | None] = [Path(p) if (p := shutil.which(CLI[platform])) else None]
198
+ if platform == "claude":
199
+ found.append(home / ".claude")
200
+ elif platform == "codex":
201
+ found.extend((home / ".codex", home / ".agents"))
202
+ elif platform == "copilot":
203
+ found.append(home / ".copilot")
204
+ elif platform == "pi":
205
+ found.append(home / ".pi")
206
+ return [item for item in found if item is not None]
207
+
208
+
209
+ def detect_platforms() -> list[str]:
210
+ detected = [name for name in PLATFORMS if any(item.exists() for item in signals_for(name))]
211
+ return detected
212
+
213
+
214
+ def resolve_platforms(spec: str | None) -> list[str]:
215
+ if not spec or spec == "auto":
216
+ detected = detect_platforms()
217
+ if detected:
218
+ return detected
219
+ warn("no agent installation detected; defaulting to Claude Code")
220
+ note("use --platform codex,copilot,pi or --platform all to choose")
221
+ return ["claude"]
222
+ if spec == "all":
223
+ return list(PLATFORMS)
224
+ requested = [item.strip().lower() for item in spec.split(",") if item.strip()]
225
+ unknown = [item for item in requested if item not in PLATFORMS]
226
+ if unknown:
227
+ die(f"unknown platform(s): {', '.join(unknown)}; choose from {', '.join(PLATFORMS)}")
228
+ return requested
229
+
230
+
231
+ def target_dir(platform: str, manifest: dict, root_override: str | None) -> Path:
232
+ config = manifest["platforms"][platform]
233
+ if root_override:
234
+ return Path(root_override).expanduser().resolve() / SKILL_NAME
235
+ if platform == "codex" and not (Path.home() / ".codex").exists() and (Path.home() / ".agents").exists():
236
+ return (Path.home() / ".agents" / "skills" / SKILL_NAME).resolve()
237
+ return Path(config["install_root"]).expanduser().resolve() / SKILL_NAME
238
+
239
+
240
+ # --------------------------------------------------------------------------- #
241
+ # python / environment
242
+ # --------------------------------------------------------------------------- #
243
+
244
+
245
+ def version_of(command: list[str]) -> tuple[int, int] | None:
246
+ try:
247
+ result = subprocess.run(
248
+ [*command, "-c", "import sys; print('%d.%d' % sys.version_info[:2])"],
249
+ capture_output=True,
250
+ text=True,
251
+ timeout=30,
252
+ check=False,
253
+ )
254
+ except Exception: # noqa: BLE001
255
+ return None
256
+ text = (result.stdout or "").strip()
257
+ if result.returncode != 0 or "." not in text:
258
+ return None
259
+ major, _, minor = text.partition(".")
260
+ return int(major), int(minor)
261
+
262
+
263
+ def find_interpreter() -> list[str]:
264
+ if tuple(sys.version_info[:2]) >= MIN_PYTHON:
265
+ return [sys.executable]
266
+ candidates: list[list[str]] = []
267
+ if IS_WINDOWS:
268
+ candidates.extend([["py", f"-{major}.{minor}"] for major in (3,) for minor in (13, 12, 11, 10)])
269
+ candidates.extend([[f"python3.{minor}"] for minor in (13, 12, 11, 10)] + [["python3"], ["python"]])
270
+ for candidate in candidates:
271
+ if not shutil.which(candidate[0]):
272
+ continue
273
+ found = version_of(candidate)
274
+ if found and found >= MIN_PYTHON:
275
+ return candidate
276
+ available = "%d.%d" % sys.version_info[:2]
277
+ die(
278
+ f"Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ is required, found {available}.\n"
279
+ " Install a newer Python (https://www.python.org/downloads/) and rerun."
280
+ )
281
+
282
+
283
+ def venv_python(venv: Path) -> Path:
284
+ return venv / ("Scripts/python.exe" if IS_WINDOWS else "bin/python")
285
+
286
+
287
+ def run(command: list[str], *, echo: bool | None = None, capture: bool = False) -> int:
288
+ """Run a command; `capture` hides its output unless it fails."""
289
+ if (ECHO if echo is None else echo) and not (ECHO and not VERBOSE and capture):
290
+ note("$ " + " ".join(str(part) for part in command))
291
+ if capture and not VERBOSE:
292
+ result = subprocess.run(
293
+ command, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
294
+ )
295
+ if result.returncode != 0 and result.stdout:
296
+ tail = result.stdout.strip().splitlines()[-25:]
297
+ for line in tail:
298
+ say(f" | {line}")
299
+ return result.returncode
300
+ return subprocess.run(command, check=False).returncode
301
+
302
+
303
+ def create_venv(venv: Path, use_uv: bool) -> None:
304
+ if venv_python(venv).is_file():
305
+ note(f"reusing environment: {venv}")
306
+ return
307
+ venv.parent.mkdir(parents=True, exist_ok=True)
308
+ interpreter = find_interpreter()
309
+ if use_uv and shutil.which("uv"):
310
+ note("using uv for a faster install")
311
+ uv_python = " ".join(interpreter) if len(interpreter) == 1 else None
312
+ if uv_python:
313
+ code = run(["uv", "venv", "--python", uv_python, str(venv)])
314
+ if code == 0 and venv_python(venv).is_file():
315
+ return
316
+ warn("uv could not create the environment; falling back to venv")
317
+ shutil.rmtree(venv, ignore_errors=True)
318
+ code = run([*interpreter, "-m", "venv", str(venv)])
319
+ if code != 0 or not venv_python(venv).is_file():
320
+ die("could not create the Python environment")
321
+
322
+
323
+ def install_dependencies(venv: Path, requirements: Path, use_uv: bool) -> None:
324
+ python = str(venv_python(venv))
325
+ flags = ["-q"] if not VERBOSE else []
326
+ if use_uv and shutil.which("uv"):
327
+ code = run(["uv", "pip", "install", *flags, "--python", python, "-r", str(requirements)], capture=True)
328
+ if code == 0:
329
+ return
330
+ warn("uv install failed; falling back to pip")
331
+ run([python, "-m", "pip", "install", "-q", "--upgrade", "--disable-pip-version-check", "--no-input", "pip"], capture=True)
332
+ code = run(
333
+ [
334
+ python,
335
+ "-m",
336
+ "pip",
337
+ "install",
338
+ *flags,
339
+ "--disable-pip-version-check",
340
+ "--no-input",
341
+ "-r",
342
+ str(requirements),
343
+ ],
344
+ capture=True,
345
+ )
346
+ if code != 0:
347
+ die("dependency installation failed; rerun with --verbose to see the full output")
348
+
349
+
350
+ # --------------------------------------------------------------------------- #
351
+ # latex (optional)
352
+ # --------------------------------------------------------------------------- #
353
+
354
+
355
+ def latex_ready() -> bool:
356
+ return bool(shutil.which("latex")) and bool(shutil.which("dvisvgm"))
357
+
358
+
359
+ LATEX_HINTS = {
360
+ "nt": [
361
+ "winget install --id MiKTeX.MiKTeX -e --accept-package-agreements --accept-source-agreements",
362
+ ],
363
+ "darwin": [
364
+ "brew install --cask basictex",
365
+ "sudo tlmgr install dvisvgm standalone amsmath amsfonts",
366
+ ],
367
+ "linux": [
368
+ "sudo apt install texlive texlive-latex-extra dvisvgm # Debian / Ubuntu",
369
+ "sudo dnf install texlive-scheme-medium dvisvgm # Fedora",
370
+ ],
371
+ }
372
+
373
+
374
+ def install_latex(enable: bool, dry_run: bool) -> None:
375
+ if latex_ready():
376
+ note("LaTeX + dvisvgm already available (equation beats enabled)")
377
+ return
378
+ key = "nt" if IS_WINDOWS else platform_module.system().lower()
379
+ hints = LATEX_HINTS.get(key, LATEX_HINTS["linux"])
380
+ if not enable:
381
+ note("LaTeX not installed: equation beats are unavailable, everything else works")
382
+ note("enable later with: python install.py --with-latex")
383
+ return
384
+ step("Installing LaTeX (dvisvgm + basic packages)")
385
+ if dry_run:
386
+ note("would run: " + "; ".join(hints))
387
+ return
388
+ if IS_WINDOWS and shutil.which("winget"):
389
+ code = run(hints[0].split())
390
+ if code != 0:
391
+ warn("winget could not install MiKTeX; install it manually:")
392
+ for hint in hints:
393
+ note(hint)
394
+ else:
395
+ note("MiKTeX installed; set 'Always install missing packages on the fly' in MiKTeX Console")
396
+ return
397
+ if platform_module.system().lower() == "darwin" and shutil.which("brew"):
398
+ run(hints[0].split())
399
+ run(hints[1].split())
400
+ return
401
+ warn("automatic LaTeX install is not supported here; run:")
402
+ for hint in hints:
403
+ note(hint)
404
+
405
+
406
+ # --------------------------------------------------------------------------- #
407
+ # install / uninstall
408
+ # --------------------------------------------------------------------------- #
409
+
410
+
411
+ def copy_skill(source: Path, target: Path) -> None:
412
+ preserve = target / ".venv"
413
+ if target.exists():
414
+ if preserve.exists():
415
+ stash = target.parent / f".{SKILL_NAME}-venv-stash"
416
+ shutil.rmtree(stash, ignore_errors=True)
417
+ preserve.rename(stash)
418
+ else:
419
+ stash = None
420
+ shutil.rmtree(target, ignore_errors=True)
421
+ if stash is not None:
422
+ shutil.copytree(source, target)
423
+ stash.rename(target / ".venv")
424
+ else:
425
+ shutil.copytree(source, target)
426
+ else:
427
+ target.parent.mkdir(parents=True, exist_ok=True)
428
+ shutil.copytree(source, target)
429
+
430
+
431
+ def read_state() -> dict:
432
+ try:
433
+ return json.loads(STATE_FILE.read_text(encoding="utf-8"))
434
+ except Exception: # noqa: BLE001
435
+ return {}
436
+
437
+
438
+ def write_state(platforms: dict[str, str], venv: Path, version: str) -> None:
439
+ previous = read_state()
440
+ merged = dict(previous.get("platforms", {}))
441
+ merged.update(platforms)
442
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
443
+ payload = {
444
+ "version": version,
445
+ "installed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
446
+ "venv": str(venv),
447
+ "platforms": merged,
448
+ }
449
+ STATE_FILE.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
450
+
451
+
452
+ def read_version(root: Path) -> str:
453
+ try:
454
+ return (root / "VERSION").read_text(encoding="utf-8").strip() or "0.0.0"
455
+ except Exception: # noqa: BLE001
456
+ return "0.0.0"
457
+
458
+
459
+ def verify(skill_dir: Path, venv: Path) -> int:
460
+ python = venv_python(venv)
461
+ check = skill_dir / "scripts" / "check_env.py"
462
+ if not python.is_file() or not check.is_file():
463
+ return 0
464
+ step("Verifying the environment")
465
+ environment = dict(os.environ, PYTHONWARNINGS="ignore::SyntaxWarning")
466
+ result = subprocess.run([str(python), str(check)], check=False, env=environment)
467
+ return result.returncode
468
+
469
+
470
+ def uninstall(dry_run: bool) -> int:
471
+ state = read_state()
472
+ targets = dict(state.get("platforms", {}))
473
+ if not targets:
474
+ manifest = load_manifest(resolve_source(None)[0])
475
+ for name in PLATFORMS:
476
+ targets[name] = str(target_dir(name, manifest, None))
477
+ venv = Path(state.get("venv", DEFAULT_VENV)).expanduser()
478
+ step("Removing installed skills" + (" (dry run)" if dry_run else ""))
479
+ for name, path in targets.items():
480
+ directory = Path(path)
481
+ if not directory.exists():
482
+ continue
483
+ if directory.name != SKILL_NAME:
484
+ warn(f"skipping {directory} (unexpected name; remove it manually)")
485
+ continue
486
+ note(f"{name}: {directory}")
487
+ if not dry_run:
488
+ shutil.rmtree(directory, ignore_errors=True)
489
+ if venv.exists():
490
+ # Normalize symlinked HOME paths (macOS /var -> /private/var, Windows
491
+ # short paths) before deciding whether this installer owns the venv.
492
+ try:
493
+ managed_venv = venv.resolve()
494
+ default_venv = DEFAULT_VENV.expanduser().resolve()
495
+ state_root = STATE_DIR.expanduser().resolve()
496
+ except OSError:
497
+ managed_venv, default_venv, state_root = venv, DEFAULT_VENV.expanduser(), STATE_DIR.expanduser()
498
+ managed = managed_venv == default_venv or state_root in managed_venv.parents
499
+ if not managed:
500
+ warn(f"leaving {venv} in place (not created by this installer)")
501
+ else:
502
+ note(f"environment: {venv}")
503
+ if not dry_run:
504
+ shutil.rmtree(managed_venv, ignore_errors=True)
505
+ if not dry_run:
506
+ STATE_FILE.unlink(missing_ok=True)
507
+ say("\nDone." if not dry_run else "\nDry run: nothing was removed.")
508
+ return 0
509
+
510
+
511
+ # --------------------------------------------------------------------------- #
512
+ # main
513
+ # --------------------------------------------------------------------------- #
514
+
515
+
516
+ def build_plan(root: Path, manifest: dict, platforms: list[str], root_override: str | None) -> dict[str, Path]:
517
+ plan: dict[str, Path] = {}
518
+ for name in platforms:
519
+ plan[name] = target_dir(name, manifest, root_override)
520
+ return plan
521
+
522
+
523
+ def main(argv: list[str] | None = None) -> int:
524
+ arguments = list(sys.argv[1:] if argv is None else argv)
525
+ # Flags are awkward to append to the universal `python -c` one-liner, so
526
+ # accept them from the environment too.
527
+ extra = os.environ.get("MMS_ARGS", "").strip()
528
+ if extra:
529
+ arguments = [*shlex.split(extra), *arguments]
530
+
531
+ parser = argparse.ArgumentParser(
532
+ prog=Path(sys.argv[0]).name,
533
+ description="Install the manim-math-video skill for your agents.",
534
+ formatter_class=argparse.RawDescriptionHelpFormatter,
535
+ epilog=(
536
+ "examples:\n"
537
+ " python install.py auto-detect agents, install everything\n"
538
+ " python install.py --platform codex one agent only\n"
539
+ " python install.py --with-latex also install a LaTeX engine\n"
540
+ " python install.py --uninstall remove skills and environment\n"
541
+ "\n"
542
+ "packaged install:\n"
543
+ " uvx manim-math-video-studio --platform codex\n"
544
+ ),
545
+ )
546
+ parser.add_argument(
547
+ "--version",
548
+ action="version",
549
+ version=f"%(prog)s {installer_version()}",
550
+ help="show the installer version and exit",
551
+ )
552
+ parser.add_argument("--platform", "--platforms", dest="platform", default="auto",
553
+ help=f"auto (default), all, or a comma list of {', '.join(PLATFORMS)}")
554
+ parser.add_argument("--root", help="install into this directory instead of the personal root")
555
+ parser.add_argument("--venv", default=str(DEFAULT_VENV), help=f"environment directory (default: {DEFAULT_VENV})")
556
+ parser.add_argument("--source", help="path to this repository (default: auto-detect or download)")
557
+ parser.add_argument(
558
+ "--ref",
559
+ default=DEFAULT_REF,
560
+ help=f"git ref to download when not installed from a checkout (default: {DEFAULT_REF})",
561
+ )
562
+ parser.add_argument("--with-latex", action="store_true", help="install a LaTeX engine for equation beats")
563
+ parser.add_argument("--no-deps", action="store_true", help="skip Python dependencies")
564
+ parser.add_argument("--no-uv", action="store_true", help="do not use uv even when it is installed")
565
+ parser.add_argument("--dry-run", action="store_true", help="show the plan without changing anything")
566
+ parser.add_argument("--uninstall", action="store_true", help="remove the installed skills and environment")
567
+ parser.add_argument("--quiet", action="store_true", help="hide subprocess commands")
568
+ parser.add_argument("--verbose", action="store_true", help="show full pip output")
569
+ args = parser.parse_args(arguments)
570
+
571
+ global ECHO, VERBOSE
572
+ ECHO = not args.quiet
573
+ VERBOSE = args.verbose
574
+
575
+
576
+ if args.uninstall:
577
+ return uninstall(args.dry_run)
578
+
579
+ root, holder = resolve_source(args.source, args.ref or DEFAULT_REF)
580
+ try:
581
+ manifest = load_manifest(root)
582
+ platforms = resolve_platforms(args.platform)
583
+ if args.root and len(platforms) != 1:
584
+ die("--root installs a single platform; combine it with --platform")
585
+ plan = build_plan(root, manifest, platforms, args.root)
586
+ venv = Path(args.venv).expanduser().resolve()
587
+ version = read_version(root)
588
+
589
+ say("manim-math-video installer")
590
+ say(f" version {version}")
591
+ say(f" python {sys.version.split()[0]} ({sys.executable})")
592
+ say(f" env {venv}")
593
+ for name, path in plan.items():
594
+ say(f" {name:<9} {path}")
595
+
596
+ missing = [name for name in plan if not (root / "dist" / name / "skills" / SKILL_NAME).is_dir()]
597
+ if missing:
598
+ die(f"missing prebuilt trees for: {', '.join(missing)}; run: python platforms/build.py")
599
+
600
+ if args.dry_run:
601
+ say("\nDry run: nothing was changed.")
602
+ return 0
603
+
604
+ step("Installing the skill")
605
+ for name, path in plan.items():
606
+ copy_skill(root / "dist" / name / "skills" / SKILL_NAME, path)
607
+ note(f"{name} -> {path}")
608
+
609
+ if args.no_deps:
610
+ warn("skipped Python dependencies (--no-deps)")
611
+ else:
612
+ step("Preparing the Python environment (2-4 min on first run)")
613
+ create_venv(venv, use_uv=not args.no_uv)
614
+ install_dependencies(venv, root / "requirements.txt", use_uv=not args.no_uv)
615
+ for path in plan.values():
616
+ (path / ".venv-path").write_text(str(venv) + "\n", encoding="utf-8", newline="\n")
617
+
618
+ install_latex(args.with_latex, dry_run=False)
619
+ write_state({name: str(path) for name, path in plan.items()}, venv, version)
620
+
621
+ code = verify(next(iter(plan.values())), venv)
622
+
623
+ step("Ready")
624
+ for name in plan:
625
+ say(f" {manifest['platforms'][name]['label']:<16} {INVOKE[name]}")
626
+ say("\n Restart your agent, then ask for a video, for example:")
627
+ say(' "Make a 90 second video on why the derivative of x squared is 2x"')
628
+ if code != 0:
629
+ warn("the environment check reported blocking issues; see the table above")
630
+ return code
631
+ finally:
632
+ if holder is not None:
633
+ holder.cleanup()
634
+
635
+
636
+ if __name__ == "__main__":
637
+ sys.exit(main())
@@ -0,0 +1,14 @@
1
+ """Installer entry point for the manim-math-video Agent Skill."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ from install import main
8
+
9
+ try:
10
+ __version__ = version("manim-math-video-studio")
11
+ except PackageNotFoundError: # running from a checkout
12
+ __version__ = "development"
13
+
14
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,10 @@
1
+ """Run the installer with ``python -m manim_math_video_studio``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from . import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: manim-math-video-studio
3
+ Version: 0.4.0
4
+ Summary: Installer for the manim-math-video Agent Skill
5
+ Author: kappa77
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kappa77/manim-math-video-studio
8
+ Project-URL: Repository, https://github.com/kappa77/manim-math-video-studio
9
+ Project-URL: Documentation, https://github.com/kappa77/manim-math-video-studio/blob/main/INSTALL.md
10
+ Project-URL: Changelog, https://github.com/kappa77/manim-math-video-studio/blob/main/CHANGELOG.md
11
+ Keywords: agent-skills,manim,video,claude-code,codex,copilot,pi
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Video
22
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ # Manim Math Video Studio
29
+
30
+ Installer for the [manim-math-video Agent Skill](https://github.com/kappa77/manim-math-video-studio) — a portable skill that turns a topic into a storyboarded, narrated Manim video with subtitles.
31
+
32
+ This package is the **thin installer**: it detects the agent harness, installs the prebuilt skill tree, and prepares one shared Python environment with Manim. The skill files themselves are downloaded from the matching GitHub release tag.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ uvx manim-math-video-studio --platform claude
38
+ ```
39
+
40
+ or with `pipx`:
41
+
42
+ ```bash
43
+ pipx run --spec manim-math-video-studio manim-math-video-studio --platform claude
44
+ ```
45
+
46
+ Replace `claude` with `codex`, `copilot`, or `pi`. Use `--platform all` to install every harness explicitly.
47
+
48
+ Supported harnesses:
49
+
50
+ | Harness | Invocation |
51
+ |---|---|
52
+ | Claude Code | `/manim-math-video` |
53
+ | Codex | `$manim-math-video` |
54
+ | GitHub Copilot | `/manim-math-video` |
55
+ | Pi | `/skill:manim-math-video` |
56
+
57
+ ## Requirements
58
+
59
+ Python 3.10+. LaTeX and `dvisvgm` are optional and only needed for equation beats. The installer prints the platform-specific install commands when either is missing.
60
+
61
+ ## Links
62
+
63
+ - [Install guide](https://github.com/kappa77/manim-math-video-studio/blob/main/INSTALL.md)
64
+ - [Usage](https://github.com/kappa77/manim-math-video-studio/blob/main/USAGE.md)
65
+ - [Source](https://github.com/kappa77/manim-math-video-studio)
66
+ - [Changelog](https://github.com/kappa77/manim-math-video-studio/blob/main/CHANGELOG.md)
67
+
68
+ MIT licensed.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README-pypi.md
4
+ README.md
5
+ VERSION
6
+ install.py
7
+ pyproject.toml
8
+ manim_math_video_studio/__init__.py
9
+ manim_math_video_studio/__main__.py
10
+ manim_math_video_studio.egg-info/PKG-INFO
11
+ manim_math_video_studio.egg-info/SOURCES.txt
12
+ manim_math_video_studio.egg-info/dependency_links.txt
13
+ manim_math_video_studio.egg-info/entry_points.txt
14
+ manim_math_video_studio.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ manim-math-video-install = manim_math_video_studio:main
3
+ manim-math-video-studio = manim_math_video_studio:main
@@ -0,0 +1,2 @@
1
+ install
2
+ manim_math_video_studio
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "manim-math-video-studio"
7
+ dynamic = ["version"]
8
+ description = "Installer for the manim-math-video Agent Skill"
9
+ readme = "README-pypi.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "kappa77" }]
14
+ keywords = ["agent-skills", "manim", "video", "claude-code", "codex", "copilot", "pi"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Multimedia :: Video",
26
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
27
+ ]
28
+ dependencies = []
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/kappa77/manim-math-video-studio"
32
+ Repository = "https://github.com/kappa77/manim-math-video-studio"
33
+ Documentation = "https://github.com/kappa77/manim-math-video-studio/blob/main/INSTALL.md"
34
+ Changelog = "https://github.com/kappa77/manim-math-video-studio/blob/main/CHANGELOG.md"
35
+
36
+ [project.scripts]
37
+ manim-math-video-studio = "manim_math_video_studio:main"
38
+ manim-math-video-install = "manim_math_video_studio:main"
39
+
40
+ [tool.setuptools]
41
+ py-modules = ["install"]
42
+ packages = ["manim_math_video_studio"]
43
+ include-package-data = false
44
+
45
+ [tool.setuptools.dynamic]
46
+ version = { file = ["VERSION"] }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+