retiam 2.0.2 → 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/agent/AGENTS.md +4 -1
- package/agent/skill/SKILL.md +7 -4
- package/agent/skill/examples/NarratedMotion.ts +10 -2
- package/agent/skill/references/api.md +4 -2
- package/agent/skill/references/explainers.md +4 -2
- package/agent/skill/references/quality.md +3 -0
- package/agent/skill/references/tts.md +154 -60
- package/bin/lesson.js +39 -12
- package/dist/{UTIF-BrQ3Ng5l.min.js → UTIF-D6vx2UEX.min.js} +1 -1
- package/dist/{UTIF-CN0-pEnd.js → UTIF-mkffT70R.js} +1 -1
- package/dist/{index-yQIdejOj.js → index-CdBPrYbr.js} +3818 -3785
- package/dist/{index-3sAseSJQ.min.js → index-jgFyrc2D.min.js} +175 -175
- package/dist/libs/explainer/index.js +74 -73
- package/dist/libs/explainer/plan.js +162 -140
- package/dist/libs/explainer/types.d.ts +5 -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 +165 -672
- 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.js +1 -1
- package/dist/{main-lQwYAed8.min.js → main-Cc4QQbSK.min.js} +1 -1
- package/dist/{main-BTk_9jtl.js → main-DMu_5Qy7.js} +1 -1
- package/dist/{msEdgeV2-BLEnvPJa.js → msEdgeV2-D3w4Xsn-.js} +30 -29
- package/dist/retiam.es.js +1 -1
- package/dist/retiam.es.min.js +1 -1
- package/dist/src/engine/entities/composites/text/mathjaxInterop.d.ts +6 -0
- package/package.json +4 -2
- package/readme.md +20 -1
package/agent/AGENTS.md
CHANGED
|
@@ -32,7 +32,10 @@ and WebGPU APIs.
|
|
|
32
32
|
- Use Retiam's built-in libraries for their supported features. For generated
|
|
33
33
|
narration, use `retiam/tts`; read the skill's `references/tts.md` and
|
|
34
34
|
`examples/NarratedMotion.ts`. Use another speech generator only when the user
|
|
35
|
-
requests it.
|
|
35
|
+
requests it. ElevenLabs is a native provider: pass its key to provider.init(),
|
|
36
|
+
direct expression with model-compatible controls, and keep captions clean.
|
|
37
|
+
Follow the TTS reference; no ElevenLabs Vite setup is needed. Diagnose failures
|
|
38
|
+
instead of silently installing Python
|
|
36
39
|
TTS tools or replacing the narration pipeline.
|
|
37
40
|
- Use `retiam/explainer` for prepared speech, named cues, explicit silent pauses
|
|
38
41
|
and synchronized beats. `npm run lesson:check` validates the contract;
|
package/agent/skill/SKILL.md
CHANGED
|
@@ -57,10 +57,13 @@ and `retiam/rough` for sketch styling.
|
|
|
57
57
|
For voiceover, speech, or narration, read [references/tts.md](references/tts.md)
|
|
58
58
|
and [examples/NarratedMotion.ts](examples/NarratedMotion.ts) before authoring.
|
|
59
59
|
That example isolates speech integration; it is not a finished video's visual standard.
|
|
60
|
-
Use `retiam/tts` for synthesis, timing, subtitles, and the scene audio bus.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
Use `retiam/tts` for synthesis, timing, subtitles, and the scene audio bus. Edge
|
|
61
|
+
is the default; when the user chooses ElevenLabs, initialize its native provider
|
|
62
|
+
with the caller's key directly, without Vite configuration. Direct the chosen
|
|
63
|
+
voice for natural questions, discoveries and resolution; keep delivery markup
|
|
64
|
+
out of captions and prepare audio before locking cues. The TTS reference covers
|
|
65
|
+
model-specific expression and prepared assets. Diagnose the selected provider's
|
|
66
|
+
actual failure instead of silently installing Python tools or switching providers.
|
|
64
67
|
|
|
65
68
|
## Author for the requested result
|
|
66
69
|
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CENTER, Circle, Concurrent, Create, CustomAnimation, Line, Scene, Text, Write, hex,
|
|
3
3
|
} from 'retiam';
|
|
4
|
-
import { Speak, Subtitles } from 'retiam/tts';
|
|
4
|
+
import { ElevenLabsTTSProvider, Speak, Subtitles, TTS } from 'retiam/tts';
|
|
5
|
+
|
|
6
|
+
// Optional application setup before starting the scene. Credentials come from
|
|
7
|
+
// the caller; browser-supplied keys are visible in that browser. No Vite changes.
|
|
8
|
+
export async function initializeElevenLabs(apiKey: string, voiceId: string) {
|
|
9
|
+
const provider = new ElevenLabsTTSProvider();
|
|
10
|
+
await provider.init({ apiKey, voiceId, modelId: 'eleven_v3', voiceSettings: { stability: 0.5 } });
|
|
11
|
+
TTS.setProvider(provider);
|
|
12
|
+
}
|
|
5
13
|
|
|
6
14
|
export default class NarratedMotion extends Scene {
|
|
7
15
|
async run() {
|
|
@@ -19,7 +27,7 @@ export default class NarratedMotion extends Scene {
|
|
|
19
27
|
this.camera.frame([[-4.5, -2.5, 0], [4.5, 3, 0]], { padding: 0.25 });
|
|
20
28
|
|
|
21
29
|
const narration = new Speak('Watch the marker move from left to right.', {
|
|
22
|
-
|
|
30
|
+
subtitles, entity: marker,
|
|
23
31
|
pulseAmplitude: 0, volume: 1,
|
|
24
32
|
});
|
|
25
33
|
// Headless tools must wait for synthesis, not a promise requiring timeline steps.
|
|
@@ -72,6 +72,7 @@ Prefer absolute values derived from progress over accumulating per-frame offsets
|
|
|
72
72
|
| `retiam/graphs` | Graph and automata entities |
|
|
73
73
|
| `retiam/physics` | Physics worlds, bodies, joints, backend adapters |
|
|
74
74
|
| `retiam/tts` | Speech, narration, and subtitles |
|
|
75
|
+
| `retiam/tts/node` | Renderer-free speech synthesis for Node preparation scripts |
|
|
75
76
|
| `retiam/rough` | Rough styling utilities |
|
|
76
77
|
| `retiam/vite` | Node-side Vite middleware for Retiam TTS; import in Vite config |
|
|
77
78
|
|
|
@@ -79,8 +80,9 @@ Follow the installed sublibrary's index declarations to its options and examples
|
|
|
79
80
|
before using it. Optional features can have their own asset or environment needs.
|
|
80
81
|
|
|
81
82
|
For narration, follow [tts.md](tts.md) and the checked
|
|
82
|
-
[NarratedMotion.ts](../examples/NarratedMotion.ts) example.
|
|
83
|
-
|
|
83
|
+
[NarratedMotion.ts](../examples/NarratedMotion.ts) example. ElevenLabs initializes
|
|
84
|
+
directly with a key on `ElevenLabsTTSProvider`, without Vite changes. The Edge TTS
|
|
85
|
+
route is already enabled by new project Vite configurations. Existing projects can add
|
|
84
86
|
`msEdgeTTSPlugin` from `retiam/vite`; see the TTS guide for the configuration and
|
|
85
87
|
failure checks. Never import the Vite plugin in browser scene code.
|
|
86
88
|
|
|
@@ -57,8 +57,10 @@ Do not expose the candidate or subtitle answer during that prediction pause.
|
|
|
57
57
|
Keep performance notes out of spoken text. A narration segment has an `id`,
|
|
58
58
|
`text`, and optional `pauseAfterMs`. Segments need not correspond one-to-one to
|
|
59
59
|
visual actions. Split at purposeful thinking pauses, retaining coherent speech
|
|
60
|
-
between them.
|
|
61
|
-
|
|
60
|
+
between them. Edge remains the default. When ElevenLabs is selected, use its
|
|
61
|
+
native provider and the initialization/performance guidance in [tts.md](tts.md).
|
|
62
|
+
Keep caption/cue `text` clean; put delivery markup in a segment's `speechText`.
|
|
63
|
+
Other generators may normalize recordings into `PreparedSpeech`.
|
|
62
64
|
Include narration in an explainer unless the user requests a silent treatment.
|
|
63
65
|
The sample's disabled narration is an offline-preview convenience, not a reason
|
|
64
66
|
to omit the requested video's spoken explanation.
|
|
@@ -28,6 +28,9 @@ style or palette overrides this default.
|
|
|
28
28
|
`retiam/tts`. Use conversational questions and precise language, synchronize
|
|
29
29
|
speech with visible events, and allow meaningful silent pauses. Keep requested
|
|
30
30
|
subtitles unobtrusive so they leave room for the mathematical construction.
|
|
31
|
+
Match vocal delivery to the beat's question, discovery, or resolution. For
|
|
32
|
+
expressive ElevenLabs delivery follow [tts.md](tts.md): choose model-compatible
|
|
33
|
+
cues, listen to a representative sample, and preserve clean caption text.
|
|
31
34
|
|
|
32
35
|
The goal is a clear, satisfying visual argument. Choose 2D or 3D according to
|
|
33
36
|
the concept. Use a slide, dashboard, or another aesthetic when requested.
|
|
@@ -1,72 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Retiam narration: synthesis and performance
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Use `retiam/tts` for speech, timing, subtitles, and the scene voiceover bus.
|
|
4
|
+
Microsoft Edge is the default. Use the native `ElevenLabsTTSProvider` when the
|
|
5
|
+
user selects ElevenLabs and supplies credentials. Preserve the chosen provider;
|
|
6
|
+
diagnose failures rather than silently switching generators or installing Python
|
|
7
|
+
TTS tools. Read the installed declarations before using newer options.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
Copy it into `src/scenes/` to preview or render it. Adapt the text, voice, and
|
|
10
|
-
motion to the brief. Use a different speech generator only if the user requests
|
|
11
|
-
one; diagnose failures in this path instead of silently replacing it.
|
|
9
|
+
## ElevenLabs: initialize the provider directly
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
No ElevenLabs Vite plugin or Vite configuration is needed. For a self-contained
|
|
12
|
+
scene example, put `new ElevenLabsTTSProvider()`, `await provider.init({ apiKey,
|
|
13
|
+
voiceId, ... })`, and `await provider.synthesize(...)` inside `Scene.run()`.
|
|
14
|
+
Pass the key directly to initialization. A `.env`, `process.env`, a preparation
|
|
15
|
+
script, and pre-generated audio are optional workflows, not setup requirements;
|
|
16
|
+
do not introduce them when the user requests a simple in-scene example.
|
|
17
|
+
Register synthesis in `composer.pendingTasks` before awaiting it for headless
|
|
18
|
+
rendering. A provider instance can be used directly; selecting it globally is
|
|
19
|
+
only needed when using the `TTS`, `Speak`, or `speak()` facades.
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
project using an engine release that provides `retiam/vite`, preserve its other
|
|
17
|
-
plugins and add:
|
|
21
|
+
To select an initialized provider globally:
|
|
18
22
|
|
|
19
23
|
```ts
|
|
20
|
-
import {
|
|
21
|
-
import { msEdgeTTSPlugin } from 'retiam/vite';
|
|
24
|
+
import { ElevenLabsTTSProvider, TTS } from 'retiam/tts';
|
|
22
25
|
|
|
23
|
-
export
|
|
26
|
+
export async function useExpressiveVoice(apiKey: string, voiceId: string) {
|
|
27
|
+
const provider = new ElevenLabsTTSProvider();
|
|
28
|
+
await provider.init({
|
|
29
|
+
apiKey, voiceId,
|
|
30
|
+
modelId: 'eleven_v3',
|
|
31
|
+
voiceSettings: { stability: 0.5 },
|
|
32
|
+
});
|
|
33
|
+
TTS.setProvider(provider);
|
|
34
|
+
return provider;
|
|
35
|
+
}
|
|
24
36
|
```
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
audio
|
|
38
|
+
`apiKey` and `voiceId` come from the caller's setup, not literal credentials
|
|
39
|
+
committed to a scene. Browser initialization uses the caller's key directly and
|
|
40
|
+
makes it accessible to that browser. For private preparation use Node's
|
|
41
|
+
`retiam/tts/node` entry point, with the same provider API, and ship the generated
|
|
42
|
+
audio. Do not add ElevenLabs credentials or middleware to `vite.config.ts`.
|
|
43
|
+
Initialization validates local settings without generating audio or fetching
|
|
44
|
+
voices. Use `await provider.fetchVoices()` when choosing a voice;
|
|
45
|
+
`provider.getVoices()` and `TTS.findVoice()` then use that fetched catalog.
|
|
46
|
+
Require a real ElevenLabs voice ID, not an Edge ID or an invented display name.
|
|
47
|
+
|
|
48
|
+
The default model is `eleven_multilingual_v2`. Select an available expressive
|
|
49
|
+
model such as `eleven_v3` or `eleven_v4` deliberately when directing with audio
|
|
50
|
+
tags. Keep a compatible model/voice pairing throughout a speaker's performance.
|
|
51
|
+
The provider accepts open model IDs, MP3 output formats, model-specific voice
|
|
52
|
+
settings, language, seed, pronunciation dictionaries, and neighboring-text context.
|
|
53
|
+
Per-line overrides go in `elevenlabs`; `speed` can also be supplied at the top
|
|
54
|
+
level. Omitted voice/rate settings inherit provider defaults through `Speak`,
|
|
55
|
+
`speak()` and `TTSNarrationComponent`. `TTS.init()` preserves the selected provider;
|
|
56
|
+
`TTS.configure()` explicitly restores the default Edge client.
|
|
57
|
+
|
|
58
|
+
## Direct a natural explanation
|
|
59
|
+
|
|
60
|
+
Choose the emotional action of a beat: inviting a prediction, weighing a
|
|
61
|
+
possibility, making a discovery, or calmly resolving the explanation. Let the
|
|
62
|
+
voice express that action. A constant promotional excitement or a cue on every
|
|
63
|
+
sentence makes an explanation harder to follow. Keep the speaker recognizable
|
|
64
|
+
across beats, with a voice suited to the requested language and audience.
|
|
65
|
+
|
|
66
|
+
Write speech for listening: concrete questions, contractions, varied sentence
|
|
67
|
+
lengths, and emphasis on the relationship changing on screen. Read technical
|
|
68
|
+
symbols as intended speech; keep formulas and concise notation in the visuals.
|
|
69
|
+
Prefer phrase or paragraph performances to synthesizing isolated words, so the
|
|
70
|
+
model can carry an idea through to its conclusion. Split at genuine explanatory
|
|
71
|
+
boundaries, preserving neighboring context when needed.
|
|
72
|
+
|
|
73
|
+
Model-specific controls:
|
|
74
|
+
|
|
75
|
+
- On v3/v4, use occasional delivery tags such as `[curious]` or `[excited]` for
|
|
76
|
+
a meaningful change in delivery. These models do not support SSML break tags;
|
|
77
|
+
use punctuation and phrasing for vocal pauses. Test the chosen voice's delivery.
|
|
78
|
+
- On v2 models, use natural punctuation and supported SSML breaks sparingly.
|
|
79
|
+
Do not assume v3 audio tags work on v2 or Edge. Edge supports its own `pitch`
|
|
80
|
+
prosody; ElevenLabs rejects nonzero Edge pitch settings.
|
|
81
|
+
- Lower stability can increase variation; higher stability can reduce it.
|
|
82
|
+
Treat settings as voice/model-dependent. Adjust a short representative sample
|
|
83
|
+
after listening, rather than maximizing style or copying one preset everywhere.
|
|
84
|
+
- Supported speed is 0.7–1.2. Start close to the voice's natural pace and use
|
|
85
|
+
explicit scene holds for prediction time. A seed is best-effort, not guaranteed
|
|
86
|
+
repeatability; approved local audio provides repeatable rendering.
|
|
87
|
+
|
|
88
|
+
Keep performance directions out of captions. Supply clean `displayText` when
|
|
89
|
+
synthesis text contains delivery or pronunciation markup:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Speak } from 'retiam/tts';
|
|
93
|
+
|
|
94
|
+
export function directedLine() {
|
|
95
|
+
return new Speak('[curious] What changes if we move this point?', {
|
|
96
|
+
displayText: 'What changes if we move this point?',
|
|
97
|
+
elevenlabs: { modelId: 'eleven_v3', voiceSettings: { stability: 0.5 } },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Listen for pronunciation, expression, unwanted vocalizations, rushed endings,
|
|
103
|
+
and pauses before approving a voice. Regenerate only the lines that need it.
|
|
104
|
+
Keep approved audio across visual revisions; settings alone cannot lock a
|
|
105
|
+
performance. Do not spend credits merely to test whether initialization works.
|
|
106
|
+
See the model-specific [ElevenLabs delivery guide](https://elevenlabs.io/docs/overview/capabilities/text-to-speech/best-practices)
|
|
107
|
+
when using an unfamiliar model or control.
|
|
108
|
+
|
|
109
|
+
## Lessons and prepared speech
|
|
110
|
+
|
|
111
|
+
For ElevenLabs lessons set `narration.provider` to `elevenlabs`, set `voice` to
|
|
112
|
+
the chosen voice ID, and put model/delivery settings under `narration.elevenlabs`.
|
|
113
|
+
Keep each segment's `text` clean for captions and cue phrases. Optional
|
|
114
|
+
`speechText` contains the exact synthesis script, including delivery tags.
|
|
115
|
+
Keep actual prediction/reveal gaps in `pauseAfterMs`, separate from vocal rhythm.
|
|
116
|
+
|
|
117
|
+
`retiam lesson prepare` supports both native providers. For ElevenLabs the CLI
|
|
118
|
+
reads `ELEVENLABS_API_KEY` from its environment and passes it to provider.init().
|
|
119
|
+
The key does not go in the lesson JSON. It caches audio and word timings and
|
|
120
|
+
writes `lesson/prepared.json` plus `public/narration/*.mp3`. Reuse those assets
|
|
121
|
+
for subsequent preview and render. Changing delivery text or voice settings
|
|
122
|
+
invalidates the corresponding prepared speech. The runtime can also synthesize
|
|
123
|
+
using an initialized matching global provider or an explicit synthesis adapter.
|
|
124
|
+
|
|
125
|
+
## Edge browser and renderer setup
|
|
126
|
+
|
|
127
|
+
Only Edge uses the existing Vite proxy. New projects already enable it. For an
|
|
128
|
+
older project, preserve other plugins and add `msEdgeTTSPlugin` from
|
|
129
|
+
`retiam/vite` on the server side. Its route is `/api/tts/msedge/synthesize` in
|
|
130
|
+
Vite development/preview and Retiam's renderer. Static hosts need that server
|
|
131
|
+
route or prepared audio. Never import the Vite plugin into scene code.
|
|
31
132
|
|
|
32
133
|
## Timeline recipe
|
|
33
134
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
check network access and the requested voice. Do not present silent audio as
|
|
67
|
-
a successful narration render.
|
|
68
|
-
4. Browser autoplay may require a click or keypress to unlock audio in Studio.
|
|
69
|
-
The headless CLI supplies its autoplay flag.
|
|
70
|
-
5. For exported video, verify an audio stream exists and listen to the result.
|
|
71
|
-
Snapshots verify visuals only. Speech generation requires network access on a
|
|
72
|
-
cache miss; the middleware caches audio in `node_modules/.cache/msedge-tts`.
|
|
135
|
+
Use [NarratedMotion.ts](../examples/NarratedMotion.ts) as the executable
|
|
136
|
+
integration recipe, adapting its speech and visual action to the brief.
|
|
137
|
+
|
|
138
|
+
- Use `Speak` for simultaneous narration and motion. Set `entity` to an entity
|
|
139
|
+
already added to the scene, so playback uses that scene's audio bus.
|
|
140
|
+
- Call `prepareAudio()` before locking timings. Register the synthesis promise
|
|
141
|
+
in `this.composer.pendingTasks` before awaiting it. Do not register playback
|
|
142
|
+
promises or `this.play(...)`: those require timeline steps and would deadlock.
|
|
143
|
+
- Derive motion duration from prepared `Speak.duration`. Pass that duration to
|
|
144
|
+
`Concurrent` too, so its defaults do not rescale the narration.
|
|
145
|
+
- `await speak(this, text, options)` handles playback, subtitles, ducking and
|
|
146
|
+
scene pacing. For headless use, pre-synthesize the same text/options and
|
|
147
|
+
register only synthesis as pending work. The provider caches that result.
|
|
148
|
+
- `TTS.speak(...)` is a lower-level client facade; it does not pace the scene.
|
|
149
|
+
- Add `Subtitles` and pass it to the narration. Caption text uses the generated
|
|
150
|
+
audio's `text`; ElevenLabs character alignment is converted to word timings
|
|
151
|
+
for lesson cues. Missing timing metadata is not a license to guess cue times.
|
|
152
|
+
- `stream()` yields sentence-sized complete audio results, not transport byte
|
|
153
|
+
chunks. A directed line with `displayText` stays one performance.
|
|
154
|
+
|
|
155
|
+
## Diagnose and verify
|
|
156
|
+
|
|
157
|
+
For ElevenLabs, check initialization, voice/model compatibility, API permissions,
|
|
158
|
+
quota/rate-limit errors, and network access. Errors surface without automatic
|
|
159
|
+
paid retries. After disposal the provider requires initialization again. For
|
|
160
|
+
Edge, a 404 means missing middleware/route; a 502 means upstream synthesis
|
|
161
|
+
failed. Inspect the actual error before changing providers.
|
|
162
|
+
|
|
163
|
+
Browser autoplay may require a user gesture; headless tools supply their own
|
|
164
|
+
autoplay flag. Verify that exported video contains non-silent audio, listen to
|
|
165
|
+
the performance, and check synchronization. A type check or visual snapshot
|
|
166
|
+
does not establish speech quality.
|
package/bin/lesson.js
CHANGED
|
@@ -15,6 +15,10 @@ const help = `Usage: retiam lesson <check|prepare|timeline|captions|styles|style
|
|
|
15
15
|
--out PATH Override the prepared JSON or SRT destination.
|
|
16
16
|
--json Emit one machine-readable result; diagnostics go to stderr.
|
|
17
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
|
+
|
|
18
22
|
Examples:
|
|
19
23
|
retiam lesson style excalidraw
|
|
20
24
|
retiam lesson check --json
|
|
@@ -79,22 +83,45 @@ export async function runLessonCLI(args) {
|
|
|
79
83
|
if (command === 'prepare') {
|
|
80
84
|
if (!plan.narration.enabled)
|
|
81
85
|
throw Error('Narration is disabled. Set narration.enabled to true before preparing speech.');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const cache = path.resolve(
|
|
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');
|
|
86
90
|
fs.mkdirSync(assets, { recursive: true });
|
|
87
|
-
|
|
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
|
+
}
|
|
88
102
|
prepared = { version: 1, voiceKey: voiceKey(plan), assets: {} };
|
|
89
103
|
try {
|
|
90
|
-
await server.listen();
|
|
91
|
-
const port = server.httpServer.address().port;
|
|
104
|
+
if (server) { await server.listen(); port = server.httpServer.address().port; }
|
|
92
105
|
// Sequential generation avoids provider throttling; cached requests return immediately.
|
|
93
106
|
for (const beat of plan.beats)
|
|
94
107
|
for (const segment of beat.narration) {
|
|
95
|
-
const request =
|
|
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');
|
|
96
112
|
let result;
|
|
97
|
-
|
|
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++) {
|
|
98
125
|
try {
|
|
99
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) });
|
|
100
127
|
if (!res.ok)
|
|
@@ -111,13 +138,13 @@ export async function runLessonCLI(args) {
|
|
|
111
138
|
console.error(`Retrying ${beat.id}/${segment.id}: ${error.message}`);
|
|
112
139
|
}
|
|
113
140
|
}
|
|
114
|
-
const hash = crypto.createHash('sha256').update(JSON.stringify(request)).digest('hex');
|
|
115
141
|
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` };
|
|
142
|
+
prepared.assets[speechKey(beat.id, segment.id)] = { text: segment.text, speechText: segment.speechText, durationMs: result.durationMs, words: result.words, url: `/narration/${hash}.mp3` };
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
finally {
|
|
120
|
-
await server
|
|
146
|
+
await server?.close();
|
|
147
|
+
await speechProvider?.dispose();
|
|
121
148
|
}
|
|
122
149
|
compileLesson(plan, prepared);
|
|
123
150
|
const dest = path.resolve(out ?? preparedFile);
|