eduvis 0.1.0__py3-none-any.whl

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.
Files changed (40) hide show
  1. eduvis/__init__.py +16 -0
  2. eduvis/__main__.py +14 -0
  3. eduvis/cli.py +302 -0
  4. eduvis/core/__init__.py +15 -0
  5. eduvis/core/elements/__init__.py +5 -0
  6. eduvis/core/elements/generic.py +141 -0
  7. eduvis/core/elements/math.py +183 -0
  8. eduvis/core/export_schema.py +219 -0
  9. eduvis/core/prompt.py +252 -0
  10. eduvis/core/registry.py +325 -0
  11. eduvis/core/schemas/__init__.py +5 -0
  12. eduvis/core/schemas/actions.py +62 -0
  13. eduvis/core/schemas/placement.py +81 -0
  14. eduvis/core/schemas/progression.py +91 -0
  15. eduvis/core/schemas/relationships.py +52 -0
  16. eduvis/core/validator.py +256 -0
  17. eduvis/renderers/__init__.py +1 -0
  18. eduvis/renderers/svg/__init__.py +5 -0
  19. eduvis/renderers/svg/element_registry.py +287 -0
  20. eduvis/renderers/svg/primitives.py +150 -0
  21. eduvis/renderers/svg/renderers_base.py +1010 -0
  22. eduvis/renderers/svg/renderers_math/__init__.py +25 -0
  23. eduvis/renderers/svg/renderers_math/equation.py +207 -0
  24. eduvis/renderers/svg/renderers_math/fraction.py +238 -0
  25. eduvis/renderers/svg/renderers_math/geometry.py +341 -0
  26. eduvis/renderers/svg/renderers_math/graph.py +142 -0
  27. eduvis/renderers/svg/renderers_math/number.py +456 -0
  28. eduvis/renderers/svg/renderers_math/solid.py +539 -0
  29. eduvis/renderers/svg/spec_renderer.py +523 -0
  30. eduvis/schemas/actions.schema.json +47 -0
  31. eduvis/schemas/lesson.schema.json +68 -0
  32. eduvis/schemas/placement.schema.json +79 -0
  33. eduvis/schemas/progression.schema.json +86 -0
  34. eduvis/schemas/relationships.schema.json +52 -0
  35. eduvis-0.1.0.dist-info/METADATA +802 -0
  36. eduvis-0.1.0.dist-info/RECORD +40 -0
  37. eduvis-0.1.0.dist-info/WHEEL +5 -0
  38. eduvis-0.1.0.dist-info/entry_points.txt +2 -0
  39. eduvis-0.1.0.dist-info/licenses/LICENSE +179 -0
  40. eduvis-0.1.0.dist-info/top_level.txt +1 -0
