Skip to content

Commit fb790d9

Browse files
cursoragentjmjava
andcommitted
feat(validate): strengthen manim layout checks and enforce single font lint
Co-authored-by: John Menke <jmjava@gmail.com>
1 parent 57fbe6a commit fb790d9

10 files changed

Lines changed: 673 additions & 51 deletions

File tree

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,30 @@ manim:
7777
vhs:
7878
vhs_path: "" # optional explicit binary path (relative to docgen.yaml or absolute)
7979
sync_from_timing: false # opt-in: allow tape Sleep rewrites from timing.json
80-
typing_ms_per_char: 55 # typing estimate used by sync-vhs
80+
typing_ms_per_char: 35 # typing estimate used by sync-vhs
8181
max_typing_sec: 3.0 # per block cap for typing estimate
82-
min_sleep_sec: 0.05 # floor for rewritten Sleep values
82+
min_sleep_sec: 0.2 # floor for rewritten Sleep values
8383

8484
pipeline:
8585
sync_vhs_after_timestamps: false # opt-in: run sync-vhs automatically in generate-all/rebuild-after-audio
8686

8787
compose:
8888
ffmpeg_timeout_sec: 300 # can also be overridden with: docgen compose --ffmpeg-timeout N
8989
warn_stale_vhs: true # warns if terminal/*.tape is newer than terminal/rendered/*.mp4
90+
91+
validation:
92+
layout:
93+
sample_interval_sec: 2.0
94+
edge_margin_px: 15
95+
min_spacing_px: 10
96+
check_overlap: true
97+
check_contrast: true
98+
min_contrast_ratio: 3.5
99+
check_font_size: true
100+
min_text_height_px: 10
101+
max_text_regions: 45
102+
check_single_font: true
103+
default_font_family: "Liberation Sans"
90104
```
91105
92106
If you edit a `.tape` file, run `docgen vhs` before `docgen compose` so compose does not use stale rendered terminal video.
@@ -100,6 +114,14 @@ docgen sync-vhs
100114
docgen vhs
101115
docgen compose
102116
```
117+
118+
Font consistency guardrails for Manim scenes:
119+
120+
- `docgen validate` checks Manim recordings for layout safety: off-screen clipping, spacing, overlap, contrast, and minimum text size.
121+
- `docgen validate` also lints scene source for single-font consistency:
122+
- requires one default `Text.set_default(font="...")` family (defaults to `Liberation Sans`)
123+
- flags `weight=BOLD`
124+
- flags mixed explicit `font=` families and `Text("...", COLOR_CONST)` positional color mistakes
103125
## System dependencies
104126

105127
- **ffmpeg** — composition and probing

docs/demos/animations/scenes.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
from manim import *
1717

18+
Text.set_default(font="Liberation Sans")
19+
1820
# ── Palette ──────────────────────────────────────────────────────────
1921
C_BG = "#1e1e2e"
2022
C_ACCENT = "#667eea"
@@ -105,9 +107,9 @@ def seg_end(i: int) -> float:
105107
return self._clock + 2
106108

107109
# ── Beat 0-1: Title card ──────────────────────────────────────
108-
title = Text("docgen", font_size=52, color=C_ACCENT, weight=BOLD)
110+
title = Text("docgen", font_size=56, color=C_ACCENT)
109111
subtitle = Text(
110-
"Markdown polished demo videos",
112+
"Markdown -> polished demo videos",
111113
font_size=22, color=C_WHITE,
112114
)
113115
subtitle.next_to(title, DOWN, buff=0.3)
@@ -117,7 +119,7 @@ def seg_end(i: int) -> float:
117119
self.timed_play(FadeIn(subtitle, shift=UP * 0.2), run_time=0.8)
118120

