@effect-uai/elevenlabs 0.6.0 → 0.7.0

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 (50) hide show
  1. package/dist/ElevenLabsMusicGenerator.d.mts +75 -0
  2. package/dist/ElevenLabsMusicGenerator.d.mts.map +1 -0
  3. package/dist/ElevenLabsMusicGenerator.mjs +191 -0
  4. package/dist/ElevenLabsMusicGenerator.mjs.map +1 -0
  5. package/dist/ElevenLabsMusicGenerator.test.mjs +98 -0
  6. package/dist/ElevenLabsMusicGenerator.test.mjs.map +1 -0
  7. package/dist/ElevenLabsSynthesizer.d.mts +10 -4
  8. package/dist/ElevenLabsSynthesizer.d.mts.map +1 -1
  9. package/dist/ElevenLabsSynthesizer.mjs +9 -33
  10. package/dist/ElevenLabsSynthesizer.mjs.map +1 -1
  11. package/dist/ElevenLabsTranscriber.d.mts +4 -4
  12. package/dist/ElevenLabsTranscriber.d.mts.map +1 -1
  13. package/dist/ElevenLabsTranscriber.mjs +9 -1
  14. package/dist/ElevenLabsTranscriber.mjs.map +1 -1
  15. package/dist/codec.d.mts +25 -2
  16. package/dist/codec.d.mts.map +1 -1
  17. package/dist/codec.mjs +18 -1
  18. package/dist/codec.mjs.map +1 -1
  19. package/dist/index.d.mts +4 -3
  20. package/dist/index.mjs +2 -1
  21. package/dist/models.d.mts +13 -1
  22. package/dist/models.d.mts.map +1 -1
  23. package/dist/musicCodec-CZni1gAQ.d.mts +42 -0
  24. package/dist/musicCodec-CZni1gAQ.d.mts.map +1 -0
  25. package/dist/musicCodec.d.mts +2 -0
  26. package/dist/musicCodec.mjs +38 -0
  27. package/dist/musicCodec.mjs.map +1 -0
  28. package/dist/realtimeStt.d.mts +2 -1
  29. package/dist/realtimeStt.d.mts.map +1 -1
  30. package/dist/realtimeStt.mjs +14 -3
  31. package/dist/realtimeStt.mjs.map +1 -1
  32. package/dist/realtimeTts.d.mts +8 -3
  33. package/dist/realtimeTts.d.mts.map +1 -1
  34. package/dist/realtimeTts.mjs +5 -3
  35. package/dist/realtimeTts.mjs.map +1 -1
  36. package/package.json +9 -3
  37. package/src/ElevenLabsMusicGenerator.test.ts +116 -0
  38. package/src/ElevenLabsMusicGenerator.ts +353 -0
  39. package/src/ElevenLabsSynthesizer.ts +17 -51
  40. package/src/ElevenLabsTranscriber.ts +11 -0
  41. package/src/codec.ts +47 -0
  42. package/src/index.ts +1 -0
  43. package/src/models.ts +17 -0
  44. package/src/musicCodec.ts +86 -0
  45. package/src/realtimeStt.ts +11 -0
  46. package/src/realtimeTts.ts +11 -0
  47. package/dist/ElevenLabsSynthesizer.test.mjs +0 -32
  48. package/dist/ElevenLabsSynthesizer.test.mjs.map +0 -1
  49. package/src/ElevenLabsSynthesizer.test.ts +0 -49
  50. /package/dist/{ElevenLabsSynthesizer.test.d.mts → ElevenLabsMusicGenerator.test.d.mts} +0 -0