eduvis/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """EduVis — educational content schema."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .core import ElementRegistry, ElementSpec, FieldSpec, validate_lesson, format_prompt_docs, get_all_schemas
6
+
7
+ __all__ = [
8
+ "__version__",
9
+ "ElementRegistry",
10
+ "ElementSpec",
11
+ "FieldSpec",
12
+ "validate_lesson",
13
+ "format_prompt_docs",
14
+ "get_all_schemas",
15
+ ]
16
+
eduvis/__main__.py ADDED
@@ -0,0 +1,14 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ # Support running directly as a script without parent package context (e.g. `python eduvis/__main__.py` or `uv run eduvis`)
5
+ if not __package__:
6
+ _ROOT = Path(__file__).resolve().parent.parent
7
+ if str(_ROOT) not in sys.path:
8
+ sys.path.insert(0, str(_ROOT))
9
+ from eduvis.cli import cli
10
+ else:
11
+ from .cli import cli
12
+
13
+ if __name__ == "__main__":
14
+ cli()
eduvis/cli.py ADDED
@@ -0,0 +1,302 @@
1
+ """
2
+ EduVis CLI — validate and render EduVis lesson YAML files.
3
+
4
+ Usage:
5
+ python -m eduvis validate lesson.yaml
6
+ python -m eduvis render lesson.yaml -o slides/
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import json
15
+
16
+ import click
17
+ import yaml
18
+
19
+ # ── EduVis Core import ────────────────────────────────────────────────────────
20
+ # Allow running from any directory: resolve the eduvis package root.
21
+ _PACKAGE_ROOT = Path(__file__).resolve().parent.parent # project root
22
+ if str(_PACKAGE_ROOT) not in sys.path:
23
+ sys.path.insert(0, str(_PACKAGE_ROOT))
24
+
25
+ from eduvis.core import validate_lesson, format_prompt_docs, get_all_schemas # noqa: E402
26
+
27
+ # ── EduVis SVG renderer ──────────────────────────────────────────────────────
28
+
29
+ # ── Constants ─────────────────────────────────────────────────────────────────
30
+ _EDUVIS_META = frozenset({"id", "placement", "actions", "relationships"})
31
+
32
+ # Layout zone mapping: EduVis layout_zone → svg_spec zone key.
33
+ _ZONE_MAP = {
34
+ "center": "center",
35
+ "left": "left",
36
+ "right": "right",
37
+ "full": "full",
38
+ "bottom": "bottom",
39
+ }
40
+
41
+ _LAYOUT_FOR_ZONE = {
42
+ "left": "two-column",
43
+ "right": "two-column",
44
+ "center": "visual + full-width",
45
+ "full": "header + full-width",
46
+ "bottom": "header + full-width",
47
+ }
48
+
49
+ # Phase → header background color + badge label.
50
+ # Colors are dark enough for white title text.
51
+ _PHASE_STYLE: dict[str, dict] = {
52
+ "hook": {"color": "#BF360C", "label": "HOOK"},
53
+ "explore": {"color": "#00695C", "label": "EXPLORE"},
54
+ "explain": {"color": "#1565C0", "label": "EXPLAIN"},
55
+ "guided_practice": {"color": "#4A148C", "label": "GUIDED"},
56
+ "independent_practice": {"color": "#1B5E20", "label": "PRACTICE"},
57
+ "challenge": {"color": "#B71C1C", "label": "CHALLENGE"},
58
+ "reflect": {"color": "#37474F", "label": "REFLECT"},
59
+ "recall": {"color": "#4E342E", "label": "RECALL"},
60
+ }
61
+
62
+ # Difficulty overrides for independent_practice (color + label).
63
+ _DIFFICULTY_STYLE: dict[str, dict] = {
64
+ "starter": {"color": "#1B5E20", "label": "STARTER"},
65
+ "routine": {"color": "#004D40", "label": "ROUTINE"},
66
+ "challenge": {"color": "#B71C1C", "label": "CHALLENGE"},
67
+ }
68
+
69
+ # Memory role → accent color for the divider line and dot.
70
+ _ROLE_COLOR: dict[str, str] = {
71
+ "anchor": "#FFD700",
72
+ "example": "#00BCD4",
73
+ "practice": "#66BB6A",
74
+ "misconception_fix":"#EF5350",
75
+ "retrieval": "#FFA726",
76
+ "review": "#9E9E9E",
77
+ }
78
+
79
+
80
+ # ── Bridge helpers ────────────────────────────────────────────────────────────
81
+
82
+ def _element_to_spec(element: dict) -> dict:
83
+ """Strip EduVis meta-fields; return only fields the SVG renderer needs."""
84
+ return {k: v for k, v in element.items() if k not in _EDUVIS_META}
85
+
86
+
87
+ def _element_title(element: dict) -> str:
88
+ """Build a human-readable slide title from an element's id and lesson_phase."""
89
+ eid = element.get("id", "")
90
+ placement = element.get("placement") or {}
91
+ phase = placement.get("lesson_phase", "")
92
+ if phase and eid:
93
+ return f"{phase.replace('_', ' ').title()}: {eid.replace('_', ' ')}"
94
+ return (eid or "slide").replace("_", " ").title()
95
+
96
+
97
+ def _build_svg_spec_yaml(element: dict) -> str:
98
+ """Convert one EduVis element dict to an svg_spec YAML string for rendering."""
99
+ placement = element.get("placement") or {}
100
+ zone = _ZONE_MAP.get(placement.get("layout_zone", "full"), "full")
101
+ layout = _LAYOUT_FOR_ZONE.get(zone, "header + full-width")
102
+
103
+ phase = placement.get("lesson_phase", "")
104
+ difficulty = placement.get("difficulty", "")
105
+ memory_role = placement.get("memory_role", "")
106
+
107
+ phase_style = _PHASE_STYLE.get(phase, {})
108
+ header_color = phase_style.get("color", "#111111")
109
+ phase_label = phase_style.get("label", "")
110
+
111
+ # Differentiate starter / routine within independent_practice
112
+ if phase == "independent_practice" and difficulty in _DIFFICULTY_STYLE:
113
+ diff_style = _DIFFICULTY_STYLE[difficulty]
114
+ header_color = diff_style["color"]
115
+ phase_label = diff_style["label"]
116
+
117
+ role_color = _ROLE_COLOR.get(memory_role, "")
118
+
119
+ spec_dict = {
120
+ "layout": layout,
121
+ "header_color": header_color,
122
+ "phase_label": phase_label,
123
+ "role_color": role_color,
124
+ "memory_role": memory_role,
125
+ "zones": {zone: [_element_to_spec(element)]},
126
+ }
127
+ return yaml.dump(spec_dict, allow_unicode=True, default_flow_style=False)
128
+
129
+
130
+ def _load_renderer():
131
+ """Load the standalone EduVis SVG renderer."""
132
+ try:
133
+ from .renderers.svg import SVGSpecRenderer # noqa: PLC0415
134
+ return SVGSpecRenderer()
135
+ except ImportError as exc:
136
+ raise click.ClickException(
137
+ f"Cannot import EduVis SVG renderer: {exc}"
138
+ ) from exc
139
+
140
+
141
+ # ── CLI ───────────────────────────────────────────────────────────────────────
142
+
143
+ @click.group()
144
+ def cli() -> None:
145
+ """EduVis - educational content schema tools."""
146
+
147
+
148
+ @cli.command()
149
+ @click.option(
150
+ "--subjects",
151
+ default="math",
152
+ show_default=True,
153
+ help="Comma-separated subject tags to include (e.g. math,science). Use '*' for all.",
154
+ )
155
+ @click.option(
156
+ "--output", "-o",
157
+ default=None,
158
+ help="Write vocabulary to this file instead of stdout.",
159
+ )
160
+ def docs(subjects: str, output: str | None) -> None:
161
+ """Print the full EduVis vocabulary for use in an LLM system prompt."""
162
+ subject_list = [s.strip() for s in subjects.split(",")]
163
+ vocab = format_prompt_docs(subject_list)
164
+ if output:
165
+ Path(output).write_text(vocab, encoding="utf-8")
166
+ click.echo(f"Vocabulary written to {output}")
167
+ else:
168
+ click.echo(vocab)
169
+
170
+
171
+ @cli.command()
172
+ @click.option(
173
+ "-o", "--output",
174
+ default="schemas",
175
+ show_default=True,
176
+ help="Directory to write JSON Schema files into.",
177
+ )
178
+ def schema(output: str) -> None:
179
+ """Export all EduVis JSON Schemas to a directory."""
180
+ out_dir = Path(output)
181
+ out_dir.mkdir(parents=True, exist_ok=True)
182
+
183
+ schemas = get_all_schemas()
184
+ for name, schema_dict in schemas.items():
185
+ out_path = out_dir / f"{name}.schema.json"
186
+ out_path.write_text(json.dumps(schema_dict, indent=2), encoding="utf-8")
187
+ click.echo(f" OK {out_path}")
188
+
189
+ click.secho(f"\nDone -- {len(schemas)} schema(s) written to {out_dir}/", fg="green")
190
+
191
+
192
+ @cli.command()
193
+ @click.argument("lesson_file", type=click.Path(exists=True, dir_okay=False))
194
+ def validate(lesson_file: str) -> None:
195
+ """Validate an EduVis lesson YAML file against all five pillars."""
196
+ with open(lesson_file, encoding="utf-8") as f:
197
+ try:
198
+ doc = yaml.safe_load(f)
199
+ except yaml.YAMLError as exc:
200
+ raise click.ClickException(f"YAML parse error: {exc}") from exc
201
+
202
+ if not isinstance(doc, dict):
203
+ raise click.ClickException("Lesson file must be a YAML mapping at the top level.")
204
+
205
+ warnings = validate_lesson(doc)
206
+
207
+ if not warnings:
208
+ click.secho(f"OK {lesson_file} -- valid, no warnings", fg="green")
209
+ else:
210
+ click.secho(f"WARN {lesson_file} -- {len(warnings)} warning(s):", fg="yellow")
211
+ for w in warnings:
212
+ click.echo(f" {w}")
213
+ sys.exit(1)
214
+
215
+
216
+ @cli.command()
217
+ @click.argument("lesson_file", type=click.Path(exists=True, dir_okay=False))
218
+ @click.option(
219
+ "-o", "--output",
220
+ default="output",
221
+ show_default=True,
222
+ help="Directory to write SVG files into.",
223
+ )
224
+ @click.option(
225
+ "--posting-group",
226
+ default="G1",
227
+ show_default=True,
228
+ help="Grade level for font sizing (G1, G2, or G3).",
229
+ )
230
+ @click.option(
231
+ "--skip-validation",
232
+ is_flag=True,
233
+ default=False,
234
+ help="Skip EduVis validation before rendering.",
235
+ )
236
+ def render(lesson_file: str, output: str, posting_group: str, skip_validation: bool) -> None:
237
+ """Render an EduVis lesson YAML to one SVG per element."""
238
+ with open(lesson_file, encoding="utf-8") as f:
239
+ try:
240
+ doc = yaml.safe_load(f)
241
+ except yaml.YAMLError as exc:
242
+ raise click.ClickException(f"YAML parse error: {exc}") from exc
243
+
244
+ if not isinstance(doc, dict):
245
+ raise click.ClickException("Lesson file must be a YAML mapping at the top level.")
246
+
247
+ # ── Validate first ────────────────────────────────────────────────────────
248
+ if not skip_validation:
249
+ warnings = validate_lesson(doc)
250
+ if warnings:
251
+ click.secho(f"WARN {len(warnings)} validation warning(s):", fg="yellow")
252
+ for w in warnings:
253
+ click.echo(f" {w}")
254
+ click.echo()
255
+
256
+ # ── Load renderer ─────────────────────────────────────────────────────────
257
+ renderer = _load_renderer()
258
+
259
+ # ── Render ────────────────────────────────────────────────────────────────
260
+ content = doc.get("content")
261
+ if not isinstance(content, list) or not content:
262
+ raise click.ClickException("No 'content' list found in the lesson file.")
263
+
264
+ out_dir = Path(output)
265
+ out_dir.mkdir(parents=True, exist_ok=True)
266
+
267
+ lesson_title = (doc.get("lesson") or {}).get("title", "")
268
+ rendered = 0
269
+ skipped = 0
270
+
271
+ for element in content:
272
+ if not isinstance(element, dict):
273
+ continue
274
+
275
+ element_id = element.get("id", f"element_{rendered + skipped + 1}")
276
+ title = f"{lesson_title} — {_element_title(element)}" if lesson_title else _element_title(element)
277
+
278
+ svg_spec_yaml = _build_svg_spec_yaml(element)
279
+
280
+ try:
281
+ svg_text = renderer.render(
282
+ svg_spec_yaml,
283
+ title=title,
284
+ posting_group=posting_group,
285
+ )
286
+ except Exception as exc:
287
+ click.secho(f" FAIL {element_id}: render error -- {exc}", fg="red")
288
+ skipped += 1
289
+ continue
290
+
291
+ out_path = out_dir / f"{element_id}.svg"
292
+ out_path.write_text(svg_text, encoding="utf-8")
293
+ click.echo(f" OK {out_path}")
294
+ rendered += 1
295
+
296
+ click.echo()
297
+ click.secho(
298
+ f"Done -- {rendered} SVG(s) written to {out_dir}/",
299
+ fg="green" if not skipped else "yellow",
300
+ )
301
+ if skipped:
302
+ click.secho(f" {skipped} element(s) skipped due to render errors.", fg="yellow")
@@ -0,0 +1,15 @@
1
+ """
2
+ EduVis Core — renderer-agnostic educational content schema.
3
+
4
+ Public API:
5
+ ElementRegistry — query element specs, generate prompt docs, validate fields
6
+ validate_lesson() — validate a complete lesson document (all five pillars)
7
+ format_prompt_docs() — full five-pillar vocabulary for LLM system prompts
8
+ """
9
+
10
+ from .registry import ElementRegistry, ElementSpec, FieldSpec
11
+ from .validator import validate_lesson
12
+ from .prompt import format_prompt_docs
13
+ from .export_schema import get_all_schemas
14
+
15
+ __all__ = ["ElementRegistry", "ElementSpec", "FieldSpec", "validate_lesson", "format_prompt_docs", "get_all_schemas"]
@@ -0,0 +1,5 @@
1
+ """EduVis Core — element spec modules."""
2
+
3
+ from . import generic, math
4
+
5
+ __all__ = ["generic", "math"]
@@ -0,0 +1,141 @@
1
+ """Generic element specs — available for all subjects."""
2
+
3
+ from ..registry import ElementSpec, FieldSpec
4
+
5
+ ELEMENT_SPECS: list[ElementSpec] = [
6
+ ElementSpec(
7
+ name="text_list",
8
+ subjects=["*"],
9
+ synopsis="items: [strings] (optional color per item)",
10
+ fields=[
11
+ FieldSpec("items", type="array", required=True,
12
+ description="List of bullet strings; prefix 'no-bullet:' to suppress bullet"),
13
+ FieldSpec("color", type="color", required=False,
14
+ description="Override bullet color for all items"),
15
+ FieldSpec("anchor", type="string", required=False, default="start",
16
+ enum=["start", "middle", "center"],
17
+ description="Text alignment"),
18
+ ],
19
+ ),
20
+ ElementSpec(
21
+ name="fact_boxes",
22
+ subjects=["*"],
23
+ synopsis="items: [{text, border_color}]",
24
+ fields=[
25
+ FieldSpec("items", type="array", required=True,
26
+ description="List of fact box dicts",
27
+ items=FieldSpec("item", type="object", properties=[
28
+ FieldSpec("text", type="string", description="Fact box content"),
29
+ FieldSpec("border_color", type="color", required=False,
30
+ description="Box border colour"),
31
+ ])),
32
+ ],
33
+ ),
34
+ ElementSpec(
35
+ name="example_panel",
36
+ subjects=["*"],
37
+ synopsis="items: [{heading, body}] — side-by-side comparison panels",
38
+ fields=[
39
+ FieldSpec("items", type="array", required=True,
40
+ description="List of panel dicts (max 3 for readability)",
41
+ items=FieldSpec("item", type="object", properties=[
42
+ FieldSpec("heading", type="string", description="Bold panel title"),
43
+ FieldSpec("body", type="string",
44
+ description="Panel body; use \\n for explicit line breaks"),
45
+ ])),
46
+ ],
47
+ ),
48
+ ElementSpec(
49
+ name="callout_box",
50
+ subjects=["*"],
51
+ synopsis="title, lines: [strings], border_color — highlighted callout",
52
+ fields=[
53
+ FieldSpec("title", type="string", required=False, description="Bold callout heading"),
54
+ FieldSpec("lines", type="array", required=True, description="Body text lines"),
55
+ FieldSpec("border_color", type="color", required=False, default="cyan"),
56
+ ],
57
+ ),
58
+ ElementSpec(
59
+ name="summary_list",
60
+ subjects=["*"],
61
+ synopsis="items: [strings] — identical to text_list, use on summary/takeaway slides",
62
+ fields=[
63
+ FieldSpec("items", type="array", required=True, description="Summary bullet strings"),
64
+ ],
65
+ notes=["PREFER summary_list on final slides to signal lesson wrap-up."],
66
+ ),
67
+ ElementSpec(
68
+ name="multiple_choice",
69
+ subjects=["*"],
70
+ synopsis="question: string, options: {A, B, C, D} — MCQ layout",
71
+ fields=[
72
+ FieldSpec("question", type="string", required=True, description="The MCQ stem"),
73
+ FieldSpec("options", type="object", required=True,
74
+ description="Exactly four options keyed A–D",
75
+ properties=[
76
+ FieldSpec("A", type="string"),
77
+ FieldSpec("B", type="string"),
78
+ FieldSpec("C", type="string"),
79
+ FieldSpec("D", type="string"),
80
+ ]),
81
+ ],
82
+ ),
83
+ ElementSpec(
84
+ name="hint_list",
85
+ subjects=["*"],
86
+ synopsis="items: [strings], final: string — numbered hints",
87
+ fields=[
88
+ FieldSpec("items", type="array", required=True,
89
+ description="Hint steps (auto-numbered unless item starts with a digit or 'Step')"),
90
+ FieldSpec("final", type="string", required=False,
91
+ description="Confirmation method shown in a box at the bottom"),
92
+ ],
93
+ ),
94
+ ElementSpec(
95
+ name="number_line",
96
+ subjects=["*"],
97
+ synopsis="range: [min, max], highlight: [{value, label, color}] — annotated number line",
98
+ fields=[
99
+ FieldSpec("range", type="array", required=True,
100
+ description="[min, max] numeric bounds"),
101
+ FieldSpec("highlight", type="array", required=False,
102
+ description="List of {value, label, color} highlight markers",
103
+ items=FieldSpec("hl", type="object", properties=[
104
+ FieldSpec("value", type="number", description="Position on the line"),
105
+ FieldSpec("label", type="string", required=False),
106
+ FieldSpec("color", type="color", required=False),
107
+ FieldSpec("type", type="string", required=False,
108
+ enum=["jump"],
109
+ description="'jump' draws a curved hop arrow"),
110
+ ])),
111
+ FieldSpec("direction_labels", type="object", required=False,
112
+ description="{left: 'Smaller', right: 'Larger'} axis end labels",
113
+ properties=[
114
+ FieldSpec("left", type="string", required=False),
115
+ FieldSpec("right", type="string", required=False),
116
+ ]),
117
+ FieldSpec("caption", type="string", required=False,
118
+ description="Title above the line"),
119
+ ],
120
+ ),
121
+ ElementSpec(
122
+ name="mixed_card",
123
+ subjects=["*"],
124
+ synopsis="ribbon_type: solve|remember|review, ribbon_label: string, items: [{type: text|math_grid, ...}] — mixed card",
125
+ fields=[
126
+ FieldSpec("ribbon_type", type="string", required=False, default="solve",
127
+ enum=["solve", "remember", "review"]),
128
+ FieldSpec("ribbon_label", type="string", required=False),
129
+ FieldSpec("items", type="array", required=True,
130
+ description="List of sub-elements to render within the card",
131
+ items=FieldSpec("item", type="object", properties=[
132
+ FieldSpec("type", type="string", required=True, enum=["text", "math_grid"]),
133
+ FieldSpec("lines", type="array", required=False),
134
+ FieldSpec("mode", type="string", required=False),
135
+ FieldSpec("rows", type="array", required=False),
136
+ FieldSpec("headers", type="array", required=False),
137
+ FieldSpec("row_colors", type="array", required=False),
138
+ ])),
139
+ ],
140
+ ),
141
+ ]