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.
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 +12 -6
  52. package/readme.md +14 -7
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 => !f.startsWith('.'));
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: '^1.0.0'
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 scene = new HelloWorldScene();
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, SDFText, Write } from "retiam";
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
- this.clearColor(hex("#080C14"));
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 SDFText("${heroTitle.replace(/"/g, '\\"')}", { fontSize: 1.1 });
305
- text.paint.fill = hex("#38BDF8");
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.error('Initialization failed:', err);
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,168 @@
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
+ Examples:
19
+ retiam lesson style excalidraw
20
+ retiam lesson check --json
21
+ retiam lesson prepare
22
+ retiam lesson timeline --json
23
+ `;
24
+ export async function runLessonCLI(args) {
25
+ const json = args.includes('--json');
26
+ if (!args.length || args.includes('--help') || args.includes('-h')) {
27
+ console.log(help);
28
+ return;
29
+ }
30
+ // Keep stdout machine-readable even when a provider logs directly.
31
+ if (json) for (const method of ['log', 'info', 'debug']) console[method] = (...values) => console.error(...values);
32
+ const command = args[0], pos = [];
33
+ let out;
34
+ for (let i = 1; i < args.length; i++) {
35
+ const a = args[i];
36
+ if (a === '--json')
37
+ continue;
38
+ if (a === '--out') {
39
+ out = args[++i];
40
+ if (!out || out.startsWith('-'))
41
+ throw new Error('--out requires a path.');
42
+ }
43
+ else if (a.startsWith('-'))
44
+ throw new Error(`Unknown option: ${a}`);
45
+ else
46
+ pos.push(a);
47
+ }
48
+ if (!['check', 'prepare', 'timeline', 'captions', 'styles', 'style'].includes(command))
49
+ throw new Error(`Unknown lesson command '${command}'.`);
50
+ const { validateLesson, assertLesson, resolveStyle, STYLE_PROFILES, voiceKey, speechKey, compileLesson, lessonCaptions } = await import('../dist/libs/explainer/plan.js');
51
+ const print = result => process.stdout.write((typeof result === 'string' ? result : JSON.stringify(result, null, 2)) + '\n');
52
+ if (command === 'styles') {
53
+ if (pos.length || out)
54
+ throw Error('styles accepts only --json.');
55
+ print({ status: 'success', default: 'mathematical', profiles: STYLE_PROFILES, custom: 'Set style to {extends:"excalidraw",background:"#102030",...} in the lesson JSON.' });
56
+ return;
57
+ }
58
+ const selected = command === 'style' ? pos.shift() : undefined;
59
+ if (pos.length > 1)
60
+ throw Error('Provide one lesson file.');
61
+ const file = path.resolve(pos[0] ?? 'lesson/lesson.json'), plan = JSON.parse(fs.readFileSync(file, 'utf8'));
62
+ const preparedFile = path.join(path.dirname(file), 'prepared.json');
63
+ if (command === 'style') {
64
+ resolveStyle(selected);
65
+ plan.style = selected;
66
+ assertLesson(plan);
67
+ fs.writeFileSync(file, JSON.stringify(plan, null, 2) + '\n');
68
+ print({ status: 'success', style: selected, file: path.relative(process.cwd(), file) });
69
+ return;
70
+ }
71
+ const diagnostics = validateLesson(plan);
72
+ if (diagnostics.length) {
73
+ print({ status: 'error', diagnostics });
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ let prepared = fs.existsSync(preparedFile) ? JSON.parse(fs.readFileSync(preparedFile, 'utf8')) : undefined;
78
+ const warnings = [];
79
+ if (command === 'prepare') {
80
+ if (!plan.narration.enabled)
81
+ throw Error('Narration is disabled. Set narration.enabled to true before preparing speech.');
82
+ if ((plan.narration.provider ?? 'msedge') !== 'msedge')
83
+ throw Error('This CLI prepares the built-in msedge voice. Import a PreparedSpeech manifest for another provider, or use prepareLesson with a synthesize adapter.');
84
+ const { createServer } = await import('vite'), { msEdgeTTSPlugin } = await import('../dist/libs/vite/index.js');
85
+ const cache = path.resolve('node_modules/.cache/msedge-tts'), assets = path.resolve('public/narration');
86
+ fs.mkdirSync(assets, { recursive: true });
87
+ const 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 } });
88
+ prepared = { version: 1, voiceKey: voiceKey(plan), assets: {} };
89
+ try {
90
+ await server.listen();
91
+ const port = server.httpServer.address().port;
92
+ // Sequential generation avoids provider throttling; cached requests return immediately.
93
+ for (const beat of plan.beats)
94
+ for (const segment of beat.narration) {
95
+ const request = { text: segment.text, voice: plan.narration.voice ?? 'en-US-AndrewNeural', speed: plan.narration.speed ?? 1, pitch: plan.narration.pitch ?? '+0Hz', volume: 1 };
96
+ let result;
97
+ for (let attempt = 0; attempt < 3; attempt++) {
98
+ try {
99
+ 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) });
100
+ if (!res.ok)
101
+ throw Error(`Native TTS HTTP ${res.status}: ${await res.text()}`);
102
+ 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'));
103
+ if (!durationMs || bytes.length < 1000)
104
+ throw Error('Native TTS returned empty audio.');
105
+ result = { bytes, durationMs, words };
106
+ break;
107
+ }
108
+ catch (error) {
109
+ if (attempt === 2)
110
+ throw error;
111
+ console.error(`Retrying ${beat.id}/${segment.id}: ${error.message}`);
112
+ }
113
+ }
114
+ const hash = crypto.createHash('sha256').update(JSON.stringify(request)).digest('hex');
115
+ fs.writeFileSync(path.join(assets, hash + '.mp3'), result.bytes);
116
+ prepared.assets[speechKey(beat.id, segment.id)] = { text: segment.text, durationMs: result.durationMs, words: result.words, url: `/narration/${hash}.mp3` };
117
+ }
118
+ }
119
+ finally {
120
+ await server.close();
121
+ }
122
+ compileLesson(plan, prepared);
123
+ const dest = path.resolve(out ?? preparedFile);
124
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
125
+ fs.writeFileSync(dest, JSON.stringify(prepared, null, 2) + '\n');
126
+ print({ status: 'success', file: path.relative(process.cwd(), dest), segments: Object.keys(prepared.assets).length });
127
+ return;
128
+ }
129
+ let timeline;
130
+ if (plan.narration.enabled && !prepared) {
131
+ warnings.push('Speech is not prepared. Run retiam lesson prepare before locking narration cues.');
132
+ if (command !== 'check')
133
+ throw Error(warnings[0]);
134
+ }
135
+ else
136
+ timeline = compileLesson(plan, prepared);
137
+ if (plan.narration.enabled && prepared)
138
+ for (const [id, asset] of Object.entries(prepared.assets)) {
139
+ if (!asset.url)
140
+ throw Error(`No audio URL for ${id}.`);
141
+ if (asset.url.startsWith('/')) {
142
+ const base = path.resolve('public'), dest = path.resolve(base, '.' + asset.url);
143
+ if (!dest.startsWith(base + path.sep) || !fs.existsSync(dest))
144
+ throw Error(`Missing local audio asset for ${id}: ${asset.url}`);
145
+ }
146
+ else
147
+ warnings.push(`Audio URL ${asset.url} needs runtime availability review.`);
148
+ }
149
+ if (command === 'captions') {
150
+ if (timeline.timing !== 'recorded')
151
+ throw Error('Silent preview timing is estimated; prepare narration before exporting captions.');
152
+ const dest = path.resolve(out ?? 'outputs/narration.srt');
153
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
154
+ fs.writeFileSync(dest, lessonCaptions(timeline));
155
+ print({ status: 'success', file: path.relative(process.cwd(), dest) });
156
+ return;
157
+ }
158
+ if (command === 'timeline') {
159
+ print({ status: 'success', ...timeline });
160
+ return;
161
+ }
162
+ 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.'] });
163
+ }
164
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
165
+ runLessonCLI(process.argv.slice(2)).catch(error => { if (process.argv.includes('--json'))
166
+ process.stdout.write(JSON.stringify({ status: 'error', message: error.message }) + '\n');
167
+ else
168
+ 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', 'Polygon', 'RegularPolygon',
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', 'Box3D', 'Cylinder', 'Cone', 'Plane',
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, Box3D).',
219
- 'Use Retiam 3D primitives: import { Model, Sphere, Box3D } from "retiam"'
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
  }