@startsimpli/llm 0.4.29 → 0.4.31
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/README.md +76 -5
- package/bin/startsimpli-speech.js +22 -0
- package/package.json +10 -3
- package/src/__tests__/fixtures/captions-golden.json +1881 -0
- package/src/__tests__/fixtures/eleven-v3-tagged-alignment.json +1 -0
- package/src/__tests__/fixtures/voices-golden.json +614 -0
- package/src/__tests__/refine.test.ts +193 -0
- package/src/__tests__/speech-captions-cli.test.ts +37 -0
- package/src/__tests__/speech-captions.test.ts +136 -0
- package/src/__tests__/speech-cli.test.ts +67 -0
- package/src/__tests__/speech-synthesize.test.ts +277 -0
- package/src/__tests__/speech-voices-cli.test.ts +45 -0
- package/src/__tests__/speech-voices.test.ts +109 -0
- package/src/index.ts +3 -0
- package/src/refine/errors.ts +23 -0
- package/src/refine/gates.ts +55 -0
- package/src/refine/index.ts +11 -0
- package/src/refine/llm-step.ts +35 -0
- package/src/refine/refine.ts +81 -0
- package/src/refine/types.ts +54 -0
- package/src/speech/alignment.ts +48 -0
- package/src/speech/bin.ts +12 -0
- package/src/speech/captions/cues.ts +108 -0
- package/src/speech/captions/formats.ts +33 -0
- package/src/speech/captions/index.ts +6 -0
- package/src/speech/captions/types.ts +18 -0
- package/src/speech/captions/words.ts +91 -0
- package/src/speech/captions-cli.ts +50 -0
- package/src/speech/cli.ts +140 -0
- package/src/speech/http.ts +11 -4
- package/src/speech/index.ts +8 -2
- package/src/speech/interface.ts +7 -5
- package/src/speech/providers/elevenlabs-models.ts +34 -0
- package/src/speech/providers/elevenlabs-tts.ts +88 -0
- package/src/speech/providers/elevenlabs.ts +23 -5
- package/src/speech/providers/mock.ts +23 -3
- package/src/speech/providers/openai.ts +34 -3
- package/src/speech/repr.ts +14 -0
- package/src/speech/service.ts +25 -14
- package/src/speech/types.ts +55 -1
- package/src/speech/voices/cast-voices.json +127 -0
- package/src/speech/voices/cast-voices.ts +169 -0
- package/src/speech/voices/emotion.ts +51 -0
- package/src/speech/voices/errors.ts +14 -0
- package/src/speech/voices/index.ts +12 -0
- package/src/speech/voices/schema.ts +53 -0
- package/src/speech/voices/tags.ts +11 -0
- package/src/speech/voices-cli.ts +43 -0
package/README.md
CHANGED
|
@@ -181,9 +181,9 @@ Per CLAUDE.md **rule 9**, LLM client wiring never belongs in app `src/`. If you
|
|
|
181
181
|
|
|
182
182
|
## Speech (`@startsimpli/llm/speech`)
|
|
183
183
|
|
|
184
|
-
Pluggable speech-to-text, same provider pattern as the text service.
|
|
185
|
-
cheapest vendor that has a key** — each provider states its
|
|
186
|
-
through to the next cheapest on failure.
|
|
184
|
+
Pluggable speech-to-text and text-to-speech, same provider pattern as the text service.
|
|
185
|
+
**Each direction picks the cheapest vendor that has a key** — each provider states its
|
|
186
|
+
list price — and falls through to the next cheapest on failure.
|
|
187
187
|
|
|
188
188
|
```ts
|
|
189
189
|
import { SpeechService } from '@startsimpli/llm/speech';
|
|
@@ -196,11 +196,39 @@ const { text, words } = await speech.transcribe({
|
|
|
196
196
|
|
|
197
197
|
| Env | Provider | List price |
|
|
198
198
|
| --- | --- | --- |
|
|
199
|
-
| `OPENAI_API_KEY` | OpenAI `gpt-4o-mini-transcribe` (default; `gpt-4o-transcribe` / `whisper-1` cost 2x) | $0.18 / hour |
|
|
200
|
-
| `ELEVENLABS_API_KEY` | ElevenLabs Scribe (keyterm hints are a billed add-on, so off unless `sendKeyterms`) | $0.22 / hour |
|
|
199
|
+
| `OPENAI_API_KEY` | OpenAI TTS (`gpt-4o-mini-tts`, no alignment) and `gpt-4o-mini-transcribe` (default; `gpt-4o-transcribe` / `whisper-1` cost 2x) | $0.18 / hour |
|
|
200
|
+
| `ELEVENLABS_API_KEY` | ElevenLabs Scribe + text-to-speech with alignment (`ELEVENLABS_VOICE_ID` = default voice); Scribe (keyterm hints are a billed add-on, so off unless `sendKeyterms`) | $0.22 / hour |
|
|
201
201
|
| `SPEECH_PROVIDER=mock` | deterministic, for tests | — |
|
|
202
202
|
| `SPEECH_PROVIDER` / `SPEECH_LANGUAGE` | force a vendor / pin the language (e.g. `en`) so short clips don't mis-detect | — |
|
|
203
203
|
|
|
204
|
+
**Text to speech**
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const { audio, mimeType, words, characters, durationSec, usage } = await speech.synthesize({
|
|
208
|
+
text: 'Plan mode changes that.',
|
|
209
|
+
voiceId, model: 'eleven_v3', outputFormat: 'mp3_44100_128', // voiceId defaults to ELEVENLABS_VOICE_ID
|
|
210
|
+
style: { tags: ['excited'], speed: 1.05, stability: 0.4 }, // tags only on models that read them
|
|
211
|
+
});
|
|
212
|
+
// usage = { characters, costUsd } at the model's list rate
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`words` come from the vendor's character alignment (absent when it gives none, as OpenAI's doesn't).
|
|
216
|
+
`[tag]` audio markup is not a word: it is dropped from `words` with the timing offsets kept; `characters`
|
|
217
|
+
is the raw alignment and `keepTags: true` keeps tags in `words`. `voiceSettings` passes raw vendor
|
|
218
|
+
settings and wins over `style`.
|
|
219
|
+
|
|
220
|
+
**CLI for non-TS callers** — `startsimpli-speech` (JSON in via `--in` or stdin, JSON on stdout, audio to a file;
|
|
221
|
+
keys from env only; exit 0 ok, 1 provider error as `{"error":{code,retryable,...}}`, 2 bad command line):
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
echo '{"text":"[excited] Hello.","model":"eleven_v3","style":{"speed":1.0}}' \
|
|
225
|
+
| startsimpli-speech synthesize --out hello.mp3 [--voice ID --model ID --format pcm_22050 --voice-settings '{"stability":0.3}']
|
|
226
|
+
# -> {"mimeType":"audio/mpeg","audioPath":"hello.mp3","bytes":...,"words":[{"text":"Hello.","start":..,"end":..}],"characters":[...],"usage":{"characters":..,"costUsd":..},...}
|
|
227
|
+
echo '{"audioPath":"hello.mp3"}' | startsimpli-speech transcribe # -> {"text":..,"words":[..],"usage":{..}}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
It runs the tsup bundle when built (`pnpm --filter @startsimpli/llm build`), else the source via `tsx`.
|
|
231
|
+
|
|
204
232
|
Retryable errors back off and retry; a failure that survives falls through to the next
|
|
205
233
|
vendor (`fallback: false` to pin one). Bad audio (`INVALID_REQUEST`) never falls through.
|
|
206
234
|
|
|
@@ -213,10 +241,53 @@ registerSpeechProvider('mine', () => (process.env.MINE_KEY ? new MyProvider(proc
|
|
|
213
241
|
```
|
|
214
242
|
|
|
215
243
|
`registerLLMProvider(name, factory)` does the same for text. Both sit on `ProviderRegistry`,
|
|
244
|
+
<<<<<<< HEAD
|
|
216
245
|
so a new modality (TTS, embeddings) reuses it rather than hard-coding a provider list.
|
|
217
246
|
|
|
247
|
+
### Voice cast library (`@startsimpli/llm/speech/voices`)
|
|
248
|
+
|
|
249
|
+
Voices keyed by neutral ids (never by avatar), with model, per-model settings, per-language rows, an emotion map, and the
|
|
250
|
+
retired / expiring ElevenLabs Default voice ids (they die 2026-12-31). Data is `src/speech/voices/cast-voices.json`.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const cv = createCastVoices(defaultCastLibrary, { expiring: 'warn' }); // warn | allow | error; `today` injectable
|
|
254
|
+
const voices = cv.resolveCast([{ id: 'maya', voice: 'maya' }, { id: 'b' }], { language: 'es' });
|
|
255
|
+
const { textSent, settings } = directEmotion(cv, voices.b, 'Hello.', 'excited');
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A member's `voice` is a library id or a real voice id; anything else is an alias and the member takes the next `default_pool`
|
|
259
|
+
entry. Unfilled slot, retired / expiring id, unknown model or language, and two members on one voice throw named errors
|
|
260
|
+
(`VoiceNotResolvableError` subclasses). CLI: `echo '{"cast":[{"id":"a"}]}' | startsimpli-speech voices resolve [--model --language --expiring --today --library]`.
|
|
261
|
+
|
|
262
|
+
### Captions (`@startsimpli/llm/speech/captions`)
|
|
263
|
+
|
|
264
|
+
Pure caption logic, no vendor and no I/O: `cuesFromWords` / `cuesFromText` / `cuesForBeat` (clause-aware splitting),
|
|
265
|
+
`holdGaps`, `cueFrameRange` (round half to even), `toSrt` / `toVtt`, and word checks: `wordsAreProportional` (is this
|
|
266
|
+
vendor alignment real or spread by character count?), `assertValidWords` (`InvalidWordsError` on empty or disordered
|
|
267
|
+
transcripts), `splitWhitespaceTokens`, `captionTiming`. Pixel and line-width layout stays with the renderer.
|
|
268
|
+
CLI: `startsimpli-speech captions --format json|srt|vtt [--out FILE] < request.json`.
|
|
269
|
+
|
|
270
|
+
## Refine with gates (`@startsimpli/llm/refine`)
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const gates = createGateRegistry<Doc, Ctx>();
|
|
274
|
+
gates.register('schema', (doc, ctx) => validate(doc)); // string[] | { text, level: 'info' }[] , sync or async
|
|
275
|
+
const out = await refineWithGates({
|
|
276
|
+
step: llmRefineStep({ provider, systemPrompt, brief, validate }), // one model call per try; inject any client
|
|
277
|
+
gates, tries: 3, // an integer >= 1, or null to loop until every gate passes
|
|
278
|
+
});
|
|
279
|
+
// out.attempts[i] = { n, result, report, notes, cost, passed }; out.cost, out.tally, out.passed
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Each try is exactly one step call; the step receives the previous result and the gates' failures as data. Informational
|
|
283
|
+
lines (`level: 'info'`) never fail a gate. A step that throws ends the loop with `RefineStepError`. This is the multi-gate,
|
|
284
|
+
caller-bounded loop; `extractValidateCorrect` stays the single-validator JSON repair loop.
|
|
285
|
+
|
|
218
286
|
## Links
|
|
219
287
|
|
|
220
288
|
- Source: https://github.com/qosha1/start-simpli/tree/main/packages/llm
|
|
221
289
|
- Homepage: https://startsimpli.com
|
|
222
290
|
- License: MIT, see [LICENSE](./LICENSE)
|
|
291
|
+
=======
|
|
292
|
+
so a new modality (embeddings) reuses it rather than hard-coding a provider list.
|
|
293
|
+
>>>>>>> eda91372 (speech: synthesize() with alignment, ElevenLabs TTS, tags and style, startsimpli-speech CLI)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher for the speech CLI. Packages here publish TypeScript source, so this
|
|
3
|
+
// runs the tsup bundle (`pnpm build`) when it exists, else the source via tsx.
|
|
4
|
+
const { existsSync } = require('node:fs');
|
|
5
|
+
const { join } = require('node:path');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
const root = join(__dirname, '..');
|
|
9
|
+
const bundle = join(root, 'dist', 'speech', 'bin.js');
|
|
10
|
+
const localTsx = join(root, 'node_modules', '.bin', 'tsx');
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
if (existsSync(bundle)) {
|
|
13
|
+
process.argv.splice(1, 1, bundle);
|
|
14
|
+
require(bundle);
|
|
15
|
+
} else {
|
|
16
|
+
const tsx = spawnSync(existsSync(localTsx) ? localTsx : 'tsx', [join(root, 'src', 'speech', 'bin.ts'), ...args], { stdio: 'inherit' });
|
|
17
|
+
if (tsx.error) {
|
|
18
|
+
process.stdout.write(JSON.stringify({ error: { code: 'USAGE', message: 'startsimpli-speech needs a build (pnpm --filter @startsimpli/llm build) or tsx on PATH' } }) + '\n');
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
process.exit(tsx.status ?? 1);
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/llm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.31",
|
|
4
4
|
"description": "Generic LLM service infrastructure — provider abstraction, retry logic, JSON extraction",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://startsimpli.com",
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./src/index.ts",
|
|
19
19
|
"./translation": "./src/translation/index.ts",
|
|
20
|
-
"./speech": "./src/speech/index.ts"
|
|
20
|
+
"./speech": "./src/speech/index.ts",
|
|
21
|
+
"./refine": "./src/refine/index.ts",
|
|
22
|
+
"./speech/voices": "./src/speech/voices/index.ts",
|
|
23
|
+
"./speech/captions": "./src/speech/captions/index.ts"
|
|
21
24
|
},
|
|
22
25
|
"files": [
|
|
23
26
|
"src",
|
|
27
|
+
"bin",
|
|
24
28
|
"README.md",
|
|
25
29
|
"LICENSE"
|
|
26
30
|
],
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
31
35
|
"zod": "^4.3.6",
|
|
32
|
-
"@startsimpli/i18n": "^0.1.
|
|
36
|
+
"@startsimpli/i18n": "^0.1.3"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@types/node": "^20.19.39",
|
|
@@ -37,6 +41,9 @@
|
|
|
37
41
|
"typescript": "^6.0.3",
|
|
38
42
|
"vitest": "^4.1.5"
|
|
39
43
|
},
|
|
44
|
+
"bin": {
|
|
45
|
+
"startsimpli-speech": "./bin/startsimpli-speech.js"
|
|
46
|
+
},
|
|
40
47
|
"scripts": {
|
|
41
48
|
"build": "tsup",
|
|
42
49
|
"dev": "tsup --watch",
|