119121
tagline = Text(
120-
"Reproducible · version-controlled · fully automated",
122+
"Reproducible - version-controlled - fully automated",
121123
font_size=16, color=GREY_B,
122124
)
123125
tagline.next_to(subtitle, DOWN, buff=0.4)
@@ -250,7 +252,7 @@ def seg_end(i: int) -> float:
250252
# ── Beat 11: Single command highlight ─────────────────────────
251253
cmd = Text(
252254
"docgen generate-all",
253-
font_size=24, color=C_ACCENT, weight=BOLD,
255+
font_size=28, color=C_ACCENT,
254256
)
255257
cmd_bg = RoundedRectangle(
256258
corner_radius=0.12, width=cmd.width + 0.6, height=cmd.height + 0.4,
@@ -274,7 +276,7 @@ def seg_end(i: int) -> float:
274276
ct.move_to(RIGHT * 4.0 + DOWN * (1.8 + ci * 0.3))
275277
config_items.add(ct)
276278

277-
cfg_label = Text("docgen.yaml", font_size=14, color=C_ORANGE, weight=BOLD)
279+
cfg_label = Text("docgen.yaml", font_size=16, color=C_ORANGE)
278280
cfg_label.move_to(RIGHT * 4.0 + DOWN * 1.4)
279281
self.timed_play(FadeIn(cfg_label), run_time=0.3)
280282

@@ -330,7 +332,7 @@ def seg_end(i: int) -> float:
330332
return self._clock + 2
331333

332334
# ── Beat 0: Title ─────────────────────────────────────────────
333-
title = Text("docgen wizard", font_size=44, color=C_ACCENT, weight=BOLD)
335+
title = Text("docgen wizard", font_size=48, color=C_ACCENT)
334336
subtitle = Text(
335337
"Local web GUI for narration authoring",
336338
font_size=20, color=C_WHITE,
@@ -573,7 +575,7 @@ def seg_end(i: int) -> float:
573575

574576
@staticmethod
575577
def _tab(label, color, active=False):
576-
t = Text(label, font_size=15, color=color, weight=BOLD if active else NORMAL)
578+
t = Text(label, font_size=16 if active else 15, color=color)
577579
underline = Line(
578580
t.get_left() + DOWN * 0.15,
579581
t.get_right() + DOWN * 0.15,

docs/demos/docgen.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ visual_map:
6666
manim:
6767
quality: 720p30
6868
manim_path: ""
69+
font: "Liberation Sans"
6970
scenes:
7071
- DocgenOverviewScene
7172
- WizardGUIScene

src/docgen/config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,16 @@ def manim_path(self) -> str | None:
9494
value = self.raw.get("manim", {}).get("manim_path")
9595
return str(value) if value else None
9696

97+
@property
98+
def manim_font(self) -> str:
99+
"""Preferred single font family for Manim Text()."""
100+
return str(self.raw.get("manim", {}).get("font", "Liberation Sans"))
101+
102+
@property
103+
def manim_font_family(self) -> str:
104+
"""Backward-compatible alias for configured single Manim font."""
105+
return self.manim_font
106+
97107
@property
98108
def vhs_config(self) -> dict[str, Any]:
99109
defaults: dict[str, Any] = {
@@ -188,10 +198,28 @@ def layout_config(self) -> dict[str, Any]:
188198
"min_spacing_px": 10,
189199
"edge_margin_px": 15,
190200
"check_overlap": True,
201+
"sample_interval_sec": 2.0,
202+
"check_contrast": True,
203+
"min_contrast_ratio": 3.5,
204+
"check_font_size": True,
205+
"min_text_height_px": 10,
206+
"max_text_regions": 45,
191207
}
192208
defaults.update(self.raw.get("validation", {}).get("layout", {}))
193209
return defaults
194210

211+
@property
212+
def manim_lint_config(self) -> dict[str, Any]:
213+
defaults: dict[str, Any] = {
214+
"enabled": True,
215+
"enforce_single_font": True,
216+
"deny_weight_bold": True,
217+
"deny_positional_text_color": True,
218+
"expected_font": self.manim_font,
219+
}
220+
defaults.update(self.raw.get("validation", {}).get("manim_lint", {}))
221+
return defaults
222+
195223
@property
196224
def av_sync_config(self) -> dict[str, Any]:
197225
defaults: dict[str, Any] = {

src/docgen/init.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ def _write_config(plan: InitPlan) -> str:
254254
"quality": "720p30",
255255
"scenes": [f"Scene{s['id']}" for s in plan.segments],
256256
"manim_path": "",
257+
"font": "Liberation Sans",
257258
},
258259
"vhs": {
259260
"vhs_path": "",
@@ -279,6 +280,13 @@ def _write_config(plan: InitPlan) -> str:
279280
},
280281
"validation": {
281282
"max_drift_sec": 2.75,
283+
"manim_lint": {
284+
"enabled": True,
285+
"enforce_single_font_family": True,
286+
"allowed_font_family": "Liberation Sans",
287+
"ban_weight_bold": True,
288+
"ban_text_positional_color": True,
289+
},
282290
"narration_lint": {
283291
"pre_tts_deny_patterns": [
284292
"target duration",

0 commit comments

Comments
 (0)