retiam 2.0.0 → 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.
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/agent/AGENTS.md +67 -0
- package/agent/CLAUDE.md +2 -0
- package/agent/skill/SKILL.md +111 -0
- package/agent/skill/examples/AreaLesson.ts +48 -0
- package/agent/skill/examples/ExplainMotion.ts +32 -0
- package/agent/skill/examples/NarratedMotion.ts +42 -0
- package/agent/skill/examples/SecantToTangent.ts +142 -0
- package/agent/skill/references/api.md +109 -0
- package/agent/skill/references/explainers.md +121 -0
- package/agent/skill/references/quality.md +148 -0
- package/agent/skill/references/styles.md +58 -0
- package/agent/skill/references/tts.md +72 -0
- package/agent/templates/explainer/lesson.json +25 -0
- package/agent/templates/explainer/review.json +6 -0
- package/agent/templates/explainer/styles.json +92 -0
- package/bin/agent-kit.js +26 -0
- package/bin/cli.js +24 -4
- package/bin/context.js +57 -0
- package/bin/init.js +71 -12
- package/bin/lesson-project.js +30 -0
- package/bin/lesson.js +168 -0
- package/bin/lint.js +6 -6
- package/bin/snapshot-runtime.js +185 -0
- package/bin/snapshot.js +32 -225
- package/dist/THIRD_PARTY_NOTICES.md +3 -3
- package/dist/UTIF-BrQ3Ng5l.min.js +1 -0
- package/dist/{UTIF-2Ear_0Uo.js → UTIF-CN0-pEnd.js} +1 -1
- package/dist/decompress-D7LzbvPD.min.js +15 -0
- package/dist/index-3sAseSJQ.min.js +10472 -0
- package/dist/{index-B2sJBy4c.js → index-yQIdejOj.js} +15 -13
- package/dist/libs/explainer/example.d.ts +3 -0
- package/dist/libs/explainer/index.d.ts +5 -0
- package/dist/libs/explainer/index.js +134 -0
- package/dist/libs/explainer/plan.d.ts +12 -0
- package/dist/libs/explainer/plan.js +197 -0
- package/dist/libs/explainer/runtime.d.ts +24 -0
- package/dist/libs/explainer/styles.d.ts +3 -0
- package/dist/libs/explainer/types.d.ts +119 -0
- package/dist/libs/explainer/visuals.d.ts +18 -0
- package/dist/libs/tts/index.js +386 -1009
- package/dist/libs/vite/index.d.ts +28 -0
- package/dist/libs/vite/index.js +158 -0
- package/dist/{main-D_G4uIju.js → main-BTk_9jtl.js} +1 -1
- package/dist/main-lQwYAed8.min.js +5939 -0
- package/dist/msEdgeV2-BLEnvPJa.js +641 -0
- package/dist/opentype.module-B-1cYAb-.min.js +1 -0
- package/dist/retiam.es.js +1 -1
- package/dist/retiam.es.min.js +1 -0
- package/dist/src/engine/animation/Animation.d.ts +1 -0
- package/package.json +12 -6
- package/readme.md +14 -7
|
@@ -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.
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
43037
|
-
|
|
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",
|
|
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,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,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;
|