retiam 2.0.1 → 2.0.2

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 (52) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/agent/AGENTS.md +67 -0
  3. package/agent/CLAUDE.md +2 -0
  4. package/agent/skill/SKILL.md +111 -0
  5. package/agent/skill/examples/AreaLesson.ts +48 -0
  6. package/agent/skill/examples/ExplainMotion.ts +32 -0
  7. package/agent/skill/examples/NarratedMotion.ts +42 -0
  8. package/agent/skill/examples/SecantToTangent.ts +142 -0
  9. package/agent/skill/references/api.md +109 -0
  10. package/agent/skill/references/explainers.md +121 -0
  11. package/agent/skill/references/quality.md +148 -0
  12. package/agent/skill/references/styles.md +58 -0
  13. package/agent/skill/references/tts.md +72 -0
  14. package/agent/templates/explainer/lesson.json +25 -0
  15. package/agent/templates/explainer/review.json +6 -0
  16. package/agent/templates/explainer/styles.json +92 -0
  17. package/bin/agent-kit.js +26 -0
  18. package/bin/cli.js +24 -4
  19. package/bin/context.js +57 -0
  20. package/bin/init.js +71 -12
  21. package/bin/lesson-project.js +30 -0
  22. package/bin/lesson.js +168 -0
  23. package/bin/lint.js +6 -6
  24. package/bin/snapshot-runtime.js +185 -0
  25. package/bin/snapshot.js +32 -225
  26. package/dist/THIRD_PARTY_NOTICES.md +3 -3
  27. package/dist/UTIF-BrQ3Ng5l.min.js +1 -0
  28. package/dist/{UTIF-2Ear_0Uo.js → UTIF-CN0-pEnd.js} +1 -1
  29. package/dist/decompress-D7LzbvPD.min.js +15 -0
  30. package/dist/index-3sAseSJQ.min.js +10472 -0
  31. package/dist/{index-B2sJBy4c.js → index-yQIdejOj.js} +15 -13
  32. package/dist/libs/explainer/example.d.ts +3 -0
  33. package/dist/libs/explainer/index.d.ts +5 -0
  34. package/dist/libs/explainer/index.js +134 -0
  35. package/dist/libs/explainer/plan.d.ts +12 -0
  36. package/dist/libs/explainer/plan.js +197 -0
  37. package/dist/libs/explainer/runtime.d.ts +24 -0
  38. package/dist/libs/explainer/styles.d.ts +3 -0
  39. package/dist/libs/explainer/types.d.ts +119 -0
  40. package/dist/libs/explainer/visuals.d.ts +18 -0
  41. package/dist/libs/tts/index.js +386 -1009
  42. package/dist/libs/vite/index.d.ts +28 -0
  43. package/dist/libs/vite/index.js +158 -0
  44. package/dist/{main-D_G4uIju.js → main-BTk_9jtl.js} +1 -1
  45. package/dist/main-lQwYAed8.min.js +5939 -0
  46. package/dist/msEdgeV2-BLEnvPJa.js +641 -0
  47. package/dist/opentype.module-B-1cYAb-.min.js +1 -0
  48. package/dist/retiam.es.js +1 -1
  49. package/dist/retiam.es.min.js +1 -0
  50. package/dist/src/engine/animation/Animation.d.ts +1 -0
  51. package/package.json +13 -8
  52. package/readme.md +7 -6
@@ -1939,14 +1939,12 @@ class Nt {
1939
1939
  if (this._intervalAccumulator += t, this._intervalAccumulator < e && this.elapsed + this._intervalAccumulator < this.duration)
1940
1940
  return !1;
1941
1941
  const i = this._intervalAccumulator;
1942
- this._intervalAccumulator = 0, this._isStepping = !0;
1943
- try {
1944
- return this.update(i);
1945
- } finally {
1946
- this._isStepping = !1;
1947
- }
1942
+ return this._intervalAccumulator = 0, this._advanceTimelineStep(i);
1948
1943
  }