@@ -0,0 +1,86 @@
1
+ import { Duration, Effect } from "effect"
2
+ import { warnDroppedWhen } from "@effect-uai/core/Capabilities"
3
+ import type { CommonGenerateMusicRequest } from "@effect-uai/core/Music"
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Composition plan (camelCase; snake-cased at the wire boundary)
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Composition plan accepted by `POST /v1/music` (and returned by
11
+ * `POST /v1/music/plan`). Mirrors the wire `MusicPrompt` schema with
12
+ * camelCase fields. Validation lives server-side (3 s to 10 min total,
13
+ * up to 30 sections, 3 s to 2 min per section, ≤200 chars per lyric
14
+ * line); we pass the body through and surface 422 errors via the
15
+ * HTTP-status mapping.
16
+ *
17
+ * Reference: https://elevenlabs.io/docs/api-reference/music/compose
18
+ */
19
+ export type ElevenLabsCompositionPlan = {
20
+ readonly positiveGlobalStyles: ReadonlyArray<string>
21
+ readonly negativeGlobalStyles: ReadonlyArray<string>
22
+ readonly sections: ReadonlyArray<ElevenLabsSongSection>
23
+ }
24
+
25
+ export type ElevenLabsSongSection = {
26
+ readonly sectionName: string
27
+ readonly positiveLocalStyles: ReadonlyArray<string>
28
+ readonly negativeLocalStyles: ReadonlyArray<string>
29
+ readonly duration: Duration.Duration
30
+ readonly lines: ReadonlyArray<string>
31
+ }
32
+
33
+ export const wireCompositionPlan = (plan: ElevenLabsCompositionPlan) => ({
34
+ positive_global_styles: plan.positiveGlobalStyles,
35
+ negative_global_styles: plan.negativeGlobalStyles,
36
+ sections: plan.sections.map((s) => ({
37
+ section_name: s.sectionName,
38
+ positive_local_styles: s.positiveLocalStyles,
39
+ negative_local_styles: s.negativeLocalStyles,
40
+ duration_ms: Duration.toMillis(s.duration),
41
+ lines: s.lines,
42
+ })),
43
+ })
44
+
45
+ export const decodeCompositionPlan = (raw: unknown): ElevenLabsCompositionPlan => {
46
+ const r = raw as {
47
+ positive_global_styles?: ReadonlyArray<string>
48
+ negative_global_styles?: ReadonlyArray<string>
49
+ sections?: ReadonlyArray<{
50
+ section_name: string
51
+ positive_local_styles?: ReadonlyArray<string>
52
+ negative_local_styles?: ReadonlyArray<string>
53
+ duration_ms: number
54
+ lines?: ReadonlyArray<string>
55
+ }>
56
+ }
57
+ return {
58
+ positiveGlobalStyles: r.positive_global_styles ?? [],
59
+ negativeGlobalStyles: r.negative_global_styles ?? [],
60
+ sections: (r.sections ?? []).map((s) => ({
61
+ sectionName: s.section_name,
62
+ positiveLocalStyles: s.positive_local_styles ?? [],
63
+ negativeLocalStyles: s.negative_local_styles ?? [],
64
+ duration: Duration.millis(s.duration_ms),
65
+ lines: s.lines ?? [],
66
+ })),
67
+ }
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Bucket-2 warn-and-drop for Common fields ElevenLabs can't honor
72
+ // in prompt mode. No prompt construction; prompts always come from
73
+ // the caller verbatim. Migrate to `dropUnsupported` when capabilities
74
+ // Phase 0 lands.
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export const warnDroppedPromptModeHints = (
78
+ request: CommonGenerateMusicRequest,
79
+ ): Effect.Effect<void> =>
80
+ warnDroppedWhen(request.lyrics, {
81
+ provider: "elevenlabs-music",
82
+ capability: "lyrics",
83
+ field: "lyrics",
84
+ reason:
85
+ "Prompt mode has no lyrics field. Use `compositionPlan` with per-section `lines` to provide lyrics.",
86
+ })
@@ -3,6 +3,7 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
3
  import * as Socket from "effect/unstable/socket/Socket"
4
4
  import * as AiError from "@effect-uai/core/AiError"
5
5
  import type { AudioFormat } from "@effect-uai/core/Audio"
6
+ import * as Capabilities from "@effect-uai/core/Capabilities"
6
7
  import * as JSONL from "@effect-uai/core/JSONL"
7
8
  import type { TranscriptEvent, WordTimestamp } from "@effect-uai/core/Transcript"
8
9
  import type { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber"
@@ -175,6 +176,7 @@ export type RealtimeOptions = {
175
176
  readonly languageCode?: string
176
177
  readonly includeTimestamps?: boolean
177
178
  readonly commitStrategy?: "manual" | "vad"
179
+ readonly keyterms?: ReadonlyArray<string>
178
180
  }
179
181
 
180
182
  export const buildWsUrl = (
@@ -192,6 +194,8 @@ export const buildWsUrl = (
192
194
  ...(opts.languageCode !== undefined && { language_code: opts.languageCode }),
193
195
  ...(opts.includeTimestamps === true && { include_timestamps: "true" }),
194
196
  })
197
+ // Vocab biasing — repeated `keyterms` param (≤50 on realtime).
198
+ for (const term of opts.keyterms ?? []) params.append("keyterms", term)
195
199
  return `${wsBase}/speech-to-text/realtime?${params.toString()}`
196
200
  }
197
201
 
@@ -247,9 +251,16 @@ export const streamTranscription =
247
251
  const slug = yield* inputFormatToSlug(request.inputFormat)
248
252
  const token = yield* fetchSingleUseToken(cfg)
249
253
  const includeTimestamps = request.wordTimestamps === true
254
+ yield* Capabilities.warnDroppedWhen(request.prompt, {
255
+ provider: "elevenlabs",
256
+ capability: "prompt",
257
+ field: "prompt",
258
+ reason: "ElevenLabs Scribe has no free-form prompt field; bias via `biasingTerms`.",
259
+ })
250
260
  const url = buildWsUrl(cfg, token, slug, {
251
261
  ...(request.model !== undefined && { model: request.model }),
252
262
  ...(request.language !== undefined && { languageCode: request.language }),
263
+ ...(request.biasingTerms !== undefined && { keyterms: request.biasingTerms }),
253
264
  includeTimestamps,
254
265
  })
255
266
  const socket = yield* Socket.makeWebSocket(url, {
@@ -3,10 +3,14 @@ import * as Socket from "effect/unstable/socket/Socket"
3
3
  import * as AiError from "@effect-uai/core/AiError"
4
4
  import type { AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
5
5
  import * as JSONL from "@effect-uai/core/JSONL"
6
+ import type { CustomPronunciation } from "@effect-uai/core/SpeechSynthesizer"
6
7
  import {
7
8
  defaultFormat,
8
9
  formatToOutputSlug,
10
+ type PronunciationDictionaryLocator,
11
+ rejectInlinePronunciations,
9
12
  type VoiceSettings,
13
+ wirePronunciationLocators,
10
14
  wireVoiceSettings,
11
15
  } from "./codec.js"
12
16
  import type { ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.js"
@@ -33,6 +37,11 @@ export type StreamSynthesizeRequest = {
33
37
  readonly languageCode?: string
34
38
  readonly voiceSettings?: VoiceSettings
35
39
  readonly autoMode?: boolean
40
+ /** Pre-provisioned pronunciation dictionaries, sent on the BOS frame. */
41
+ readonly pronunciationDictionaryLocators?: ReadonlyArray<PronunciationDictionaryLocator>
42
+ /** Carried from the Common request so the WS path can reject inline
43
+ * pronunciations (ElevenLabs has no stateless inline IPA path). */
44
+ readonly pronunciations?: ReadonlyArray<CustomPronunciation>
36
45
  }
37
46
 
38
47
  // ---------------------------------------------------------------------------
@@ -56,6 +65,7 @@ const bosFrame = (cfg: Config, request: StreamSynthesizeRequest) => {
56
65
  text: " ",
57
66
  "xi-api-key": Redacted.value(cfg.apiKey),
58
67
  ...(vs !== undefined && { voice_settings: vs }),
68
+ ...wirePronunciationLocators(request.pronunciationDictionaryLocators),
59
69
  })
60
70
  }
61
71
 
@@ -117,6 +127,7 @@ export const streamSynthesis =
117
127
  ): Stream.Stream<AudioChunk, AiError.AiError | E, R | Socket.WebSocketConstructor> =>
118
128
  Stream.unwrap(
119
129
  Effect.gen(function* () {
130
+ yield* rejectInlinePronunciations(request.pronunciations)
120
131
  const slug = yield* formatToOutputSlug(request.outputFormat ?? defaultFormat)
121
132
  // ElevenLabs closes `/stream-input` with code 1000 after delivering the
122
133
  // final audio chunk. Effect's default treats all close codes as errors,
@@ -1,32 +0,0 @@
1
- import { i as it, n as globalExpect, r as describe, t as import_dist } from "./dist-DMsg8YLb.mjs";
2
- import { layer } from "./ElevenLabsSynthesizer.mjs";
3
- import { Effect, Layer, Redacted, Stream } from "effect";
4
- import { FetchHttpClient } from "effect/unstable/http";
5
- import * as Socket from "effect/unstable/socket/Socket";
6
- import * as SpeechSynthesizer$1 from "@effect-uai/core/SpeechSynthesizer";
7
- const live = layer({ apiKey: Redacted.make("test-key") }).pipe(Layer.provide(FetchHttpClient.layer), Layer.provide(Socket.layerWebSocketConstructorGlobal));
8
- describe("ElevenLabsSynthesizer Layer (compile-time)", () => {
9
- it("registers `TtsIncrementalText` — `streamSynthesisFrom` clears R to never", () => {
10
- const audio = Stream.fromIterable(["hello ", "world"]).pipe(SpeechSynthesizer$1.streamSynthesisFrom({
11
- model: "eleven_flash_v2_5",
12
- voiceId: "JBFqnCBsd6RMkjVDRZzb"
13
- }));
14
- (0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(live))).toEqualTypeOf();
15
- });
16
- it("sync `synthesize` and chunked `streamSynthesis` require no marker", () => {
17
- (0, import_dist.expectTypeOf)(SpeechSynthesizer$1.synthesize({
18
- text: "hi",
19
- model: "eleven_multilingual_v2",
20
- voiceId: "JBFqnCBsd6RMkjVDRZzb"
21
- }).pipe(Effect.provide(live))).toEqualTypeOf();
22
- globalExpect(Stream.runCollect(SpeechSynthesizer$1.streamSynthesis({
23
- text: "hi",
24
- model: "eleven_multilingual_v2",
25
- voiceId: "JBFqnCBsd6RMkjVDRZzb"
26
- })).pipe(Effect.provide(live))).toBeDefined();
27
- });
28
- });
29
- //#endregion
30
- export {};
31
-
32
- //# sourceMappingURL=ElevenLabsSynthesizer.test.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ElevenLabsSynthesizer.test.mjs","names":["ElevenLabsSynthesizer.layer","SpeechSynthesizer"],"sources":["../src/ElevenLabsSynthesizer.test.ts"],"sourcesContent":["import { Effect, Layer, Redacted, Stream } from \"effect\"\nimport { FetchHttpClient } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport type * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob } from \"@effect-uai/core/Audio\"\nimport * as SpeechSynthesizer from \"@effect-uai/core/SpeechSynthesizer\"\nimport * as ElevenLabsSynthesizer from \"./ElevenLabsSynthesizer.js\"\n\nconst cfg: ElevenLabsSynthesizer.Config = { apiKey: Redacted.make(\"test-key\") }\n// FetchHttpClient + globalThis.WebSocket are required for `make`, but these\n// tests only exercise the codec and compile-time gating — no real network\n// call is made.\nconst live = ElevenLabsSynthesizer.layer(cfg).pipe(\n Layer.provide(FetchHttpClient.layer),\n Layer.provide(Socket.layerWebSocketConstructorGlobal),\n)\n\ndescribe(\"ElevenLabsSynthesizer Layer (compile-time)\", () => {\n it(\"registers `TtsIncrementalText` — `streamSynthesisFrom` clears R to never\", () => {\n const tokens: Stream.Stream<string> = Stream.fromIterable([\"hello \", \"world\"])\n const audio = tokens.pipe(\n SpeechSynthesizer.streamSynthesisFrom({\n model: \"eleven_flash_v2_5\",\n voiceId: \"JBFqnCBsd6RMkjVDRZzb\",\n }),\n )\n const program = Stream.runDrain(audio).pipe(Effect.provide(live))\n expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()\n })\n\n it(\"sync `synthesize` and chunked `streamSynthesis` require no marker\", () => {\n const synth = SpeechSynthesizer.synthesize({\n text: \"hi\",\n model: \"eleven_multilingual_v2\",\n voiceId: \"JBFqnCBsd6RMkjVDRZzb\",\n }).pipe(Effect.provide(live))\n expectTypeOf(synth).toEqualTypeOf<Effect.Effect<AudioBlob, AiError.AiError, never>>()\n\n const _stream = Stream.runCollect(\n SpeechSynthesizer.streamSynthesis({\n text: \"hi\",\n model: \"eleven_multilingual_v2\",\n voiceId: \"JBFqnCBsd6RMkjVDRZzb\",\n }),\n ).pipe(Effect.provide(live))\n expect(_stream).toBeDefined()\n })\n})\n"],"mappings":";;;;;;AAaA,MAAM,OAAOA,MAA4B,EAJG,QAAQ,SAAS,KAAK,WAAW,EAIpC,CAAI,CAAC,KAC5C,MAAM,QAAQ,gBAAgB,MAAM,EACpC,MAAM,QAAQ,OAAO,gCAAgC,CACtD;AAED,SAAS,oDAAoD;AAC3D,IAAG,kFAAkF;EAEnF,MAAM,QADgC,OAAO,aAAa,CAAC,UAAU,QAAQ,CACzD,CAAC,KACnBC,oBAAkB,oBAAoB;GACpC,OAAO;GACP,SAAS;GACV,CAAC,CACH;AAED,GAAA,GAAA,YAAA,cADgB,OAAO,SAAS,MAAM,CAAC,KAAK,OAAO,QAAQ,KAAK,CAC5C,CAAC,CAAC,eAA4D;GAClF;AAEF,IAAG,2EAA2E;AAM5E,GAAA,GAAA,YAAA,cALcA,oBAAkB,WAAW;GACzC,MAAM;GACN,OAAO;GACP,SAAS;GACV,CAAC,CAAC,KAAK,OAAO,QAAQ,KAAK,CACV,CAAC,CAAC,eAAiE;AASrF,eAPgB,OAAO,WACrBA,oBAAkB,gBAAgB;GAChC,MAAM;GACN,OAAO;GACP,SAAS;GACV,CAAC,CACH,CAAC,KAAK,OAAO,QAAQ,KAAK,CACb,CAAC,CAAC,aAAa;GAC7B;EACF"}
@@ -1,49 +0,0 @@
1
- import { Effect, Layer, Redacted, Stream } from "effect"
2
- import { FetchHttpClient } from "effect/unstable/http"
3
- import * as Socket from "effect/unstable/socket/Socket"
4
- import { describe, expect, expectTypeOf, it } from "vitest"
5
- import type * as AiError from "@effect-uai/core/AiError"
6
- import type { AudioBlob } from "@effect-uai/core/Audio"
7
- import * as SpeechSynthesizer from "@effect-uai/core/SpeechSynthesizer"
8
- import * as ElevenLabsSynthesizer from "./ElevenLabsSynthesizer.js"
9
-
10
- const cfg: ElevenLabsSynthesizer.Config = { apiKey: Redacted.make("test-key") }
11
- // FetchHttpClient + globalThis.WebSocket are required for `make`, but these
12
- // tests only exercise the codec and compile-time gating — no real network
13
- // call is made.
14
- const live = ElevenLabsSynthesizer.layer(cfg).pipe(
15
- Layer.provide(FetchHttpClient.layer),
16
- Layer.provide(Socket.layerWebSocketConstructorGlobal),
17
- )
18
-
19
- describe("ElevenLabsSynthesizer Layer (compile-time)", () => {
20
- it("registers `TtsIncrementalText` — `streamSynthesisFrom` clears R to never", () => {
21
- const tokens: Stream.Stream<string> = Stream.fromIterable(["hello ", "world"])
22
- const audio = tokens.pipe(
23
- SpeechSynthesizer.streamSynthesisFrom({
24
- model: "eleven_flash_v2_5",
25
- voiceId: "JBFqnCBsd6RMkjVDRZzb",
26
- }),
27
- )
28
- const program = Stream.runDrain(audio).pipe(Effect.provide(live))
29
- expectTypeOf(program).toEqualTypeOf<Effect.Effect<void, AiError.AiError, never>>()
30
- })
31
-
32
- it("sync `synthesize` and chunked `streamSynthesis` require no marker", () => {
33
- const synth = SpeechSynthesizer.synthesize({
34
- text: "hi",
35
- model: "eleven_multilingual_v2",
36
- voiceId: "JBFqnCBsd6RMkjVDRZzb",
37
- }).pipe(Effect.provide(live))
38
- expectTypeOf(synth).toEqualTypeOf<Effect.Effect<AudioBlob, AiError.AiError, never>>()
39
-
40
- const _stream = Stream.runCollect(
41
- SpeechSynthesizer.streamSynthesis({
42
- text: "hi",
43
- model: "eleven_multilingual_v2",
44
- voiceId: "JBFqnCBsd6RMkjVDRZzb",
45
- }),
46
- ).pipe(Effect.provide(live))
47
- expect(_stream).toBeDefined()
48
- })
49
- })