# Ml Content

> Generate publication-grade ML explainer videos and carousels the way 3Blue1Brown actually builds them — in real manimGL (NOT Manim Community Edition), as a tiny domain DSL of self-arranging Mobjects choreographed into transform-driven beats where every motion carries meaning. Overlap is prevented at construction time, not policed after render. Use for: 3b1b-style ML videos, paper-figure animation…

- **Type:** Skill
- **Install:** `agentstack add skill-thtskaran-claude-skills-ml-content`
- **Verified:** Pending review
- **Seller:** [thtskaran](https://agentstack.voostack.com/s/thtskaran)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [thtskaran](https://github.com/thtskaran)
- **Source:** https://github.com/thtskaran/claude-skills/tree/master/ml-content

## Install

```sh
agentstack add skill-thtskaran-claude-skills-ml-content
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# ml-content

Generate ML explainer content that looks like 3Blue1Brown, not like AI slop.

> ## ⚠️ PRIME DIRECTIVE — treat every video as nuclear. ZERO errors ship.
> The content goes public to an audience that **will** fact-check it. A single wrong number, mislabeled quantity, or overstated claim destroys trust in everything else and gets screenshotted. So: **nothing — not even slightly — may be wrong.** Every spoken line, every on-screen number and label, every caption, and the thumbnail must be **verified against the primary source before it is rendered, and audited again on the rendered video before it ships** (§10, the non-negotiable gate). If you cannot cite the exact source line for a claim, you do not say it, write it, or put it on screen. Soften it or cut it. **No "dramatic license" on numbers.** When in doubt, it is wrong until proven right.

This skill was rebuilt from a full read of **Grant Sanderson's actual production code** (`github.com/3b1b/videos`, 503K LOC, 2015→2026) and **the real manimGL engine** (`github.com/3b1b/manim`). Every rule below is grounded in that source with `file:line` citations. Where this skill once guessed, it now measures.

**The video engine is manimGL** (the 3b1b version), driven by `manimgl`. Manim Community Edition (CE) is a *different library with a different, incompatible API* — code written for one crashes on the other. Static IG carousels use HTML/matplotlib (see the Carousel section); everything animated is manimGL.

---

## 0. Why the old output was slop (read this once)

The previous version of this skill produced overlapping elements, weak animation, no consistency, and infographic-feeling stills. The root causes, now fixed:

1. **It shipped Manim CE code while preaching manimGL.** The old template used `from manim import *`, `MathTex`, `ThreeDScene`, `set_camera_orientation`, `set_fill_by_value`, `Create`, `begin_ambient_camera_rotation` — **none of which exist in manimGL** (`grep` over the entire engine = 0 hits). It would not even run.
2. **100% of real output was actually built in matplotlib**, hand-placing ~57 text calls + ~20 boxes per scene with absolute coordinates. That is the worst possible tool for animation: no relative layout, no transform system, no camera. Overlap is guaranteed.
3. **It treated overlap as a validation problem** (bbox asserts, frame validators, ffmpeg caption-pads) instead of a *construction* problem. 3b1b never validates overlap — it makes overlap structurally impossible by building self-arranging objects.
4. **It treated motion as decoration.** Elements faded in from nowhere as disconnected islands. In real 3b1b, objects are *born from the thing they abstract* (`TransformFromCopy`), so every motion teaches.
5. **Planning was marketing copy with word-count targets.** Real 3b1b planning is an ordered list of named teaching beats that reads top-to-bottom as the narration.

The fix is a different mental model, encoded as the Six Laws below.

---

## 1. The engine reality (the single most important section)

**Start every manim file with:**
```python
from manim_imports_ext import *      # in the 3b1b/videos repo
# or, standalone:  from manimlib import *
```

**Scene base class is `InteractiveScene`** (`interactive_scene.py:66`) — for 2D *and* 3D. In the entire 2025 corpus: `InteractiveScene` subclassed 385 times, `ThreeDScene` (`scene.py:930`) 0 times — it exists but 3b1b never uses it. 3D is achieved on an `InteractiveScene` by moving `self.frame`.

**The camera is `self.frame`** (a `CameraFrame` mobject; `scene.py:112`). Local alias `frame = self.frame` appears 114× in 2025 code. `self.camera.frame` appears 0×.

**Render / iterate:**
```bash
manimgl file.py SceneName              # render
manimgl file.py SceneName -se 120      # drop into IPython at line 120 (the dev loop)
manimgl file.py SceneName -w           # write to file
# inside the embed: checkpoint_paste() runs clipboard code with checkpoint rewind
```

### CE landmines — Table A: these genuinely CRASH on manimGL (absent symbols)

| You must NOT emit (CE) | Use instead (manimGL) | Evidence |
|---|---|---|
| `from manim import *` | `from manim_imports_ext import *` / `from manimlib import *` | CLAUDE.md:59 (loads the wrong library) |
| `MathTex(...)` | `Tex(...)` | grep MathTex over repo = 0; CLAUDE.md:82 |
| `self.set_camera_orientation(phi=,theta=,zoom=)` | `self.frame.reorient(theta, phi, gamma, center, height)` | absent; `camera_frame.py:172` |
| `self.move_camera(...)` | `self.play(self.frame.animate.reorient(...))` | absent |
| `self.begin_ambient_camera_rotation()` | `self.frame.add_ambient_rotation(1 * DEG)` | absent; `camera_frame.py:212` |
| `self.add_fixed_in_frame_mobjects(m)` | `m.fix_in_frame()` | absent (no scene-level helper) |
| `Create(m)` | `ShowCreation(m)` | absent; `creation.py:48` |
| `Unwrite(m)` | `Uncreate(m)` or `FadeOut(m)` | absent |
| `surface.set_fill_by_value(...)` | `surface.set_color(c, opacity)` / `set_color_by_xyz_func(...)` | absent |
| `Circumscribe(m)` | `FlashAround(m)` | absent in engine (the one truly-missing indicator) |
| `Wiggle(m)` (class) | `WiggleOutThenIn(m)` or `rate_func=wiggle` | no `Wiggle` class; `indication.py:355` |
| `ease_in_out_*`, `smoothstep`, `easeOutCubic` | the 15 real rate funcs (§7) | not in `rate_functions.py` |
| `FadeInFrom`, `FadeOutAndShift`, `SpinInFromNothing`, `AddTextLetterByLetter` | `FadeIn(m, shift=, scale=)`, `AddTextWordByWord` | absent |

**Self-check (must return nothing):**
```
grep -nE 'MathTex|from manim import \*|set_camera_orientation|begin_ambient_camera_rotation|move_camera|add_fixed_in_frame_mobjects|set_fill_by_value|\bCreate\(|\bCircumscribe\b|\bWiggle\(|\bUnwrite\(|FadeInFrom|SpinInFromNothing|AddTextLetterByLetter' your_file.py
```

### Table B: these RUN on manimGL but are wrong/stale style — don't emit anyway

| Avoid (valid but off-style) | Prefer | Why |
|---|---|---|
| `class S(Scene)` / `class S(ThreeDScene)` | `class S(InteractiveScene)` | both exist & run, but 2025 corpus is 385× InteractiveScene, 0× ThreeDScene; 3D uses `self.frame` |
| `TransformMatchingTex(a, b)` | `TransformMatchingStrings(a, b)` | `TransformMatchingTex` exists (subclasses Strings) but 3b1b uses Strings |
| `eq.set_color_by_tex(tok, c)` | `Tex(R"...", t2c={tok: c})` or inline `eq[tok].set_color(c)` | `set_color_by_tex` is a real Tex method (`tex_mobject.py:207`) but ~unused; inline `set_color` dominates |
| `Indicate(m)` / `CircleIndicate(m)` | `FlashAround(m)` / `Flash` / `FlashUnder` | both exist & are used (`indication.py:73,142`); modern corpus just reaches for Flash* far more |
| `DEGREES` | `DEG` | `DEGREES` is a live alias of `DEG` (won't crash); 2025 uses `DEG` ~520× vs `DEGREES` 3× |

> The split matters: don't let a self-check grep reject valid 3b1b code. `Indicate`, `TransformMatchingTex`, `set_color_by_tex`, and `DEGREES` are *style* calls, not crashers — only Table A NameErrors.

---

## 2. The mental model (what makes it 3b1b, not slop)

> You are not laying out a frame. You are building a **small cast of self-arranging objects** and **transforming them through a sequence of beats** where each motion is the explanation.

Two layers, every video:

1. **`helpers.py` — the domain DSL.** 4–10 `Mobject` subclasses that build and arrange *themselves* (`WeightMatrix`, `NumericEmbedding`, `EmbeddingArray`, `Dial`, `NeuralNetwork`, `ContextAnimation` — `_2024/transformers/helpers.py`), plus 2–5 `show_*(scene, ...)` choreography verbs. **Define the cast before writing a single `construct()`.**
2. **Scene files** — thin `InteractiveScene` subclasses whose `construct()` is a flat list of `# Beat name` comments, each assembling DSL objects and animating them.

Everything below serves this model.

---

## 3. The Six Laws (non-negotiable)

### Law 1 — Relative layout only. Overlap is structural, not policed.

Real 3b1b layout is overwhelmingly relative: `next_to` used ~10,500×, `arrange` ~2,460× across the repo, versus only a handful of absolute `move_to([x,y,z])` content placements — and nearly all of those absolutes are the *camera*, never content. **Forbid absolute content coordinates.** `move_to([x,y,z])` and `frame.animate.move_to(...)` are for the camera/light source only.

Allowed positioning primitives — content position ALWAYS comes from one of these:
```python
a.next_to(b, DOWN, buff=MED_LARGE_BUFF)     # relative to another object
a.next_to(b, RIGHT).match_y(c)              # compound: x from b, y aligned to c
a.align_to(b, LEFT)                         # share an edge
a.to_edge(UP, buff=LARGE_BUFF)              # to a frame edge (chrome only)
a.to_corner(UL)                             # to a corner (chrome only)
VGroup(*items).arrange(DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT)
VGroup(*items).arrange_in_grid(rows, cols, buff=...)
dots = Dot().get_grid(n_rows, n_cols, buff_ratio=0.5)
a.match_x(b) / a.match_y(b) / a.match_width(b) / a.match_height(b)
```

**Boxes and pills are content-sized, never hand-sized:**
```python
rect = SurroundingRectangle(label, buff=SMALL_BUFF)   # measures real glyph bounds
brace = Brace(group, DOWN); brace.get_text("12,288")   # fits the span, anchors at tip
under = Underline(word)                                 # width derived from content
```
`SurroundingRectangle` is used ~1,400× in the repo and **cannot clip or mis-center** because its size = `target.get_shape() + 2*buff` (`shape_matchers.py:22`). Delete every `text_width()` character-count heuristic — it has no analog in real code.

**The buff ladder is the ONLY source of gaps** (`default_config.yml:109`):
```
SMALL_BUFF=0.1   MED_SMALL_BUFF=0.25   MED_LARGE_BUFF=0.5   LARGE_BUFF=1.0
```
Never write `buff=0.37`. Consistent margins across scenes come for free from this ladder.

**Build the group, then place the group.** Assemble a `VGroup` declaratively, `.arrange()` its internals once, then position the whole thing. This is the dominant pattern across the repo's thousands of `VGroup`s. Never position leaf elements against screen coordinates.

**Labels on moving targets follow live:** `label.always.next_to(target, UP, buff=SMALL_BUFF)` (or `add_updater`). A single-frame bbox assert can't catch a mid-animation collision; a re-running `next_to` never collides.

> Because of Law 1, the old "Five-Layer Defense" (bbox asserts + frame validator + ffmpeg caption pad) is **retired**. Overlap is prevented at construction. Keep at most a light visual probe-frame check for *taste and timing*, not collision.

### Law 2 — Motion carries meaning. Born-from, never spawn.

The highest-frequency, highest-leverage technique in the corpus: **a key object enters by transforming from the concrete thing it abstracts**, so the causal link is visible.

```python
# the DALL·E image literally dissolves INTO the numeric vector entries (attention.py:128)
self.play(LaggedStart(*(bake_mobject_into_vector_entries(img, vec) for img, vec in ...)))
# 12,288 numbers collapse into a single symbol E_n  (attention.py:200)
self.play(FadeTransform(entry, sym))
# a formula term is delivered by morphing the data column it denotes (ml_basics.py:642)
self.play(TransformFromCopy(data_column, x_symbols))
```

Transform-family hard counts (attention.py): **`TransformFromCopy` 46 > `FadeTransform` 31 > `ReplacementTransform` 6 > plain `Transform` ~5 ; `TransformMatchingTex` 0.**
- **`TransformFromCopy(src, dst)` is THE workhorse** — source persists, a copy morphs to the destination, so the viewer sees "this *becomes* that" while "this" is still there.
- For equation retitles, `TransformMatchingStrings` (not `...Tex`).
- **Never `FadeIn` a load-bearing object from nothing.** Decorative scaffolding can fade in; the thing the lesson is about must be born from its referent.

**`time_span=(start, end)` choreographs a reveal inside ONE `self.play`** (used 638× in the repo) so a camera move and a multi-part reveal cascade together instead of firing simultaneously:
```python
# the softmax aha — one play, run_time=3, cascaded (attention.py:921)
self.play(
    self.frame.animate.reorient(...),
    GrowArrow(arrow, time_span=(1, 2)),
    FadeIn(label, time_span=(1, 2)),
    TransformFromCopy(ndp_col, softmax_col, time_span=(1.5, 3)),
    run_time=3,
)
```

**`generate_target` / `MoveToTarget`** (127 refs) is how dozens of objects snap into a new arrangement with zero hand-keyed coordinates:
```python
grp.target = grp.generate_target()
grp.target.arrange(RIGHT, buff=0.15)   # mutate the target with normal layout calls
grp.target.scale(0.65).next_to(anchor, DOWN)
self.play(MoveToTarget(grp))
```

### Law 3 — A tiny domain DSL gives consistency.

Every video defines ~6 custom Mobjects/Animations in a `helpers.py` plus ~6 scene-local `get_*`/`show_*` factories. A *vocabulary, not a framework*. This is the mechanism for both consistency and no-overlap (the objects arrange themselves).

> **For ML content, don't start from scratch — vendor 3b1b's transformer DSL** (`NumericEmbedding`, `WeightMatrix`, `EmbeddingArray`, `ContextAnimation`, `value_to_color`). It is the single fastest path to the authentic "columns of real numbers" look. See §13.

**Mobject-subclass recipe** (model: `Dial`, `helpers.py:655`; `MachineWithDials:761`):
```python
class WidgetThing(VGroup):
    def __init__(self, value=0, ...):
        # 1. build sub-parts
        body = Rectangle(...); needle = Line(...)
        # 2. lay them out RELATIVELY (ratio buffers, never move_to([x,y,0]))
        ticks = Line(...).get_grid(1, n, buff_ratio=0.5)
        ticks.set_width(body.get_width() - SMALL_BUFF); ticks.move_to(body)
        # 3. assemble, 4. name every meaningful part
        super().__init__(body, ticks, needle)
        self.body, self.needle = body, needle
        # 5. a state mutator that recomputes geometry+style from the logical value
        self.set_value(value)
    def set_value(self, v):
        self.needle.put_start_and_end_on(self.get_center(), self._value_to_point(v))
        self.needle.set_color(value_to_color(v))     # color is a pure function of value
    def animate_set_value(self, v, **kw):            # 6. methods that RETURN animations
        return AnimationGroup(self.animate.set_value(v), ...)
```

**`show_*(scene, ...)` choreography-verb recipe** (model: `show_matrix_vector_product`, `helpers.py:97`):
- first arg is `scene`; the function calls `scene.play/scene.wait` internally,
- owns its transient highlights via a `last_rects`/`to_fade` accumulator so **exactly one highlight is ever on screen**,
- **returns** the persistent mobjects it created.

**Variants are 2-line subclasses of a shared base overriding one class attribute** — never a copy-pasted `construct()` (model `HighlightEarthOrbit(NearestPlanets)` with `highlighted_orbit = 2`, and its sibling `HighlightMarsOrbit(NearestPlanets)` with `= 3`; `planets.py:2202`). Domain numbers live in ALL-CAPS module constants with one `conversion_factor` (`planets.py:4`), so sizes can't disagree across scenes.

**Large data uses honest ellipsis:** render a finite set, swap one element for `Tex(R"\dots")` (or `ellipses_row=-2`, default in `WeightMatrix`), so big arrays read as big without overflowing (`helpers.py:478, 620`).

### Law 4 — Earned 3D only, and 3D is never static.

3D is earned **only when a quantity's dimensionality is the payload** (a vector space, a surface `f(x,y)=z`, a volumetric field). Roughly half of even the attention video is intentionally flat. A flat data series in 3D is the #1 faux-3D tell — and the old matplotlib "isometric stack of parallelograms" cube is exactly the slop to never produce.

> **Production lesson (§13): 3D actively HURTS a *stack of thin layers*** (interleaved attention layers, a residual tower) — viewed at an angle they collapse into one solid block and the per-layer colors vanish. Render those **flat, face-on**. Reserve 3D for clouds, surfaces, and collapsing volumes.

> **Counter-lesson — over-flattening is its own slop (learned shipping the DeepSeek/Qwen attention seri

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [thtskaran](https://github.com/thtskaran)
- **Source:** [thtskaran/claude-skills](https://github.com/thtskaran/claude-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-thtskaran-claude-skills-ml-content
- Seller: https://agentstack.voostack.com/s/thtskaran
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