1949
- this._isStepping = !0;
1944
+ return this._advanceTimelineStep(t);
1945
+ }
1946
+ _advanceTimelineStep(t) {
1947
+ !this._startFired && t >= Math.max(0, this.delay) && (this._startFired = !0, this.onStart?.()), this._isStepping = !0;
1950
1948
  try {
1951
1949
  return this.update(t);
1952
1950
  } finally {
@@ -17542,7 +17540,7 @@ const lp = {
17542
17540
  // --- NEW: Convert to a static async preload method ---
17543
17541
  static async preloadModel(t, e, i = "rt_hdr_alb_nrm") {
17544
17542
  if (!(this.modelState === "ready" && this.currentModel === i))
17545
- return this.modelState === "loading" ? this.loadPromise : (this.modelState = "loading", this.loadPromise = import("./main-D_G4uIju.js").then(async (r) => {
17543
+ return this.modelState === "loading" ? this.loadPromise : (this.modelState = "loading", this.loadPromise = import("./main-BTk_9jtl.js").then(async (r) => {
17546
17544
  const n = typeof e.requestAdapterInfo == "function" ? await e.requestAdapterInfo() : e.info, s = lp[i];
17547
17545
  if (!s)
17548
17546
  throw new Error(`Unsupported OIDN model configured: ${i}`);
@@ -20087,7 +20085,7 @@ class gt {
20087
20085
  if (!i) continue;
20088
20086
  const T = g.toLowerCase();
20089
20087
  if (T.endsWith(".tif") || T.endsWith(".tiff")) {
20090
- const I = await (await fetch(g)).arrayBuffer(), C = await import("./UTIF-2Ear_0Uo.js").then((x) => x.U), m = C.default || C, p = m.decode(I);
20088
+ const I = await (await fetch(g)).arrayBuffer(), C = await import("./UTIF-CN0-pEnd.js").then((x) => x.U), m = C.default || C, p = m.decode(I);
20091
20089
  if (typeof m.decodeImage == "function") m.decodeImage(I, p[0]);
20092
20090
  else if (typeof m.decodeImages == "function") m.decodeImages(I, p);
20093
20091
  else throw new Error("UTIF decode function not found.");
@@ -28560,7 +28558,7 @@ class Xn extends Pe {
28560
28558
  try {
28561
28559
  const e = t.toLowerCase();
28562
28560
  if (e.endsWith(".tif") || e.endsWith(".tiff")) {
28563
- const r = await (await fetch(t)).arrayBuffer(), n = await import("./UTIF-2Ear_0Uo.js").then((c) => c.U), s = n.default || n, A = s.decode(r);
28561
+ const r = await (await fetch(t)).arrayBuffer(), n = await import("./UTIF-CN0-pEnd.js").then((c) => c.U), s = n.default || n, A = s.decode(r);
28564
28562
  if (typeof s.decodeImage == "function")
28565
28563
  s.decodeImage(r, A[0]);
28566
28564
  else if (typeof s.decodeImages == "function")
@@ -43033,10 +43031,14 @@ class UB {
43033
43031
  if (!(this.context.state === "running" || typeof window > "u"))
43034
43032
  return new Promise((t) => {
43035
43033
  const e = () => {
43036
- this.context.resume().catch(() => {
43037
- }).finally(() => t()), window.removeEventListener("pointerdown", e), window.removeEventListener("keydown", e), window.removeEventListener("click", e);
43034
+ this.context.removeEventListener("statechange", i), window.removeEventListener("pointerdown", r), window.removeEventListener("keydown", r), window.removeEventListener("click", r), t();
43035
+ }, i = () => {
43036
+ this.context.state === "running" && e();
43037
+ }, r = () => {
43038
+ this.context.resume().then(i).catch(() => {
43039
+ });
43038
43040
  };
43039
- window.addEventListener("pointerdown", e, { once: !0, passive: !0 }), window.addEventListener("keydown", e, { once: !0, passive: !0 }), window.addEventListener("click", e, { once: !0, passive: !0 });
43041
+ this.context.addEventListener("statechange", i), window.addEventListener("pointerdown", r, { once: !0, passive: !0 }), window.addEventListener("keydown", r, { once: !0, passive: !0 }), window.addEventListener("click", r, { once: !0, passive: !0 }), i();
43040
43042
  });
43041
43043
  }
43042
43044
  /**
@@ -0,0 +1,3 @@
1
+ import { LessonPlan, StyleInput } from './types';
2
+ /** A small executable reference, to replace with the requested subject's construction. */
3
+ export declare function createExampleLesson(style?: StyleInput): LessonPlan;
@@ -0,0 +1,5 @@
1
+ /** Teaching plans, visual profiles, narration cues, and a shared production clock. */
2
+ export * from './plan';
3
+ export * from './runtime';
4
+ export * from './visuals';
5
+ export * from './example';
@@ -0,0 +1,134 @@
1
+ import { assertLesson as S, voiceKey as x, speechKey as y, compileLesson as E, resolveStyle as A } from "./plan.js";
2
+ import { STYLE_PROFILES as ae, lessonCaptions as oe, validateLesson as se } from "./plan.js";
3
+ import { CustomAnimation as v, TrackAnimation as q, Waiting as C, hex as b, Color as T, Excalidraw as $, Create as z, Tex as N, CENTER as D, Rate as k } from "retiam";
4
+ import { TTS as L } from "retiam/tts";
5
+ /*! Retiam: noncommercial use only unless separately licensed. See ../../LICENSE and ../../COMMERCIAL_LICENSE.md. Third-party notices: ../../THIRD_PARTY_NOTICES.md and ../../THIRD_PARTY_LICENSES.txt. */
6
+ function Y(r, t, i = {}) {
7
+ S(t);
8
+ const d = (async () => {
9
+ const s = /* @__PURE__ */ new Map();
10
+ let a = i.prepared;
11
+ if (t.narration.enabled) {
12
+ if (!a && !i.synthesize && (t.narration.provider ?? "msedge") !== "msedge")
13
+ throw new Error("Provide prepared speech or a synthesize adapter for this narration provider.");
14
+ const m = r.audio;
15
+ if (a && a.voiceKey !== x(t))
16
+ throw new Error("Prepared narration uses different voice settings. Prepare narration again.");
17
+ a = a ?? { version: 1, voiceKey: x(t), assets: {} };
18
+ const c = t.beats.flatMap((n) => n.narration.map((e) => ({ beat: n.id, segment: e })));
19
+ let w = 0;
20
+ await Promise.all(Array.from({ length: Math.min(3, c.length) }, async () => {
21
+ for (; w < c.length; ) {
22
+ const n = c[w++], e = y(n.beat, n.segment.id);
23
+ let u;
24
+ if (i.prepared) {
25
+ const o = a.assets[e];
26
+ if (!o || o.text !== n.segment.text || !o.url)
27
+ throw new Error(`Missing/stale prepared asset: ${e}.`);
28
+ const g = await fetch(o.url);
29
+ if (!g.ok)
30
+ throw new Error(`Narration asset ${o.url}: HTTP ${g.status}.`);
31
+ if (u = await m.context.decodeAudioData(await g.arrayBuffer()), Math.abs(u.duration * 1e3 - o.durationMs) > 120)
32
+ throw new Error(`Narration duration mismatch: ${e}.`);
33
+ } else {
34
+ const o = await (i.synthesize?.(n.segment.text) ?? L.synthesize(n.segment.text, { voice: t.narration.voice ?? "en-US-AndrewNeural", speed: t.narration.speed ?? 1, pitch: t.narration.pitch ?? "+0Hz", volume: 1 }));
35
+ a.assets[e] = { text: n.segment.text, durationMs: o.durationMs, words: (o.wordBoundaries ?? []).map((g) => ({ text: g.text, offsetMs: g.offsetMs, durationMs: g.durationMs })) }, u = o.rawBuffer ? await m.context.decodeAudioData(o.rawBuffer.slice(0)) : o.toAudioBuffer(m.context);
36
+ }
37
+ s.set(e, u);
38
+ }
39
+ }));
40
+ }
41
+ const l = E(t, a, i.fps ?? 60), h = A(t.style);
42
+ return { plan: t, timeline: l, style: h, async run(m) {
43
+ const c = new Set(t.beats.map((e) => e.id));
44
+ for (const e of Object.keys(m))
45
+ if (!c.has(e))
46
+ throw new Error(`Unknown beat handler '${e}'.`);
47
+ const w = [];
48
+ for (const e of l.beats) {
49
+ const u = m[e.id];
50
+ if (!u)
51
+ throw new Error(`Missing beat handler '${e.id}'.`);
52
+ const o = (f) => {
53
+ const p = e.cues[f];
54
+ if (p === void 0)
55
+ throw new Error(`Unknown cue '${e.id}/${f}'.`);
56
+ return p;
57
+ }, g = u({ beat: e, durationMs: e.durationMs, style: h, cue: o, at: (f, p) => new v(() => {
58
+ }, { delay: o(f), duration: 1, onStart: p }) });
59
+ for (const f of g)
60
+ if (!Number.isFinite(f.delay) || !Number.isFinite(f.duration) || f.delay < 0 || f.duration < 0 || f.delay + f.duration > e.durationMs + 0.1)
61
+ throw new Error(`Animation exceeds beat '${e.id}' (${e.durationMs} ms).`);
62
+ const M = new q([...g, new C({ duration: e.durationMs })]);
63
+ M.delay = e.startMs, w.push(M);
64
+ }
65
+ const n = r.composer?.time ?? 0;
66
+ if (t.narration.enabled)
67
+ for (const e of l.beats)
68
+ for (const u of e.segments) {
69
+ const o = s.get(y(e.id, u.id));
70
+ r.audio.timeline.addClip(o, n + e.startMs + u.startMs, r.audio.voiceoverGain, { volume: t.narration.volume ?? 1, bus: "voiceover" }), r.audio.timeline.addVolumeAutomation("bgm", n + e.startMs + u.startMs, r.audio.bgmVolume * 0.25, 200), r.audio.timeline.addVolumeAutomation("bgm", n + e.startMs + u.startMs + u.durationMs, r.audio.bgmVolume, 200);
71
+ }
72
+ r.clearColor(b(h.background)), await r.play(new q(w));
73
+ } };
74
+ })();
75
+ return r.composer?.pendingTasks.push(d), d;
76
+ }
77
+ function J(r, t, i = "primary", d = !1) {
78
+ const s = t[i];
79
+ return r.setStroke(t.strokeWidth, b(s)), r.setFill(d ? b(s).alpha(0.18) : T.NONE), t.rough && r.toExcalidraw({ preset: "clean", stroke: s, strokeWidth: t.strokeWidth, roughness: t.roughness, fill: d ? s : "transparent", fillStyle: t.fillStyle, fillGap: 0.18, fillAlpha: 0.1, seed: 17, boil: !1 }), r;
80
+ }
81
+ function Q(r, t, i = 0, d = t.motion.drawMs) {
82
+ return t.rough ? $.createAnim(r, { delay: i, duration: d }) : new z(r, { delay: i, duration: d });
83
+ }
84
+ function X(r, t, i, d, s, a = {}) {
85
+ const l = new N(t, { fontSize: 0.42, anchor: D, fill: b(s.primary), opacity: 0 });
86
+ r.add(l);
87
+ let h, m;
88
+ return new v((c) => {
89
+ l.transform.position.set([h[0] + (m[0] - h[0]) * c, h[1] + (m[1] - h[1]) * c + (a.arc ?? 0.45) * Math.sin(Math.PI * c), h[2] + (m[2] - h[2]) * c]), l.setOpacity(Math.min(1, c * 8, (1 - c) * 8));
90
+ }, { delay: a.delay ?? 0, duration: a.duration ?? s.motion.moveMs, rate_func: k.smooth(), onStart: () => {
91
+ h = i(), m = d();
92
+ }, onComplete: () => r.remove(l) });
93
+ }
94
+ function Z(r, t = 0, i = 900, d = 0.1) {
95
+ let s;
96
+ return new v((a) => {
97
+ const l = 1 + d * Math.sin(Math.PI * a);
98
+ r.transform.setScale([s[0] * l, s[1] * l, s[2]]);
99
+ }, { delay: t, duration: i, rate_func: k.smooth(), onStart: () => {
100
+ const a = r.transform.getScale();
101
+ s = [a.x, a.y, a.z];
102
+ } });
103
+ }
104
+ const P = 1, B = "Why does area grow so fast?", H = "How a square's area changes when its side length doubles", I = "Curious beginners", F = "See why doubling both side lengths produces four unit squares.", K = "mathematical", O = { enabled: !1, provider: "msedge", voice: "en-US-AndrewNeural", speed: 1, pitch: "+0Hz", volume: 1 }, V = [], W = [{ id: "area-model", text: "A square of side two can be partitioned into four squares of side one.", sourceIds: [], illustrative: !0 }], j = [{ id: "predict", goal: "Invite a prediction before exposing the area.", visibleChange: "Draw one unit square and mark its side length.", focus: "The original square", minDurationMs: 4500, narration: [{ id: "question", text: "Here is one unit square. If we double its side length, what happens to its area?", pauseAfterMs: 900 }], cues: [{ id: "show", segment: "question" }] }, { id: "grow", goal: "Separate a change in side length from a change in area.", visibleChange: "Expand the outline while retaining the original square.", focus: "Both sides grow", minDurationMs: 5e3, narration: [{ id: "expand", text: "Let's double both sides. The original square still fits in the corner. How much new space have we made?", pauseAfterMs: 700 }], cues: [{ id: "expand", segment: "expand" }] }, { id: "count", goal: "Discover the four equal areas through decomposition.", visibleChange: "Reveal the three additional unit squares in sequence.", focus: "Four equal pieces", minDurationMs: 5e3, narration: [{ id: "reveal", text: "One here, another above, and one more in the corner. Together with our original square, that makes four." }], cues: [{ id: "reveal", segment: "reveal" }] }, { id: "connect", goal: "Connect the construction to the general area relationship.", visibleChange: "Keep all four pieces visible and reveal the equation below them.", focus: "Side length and area", minDurationMs: 4500, narration: [{ id: "insight", text: "Two times the side length gives four times the area. That's the square in side squared: two growing directions, multiplied together." }], cues: [{ id: "equation", segment: "insight" }] }], G = {
105
+ version: P,
106
+ title: B,
107
+ topic: H,
108
+ audience: I,
109
+ objective: F,
110
+ style: K,
111
+ narration: O,
112
+ sources: V,
113
+ claims: W,
114
+ beats: j
115
+ };
116
+ function ee(r = "mathematical") {
117
+ return { ...structuredClone(G), style: r };
118
+ }
119
+ export {
120
+ ae as STYLE_PROFILES,
121
+ S as assertLesson,
122
+ E as compileLesson,
123
+ ee as createExampleLesson,
124
+ Q as drawGeometry,
125
+ Z as emphasize,
126
+ oe as lessonCaptions,
127
+ Y as prepareLesson,
128
+ A as resolveStyle,
129
+ y as speechKey,
130
+ J as styleGeometry,
131
+ X as transferValue,
132
+ se as validateLesson,
133
+ x as voiceKey
134
+ };
@@ -0,0 +1,12 @@
1
+ import { LessonPlan, PreparedSpeech, CompiledLesson, Diagnostic } from './types';
2
+ export * from './types';
3
+ export { STYLE_PROFILES, resolveStyle } from './styles';
4
+ export declare const speechKey: (beat: string, segment: string) => string;
5
+ export declare const voiceKey: (plan: LessonPlan) => string;
6
+ /** Structural checks do not claim that facts, visuals, or teaching quality are correct. */
7
+ export declare function validateLesson(value: unknown): Diagnostic[];
8
+ export declare function assertLesson(value: unknown): asserts value is LessonPlan;
9
+ /** Compile one shared clock. Timing estimates are explicitly labeled until speech is prepared. */
10
+ export declare function compileLesson(plan: LessonPlan, prepared?: PreparedSpeech, fps?: number): CompiledLesson;
11
+ /** Cue groups never span a segment boundary or expose the answer during a prediction pause. */
12
+ export declare function lessonCaptions(timeline: CompiledLesson, wordsPerCue?: number): string;
@@ -0,0 +1,197 @@
1
+ /*! Retiam: noncommercial use only unless separately licensed. See ../../LICENSE and ../../COMMERCIAL_LICENSE.md. Third-party notices: ../../THIRD_PARTY_NOTICES.md and ../../THIRD_PARTY_LICENSES.txt. */
2
+ const b = { name: "Dijkstra-style mathematical explainer", background: "#0A1020", ink: "#ECF0F5", muted: "#74869C", primary: "#6CD5FF", secondary: "#F7CF73", font: "roboto", strokeWidth: 0.035, rough: !1, roughness: 0, fillStyle: "solid", motion: { drawMs: 1200, moveMs: 1400, holdMs: 700 }, accents: ["#65F0C4", "#FF879F", "#BA98FF", "#55DDE0"] }, k = { name: "Excalidraw whiteboard", background: "#FFFDF7", ink: "#263238", muted: "#7B858C", primary: "#1971C2", secondary: "#D46B08", font: "excalifont", strokeWidth: 0.04, rough: !0, roughness: 0.7, fillStyle: "hachure", motion: { drawMs: 1600, moveMs: 1500, holdMs: 900 }, accents: ["#1971C2", "#D46B08"] }, E = { name: "Minimal vector", background: "#F8FAFC", ink: "#17212B", muted: "#68788A", primary: "#2563EB", secondary: "#C2410C", font: "roboto", strokeWidth: 0.025, rough: !1, roughness: 0, fillStyle: "solid", motion: { drawMs: 900, moveMs: 1100, holdMs: 650 }, accents: ["#2563EB", "#C2410C"] }, F = { name: "Cinematic diagrams", background: "#080B18", ink: "#F4F1FF", muted: "#8290AA", primary: "#B79AFF", secondary: "#58E0CE", font: "roboto", strokeWidth: 0.04, rough: !1, roughness: 0, fillStyle: "solid", motion: { drawMs: 1500, moveMs: 2e3, holdMs: 1e3 }, accents: ["#B79AFF", "#58E0CE"] }, A = {
3
+ mathematical: b,
4
+ excalidraw: k,
5
+ minimal: E,
6
+ cinematic: F
7
+ }, w = A;
8
+ function x(n) {
9
+ const i = typeof n == "string" ? n : n?.extends;
10
+ if (!Object.hasOwn(w, i ?? ""))
11
+ throw new Error(`Unknown style '${i}'. Choose ${Object.keys(w).join(", ")} or extend one in the lesson JSON.`);
12
+ const e = w[i], r = typeof n == "string" ? {} : n;
13
+ for (const s of Object.keys(r))
14
+ if (s !== "extends" && !Object.hasOwn(e, s))
15
+ throw new Error(`Unknown style token '${s}'.`);
16
+ if (r.motion !== void 0 && (!r.motion || typeof r.motion != "object" || Array.isArray(r.motion)))
17
+ throw new Error("Style motion must be an object.");
18
+ for (const s of Object.keys(r.motion ?? {}))
19
+ if (!Object.hasOwn(e.motion, s))
20
+ throw new Error(`Unknown style motion token '${s}'.`);
21
+ const a = { ...e, ...r, motion: { ...e.motion, ...r.motion } };
22
+ for (const s of ["background", "ink", "muted", "primary", "secondary"])
23
+ if (!/^#[\da-f]{6}$/i.test(a[s]))
24
+ throw new Error(`Style ${s} must be a six-digit hex color.`);
25
+ if (!a.font?.trim() || !a.name?.trim() || typeof a.rough != "boolean" || !["solid", "hachure"].includes(a.fillStyle))
26
+ throw new Error("Invalid style font, name, rough flag, or fillStyle.");
27
+ if (!(a.strokeWidth > 0 && a.strokeWidth <= 0.5) || !(a.roughness >= 0 && a.roughness <= 3))
28
+ throw new Error("Invalid style strokeWidth or roughness.");
29
+ for (const [s, f] of Object.entries(a.motion))
30
+ if (!Number.isFinite(f) || f < 0)
31
+ throw new Error(`Invalid style motion.${s}.`);
32
+ if (!Array.isArray(a.accents) || !a.accents.length || a.accents.some((s) => !/^#[\da-f]{6}$/i.test(s)))
33
+ throw new Error("Style accents must contain hex colors.");
34
+ return { ...a, accents: [...a.accents] };
35
+ }
36
+ const M = /^[a-z][a-z0-9-]*$/, y = (n) => typeof n == "string" && n.trim().length > 0, v = (n) => n.toLowerCase().replace(/[^\p{L}\p{N}]/gu, ""), S = (n, i) => `${n}/${i}`, I = (n) => JSON.stringify({ provider: n.narration.provider ?? "msedge", voice: n.narration.voice ?? "en-US-AndrewNeural", speed: n.narration.speed ?? 1, pitch: n.narration.pitch ?? "+0Hz" });
37
+ function C(n) {
38
+ const i = [], e = (t, d) => i.push({ path: t, message: d });
39
+ if (!n || typeof n != "object" || Array.isArray(n))
40
+ return [{ path: "$", message: "Expected a lesson object." }];
41
+ const r = n;
42
+ r.version !== 1 && e("version", "Expected version 1.");
43
+ for (const t of ["title", "topic", "audience", "objective"])
44
+ y(r[t]) || e(t, "Provide a nonempty string.");
45
+ try {
46
+ x(r.style);
47
+ } catch (t) {
48
+ e("style", t.message);
49
+ }
50
+ (!r.narration || typeof r.narration.enabled != "boolean") && e("narration.enabled", "Choose true or false."), r.narration?.speed !== void 0 && (!Number.isFinite(r.narration.speed) || r.narration.speed <= 0) && e("narration.speed", "Must be positive."), r.narration?.volume !== void 0 && (!Number.isFinite(r.narration.volume) || r.narration.volume < 0 || r.narration.volume > 1) && e("narration.volume", "Must be between 0 and 1."), r.narration?.voice !== void 0 && !y(r.narration.voice) && e("narration.voice", "Must be a voice ID.");
51
+ for (const t of ["provider", "pitch"])
52
+ r.narration?.[t] !== void 0 && !y(r.narration[t]) && e(`narration.${t}`, "Must be a nonempty string.");
53
+ const a = /* @__PURE__ */ new Set();
54
+ Array.isArray(r.sources) ? r.sources.forEach((t, d) => {
55
+ !t || !M.test(t.id) || a.has(t.id) || !y(t.title) ? e(`sources[${d}]`, "Provide a unique source ID and title.") : a.add(t.id);
56
+ }) : e("sources", "Provide a sources array; it may be empty for an illustrative construction.");
57
+ const s = (t, d) => {
58
+ if (!Array.isArray(t))
59
+ e(d, "Expected source IDs.");
60
+ else
61
+ for (const o of t)
62
+ a.has(o) || e(d, `Unknown source '${o}'.`);
63
+ }, f = /* @__PURE__ */ new Set();
64
+ if (r.claims !== void 0 && !Array.isArray(r.claims))
65
+ e("claims", "Expected an array.");
66
+ else
67
+ for (const [t, d] of (r.claims ?? []).entries()) {
68
+ if (!d || !M.test(d.id) || f.has(d.id) || !y(d.text)) {
69
+ e(`claims[${t}]`, "Provide a unique claim ID and text.");
70
+ continue;
71
+ }
72
+ f.add(d.id), s(d.sourceIds, `claims[${t}].sourceIds`), d.illustrative !== void 0 && typeof d.illustrative != "boolean" && e(`claims[${t}].illustrative`, "Must be a boolean."), !d.illustrative && !d.sourceIds?.length && e(`claims[${t}].sourceIds`, "Factual claims need a source; mark illustrative models explicitly.");
73
+ }
74
+ const h = /* @__PURE__ */ new Set();
75
+ return !Array.isArray(r.beats) || !r.beats.length ? (e("beats", "Provide at least one beat."), i) : (r.beats.forEach((t, d) => {
76
+ const o = `beats[${d}]`;
77
+ if (!t || !M.test(t.id) || h.has(t.id)) {
78
+ e(o, "Beat IDs must be unique lowercase identifiers.");
79
+ return;
80
+ }
81
+ h.add(t.id);
82
+ for (const c of ["goal", "visibleChange", "focus"])
83
+ y(t[c]) || e(`${o}.${c}`, "Describe the learning goal, visible event, and attention target.");
84
+ t.minDurationMs !== void 0 && (!Number.isFinite(t.minDurationMs) || t.minDurationMs < 0) && e(`${o}.minDurationMs`, "Must be nonnegative."), t.sourceIds !== void 0 && s(t.sourceIds, `${o}.sourceIds`);
85
+ const l = /* @__PURE__ */ new Map();
86
+ !Array.isArray(t.narration) || !t.narration.length ? e(`${o}.narration`, "Provide at least one spoken segment, also used for silent preview timing.") : t.narration.forEach((c, u) => {
87
+ if (!c || !M.test(c.id) || l.has(c.id) || !y(c.text)) {
88
+ e(`${o}.narration[${u}]`, "Provide a unique segment ID and nonempty spoken text.");
89
+ return;
90
+ }
91
+ l.set(c.id, c.text), c.pauseAfterMs !== void 0 && (!Number.isFinite(c.pauseAfterMs) || c.pauseAfterMs < 0) && e(`${o}.narration[${u}].pauseAfterMs`, "Must be nonnegative.");
92
+ });
93
+ const m = /* @__PURE__ */ new Set();
94
+ if (t.cues !== void 0 && !Array.isArray(t.cues))
95
+ e(`${o}.cues`, "Expected an array.");
96
+ else
97
+ for (const [c, u] of (t.cues ?? []).entries()) {
98
+ if (!u || !M.test(u.id) || m.has(u.id)) {
99
+ e(`${o}.cues[${c}]`, "Provide a unique cue ID.");
100
+ continue;
101
+ }
102
+ m.add(u.id), l.has(u.segment) || e(`${o}.cues[${c}].segment`, "Cue references an unknown narration segment."), u.phrase !== void 0 && (!y(u.phrase) || !v(l.get(u.segment) ?? "").includes(v(u.phrase))) && e(`${o}.cues[${c}].phrase`, "Phrase must occur in the specified spoken segment."), u.occurrence !== void 0 && (!Number.isSafeInteger(u.occurrence) || u.occurrence < 1) && e(`${o}.cues[${c}].occurrence`, "Must be a positive occurrence number."), u.offsetMs !== void 0 && (!Number.isFinite(u.offsetMs) || u.offsetMs < 0) && e(`${o}.cues[${c}].offsetMs`, "Must be nonnegative.");
103
+ }
104
+ }), i);
105
+ }
106
+ function D(n) {
107
+ const i = C(n);
108
+ if (i.length)
109
+ throw new Error(i.map((e) => `${e.path}: ${e.message}`).join(`
110
+ `));
111
+ }
112
+ function j(n, i) {
113
+ const e = i.trim().split(/\s+/).map(v), r = [];
114
+ return n.forEach((a, s) => {
115
+ e.every((f, h) => v(n[s + h]?.text ?? "") === f) && r.push(s);
116
+ }), r;
117
+ }
118
+ function N(n, i, e = 60) {
119
+ if (D(n), !Number.isSafeInteger(e) || e < 1 || e > 240)
120
+ throw new Error("fps must be an integer from 1 to 240.");
121
+ if (n.narration.enabled && (!i || i.version !== 1 || !i.assets || typeof i.assets != "object" || i.voiceKey !== I(n)))
122
+ throw new Error("Narration is not prepared for these voice settings. Run retiam lesson prepare.");
123
+ let r = 0;
124
+ const a = [];
125
+ for (const s of n.beats) {
126
+ let f = 0;
127
+ const h = [];
128
+ for (const o of s.narration) {
129
+ const l = n.narration.enabled ? i.assets[S(s.id, o.id)] : void 0;
130
+ if (n.narration.enabled && (!l || l.text !== o.text))
131
+ throw new Error(`Missing or stale audio: ${s.id}/${o.id}. Prepare narration again.`);
132
+ const m = l?.durationMs ?? Math.max(900, o.text.trim().split(/\s+/).length * 360);
133
+ if (!Number.isFinite(m) || m <= 0)
134
+ throw new Error(`Invalid speech duration: ${s.id}/${o.id}.`);
135
+ if (l && (!Array.isArray(l.words) || !l.words.length))
136
+ throw new Error(`Missing word timings: ${s.id}/${o.id}.`);
137
+ const c = l?.words ?? o.text.trim().split(/\s+/).map((g, $, p) => ({ text: g, offsetMs: m * $ / p.length, durationMs: m / p.length }));
138
+ let u = -1;
139
+ for (const g of c) {
140
+ if (!y(g.text) || !Number.isFinite(g.offsetMs) || !Number.isFinite(g.durationMs) || g.offsetMs < u - 0.1 || g.durationMs < 0 || g.offsetMs + g.durationMs > m + 1)
141
+ throw new Error(`Invalid word timings: ${s.id}/${o.id}.`);
142
+ u = g.offsetMs + g.durationMs;
143
+ }
144
+ h.push({ ...l, text: o.text, id: o.id, durationMs: m, words: c, startMs: f, pauseAfterMs: o.pauseAfterMs ?? 0 }), f += m + (o.pauseAfterMs ?? 0);
145
+ }
146
+ const t = Math.ceil(Math.max(s.minDurationMs ?? 0, f + 450) * e / 1e3 - 1e-8) * 1e3 / e, d = {};
147
+ for (const o of s.cues ?? []) {
148
+ const l = h.find((c) => c.id === o.segment);
149
+ let m = l.startMs;
150
+ if (o.phrase) {
151
+ const c = j(l.words, o.phrase);
152
+ if (c.length > 1 && o.occurrence === void 0)
153
+ throw new Error(`Ambiguous cue '${s.id}/${o.id}'; set occurrence.`);
154
+ const u = c[(o.occurrence ?? 1) - 1];
155
+ if (u === void 0)
156
+ throw new Error(`Unaligned cue '${s.id}/${o.id}': ${o.phrase}.`);
157
+ m += l.words[u].offsetMs;
158
+ }
159
+ if (m += o.offsetMs ?? 0, m > l.startMs + l.durationMs)
160
+ throw new Error(`Cue '${s.id}/${o.id}' falls outside its speech segment.`);
161
+ d[o.id] = m;
162
+ }
163
+ a.push({ id: s.id, startMs: r, durationMs: t, segments: h, cues: d }), r += t;
164
+ }
165
+ return { version: 1, timing: n.narration.enabled ? "recorded" : "estimated", fps: e, durationMs: r, beats: a };
166
+ }
167
+ function O(n, i = 8) {
168
+ if (!Number.isInteger(i) || i < 1)
169
+ throw new Error("wordsPerCue must be positive.");
170
+ const e = (s) => {
171
+ const f = Math.round(s);
172
+ return `${String(Math.floor(f / 36e5)).padStart(2, "0")}:${String(Math.floor(f / 6e4) % 60).padStart(2, "0")}:${String(Math.floor(f / 1e3) % 60).padStart(2, "0")},${String(f % 1e3).padStart(3, "0")}`;
173
+ };
174
+ let r = 0;
175
+ const a = [];
176
+ for (const s of n.beats)
177
+ for (const f of s.segments)
178
+ for (let h = 0; h < f.words.length; h += i) {
179
+ const t = f.words.slice(h, h + i), d = t.at(-1);
180
+ a.push(`${++r}
181
+ ${e(s.startMs + f.startMs + t[0].offsetMs)} --> ${e(s.startMs + f.startMs + d.offsetMs + d.durationMs)}
182
+ ${t.map((o) => o.text).join(" ")}
183
+ `);
184
+ }
185
+ return a.join(`
186
+ `);
187
+ }
188
+ export {
189
+ w as STYLE_PROFILES,
190
+ D as assertLesson,
191
+ N as compileLesson,
192
+ O as lessonCaptions,
193
+ x as resolveStyle,
194
+ S as speechKey,
195
+ C as validateLesson,
196
+ I as voiceKey
197
+ };
@@ -0,0 +1,24 @@
1
+ import { Animation, Scene } from 'retiam';
2
+ import { TTSAudioResult } from 'retiam/tts';
3
+ import { LessonPlan, PreparedSpeech, CompiledLesson, CompiledBeat, StyleProfile } from './types';
4
+ export interface BeatContext {
5
+ beat: CompiledBeat;
6
+ durationMs: number;
7
+ style: StyleProfile;
8
+ cue(id: string): number;
9
+ at(id: string, action: () => void): Animation;
10
+ }
11
+ export interface PreparedLesson {
12
+ plan: LessonPlan;
13
+ timeline: CompiledLesson;
14
+ style: StyleProfile;
15
+ /** Handlers build animations up front. Put state changes inside animations or at(). */
16
+ run(handlers: Record<string, (beat: BeatContext) => Animation[]>): Promise<void>;
17
+ }
18
+ export interface PrepareLessonOptions {
19
+ prepared?: PreparedSpeech;
20
+ fps?: number;
21
+ /** Optional adapter; defaults to the active retiam/tts provider. */
22
+ synthesize?: (text: string) => Promise<TTSAudioResult>;
23
+ }
24
+ export declare function prepareLesson(scene: Scene, plan: LessonPlan, options?: PrepareLessonOptions): Promise<PreparedLesson>;
@@ -0,0 +1,3 @@
1
+ import { StyleId, StyleInput, StyleProfile } from './types';
2
+ export declare const STYLE_PROFILES: Readonly<Record<StyleId, StyleProfile>>;
3
+ export declare function resolveStyle(input: StyleInput): StyleProfile;
@@ -0,0 +1,119 @@
1
+ /** A visual profile is independent of the topic and teaching sequence. */
2
+ export type StyleId = 'mathematical' | 'excalidraw' | 'minimal' | 'cinematic';
3
+ export interface StyleProfile {
4
+ name: string;
5
+ background: string;
6
+ ink: string;
7
+ muted: string;
8
+ primary: string;
9
+ secondary: string;
10
+ accents: string[];
11
+ font: string;
12
+ strokeWidth: number;
13
+ rough: boolean;
14
+ roughness: number;
15
+ fillStyle: 'solid' | 'hachure';
16
+ motion: {
17
+ drawMs: number;
18
+ moveMs: number;
19
+ holdMs: number;
20
+ };
21
+ }
22
+ export type StyleInput = StyleId | ({
23
+ extends: StyleId;
24
+ } & Partial<Omit<StyleProfile, 'motion'>> & {
25
+ motion?: Partial<StyleProfile['motion']>;
26
+ });
27
+ export interface LessonSource {
28
+ id: string;
29
+ title: string;
30
+ url?: string;
31
+ note?: string;
32
+ }
33
+ export interface LessonClaim {
34
+ id: string;
35
+ text: string;
36
+ sourceIds: string[];
37
+ illustrative?: boolean;
38
+ }
39
+ export interface NarrationSegment {
40
+ id: string;
41
+ text: string;
42
+ pauseAfterMs?: number;
43
+ }
44
+ /** Phrases are scoped to a segment. Repeated phrases require an explicit occurrence (1-based). */
45
+ export interface LessonCue {
46
+ id: string;
47
+ segment: string;
48
+ phrase?: string;
49
+ occurrence?: number;
50
+ offsetMs?: number;
51
+ }
52
+ export interface LessonBeat {
53
+ id: string;
54
+ goal: string;
55
+ visibleChange: string;
56
+ focus: string;
57
+ minDurationMs?: number;
58
+ narration: NarrationSegment[];
59
+ cues?: LessonCue[];
60
+ sourceIds?: string[];
61
+ }
62
+ export interface LessonPlan {
63
+ version: 1;
64
+ title: string;
65
+ topic: string;
66
+ audience: string;
67
+ objective: string;
68
+ style: StyleInput;
69
+ narration: {
70
+ enabled: boolean;
71
+ provider?: string;
72
+ voice?: string;
73
+ speed?: number;
74
+ pitch?: string;
75
+ volume?: number;
76
+ };
77
+ sources: LessonSource[];
78
+ claims?: LessonClaim[];
79
+ beats: LessonBeat[];
80
+ }
81
+ export interface TimedWord {
82
+ text: string;
83
+ offsetMs: number;
84
+ durationMs: number;
85
+ }
86
+ export interface SpeechAsset {
87
+ text: string;
88
+ durationMs: number;
89
+ words: TimedWord[];
90
+ url?: string;
91
+ }
92
+ export interface PreparedSpeech {
93
+ version: 1;
94
+ voiceKey: string;
95
+ assets: Record<string, SpeechAsset>;
96
+ }
97
+ export interface CompiledSegment extends SpeechAsset {
98
+ id: string;
99
+ startMs: number;
100
+ pauseAfterMs: number;
101
+ }
102
+ export interface CompiledBeat {
103
+ id: string;
104
+ startMs: number;
105
+ durationMs: number;
106
+ segments: CompiledSegment[];
107
+ cues: Record<string, number>;
108
+ }
109
+ export interface CompiledLesson {
110
+ version: 1;
111
+ timing: 'recorded' | 'estimated';
112
+ fps: number;
113
+ durationMs: number;
114
+ beats: CompiledBeat[];
115
+ }
116
+ export interface Diagnostic {
117
+ path: string;
118
+ message: string;
119
+ }
@@ -0,0 +1,18 @@
1
+ import { Animation, Entity, Scene, VectorEntity } from 'retiam';
2
+ import { StyleProfile } from './types';
3
+ export type LessonPoint = [
4
+ number,
5
+ number,
6
+ number
7
+ ];
8
+ /** Apply a profile to geometry; text keeps readable glyphs and uses the profile's font. */
9
+ export declare function styleGeometry<T extends VectorEntity>(entity: T, style: StyleProfile, role?: 'primary' | 'secondary' | 'ink', fill?: boolean): T;
10
+ export declare function drawGeometry(entity: VectorEntity, style: StyleProfile, delay?: number, duration?: number): Animation;
11
+ /** Move a value between representations while preserving its visual identity. */
12
+ export declare function transferValue(scene: Scene, text: string, from: () => LessonPoint, to: () => LessonPoint, style: StyleProfile, options?: {
13
+ delay?: number;
14
+ duration?: number;
15
+ arc?: number;
16
+ }): Animation;
17
+ /** Attention without changing the object's persistent position or scale. */
18
+ export declare function emphasize(entity: Entity, delay?: number, duration?: number, strength?: number): Animation;