retiam 2.0.1 → 2.0.3
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 +70 -0
- package/agent/CLAUDE.md +2 -0
- package/agent/skill/SKILL.md +114 -0
- package/agent/skill/examples/AreaLesson.ts +48 -0
- package/agent/skill/examples/ExplainMotion.ts +32 -0
- package/agent/skill/examples/NarratedMotion.ts +50 -0
- package/agent/skill/examples/SecantToTangent.ts +142 -0
- package/agent/skill/references/api.md +111 -0
- package/agent/skill/references/explainers.md +123 -0
- package/agent/skill/references/quality.md +151 -0
- package/agent/skill/references/styles.md +58 -0
- package/agent/skill/references/tts.md +166 -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 +195 -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-D6vx2UEX.min.js +1 -0
- package/dist/{UTIF-2Ear_0Uo.js → UTIF-mkffT70R.js} +1 -1
- package/dist/decompress-D7LzbvPD.min.js +15 -0
- package/dist/{index-B2sJBy4c.js → index-CdBPrYbr.js} +3830 -3795
- package/dist/index-jgFyrc2D.min.js +10472 -0
- package/dist/libs/explainer/example.d.ts +3 -0
- package/dist/libs/explainer/index.d.ts +5 -0
- package/dist/libs/explainer/index.js +135 -0
- package/dist/libs/explainer/plan.d.ts +12 -0
- package/dist/libs/explainer/plan.js +219 -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 +124 -0
- package/dist/libs/explainer/visuals.d.ts +18 -0
- package/dist/libs/tts/animations/Speak.d.ts +13 -7
- package/dist/libs/tts/components/TTSNarrationComponent.d.ts +9 -7
- package/dist/libs/tts/core/TTSCache.d.ts +3 -3
- package/dist/libs/tts/core/TTSClient.d.ts +3 -3
- package/dist/libs/tts/core/types.d.ts +48 -6
- package/dist/libs/tts/index.d.ts +2 -1
- package/dist/libs/tts/index.js +203 -1333
- package/dist/libs/tts/node/index.d.ts +10 -0
- package/dist/libs/tts/node/index.js +791 -0
- package/dist/libs/tts/providers/ElevenLabsTTSProvider.d.ts +30 -0
- package/dist/libs/tts/providers/ITTSProvider.d.ts +2 -2
- package/dist/libs/vite/index.d.ts +28 -0
- package/dist/libs/vite/index.js +158 -0
- package/dist/main-Cc4QQbSK.min.js +5939 -0
- package/dist/{main-D_G4uIju.js → main-DMu_5Qy7.js} +1 -1
- package/dist/msEdgeV2-D3w4Xsn-.js +642 -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/dist/src/engine/entities/composites/text/mathjaxInterop.d.ts +6 -0
- package/package.json +15 -8
- package/readme.md +27 -7
package/bin/cli.js
CHANGED
|
@@ -20,13 +20,17 @@ const exportScript = path.join(__dirname, 'export.js');
|
|
|
20
20
|
const lintScript = path.join(__dirname, 'lint.js');
|
|
21
21
|
const snapshotScript = path.join(__dirname, 'snapshot.js');
|
|
22
22
|
const initScript = path.join(__dirname, 'init.js');
|
|
23
|
+
const contextScript = path.join(__dirname, 'context.js');
|
|
24
|
+
const lessonScript = path.join(__dirname, 'lesson.js');
|
|
23
25
|
|
|
24
26
|
function runScript(scriptPath, scriptArgs) {
|
|
25
27
|
if (scriptPath === renderScript || scriptPath === snapshotScript) {
|
|
26
28
|
try {
|
|
27
29
|
createRequire(import.meta.url).resolve('puppeteer');
|
|
28
30
|
} catch {
|
|
29
|
-
|
|
31
|
+
const message = 'Retiam installation is incomplete: its required Puppeteer dependency is missing. Reinstall Retiam in your project with: npm install retiam';
|
|
32
|
+
if (scriptPath === snapshotScript && scriptArgs.includes('--json')) console.log(JSON.stringify({ status: 'error', message }));
|
|
33
|
+
console.error(message);
|
|
30
34
|
process.exit(1);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -48,6 +52,8 @@ Usage:
|
|
|
48
52
|
npx retiam help <command> (detailed help for a single command)
|
|
49
53
|
|
|
50
54
|
Commands Overview:
|
|
55
|
+
lesson Teaching plans, styles, native narration, cue timelines, and captions
|
|
56
|
+
context Locate version-matched agent guidance, examples, and API declarations
|
|
51
57
|
init, create Initialize a complete Retiam WebGPU animation skeleton project
|
|
52
58
|
lint Statically analyze scenes for runtime hazards and async timing bugs
|
|
53
59
|
snapshot Capture multi-timestamp visual PNG keyframes via headless WebGPU
|
|
@@ -85,7 +91,7 @@ Commands Overview:
|
|
|
85
91
|
|
|
86
92
|
================================================================================
|
|
87
93
|
2. SNAPSHOT (npx retiam snapshot <file> [options])
|
|
88
|
-
|
|
94
|
+
Run the named scene directly and capture PNGs at fixed-step timeline seconds.
|
|
89
95
|
|
|
90
96
|
Arguments:
|
|
91
97
|
<file> Target scene TypeScript file (e.g. MyScene.ts)
|
|
@@ -95,6 +101,8 @@ Commands Overview:
|
|
|
95
101
|
-w, --width <px> Snapshot width in pixels (default: 1280)
|
|
96
102
|
-h, --height <px> Snapshot height in pixels (default: 720)
|
|
97
103
|
-o, --out <dir> Output directory for images (default: ./outputs/snapshot)
|
|
104
|
+
--fps <num> Timeline step rate (default: 60)
|
|
105
|
+
--timeout <ms> Browser operation timeout (default: 120000)
|
|
98
106
|
--json Output structured JSON for agents & CI automation
|
|
99
107
|
--help Show snapshot-specific help
|
|
100
108
|
|
|
@@ -113,6 +121,8 @@ Commands Overview:
|
|
|
113
121
|
Options:
|
|
114
122
|
-n, --name <name> Project name for package.json (default: directory name)
|
|
115
123
|
--title <text> Hero text for starter Hello World scene (default: Hello World)
|
|
124
|
+
--template <name> basic (default) or explainer worked construction
|
|
125
|
+
--style <name> mathematical (Dijkstra default), excalidraw, minimal, cinematic
|
|
116
126
|
--pm <manager> Package manager: npm, pnpm, yarn, bun (default: auto or npm)
|
|
117
127
|
-f, --force Overwrite destination directory if files already exist
|
|
118
128
|
-y, --yes Skip interactive prompts and use defaults
|
|
@@ -167,7 +177,13 @@ Commands Overview:
|
|
|
167
177
|
`);
|
|
168
178
|
}
|
|
169
179
|
|
|
170
|
-
if (command === '
|
|
180
|
+
if (command === 'lesson') {
|
|
181
|
+
runScript(lessonScript, args.slice(1));
|
|
182
|
+
}
|
|
183
|
+
else if (command === 'context') {
|
|
184
|
+
runScript(contextScript, args.slice(1));
|
|
185
|
+
}
|
|
186
|
+
else if (command === 'lint') {
|
|
171
187
|
// Usage: npx retiam lint src/MyScene.ts [--json]
|
|
172
188
|
runScript(lintScript, args.slice(1));
|
|
173
189
|
}
|
|
@@ -189,7 +205,11 @@ else if (command === 'render') {
|
|
|
189
205
|
}
|
|
190
206
|
else if (command === 'help') {
|
|
191
207
|
const sub = args[1];
|
|
192
|
-
if (sub === '
|
|
208
|
+
if (sub === 'lesson') {
|
|
209
|
+
runScript(lessonScript, ['--help']);
|
|
210
|
+
} else if (sub === 'context') {
|
|
211
|
+
runScript(contextScript, ['--help']);
|
|
212
|
+
} else if (sub === 'lint') {
|
|
193
213
|
runScript(lintScript, ['--help']);
|
|
194
214
|
} else if (sub === 'snapshot') {
|
|
195
215
|
runScript(snapshotScript, ['--help']);
|
package/bin/context.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { agentKitRoot } from './agent-kit.js';
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
9
|
+
console.log('Usage: retiam context [--json]\nLocate guidance, examples, and declarations for this installed Retiam version.');
|
|
10
|
+
} else if (args.some(arg => arg !== '--json')) {
|
|
11
|
+
const message = 'Unknown option. Usage: retiam context [--json]';
|
|
12
|
+
if (args.includes('--json')) console.log(JSON.stringify({ status: 'error', message }));
|
|
13
|
+
else console.error(message);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
} else {
|
|
16
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
17
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
18
|
+
const declarations = path.join(root, manifest.types);
|
|
19
|
+
const sublibraries = Object.keys(manifest.exports).filter(key => key.startsWith('./') && !key.includes('*'));
|
|
20
|
+
const libs = path.join(root, 'dist/libs');
|
|
21
|
+
if (fs.existsSync(libs)) {
|
|
22
|
+
for (const entry of fs.readdirSync(libs, { withFileTypes: true })) {
|
|
23
|
+
if (entry.isDirectory() && fs.existsSync(path.join(libs, entry.name, 'index.d.ts'))) sublibraries.push(`./${entry.name}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const result = {
|
|
27
|
+
status: 'success', version: manifest.version, packageRoot: root,
|
|
28
|
+
skill: path.join(agentKitRoot, 'skill/SKILL.md'),
|
|
29
|
+
api: path.join(agentKitRoot, 'skill/references/api.md'),
|
|
30
|
+
quality: path.join(agentKitRoot, 'skill/references/quality.md'),
|
|
31
|
+
tts: path.join(agentKitRoot, 'skill/references/tts.md'),
|
|
32
|
+
explainers: path.join(agentKitRoot, 'skill/references/explainers.md'),
|
|
33
|
+
styles: path.join(agentKitRoot, 'skill/references/styles.md'),
|
|
34
|
+
lessonExample: path.join(agentKitRoot, 'templates/explainer/lesson.json'),
|
|
35
|
+
examples: path.join(agentKitRoot, 'skill/examples'),
|
|
36
|
+
declarations: fs.existsSync(declarations) ? declarations : null,
|
|
37
|
+
sublibraries: [...new Set(sublibraries)].map(name => ({
|
|
38
|
+
import: `retiam/${name.slice(2)}`,
|
|
39
|
+
declarations: manifest.exports[name]?.types
|
|
40
|
+
? path.join(root,manifest.exports[name].types)
|
|
41
|
+
: name === './license-notices'
|
|
42
|
+
? path.join(root, 'bin/third-party.d.ts')
|
|
43
|
+
: path.join(libs, name.slice(2), 'index.d.ts'),
|
|
44
|
+
})),
|
|
45
|
+
commands: {
|
|
46
|
+
check: 'npm run check',
|
|
47
|
+
lint: 'retiam lint <scene.ts> --json',
|
|
48
|
+
snapshot: 'retiam snapshot <scene.ts> --times 0,1,3 --json',
|
|
49
|
+
render: 'retiam render <scene.ts>',
|
|
50
|
+
lessonCheck: 'retiam lesson check --json',
|
|
51
|
+
lessonPrepare: 'retiam lesson prepare',
|
|
52
|
+
lessonTimeline: 'retiam lesson timeline --json',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
if (args.includes('--json')) console.log(JSON.stringify(result, null, 2));
|
|
56
|
+
else console.log(`Retiam ${result.version}\n\nSkill: ${result.skill}\nAPI: ${result.api}\nQuality: ${result.quality}\nExamples: ${result.examples}\nDeclarations: ${result.declarations || 'not built'}\n\nType-check → lint → snapshot and inspect → preview → render.\nUse --json for machine-readable paths and commands.`);
|
|
57
|
+
}
|
package/bin/init.js
CHANGED
|
@@ -9,6 +9,8 @@ import path from 'path';
|
|
|
9
9
|
import readline from 'readline/promises';
|
|
10
10
|
import { spawnSync } from 'child_process';
|
|
11
11
|
import { copyThirdPartyNotices } from './third-party.js';
|
|
12
|
+
import { copyAgentKit } from './agent-kit.js';
|
|
13
|
+
import {validateStyleName,explainerSceneSource} from './lesson-project.js';
|
|
12
14
|
|
|
13
15
|
const args = process.argv.slice(2);
|
|
14
16
|
|
|
@@ -28,6 +30,8 @@ Arguments:
|
|
|
28
30
|
Options:
|
|
29
31
|
-n, --name <name> Project name for package.json (default: directory name)
|
|
30
32
|
--title <text> Hero text to display in Hello World scene (default: Hello World)
|
|
33
|
+
--template <name> basic (default) or explainer (worked teaching example)
|
|
34
|
+
--style <name> mathematical (Dijkstra default), excalidraw, minimal, cinematic
|
|
31
35
|
--pm <manager> Package manager: npm, pnpm, yarn, bun (default: auto-detected or npm)
|
|
32
36
|
-f, --force Overwrite destination directory if files already exist
|
|
33
37
|
-y, --yes Skip interactive prompts and use defaults
|
|
@@ -65,6 +69,7 @@ let nonInteractive = false;
|
|
|
65
69
|
let runInstall = false;
|
|
66
70
|
let initGit = false;
|
|
67
71
|
let jsonOutput = false;
|
|
72
|
+
let templateKind='basic', selectedStyle='mathematical';
|
|
68
73
|
|
|
69
74
|
for (let i = 0; i < args.length; i++) {
|
|
70
75
|
const arg = args[i];
|
|
@@ -83,6 +88,10 @@ for (let i = 0; i < args.length; i++) {
|
|
|
83
88
|
projectName = args[++i];
|
|
84
89
|
} else if (arg === '--title') {
|
|
85
90
|
heroTitle = args[++i] || 'Hello World';
|
|
91
|
+
} else if(arg==='--template') {
|
|
92
|
+
templateKind=args[++i];
|
|
93
|
+
} else if(arg==='--style') {
|
|
94
|
+
selectedStyle=args[++i];
|
|
86
95
|
} else if (arg === '--pm') {
|
|
87
96
|
packageManager = args[++i] || packageManager;
|
|
88
97
|
} else if (!arg.startsWith('-') && !targetDir) {
|
|
@@ -91,6 +100,9 @@ for (let i = 0; i < args.length; i++) {
|
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
async function main() {
|
|
103
|
+
if(typeof selectedStyle!=='string')throw new Error('--style requires a profile name.');
|
|
104
|
+
validateStyleName(selectedStyle);
|
|
105
|
+
if(!['basic','explainer'].includes(templateKind))throw new Error('Choose --template basic or explainer.');
|
|
94
106
|
// Interactive setup if TTY and not in --yes / --json mode
|
|
95
107
|
const isTTY = process.stdin.isTTY && !nonInteractive;
|
|
96
108
|
|
|
@@ -147,7 +159,7 @@ async function main() {
|
|
|
147
159
|
|
|
148
160
|
// Conflict check
|
|
149
161
|
if (fs.existsSync(resolvedDir)) {
|
|
150
|
-
const existingFiles = fs.readdirSync(resolvedDir).filter(f =>
|
|
162
|
+
const existingFiles = fs.readdirSync(resolvedDir).filter(f => f !== '.git' && f !== '.DS_Store');
|
|
151
163
|
if (existingFiles.length > 0 && !force) {
|
|
152
164
|
if (jsonOutput) {
|
|
153
165
|
console.log(JSON.stringify({
|
|
@@ -175,12 +187,19 @@ async function main() {
|
|
|
175
187
|
type: 'module',
|
|
176
188
|
scripts: {
|
|
177
189
|
dev: 'node tools/prepare-notices.mjs && vite',
|
|
190
|
+
check: 'tsc --noEmit',
|
|
191
|
+
'lint:scene': 'retiam lint src/scenes/HelloWorld.ts',
|
|
192
|
+
snapshot: 'retiam snapshot',
|
|
178
193
|
build: 'tsc && node tools/prepare-notices.mjs && vite build',
|
|
179
194
|
preview: 'vite preview',
|
|
180
|
-
render: 'retiam render src/scenes/HelloWorld.ts'
|
|
195
|
+
render: 'retiam render src/scenes/HelloWorld.ts',
|
|
196
|
+
'lesson:check': 'retiam lesson check --json',
|
|
197
|
+
'lesson:prepare': 'retiam lesson prepare',
|
|
198
|
+
'lesson:timeline': 'retiam lesson timeline --json',
|
|
199
|
+
'lesson:captions': 'retiam lesson captions'
|
|
181
200
|
},
|
|
182
201
|
dependencies: {
|
|
183
|
-
retiam: '
|
|
202
|
+
retiam: 'latest'
|
|
184
203
|
},
|
|
185
204
|
devDependencies: {
|
|
186
205
|
'@webgpu/types': '^0.1.31',
|
|
@@ -200,16 +219,20 @@ async function main() {
|
|
|
200
219
|
strict: true,
|
|
201
220
|
esModuleInterop: true,
|
|
202
221
|
skipLibCheck: true,
|
|
203
|
-
isolatedModules: true
|
|
222
|
+
isolatedModules: true,
|
|
223
|
+
resolveJsonModule: true
|
|
204
224
|
},
|
|
205
225
|
include: ['src']
|
|
206
226
|
};
|
|
207
227
|
|
|
208
228
|
// 3. vite.config.ts
|
|
209
229
|
const viteConfig = `import { defineConfig } from 'vite';
|
|
230
|
+
import { msEdgeTTSPlugin } from 'retiam/vite';
|
|
210
231
|
|
|
211
232
|
export default defineConfig({
|
|
233
|
+
plugins: [msEdgeTTSPlugin()],
|
|
212
234
|
optimizeDeps: {
|
|
235
|
+
exclude: ['retiam', 'msedge-tts'],
|
|
213
236
|
esbuildOptions: {
|
|
214
237
|
target: 'es2022',
|
|
215
238
|
supported: {
|
|
@@ -278,7 +301,12 @@ async function bootstrap() {
|
|
|
278
301
|
canvas.height = window.innerHeight;
|
|
279
302
|
});
|
|
280
303
|
|
|
281
|
-
const
|
|
304
|
+
const file = new URLSearchParams(window.location.search).get('file');
|
|
305
|
+
const SceneClass = file
|
|
306
|
+
? (await import(/* @vite-ignore */ '/' + file.replaceAll('\\\\', '/').replace(/^\\/+/, ''))).default
|
|
307
|
+
: HelloWorldScene;
|
|
308
|
+
if (typeof SceneClass !== 'function') throw new Error('The scene must default-export a Scene class.');
|
|
309
|
+
const scene = new SceneClass();
|
|
282
310
|
await scene.start(canvas);
|
|
283
311
|
}
|
|
284
312
|
|
|
@@ -288,7 +316,9 @@ bootstrap().catch((err) => {
|
|
|
288
316
|
`;
|
|
289
317
|
|
|
290
318
|
// 6. src/scenes/HelloWorld.ts
|
|
291
|
-
const helloWorldTs = `import { Scene, hex,
|
|
319
|
+
const helloWorldTs = `import { CENTER, Scene, hex, Text, Write } from "retiam";
|
|
320
|
+
import {resolveStyle,type StyleInput} from "retiam/explainer";
|
|
321
|
+
import lesson from '../../lesson/lesson.json';
|
|
292
322
|
|
|
293
323
|
/**
|
|
294
324
|
* Starter Retiam Scene: Hello World
|
|
@@ -298,11 +328,12 @@ export default class HelloWorldScene extends Scene {
|
|
|
298
328
|
public async run(): Promise<void> {
|
|
299
329
|
// 1. Setup camera and obsidian canvas
|
|
300
330
|
this.camera.position.set([0, 0, 14]);
|
|
301
|
-
|
|
331
|
+
const style=resolveStyle(lesson.style as StyleInput);
|
|
332
|
+
this.clearColor(hex(style.background));
|
|
302
333
|
|
|
303
334
|
// 2. Create the hero typography
|
|
304
|
-
const text = new
|
|
305
|
-
text.paint.fill = hex(
|
|
335
|
+
const text = new Text(${JSON.stringify(heroTitle)}, { fontSize: 1.1, anchor: CENTER, font:style.font });
|
|
336
|
+
text.paint.fill = hex(style.primary);
|
|
306
337
|
text.center();
|
|
307
338
|
|
|
308
339
|
// 3. Register entity and frame camera with balanced padding
|
|
@@ -352,6 +383,29 @@ A cinematic WebGPU mathematical animation built with [Retiam](https://github.com
|
|
|
352
383
|
- \`src/scenes/HelloWorld.ts\`: Starter scene with hero typography and \`Write\` animation.
|
|
353
384
|
- \`vite.config.ts\`: Real-time WebGPU bundler configuration with hot module reload.
|
|
354
385
|
|
|
386
|
+
## AI agents
|
|
387
|
+
|
|
388
|
+
Start with \`AGENTS.md\` and \`.agents/skills/retiam/SKILL.md\` for the local authoring
|
|
389
|
+
workflow, API notes, checked example, and visual review criteria. Run
|
|
390
|
+
\`npx retiam context --json\` to locate guidance for the installed engine version.
|
|
391
|
+
Type-check with \`npm run check\`, lint each changed scene, then capture and inspect
|
|
392
|
+
snapshots before rendering. See the skill for commands and environment requirements.
|
|
393
|
+
|
|
394
|
+
Educational projects also include \`lesson/lesson.json\`, style profiles and a review
|
|
395
|
+
record. The default visual/teaching style matches the Dijkstra explainer: navy,
|
|
396
|
+
bright stable colors, continuous constructions and conversational discovery.
|
|
397
|
+
Ask your agent to replace the sample topic and author the corresponding scene.
|
|
398
|
+
\`--template explainer\` starts with a worked square-area construction. To choose
|
|
399
|
+
another look, use \`npx retiam lesson style excalidraw\`; custom JSON profiles can
|
|
400
|
+
extend mathematical, excalidraw, minimal or cinematic. Read the skill's
|
|
401
|
+
\`references/explainers.md\` and \`references/styles.md\` for the full workflow.
|
|
402
|
+
|
|
403
|
+
Run \`npm run lesson:check\` and \`npm run lesson:timeline\` to inspect the plan.
|
|
404
|
+
The sample previews silently offline. Set \`narration.enabled\` to true and run
|
|
405
|
+
\`npm run lesson:prepare\` for native Retiam speech, then
|
|
406
|
+
\`npm run lesson:captions\` to export timed SRT captions. Inspect frames and listen
|
|
407
|
+
to the final render; automated checks do not establish teaching quality.
|
|
408
|
+
|
|
355
409
|
## Licensing
|
|
356
410
|
|
|
357
411
|
Retiam and its supplied starter code are free for noncommercial use under
|
|
@@ -375,8 +429,9 @@ Your own media and dependencies need their own notices and permissions.
|
|
|
375
429
|
fs.writeFileSync(path.join(resolvedDir, '.gitignore'), gitignore, 'utf8');
|
|
376
430
|
fs.writeFileSync(path.join(resolvedDir, 'README.md'), readme, 'utf8');
|
|
377
431
|
fs.writeFileSync(path.join(srcDir, 'main.ts'), mainTs, 'utf8');
|
|
378
|
-
fs.writeFileSync(path.join(scenesDir, 'HelloWorld.ts'), helloWorldTs, 'utf8');
|
|
432
|
+
fs.writeFileSync(path.join(scenesDir, 'HelloWorld.ts'), templateKind==='explainer'?explainerSceneSource():helloWorldTs, 'utf8');
|
|
379
433
|
copyThirdPartyNotices(resolvedDir);
|
|
434
|
+
const agentFiles = copyAgentKit(resolvedDir,{style:selectedStyle,title:heroTitle==='Hello World'?undefined:heroTitle});
|
|
380
435
|
fs.mkdirSync(path.join(resolvedDir, 'tools'), { recursive: true });
|
|
381
436
|
fs.copyFileSync(new URL('./prepare-notices.js', import.meta.url), path.join(resolvedDir, 'tools/prepare-notices.mjs'));
|
|
382
437
|
|
|
@@ -393,7 +448,8 @@ Your own media and dependencies need their own notices and permissions.
|
|
|
393
448
|
'COMMERCIAL_LICENSE.md',
|
|
394
449
|
'THIRD_PARTY_NOTICES.md',
|
|
395
450
|
'THIRD_PARTY_LICENSES.txt',
|
|
396
|
-
'tools/prepare-notices.mjs'
|
|
451
|
+
'tools/prepare-notices.mjs',
|
|
452
|
+
...agentFiles
|
|
397
453
|
];
|
|
398
454
|
|
|
399
455
|
// Optional git init
|
|
@@ -424,6 +480,8 @@ Your own media and dependencies need their own notices and permissions.
|
|
|
424
480
|
absolutePath: resolvedDir,
|
|
425
481
|
projectName: resolvedName,
|
|
426
482
|
packageManager,
|
|
483
|
+
template:templateKind,
|
|
484
|
+
style:selectedStyle,
|
|
427
485
|
files: createdFiles
|
|
428
486
|
}, null, 2));
|
|
429
487
|
} else {
|
|
@@ -444,6 +502,7 @@ Your own media and dependencies need their own notices and permissions.
|
|
|
444
502
|
}
|
|
445
503
|
|
|
446
504
|
main().catch((err) => {
|
|
447
|
-
console.
|
|
505
|
+
if (jsonOutput) console.log(JSON.stringify({ status: 'error', message: err.message }));
|
|
506
|
+
else console.error('Initialization failed:', err);
|
|
448
507
|
process.exit(1);
|
|
449
508
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const assets = fileURLToPath(new URL('../agent/templates/explainer/', import.meta.url));
|
|
5
|
+
export function validateStyleName(style = 'mathematical') {
|
|
6
|
+
const profiles = JSON.parse(fs.readFileSync(path.join(assets, 'styles.json'), 'utf8'));
|
|
7
|
+
if (!Object.hasOwn(profiles, style))
|
|
8
|
+
throw new Error(`Unknown style '${style}'. Choose ${Object.keys(profiles).join(', ')}.`);
|
|
9
|
+
return style;
|
|
10
|
+
}
|
|
11
|
+
export function copyLessonKit(directory, { style = 'mathematical', title } = {}) {
|
|
12
|
+
validateStyleName(style);
|
|
13
|
+
const dest = path.join(directory, 'lesson');
|
|
14
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
15
|
+
const plan = JSON.parse(fs.readFileSync(path.join(assets, 'lesson.json'), 'utf8'));
|
|
16
|
+
plan.style = style;
|
|
17
|
+
if (title)
|
|
18
|
+
plan.title = title;
|
|
19
|
+
fs.writeFileSync(path.join(dest, 'lesson.json'), JSON.stringify(plan, null, 2) + '\n');
|
|
20
|
+
for (const file of ['styles.json', 'review.json'])
|
|
21
|
+
fs.copyFileSync(path.join(assets, file), path.join(dest, file));
|
|
22
|
+
return ['lesson/lesson.json', 'lesson/styles.json', 'lesson/review.json'];
|
|
23
|
+
}
|
|
24
|
+
export function explainerSceneSource() {
|
|
25
|
+
const example = fs.readFileSync(new URL('../agent/skill/examples/AreaLesson.ts', import.meta.url), 'utf8');
|
|
26
|
+
return example.replace(/\/\/ lesson-config:start[\s\S]*?\/\/ lesson-config:end/, `import lessonData from '../../lesson/lesson.json';
|
|
27
|
+
const plan=lessonData as LessonPlan;
|
|
28
|
+
const preparedFiles=import.meta.glob('../../lesson/prepared.json',{eager:true,import:'default'});
|
|
29
|
+
const prepared=preparedFiles['../../lesson/prepared.json'] as PreparedSpeech|undefined;`);
|
|
30
|
+
}
|
package/bin/lesson.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const help = `Usage: retiam lesson <check|prepare|timeline|captions|styles|style> [lesson/lesson.json] [--json]
|
|
7
|
+
|
|
8
|
+
check Validate the plan and any prepared speech; list the remaining human/agent review.
|
|
9
|
+
prepare Cache native speech in public/narration and write lesson/prepared.json.
|
|
10
|
+
timeline Print the compiled beat, cue and segment times (milliseconds).
|
|
11
|
+
captions Write outputs/narration.srt; preserves thinking-pause boundaries.
|
|
12
|
+
styles List visual profiles. Default: mathematical (the Dijkstra video aesthetic).
|
|
13
|
+
style NAME Persist a profile in the lesson. Custom profiles extend one in JSON.
|
|
14
|
+
|
|
15
|
+
--out PATH Override the prepared JSON or SRT destination.
|
|
16
|
+
--json Emit one machine-readable result; diagnostics go to stderr.
|
|
17
|
+
|
|
18
|
+
ElevenLabs: set narration.provider to "elevenlabs", choose narration.voice,
|
|
19
|
+
and supply ELEVENLABS_API_KEY in the command environment. The CLI passes it
|
|
20
|
+
directly to provider.init(); no Vite configuration is required.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
retiam lesson style excalidraw
|
|
24
|
+
retiam lesson check --json
|
|
25
|
+
retiam lesson prepare
|
|
26
|
+
retiam lesson timeline --json
|
|
27
|
+
`;
|
|
28
|
+
export async function runLessonCLI(args) {
|
|
29
|
+
const json = args.includes('--json');
|
|
30
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
31
|
+
console.log(help);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Keep stdout machine-readable even when a provider logs directly.
|
|
35
|
+
if (json) for (const method of ['log', 'info', 'debug']) console[method] = (...values) => console.error(...values);
|
|
36
|
+
const command = args[0], pos = [];
|
|
37
|
+
let out;
|
|
38
|
+
for (let i = 1; i < args.length; i++) {
|
|
39
|
+
const a = args[i];
|
|
40
|
+
if (a === '--json')
|
|
41
|
+
continue;
|
|
42
|
+
if (a === '--out') {
|
|
43
|
+
out = args[++i];
|
|
44
|
+
if (!out || out.startsWith('-'))
|
|
45
|
+
throw new Error('--out requires a path.');
|
|
46
|
+
}
|
|
47
|
+
else if (a.startsWith('-'))
|
|
48
|
+
throw new Error(`Unknown option: ${a}`);
|
|
49
|
+
else
|
|
50
|
+
pos.push(a);
|
|
51
|
+
}
|
|
52
|
+
if (!['check', 'prepare', 'timeline', 'captions', 'styles', 'style'].includes(command))
|
|
53
|
+
throw new Error(`Unknown lesson command '${command}'.`);
|
|
54
|
+
const { validateLesson, assertLesson, resolveStyle, STYLE_PROFILES, voiceKey, speechKey, compileLesson, lessonCaptions } = await import('../dist/libs/explainer/plan.js');
|
|
55
|
+
const print = result => process.stdout.write((typeof result === 'string' ? result : JSON.stringify(result, null, 2)) + '\n');
|
|
56
|
+
if (command === 'styles') {
|
|
57
|
+
if (pos.length || out)
|
|
58
|
+
throw Error('styles accepts only --json.');
|
|
59
|
+
print({ status: 'success', default: 'mathematical', profiles: STYLE_PROFILES, custom: 'Set style to {extends:"excalidraw",background:"#102030",...} in the lesson JSON.' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const selected = command === 'style' ? pos.shift() : undefined;
|
|
63
|
+
if (pos.length > 1)
|
|
64
|
+
throw Error('Provide one lesson file.');
|
|
65
|
+
const file = path.resolve(pos[0] ?? 'lesson/lesson.json'), plan = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
66
|
+
const preparedFile = path.join(path.dirname(file), 'prepared.json');
|
|
67
|
+
if (command === 'style') {
|
|
68
|
+
resolveStyle(selected);
|
|
69
|
+
plan.style = selected;
|
|
70
|
+
assertLesson(plan);
|
|
71
|
+
fs.writeFileSync(file, JSON.stringify(plan, null, 2) + '\n');
|
|
72
|
+
print({ status: 'success', style: selected, file: path.relative(process.cwd(), file) });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const diagnostics = validateLesson(plan);
|
|
76
|
+
if (diagnostics.length) {
|
|
77
|
+
print({ status: 'error', diagnostics });
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let prepared = fs.existsSync(preparedFile) ? JSON.parse(fs.readFileSync(preparedFile, 'utf8')) : undefined;
|
|
82
|
+
const warnings = [];
|
|
83
|
+
if (command === 'prepare') {
|
|
84
|
+
if (!plan.narration.enabled)
|
|
85
|
+
throw Error('Narration is disabled. Set narration.enabled to true before preparing speech.');
|
|
86
|
+
const providerName = plan.narration.provider ?? 'msedge';
|
|
87
|
+
if (!['msedge', 'elevenlabs'].includes(providerName))
|
|
88
|
+
throw Error('Import a PreparedSpeech manifest for this provider, or use prepareLesson with a synthesize adapter.');
|
|
89
|
+
const cache = path.resolve(`node_modules/.cache/${providerName}-tts`), assets = path.resolve('public/narration');
|
|
90
|
+
fs.mkdirSync(assets, { recursive: true });
|
|
91
|
+
let server, speechProvider, port;
|
|
92
|
+
if (providerName === 'elevenlabs') {
|
|
93
|
+
const { ElevenLabsTTSProvider } = await import('../dist/libs/tts/node/index.js');
|
|
94
|
+
speechProvider = new ElevenLabsTTSProvider();
|
|
95
|
+
await speechProvider.init({ ...plan.narration.elevenlabs,
|
|
96
|
+
apiKey: process.env.ELEVENLABS_API_KEY, voiceId: plan.narration.voice });
|
|
97
|
+
fs.mkdirSync(cache, { recursive: true });
|
|
98
|
+
} else {
|
|
99
|
+
const { createServer } = await import('vite'), { msEdgeTTSPlugin } = await import('../dist/libs/vite/index.js');
|
|
100
|
+
server = await createServer({ configFile: false, logLevel: 'error', plugins: [msEdgeTTSPlugin({ cacheDir: cache, verbose: false })], server: { host: '127.0.0.1', port: 0, open: false, hmr: false, watch: null }, optimizeDeps: { noDiscovery: true } });
|
|
101
|
+
}
|
|
102
|
+
prepared = { version: 1, voiceKey: voiceKey(plan), assets: {} };
|
|
103
|
+
try {
|
|
104
|
+
if (server) { await server.listen(); port = server.httpServer.address().port; }
|
|
105
|
+
// Sequential generation avoids provider throttling; cached requests return immediately.
|
|
106
|
+
for (const beat of plan.beats)
|
|
107
|
+
for (const segment of beat.narration) {
|
|
108
|
+
const request = providerName === 'msedge'
|
|
109
|
+
? { text: segment.speechText ?? segment.text, voice: plan.narration.voice ?? 'en-US-AndrewNeural', speed: plan.narration.speed ?? 1, pitch: plan.narration.pitch ?? '+0Hz', volume: 1 }
|
|
110
|
+
: { text: segment.speechText ?? segment.text, displayText: segment.text, voice: plan.narration.voice, speed: plan.narration.speed, pitch: plan.narration.pitch, elevenlabs: plan.narration.elevenlabs };
|
|
111
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify({ provider: providerName, request })).digest('hex');
|
|
112
|
+
let result;
|
|
113
|
+
if (speechProvider) {
|
|
114
|
+
const audioFile = path.join(cache, hash + '.mp3'), metadataFile = path.join(cache, hash + '.json');
|
|
115
|
+
if (fs.existsSync(audioFile) && fs.existsSync(metadataFile)) {
|
|
116
|
+
result = { ...JSON.parse(fs.readFileSync(metadataFile, 'utf8')), bytes: fs.readFileSync(audioFile) };
|
|
117
|
+
} else {
|
|
118
|
+
const audio = await speechProvider.synthesize(request.text, request);
|
|
119
|
+
result = { bytes: Buffer.from(audio.rawBuffer), durationMs: audio.durationMs, words: audio.wordBoundaries ?? [] };
|
|
120
|
+
if (!result.words.length) throw Error('ElevenLabs returned no word timings; narration was not prepared.');
|
|
121
|
+
fs.writeFileSync(audioFile, result.bytes);
|
|
122
|
+
fs.writeFileSync(metadataFile, JSON.stringify({ durationMs: result.durationMs, words: result.words }));
|
|
123
|
+
}
|
|
124
|
+
} else for (let attempt = 0; attempt < 3; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/tts/msedge/synthesize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), signal: AbortSignal.timeout(90000) });
|
|
127
|
+
if (!res.ok)
|
|
128
|
+
throw Error(`Native TTS HTTP ${res.status}: ${await res.text()}`);
|
|
129
|
+
const bytes = Buffer.from(await res.arrayBuffer()), durationMs = Number(res.headers.get('x-audio-duration-ms')), words = JSON.parse(decodeURIComponent(res.headers.get('x-word-boundaries') ?? '%5B%5D'));
|
|
130
|
+
if (!durationMs || bytes.length < 1000)
|
|
131
|
+
throw Error('Native TTS returned empty audio.');
|
|
132
|
+
result = { bytes, durationMs, words };
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (attempt === 2)
|
|
137
|
+
throw error;
|
|
138
|
+
console.error(`Retrying ${beat.id}/${segment.id}: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
fs.writeFileSync(path.join(assets, hash + '.mp3'), result.bytes);
|
|
142
|
+
prepared.assets[speechKey(beat.id, segment.id)] = { text: segment.text, speechText: segment.speechText, durationMs: result.durationMs, words: result.words, url: `/narration/${hash}.mp3` };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await server?.close();
|
|
147
|
+
await speechProvider?.dispose();
|
|
148
|
+
}
|
|
149
|
+
compileLesson(plan, prepared);
|
|
150
|
+
const dest = path.resolve(out ?? preparedFile);
|
|
151
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
152
|
+
fs.writeFileSync(dest, JSON.stringify(prepared, null, 2) + '\n');
|
|
153
|
+
print({ status: 'success', file: path.relative(process.cwd(), dest), segments: Object.keys(prepared.assets).length });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let timeline;
|
|
157
|
+
if (plan.narration.enabled && !prepared) {
|
|
158
|
+
warnings.push('Speech is not prepared. Run retiam lesson prepare before locking narration cues.');
|
|
159
|
+
if (command !== 'check')
|
|
160
|
+
throw Error(warnings[0]);
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
timeline = compileLesson(plan, prepared);
|
|
164
|
+
if (plan.narration.enabled && prepared)
|
|
165
|
+
for (const [id, asset] of Object.entries(prepared.assets)) {
|
|
166
|
+
if (!asset.url)
|
|
167
|
+
throw Error(`No audio URL for ${id}.`);
|
|
168
|
+
if (asset.url.startsWith('/')) {
|
|
169
|
+
const base = path.resolve('public'), dest = path.resolve(base, '.' + asset.url);
|
|
170
|
+
if (!dest.startsWith(base + path.sep) || !fs.existsSync(dest))
|
|
171
|
+
throw Error(`Missing local audio asset for ${id}: ${asset.url}`);
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
warnings.push(`Audio URL ${asset.url} needs runtime availability review.`);
|
|
175
|
+
}
|
|
176
|
+
if (command === 'captions') {
|
|
177
|
+
if (timeline.timing !== 'recorded')
|
|
178
|
+
throw Error('Silent preview timing is estimated; prepare narration before exporting captions.');
|
|
179
|
+
const dest = path.resolve(out ?? 'outputs/narration.srt');
|
|
180
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
181
|
+
fs.writeFileSync(dest, lessonCaptions(timeline));
|
|
182
|
+
print({ status: 'success', file: path.relative(process.cwd(), dest) });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (command === 'timeline') {
|
|
186
|
+
print({ status: 'success', ...timeline });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
print({ status: 'success', automated: { schema: 'passed', timing: timeline?.timing ?? 'not-prepared', speechAssets: plan.narration.enabled ? prepared ? 'checked' : 'not-prepared' : 'disabled' }, warnings, style: resolveStyle(plan.style), snapshotTimes: timeline?.beats.flatMap(b => [b.startMs / 1000, (b.startMs + b.durationMs * .6) / 1000]), requiredReview: ['Verify claims and illustrative assumptions.', 'Inspect rendered frames for overlap, continuity, and readability.', 'Watch motion and narration at normal speed.', 'Check final video/audio streams and caption timing.'] });
|
|
190
|
+
}
|
|
191
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
192
|
+
runLessonCLI(process.argv.slice(2)).catch(error => { if (process.argv.includes('--json'))
|
|
193
|
+
process.stdout.write(JSON.stringify({ status: 'error', message: error.message }) + '\n');
|
|
194
|
+
else
|
|
195
|
+
console.error(error.message); process.exitCode = 1; });
|
package/bin/lint.js
CHANGED
|
@@ -99,11 +99,11 @@ function getLineAndCol(node) {
|
|
|
99
99
|
|
|
100
100
|
// Known Retiam Entity classes
|
|
101
101
|
const ENTITY_CLASSES = new Set([
|
|
102
|
-
'Circle', 'Rect', 'Line', 'Arrow', '
|
|
102
|
+
'Circle', 'Rect', 'Line', 'Arrow', 'Poly', 'RegularPolygon',
|
|
103
103
|
'Arc', 'Sector', 'Annulus', 'Dot', 'GlowDot', 'Path', 'SVGShape', 'SVG',
|
|
104
|
-
'SDFText', 'Tex', 'Code', 'Axes', 'NumberLine', 'Graph', 'BarChart',
|
|
105
|
-
'PieChart', 'Model', 'Sphere', '
|
|
106
|
-
'Mesh', 'MorphModel', 'TracePath', 'VBox', 'HBox', 'Group'
|
|
104
|
+
'Text', 'SDFText', 'Tex', 'Code', 'Axes', 'NumberLine', 'Graph', 'BarChart',
|
|
105
|
+
'PieChart', 'Model', 'Sphere', 'Box', 'Cylinder', 'Cone', 'Plane',
|
|
106
|
+
'Mesh', 'MorphModel', 'TracePath', 'VBox', 'HBox', 'Group', 'VGroup'
|
|
107
107
|
]);
|
|
108
108
|
|
|
109
109
|
// Track instantiated entities to check if they are added
|
|
@@ -215,8 +215,8 @@ function checkNode(node) {
|
|
|
215
215
|
'retiam/no-threejs',
|
|
216
216
|
line,
|
|
217
217
|
col,
|
|
218
|
-
'Detected Three.js reference. Retiam uses native WebGPU entities (e.g. Model, Sphere,
|
|
219
|
-
'Use Retiam 3D primitives: import { Model, Sphere,
|
|
218
|
+
'Detected Three.js reference. Retiam uses native WebGPU entities (e.g. Model, Sphere, Box).',
|
|
219
|
+
'Use Retiam 3D primitives: import { Model, Sphere, Box } from "retiam"'
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
222
|
}
|