@effect-uai/elevenlabs 0.5.2 → 0.6.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.
@@ -1,5 +1,6 @@
1
1
  import { VoiceSettings } from "./codec.mjs";
2
2
  import { ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.mjs";
3
+ import { ElevenLabsRegion } from "./region.mjs";
3
4
  import { Context, Effect, Layer, Redacted, Stream } from "effect";
4
5
  import { HttpClient } from "effect/unstable/http";
5
6
  import * as Socket from "effect/unstable/socket/Socket";
@@ -29,27 +30,31 @@ type ElevenLabsSynthesizerService = {
29
30
  readonly synthesize: (r: ElevenLabsSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>;
30
31
  readonly streamSynthesis: (r: ElevenLabsSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError>;
31
32
  readonly streamSynthesisFrom: SpeechSynthesizerService["streamSynthesisFrom"];
33
+ readonly synthesizeDialogue: SpeechSynthesizerService["synthesizeDialogue"];
34
+ readonly streamSynthesizeDialogue: SpeechSynthesizerService["streamSynthesizeDialogue"];
32
35
  };
33
36
  declare const ElevenLabsSynthesizer_base: Context.ServiceClass<ElevenLabsSynthesizer, "@betalyra/effect-uai/providers/elevenlabs/ElevenLabsSynthesizer", ElevenLabsSynthesizerService>;
34
37
  declare class ElevenLabsSynthesizer extends ElevenLabsSynthesizer_base {}
35
38
  type Config = {
36
39
  readonly apiKey: Redacted.Redacted;
37
40
  readonly baseUrl?: string;
41
+ readonly region?: ElevenLabsRegion;
38
42
  };
39
43
  declare const make: (cfg: Config) => Effect.Effect<{
40
- synthesize: (r: ElevenLabsSynthesizeRequest) => Effect.Effect<{
41
- format: any;
42
- bytes: Uint8Array<ArrayBuffer>;
43
- }, AiError.AiError, never>;
44
- streamSynthesis: (r: ElevenLabsSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError, never>;
45
- streamSynthesisFrom: (textIn: any, request: any) => Stream.Stream<AudioChunk, unknown, unknown>;
44
+ synthesize: (r: ElevenLabsSynthesizeRequest) => Effect.Effect<any, any, never>;
45
+ streamSynthesis: (r: ElevenLabsSynthesizeRequest) => Stream.Stream<AudioChunk, any, never>;
46
+ streamSynthesisFrom: (textIn: any, request: any) => Stream.Stream<AudioChunk, any, unknown>;
47
+ synthesizeDialogue: (request: any) => Effect.Effect<any, any, never>;
48
+ streamSynthesizeDialogue: (request: any) => Stream.Stream<AudioChunk, any, never>;
46
49
  }, never, Socket.WebSocketConstructor | HttpClient.HttpClient>;
47
50
  /**
48
51
  * Layer registers `ElevenLabsSynthesizer`, the generic
49
- * `SpeechSynthesizer`, **and `TtsIncrementalText`** — incremental
50
- * text-in is wired to the realtime `/stream-input` WebSocket. Provide
51
- * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer at
52
- * the call site.
52
+ * `SpeechSynthesizer`, the `TtsIncrementalText` marker (incremental
53
+ * text-in via `/stream-input` WebSocket), **and `MultiSpeakerTts`** —
54
+ * multi-speaker dialogue is wired to `POST /v1/text-to-dialogue`
55
+ * (sync) and `/v1/text-to-dialogue/stream` (chunked). Provide
56
+ * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer
57
+ * at the call site.
53
58
  */
54
59
  declare const layer: (cfg: Config) => Layer.Layer<unknown, never, Socket.WebSocketConstructor | HttpClient.HttpClient>;
55
60
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ElevenLabsSynthesizer.d.mts","names":[],"sources":["../src/ElevenLabsSynthesizer.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAiCY,2BAAA,GAA8B,IAAA,CAAK,uBAAA;EAAA,SACpC,KAAA,EAAO,kBAAA;EAAA,SACP,OAAA,EAAS,iBAAA;EAAA,SACT,aAAA,GAAgB,aAAA;EAAA,SAChB,IAAA;EAAA,SACA,YAAA;EAAA,SACA,QAAA;AAAA;AAAA,KAGC,4BAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,2BAAA,KAAgC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SACjF,eAAA,GACP,CAAA,EAAG,2BAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;AAAA;AAAA,cAC/B,0BAAA;cAEY,qBAAA,SAA8B,0BAAA;AAAA,KAK/B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAoEvD,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA;;;;;;;;AAjFhC;;;;;;;AAAA,cAwGa,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,iBAAA,MAAA,CAAA,oBAAA,GAAA,UAAA,CAAA,UAAA"}
1
+ {"version":3,"file":"ElevenLabsSynthesizer.d.mts","names":[],"sources":["../src/ElevenLabsSynthesizer.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;KAqCY,2BAAA,GAA8B,IAAA,CAAK,uBAAA;EAAA,SACpC,KAAA,EAAO,kBAAA;EAAA,SACP,OAAA,EAAS,iBAAA;EAAA,SACT,aAAA,GAAgB,aAAA;EAAA,SAChB,IAAA;EAAA,SACA,YAAA;EAAA,SACA,QAAA;AAAA;AAAA,KAGC,4BAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,2BAAA,KAAgC,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SACjF,eAAA,GACP,CAAA,EAAG,2BAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;EAAA,SACrB,kBAAA,EAAoB,wBAAA;EAAA,SACpB,wBAAA,EAA0B,wBAAA;AAAA;AAAA,cACpC,0BAAA;cAEY,qBAAA,SAA8B,0BAAA;AAAA,KAK/B,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,gBAAA;AAAA;AAAA,cA+KP,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA;;;;;;;;;;;;;;;;cAiCnB,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,iBAAA,MAAA,CAAA,oBAAA,GAAA,UAAA,CAAA,UAAA"}
@@ -1,10 +1,11 @@
1
1
  import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
2
  import { defaultFormat, formatToOutputSlug, httpStatusError, transportFailure, wireVoiceSettings } from "./codec.mjs";
3
+ import { resolveHost } from "./region.mjs";
3
4
  import { streamSynthesis } from "./realtimeTts.mjs";
4
- import { Context, Effect, Layer, Redacted, Stream } from "effect";
5
+ import { Array, Context, Effect, Layer, Redacted, Stream } from "effect";
5
6
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
6
7
  import * as Socket from "effect/unstable/socket/Socket";
7
- import { SpeechSynthesizer, TtsIncrementalText } from "@effect-uai/core/SpeechSynthesizer";
8
+ import { MultiSpeakerTts, SpeechSynthesizer, TtsIncrementalText } from "@effect-uai/core/SpeechSynthesizer";
8
9
  //#region src/ElevenLabsSynthesizer.ts
9
10
  var ElevenLabsSynthesizer_exports = /* @__PURE__ */ __exportAll({
10
11
  ElevenLabsSynthesizer: () => ElevenLabsSynthesizer,
@@ -12,8 +13,37 @@ var ElevenLabsSynthesizer_exports = /* @__PURE__ */ __exportAll({
12
13
  make: () => make
13
14
  });
14
15
  var ElevenLabsSynthesizer = class extends Context.Service()("@betalyra/effect-uai/providers/elevenlabs/ElevenLabsSynthesizer") {};
16
+ /**
17
+ * Models that honor inline `<phoneme>` SSML tags. Other models silently
18
+ * drop the tags server-side, so we don't bother rewriting the text for
19
+ * them — the pronunciation overrides are silently ignored.
20
+ *
21
+ * Reference: https://elevenlabs.io/docs/best-practices/prompting/controls
22
+ */
23
+ const PHONEME_SUPPORTED_MODELS = new Set([
24
+ "eleven_flash_v2",
25
+ "eleven_english_v1",
26
+ "eleven_monolingual_v1"
27
+ ]);
28
+ const wireAlphabet = (encoding) => encoding === "ipa" ? "ipa" : encoding === "cmu-arpabet" ? "cmu-arpabet" : void 0;
29
+ const phonemeTag = (phrase, pronunciation, alphabet) => `<phoneme alphabet="${alphabet}" ph="${pronunciation}">${phrase}</phoneme>`;
30
+ const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ /**
32
+ * Apply pronunciation overrides to a text string. Only applies for
33
+ * phoneme-supported models; only IPA and CMU Arpabet are honored
34
+ * (x-sampa is silently dropped). Whole-word, case-insensitive
35
+ * replacement.
36
+ */
37
+ const applyPronunciations = (text, model, pronunciations) => {
38
+ if (pronunciations === void 0 || !PHONEME_SUPPORTED_MODELS.has(model)) return text;
39
+ return Array.reduce(pronunciations, text, (acc, p) => {
40
+ const alphabet = wireAlphabet(p.encoding);
41
+ if (alphabet === void 0) return acc;
42
+ return acc.replace(new RegExp(`\\b${escapeForRegex(p.phrase)}\\b`, "i"), phonemeTag(p.phrase, p.pronunciation, alphabet));
43
+ });
44
+ };
15
45
  const buildBody = (r) => ({
16
- text: r.text,
46
+ text: applyPronunciations(r.text, r.model, r.pronunciations),
17
47
  model_id: r.model,
18
48
  ...r.languageCode !== void 0 && { language_code: r.languageCode },
19
49
  ...r.seed !== void 0 && { seed: r.seed },
@@ -21,11 +51,52 @@ const buildBody = (r) => ({
21
51
  ...r.nextText !== void 0 && { next_text: r.nextText },
22
52
  ...r.voiceSettings !== void 0 && { voice_settings: wireVoiceSettings(r.voiceSettings) }
23
53
  });
24
- const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.elevenlabs.io/v1";
54
+ const DEFAULT_DIALOGUE_MODEL = "eleven_v3";
55
+ const buildDialogueBody = (r) => ({
56
+ inputs: r.turns.map((t) => ({
57
+ voice_id: t.voiceId,
58
+ text: applyPronunciations(t.text, r.model, r.pronunciations)
59
+ })),
60
+ model_id: r.model ?? DEFAULT_DIALOGUE_MODEL,
61
+ ...r.languageCode !== void 0 && { language_code: r.languageCode }
62
+ });
63
+ const buildDialogueHttpRequest = (cfg, r, path) => Effect.gen(function* () {
64
+ const format = r.outputFormat ?? defaultFormat;
65
+ const slug = yield* formatToOutputSlug(format);
66
+ const url = `${resolveHost(cfg)}/text-to-dialogue${path}?output_format=${slug}`;
67
+ return {
68
+ httpRequest: HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(buildDialogueBody(r))),
69
+ format
70
+ };
71
+ });
72
+ const synthesizeDialogueImpl = (cfg) => (request) => Effect.gen(function* () {
73
+ const client = yield* HttpClient.HttpClient;
74
+ const { httpRequest, format } = yield* buildDialogueHttpRequest(cfg, request, "");
75
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
76
+ if (response.status >= 400) {
77
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
78
+ return yield* Effect.fail(httpStatusError(response.status, text));
79
+ }
80
+ const bytes = yield* response.arrayBuffer.pipe(Effect.mapError(transportFailure));
81
+ return {
82
+ format,
83
+ bytes: new Uint8Array(bytes)
84
+ };
85
+ });
86
+ const streamSynthesizeDialogueImpl = (cfg) => (request) => Stream.unwrap(Effect.gen(function* () {
87
+ const client = yield* HttpClient.HttpClient;
88
+ const { httpRequest } = yield* buildDialogueHttpRequest(cfg, request, "/stream");
89
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
90
+ if (response.status >= 400) {
91
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
92
+ return Stream.fail(httpStatusError(response.status, text));
93
+ }
94
+ return response.stream.pipe(Stream.mapError(transportFailure), Stream.map((bytes) => ({ bytes })));
95
+ }));
25
96
  const buildHttpRequest = (cfg, r, path) => Effect.gen(function* () {
26
97
  const format = r.outputFormat ?? defaultFormat;
27
98
  const slug = yield* formatToOutputSlug(format);
28
- const url = `${baseUrl(cfg)}/text-to-speech/${r.voiceId}${path}?output_format=${slug}`;
99
+ const url = `${resolveHost(cfg)}/text-to-speech/${r.voiceId}${path}?output_format=${slug}`;
29
100
  return {
30
101
  httpRequest: HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(buildBody(r))),
31
102
  format
@@ -61,21 +132,27 @@ const make = (cfg) => Effect.gen(function* () {
61
132
  return {
62
133
  synthesize: (r) => synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
63
134
  streamSynthesis: (r) => streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),
64
- streamSynthesisFrom: (textIn, request) => streamSynthesis(cfg)(textIn, request).pipe(Stream.provideService(Socket.WebSocketConstructor, ctor))
135
+ streamSynthesisFrom: (textIn, request) => streamSynthesis(cfg)(textIn, request).pipe(Stream.provideService(Socket.WebSocketConstructor, ctor)),
136
+ synthesizeDialogue: (request) => synthesizeDialogueImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
137
+ streamSynthesizeDialogue: (request) => streamSynthesizeDialogueImpl(cfg)(request).pipe(Stream.provideService(HttpClient.HttpClient, client))
65
138
  };
66
139
  });
67
140
  /**
68
141
  * Layer registers `ElevenLabsSynthesizer`, the generic
69
- * `SpeechSynthesizer`, **and `TtsIncrementalText`** — incremental
70
- * text-in is wired to the realtime `/stream-input` WebSocket. Provide
71
- * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer at
72
- * the call site.
142
+ * `SpeechSynthesizer`, the `TtsIncrementalText` marker (incremental
143
+ * text-in via `/stream-input` WebSocket), **and `MultiSpeakerTts`** —
144
+ * multi-speaker dialogue is wired to `POST /v1/text-to-dialogue`
145
+ * (sync) and `/v1/text-to-dialogue/stream` (chunked). Provide
146
+ * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer
147
+ * at the call site.
73
148
  */
74
149
  const layer = (cfg) => Layer.mergeAll(Layer.effect(ElevenLabsSynthesizer, make(cfg)), Layer.effect(SpeechSynthesizer, Effect.map(make(cfg), (s) => ({
75
150
  synthesize: (req) => s.synthesize(req),
76
151
  streamSynthesis: (req) => s.streamSynthesis(req),
77
- streamSynthesisFrom: (textIn, req) => s.streamSynthesisFrom(textIn, req)
78
- }))), Layer.succeed(TtsIncrementalText, void 0));
152
+ streamSynthesisFrom: (textIn, req) => s.streamSynthesisFrom(textIn, req),
153
+ synthesizeDialogue: s.synthesizeDialogue,
154
+ streamSynthesizeDialogue: s.streamSynthesizeDialogue
155
+ }))), Layer.succeed(TtsIncrementalText, void 0), Layer.succeed(MultiSpeakerTts, void 0));
79
156
  //#endregion
80
157
  export { ElevenLabsSynthesizer, layer, make, ElevenLabsSynthesizer_exports as t };
81
158
 
@@ -1 +1 @@
1
- {"version":3,"file":"ElevenLabsSynthesizer.mjs","names":["realtimeStream"],"sources":["../src/ElevenLabsSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport type * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk } from \"@effect-uai/core/Audio\"\nimport {\n type CommonStreamSynthesizeRequest,\n type CommonSynthesizeRequest,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n TtsIncrementalText,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n defaultFormat,\n formatToOutputSlug,\n httpStatusError,\n transportFailure,\n type VoiceSettings,\n wireVoiceSettings,\n} from \"./codec.js\"\nimport type { ElevenLabsTtsModel, ElevenLabsVoiceId } from \"./models.js\"\nimport { streamSynthesis as realtimeStream } from \"./realtimeTts.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * ElevenLabs-typed synthesize request. `voiceSettings` exposes the\n * provider's prosody/timbre controls; `seed` makes generation\n * deterministic; `previousText` / `nextText` thread context across\n * sequential calls for natural prosody.\n */\nexport type ElevenLabsSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: ElevenLabsTtsModel\n readonly voiceId: ElevenLabsVoiceId\n readonly voiceSettings?: VoiceSettings\n readonly seed?: number\n readonly previousText?: string\n readonly nextText?: string\n}\n\nexport type ElevenLabsSynthesizerService = {\n readonly synthesize: (r: ElevenLabsSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: ElevenLabsSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n}\n\nexport class ElevenLabsSynthesizer extends Context.Service<\n ElevenLabsSynthesizer,\n ElevenLabsSynthesizerService\n>()(\"@betalyra/effect-uai/providers/elevenlabs/ElevenLabsSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Codec — request → JSON body\n// ---------------------------------------------------------------------------\n\nconst buildBody = (r: ElevenLabsSynthesizeRequest) => ({\n text: r.text,\n model_id: r.model,\n ...(r.languageCode !== undefined && { language_code: r.languageCode }),\n ...(r.seed !== undefined && { seed: r.seed }),\n ...(r.previousText !== undefined && { previous_text: r.previousText }),\n ...(r.nextText !== undefined && { next_text: r.nextText }),\n ...(r.voiceSettings !== undefined && { voice_settings: wireVoiceSettings(r.voiceSettings) }),\n})\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing (sync + chunked-HTTP streaming)\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://api.elevenlabs.io/v1\"\n\nconst buildHttpRequest = (cfg: Config, r: ElevenLabsSynthesizeRequest, path: \"\" | \"/stream\") =>\n Effect.gen(function* () {\n const format = r.outputFormat ?? defaultFormat\n const slug = yield* formatToOutputSlug(format)\n const url = `${baseUrl(cfg)}/text-to-speech/${r.voiceId}${path}?output_format=${slug}`\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(buildBody(r)),\n )\n return { httpRequest, format }\n })\n\nconst synthesizeImpl = (cfg: Config) => (request: ElevenLabsSynthesizeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { httpRequest, format } = yield* buildHttpRequest(cfg, request, \"\")\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const bytes = yield* response.arrayBuffer.pipe(Effect.mapError(transportFailure))\n return { format, bytes: new Uint8Array(bytes) } satisfies AudioBlob\n })\n\nconst streamSynthesisImpl = (cfg: Config) => (request: ElevenLabsSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { httpRequest } = yield* buildHttpRequest(cfg, request, \"/stream\")\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n return response.stream.pipe(\n Stream.mapError(transportFailure),\n Stream.map((bytes): AudioChunk => ({ bytes })),\n )\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const ctor = yield* Socket.WebSocketConstructor\n return {\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),\n streamSynthesisFrom: (textIn, request) =>\n realtimeStream(cfg)(textIn, request as ElevenLabsSynthesizeRequest).pipe(\n Stream.provideService(Socket.WebSocketConstructor, ctor),\n ),\n } satisfies ElevenLabsSynthesizerService\n })\n\n/**\n * Layer registers `ElevenLabsSynthesizer`, the generic\n * `SpeechSynthesizer`, **and `TtsIncrementalText`** — incremental\n * text-in is wired to the realtime `/stream-input` WebSocket. Provide\n * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer at\n * the call site.\n */\nexport const layer = (cfg: Config) =>\n Layer.mergeAll(\n Layer.effect(ElevenLabsSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as ElevenLabsSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as ElevenLabsSynthesizeRequest),\n streamSynthesisFrom: (textIn, req: CommonStreamSynthesizeRequest) =>\n s.streamSynthesisFrom(textIn, req),\n }),\n ),\n ),\n Layer.succeed(TtsIncrementalText, undefined),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"ElevenLabsSynthesizer.mjs","names":["Arr","realtimeStream"],"sources":["../src/ElevenLabsSynthesizer.ts"],"sourcesContent":["import { Array as Arr, Context, Effect, Layer, Redacted, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport type * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk } from \"@effect-uai/core/Audio\"\nimport {\n type CommonStreamSynthesizeRequest,\n type CommonSynthesizeDialogueRequest,\n type CommonSynthesizeRequest,\n type CustomPronunciation,\n MultiSpeakerTts,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n TtsIncrementalText,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n defaultFormat,\n formatToOutputSlug,\n httpStatusError,\n transportFailure,\n type VoiceSettings,\n wireVoiceSettings,\n} from \"./codec.js\"\nimport type { ElevenLabsTtsModel, ElevenLabsVoiceId } from \"./models.js\"\nimport { streamSynthesis as realtimeStream } from \"./realtimeTts.js\"\nimport { type ElevenLabsRegion, resolveHost } from \"./region.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * ElevenLabs-typed synthesize request. `voiceSettings` exposes the\n * provider's prosody/timbre controls; `seed` makes generation\n * deterministic; `previousText` / `nextText` thread context across\n * sequential calls for natural prosody.\n */\nexport type ElevenLabsSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: ElevenLabsTtsModel\n readonly voiceId: ElevenLabsVoiceId\n readonly voiceSettings?: VoiceSettings\n readonly seed?: number\n readonly previousText?: string\n readonly nextText?: string\n}\n\nexport type ElevenLabsSynthesizerService = {\n readonly synthesize: (r: ElevenLabsSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: ElevenLabsSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n readonly synthesizeDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"]\n readonly streamSynthesizeDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"]\n}\n\nexport class ElevenLabsSynthesizer extends Context.Service<\n ElevenLabsSynthesizer,\n ElevenLabsSynthesizerService\n>()(\"@betalyra/effect-uai/providers/elevenlabs/ElevenLabsSynthesizer\") {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n readonly region?: ElevenLabsRegion\n}\n\n// ---------------------------------------------------------------------------\n// Pronunciations — SSML <phoneme> rewrite (gated to legacy models)\n// ---------------------------------------------------------------------------\n\n/**\n * Models that honor inline `<phoneme>` SSML tags. Other models silently\n * drop the tags server-side, so we don't bother rewriting the text for\n * them — the pronunciation overrides are silently ignored.\n *\n * Reference: https://elevenlabs.io/docs/best-practices/prompting/controls\n */\nconst PHONEME_SUPPORTED_MODELS: ReadonlySet<string> = new Set([\n \"eleven_flash_v2\",\n \"eleven_english_v1\",\n \"eleven_monolingual_v1\",\n])\n\nconst wireAlphabet = (encoding: CustomPronunciation[\"encoding\"]): string | undefined =>\n encoding === \"ipa\" ? \"ipa\" : encoding === \"cmu-arpabet\" ? \"cmu-arpabet\" : undefined\n\nconst phonemeTag = (phrase: string, pronunciation: string, alphabet: string): string =>\n `<phoneme alphabet=\"${alphabet}\" ph=\"${pronunciation}\">${phrase}</phoneme>`\n\nconst escapeForRegex = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\n/**\n * Apply pronunciation overrides to a text string. Only applies for\n * phoneme-supported models; only IPA and CMU Arpabet are honored\n * (x-sampa is silently dropped). Whole-word, case-insensitive\n * replacement.\n */\nconst applyPronunciations = (\n text: string,\n model: string,\n pronunciations: ReadonlyArray<CustomPronunciation> | undefined,\n): string => {\n if (pronunciations === undefined || !PHONEME_SUPPORTED_MODELS.has(model)) return text\n return Arr.reduce(pronunciations, text, (acc, p) => {\n const alphabet = wireAlphabet(p.encoding)\n if (alphabet === undefined) return acc\n return acc.replace(\n new RegExp(`\\\\b${escapeForRegex(p.phrase)}\\\\b`, \"i\"),\n phonemeTag(p.phrase, p.pronunciation, alphabet),\n )\n })\n}\n\n// ---------------------------------------------------------------------------\n// Codec — request → JSON body\n// ---------------------------------------------------------------------------\n\nconst buildBody = (r: ElevenLabsSynthesizeRequest) => ({\n text: applyPronunciations(r.text, r.model, r.pronunciations),\n model_id: r.model,\n ...(r.languageCode !== undefined && { language_code: r.languageCode }),\n ...(r.seed !== undefined && { seed: r.seed }),\n ...(r.previousText !== undefined && { previous_text: r.previousText }),\n ...(r.nextText !== undefined && { next_text: r.nextText }),\n ...(r.voiceSettings !== undefined && { voice_settings: wireVoiceSettings(r.voiceSettings) }),\n})\n\n// ---------------------------------------------------------------------------\n// Dialogue — POST /v1/text-to-dialogue (+ /stream variant)\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_DIALOGUE_MODEL = \"eleven_v3\"\n\nconst buildDialogueBody = (r: CommonSynthesizeDialogueRequest) => ({\n inputs: r.turns.map((t) => ({\n voice_id: t.voiceId,\n text: applyPronunciations(t.text, r.model, r.pronunciations),\n })),\n model_id: r.model ?? DEFAULT_DIALOGUE_MODEL,\n ...(r.languageCode !== undefined && { language_code: r.languageCode }),\n})\n\nconst buildDialogueHttpRequest = (\n cfg: Config,\n r: CommonSynthesizeDialogueRequest,\n path: \"\" | \"/stream\",\n) =>\n Effect.gen(function* () {\n const format = r.outputFormat ?? defaultFormat\n const slug = yield* formatToOutputSlug(format)\n const url = `${resolveHost(cfg)}/text-to-dialogue${path}?output_format=${slug}`\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(buildDialogueBody(r)),\n )\n return { httpRequest, format }\n })\n\nconst synthesizeDialogueImpl = (cfg: Config) => (request: CommonSynthesizeDialogueRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { httpRequest, format } = yield* buildDialogueHttpRequest(cfg, request, \"\")\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const bytes = yield* response.arrayBuffer.pipe(Effect.mapError(transportFailure))\n return { format, bytes: new Uint8Array(bytes) } satisfies AudioBlob\n })\n\nconst streamSynthesizeDialogueImpl = (cfg: Config) => (request: CommonSynthesizeDialogueRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { httpRequest } = yield* buildDialogueHttpRequest(cfg, request, \"/stream\")\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n return response.stream.pipe(\n Stream.mapError(transportFailure),\n Stream.map((bytes): AudioChunk => ({ bytes })),\n )\n }),\n )\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing (sync + chunked-HTTP streaming)\n// ---------------------------------------------------------------------------\n\nconst buildHttpRequest = (cfg: Config, r: ElevenLabsSynthesizeRequest, path: \"\" | \"/stream\") =>\n Effect.gen(function* () {\n const format = r.outputFormat ?? defaultFormat\n const slug = yield* formatToOutputSlug(format)\n const url = `${resolveHost(cfg)}/text-to-speech/${r.voiceId}${path}?output_format=${slug}`\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(buildBody(r)),\n )\n return { httpRequest, format }\n })\n\nconst synthesizeImpl = (cfg: Config) => (request: ElevenLabsSynthesizeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { httpRequest, format } = yield* buildHttpRequest(cfg, request, \"\")\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const bytes = yield* response.arrayBuffer.pipe(Effect.mapError(transportFailure))\n return { format, bytes: new Uint8Array(bytes) } satisfies AudioBlob\n })\n\nconst streamSynthesisImpl = (cfg: Config) => (request: ElevenLabsSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { httpRequest } = yield* buildHttpRequest(cfg, request, \"/stream\")\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n return response.stream.pipe(\n Stream.mapError(transportFailure),\n Stream.map((bytes): AudioChunk => ({ bytes })),\n )\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const ctor = yield* Socket.WebSocketConstructor\n return {\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),\n streamSynthesisFrom: (textIn, request) =>\n realtimeStream(cfg)(textIn, request as ElevenLabsSynthesizeRequest).pipe(\n Stream.provideService(Socket.WebSocketConstructor, ctor),\n ),\n synthesizeDialogue: (request) =>\n synthesizeDialogueImpl(cfg)(request).pipe(\n Effect.provideService(HttpClient.HttpClient, client),\n ),\n streamSynthesizeDialogue: (request) =>\n streamSynthesizeDialogueImpl(cfg)(request).pipe(\n Stream.provideService(HttpClient.HttpClient, client),\n ),\n } satisfies ElevenLabsSynthesizerService\n })\n\n/**\n * Layer registers `ElevenLabsSynthesizer`, the generic\n * `SpeechSynthesizer`, the `TtsIncrementalText` marker (incremental\n * text-in via `/stream-input` WebSocket), **and `MultiSpeakerTts`** —\n * multi-speaker dialogue is wired to `POST /v1/text-to-dialogue`\n * (sync) and `/v1/text-to-dialogue/stream` (chunked). Provide\n * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer\n * at the call site.\n */\nexport const layer = (cfg: Config) =>\n Layer.mergeAll(\n Layer.effect(ElevenLabsSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as ElevenLabsSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as ElevenLabsSynthesizeRequest),\n streamSynthesisFrom: (textIn, req: CommonStreamSynthesizeRequest) =>\n s.streamSynthesisFrom(textIn, req),\n synthesizeDialogue: s.synthesizeDialogue,\n streamSynthesizeDialogue: s.streamSynthesizeDialogue,\n }),\n ),\n ),\n Layer.succeed(TtsIncrementalText, undefined),\n Layer.succeed(MultiSpeakerTts, undefined),\n )\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { ElevenLabsSttModel } from "./models.mjs";
2
+ import { ElevenLabsRegion } from "./region.mjs";
2
3
  import { Context, Effect, Layer, Redacted, Stream } from "effect";
3
4
  import { HttpClient } from "effect/unstable/http";
4
5
  import * as Socket from "effect/unstable/socket/Socket";
@@ -32,10 +33,11 @@ declare class ElevenLabsTranscriber extends ElevenLabsTranscriber_base {}
32
33
  type Config = {
33
34
  readonly apiKey: Redacted.Redacted;
34
35
  readonly baseUrl?: string;
36
+ readonly region?: ElevenLabsRegion;
35
37
  };
36
38
  declare const make: (cfg: Config) => Effect.Effect<{
37
- transcribe: (r: ElevenLabsTranscribeRequest) => Effect.Effect<any, AiError.AiError, never>;
38
- streamTranscriptionFrom: (audioIn: any, request: any) => Stream.Stream<TranscriptEvent, unknown, unknown>;
39
+ transcribe: (r: ElevenLabsTranscribeRequest) => Effect.Effect<any, any, never>;
40
+ streamTranscriptionFrom: (audioIn: any, request: any) => Stream.Stream<TranscriptEvent, any, unknown>;
39
41
  }, never, Socket.WebSocketConstructor | HttpClient.HttpClient>;
40
42
  /**
41
43
  * Layer registers `ElevenLabsTranscriber`, the generic `Transcriber`,
@@ -1 +1 @@
1
- {"version":3,"file":"ElevenLabsTranscriber.d.mts","names":[],"sources":["../src/ElevenLabsTranscriber.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KA0BY,2BAAA,GAA8B,IAAA,CAAK,uBAAA;EAAA,SACpC,KAAA,EAAO,kBAAA;EAAA,SACP,WAAA;EAAA,SACA,WAAA;EAAA,SACA,QAAA;AAAA;AAAA,KAGC,4BAAA;EAAA,SACD,UAAA,GACP,CAAA,EAAG,2BAAA,KACA,MAAA,CAAO,MAAA,CAAO,gBAAA,EAAkB,OAAA,CAAQ,OAAA;EAAA,SACpC,uBAAA,EAAyB,kBAAA;AAAA;AAAA,cACnC,0BAAA;cAEY,qBAAA,SAA8B,0BAAA;AAAA,KAK/B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAgGvD,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA;;;;AA5GhC;;;;;;;AAAA,cAkIa,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,iBAAA,MAAA,CAAA,oBAAA,GAAA,UAAA,CAAA,UAAA"}
1
+ {"version":3,"file":"ElevenLabsTranscriber.d.mts","names":[],"sources":["../src/ElevenLabsTranscriber.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;KA2BY,2BAAA,GAA8B,IAAA,CAAK,uBAAA;EAAA,SACpC,KAAA,EAAO,kBAAA;EAAA,SACP,WAAA;EAAA,SACA,WAAA;EAAA,SACA,QAAA;AAAA;AAAA,KAGC,4BAAA;EAAA,SACD,UAAA,GACP,CAAA,EAAG,2BAAA,KACA,MAAA,CAAO,MAAA,CAAO,gBAAA,EAAkB,OAAA,CAAQ,OAAA;EAAA,SACpC,uBAAA,EAAyB,kBAAA;AAAA;AAAA,cACnC,0BAAA;cAEY,qBAAA,SAA8B,0BAAA;AAAA,KAK/B,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,gBAAA;AAAA;AAAA,cA+FP,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA;;;;;;;;;;;cAsBnB,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,iBAAA,MAAA,CAAA,oBAAA,GAAA,UAAA,CAAA,UAAA"}
@@ -1,5 +1,6 @@
1
1
  import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
2
2
  import { audioToBlob, defaultFileName, httpStatusError, transportFailure } from "./codec.mjs";
3
+ import { resolveHost } from "./region.mjs";
3
4
  import { streamTranscription } from "./realtimeStt.mjs";
4
5
  import { Context, Effect, Layer, Redacted, Schema, Stream } from "effect";
5
6
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
@@ -49,11 +50,10 @@ const buildForm = (request) => Effect.gen(function* () {
49
50
  if (request.audioEvents !== void 0) form.set("tag_audio_events", request.audioEvents ? "true" : "false");
50
51
  return form;
51
52
  });
52
- const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.elevenlabs.io/v1";
53
53
  const transcribeImpl = (cfg) => (request) => Effect.gen(function* () {
54
54
  const client = yield* HttpClient.HttpClient;
55
55
  const form = yield* buildForm(request);
56
- const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/speech-to-text`).pipe(HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyFormData(form));
56
+ const httpRequest = HttpClientRequest.post(`${resolveHost(cfg)}/speech-to-text`).pipe(HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyFormData(form));
57
57
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
58
58
  if (response.status >= 400) {
59
59
  const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
@@ -1 +1 @@
1
- {"version":3,"file":"ElevenLabsTranscriber.mjs","names":[],"sources":["../src/ElevenLabsTranscriber.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { TranscriptResult, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport {\n type CommonTranscribeRequest,\n SttStreaming,\n Transcriber,\n type TranscriberService,\n} from \"@effect-uai/core/Transcriber\"\nimport { audioToBlob, defaultFileName, httpStatusError, transportFailure } from \"./codec.js\"\nimport type { ElevenLabsSttModel } from \"./models.js\"\nimport { streamTranscription } from \"./realtimeStt.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * ElevenLabs-typed transcribe request. `model` narrows to\n * `ElevenLabsSttModel`. `numSpeakers` hints diarization. `audioEvents`\n * surfaces `(laughter)` / `(music)` tags inline in `text` (provider\n * default: true). `fileName` overrides the multipart upload's\n * `filename`; ElevenLabs uses the extension to detect format.\n */\nexport type ElevenLabsTranscribeRequest = Omit<CommonTranscribeRequest, \"model\"> & {\n readonly model: ElevenLabsSttModel\n readonly numSpeakers?: number\n readonly audioEvents?: boolean\n readonly fileName?: string\n}\n\nexport type ElevenLabsTranscriberService = {\n readonly transcribe: (\n r: ElevenLabsTranscribeRequest,\n ) => Effect.Effect<TranscriptResult, AiError.AiError>\n readonly streamTranscriptionFrom: TranscriberService[\"streamTranscriptionFrom\"]\n}\n\nexport class ElevenLabsTranscriber extends Context.Service<\n ElevenLabsTranscriber,\n ElevenLabsTranscriberService\n>()(\"@betalyra/effect-uai/providers/elevenlabs/ElevenLabsTranscriber\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Response schema\n// ---------------------------------------------------------------------------\n\nconst Word = Schema.Struct({\n text: Schema.String,\n start: Schema.optional(Schema.Number),\n end: Schema.optional(Schema.Number),\n type: Schema.optional(Schema.String),\n speaker_id: Schema.optional(Schema.String),\n logprob: Schema.optional(Schema.Number),\n})\n\nconst Wire = Schema.Struct({\n text: Schema.String,\n language_code: Schema.optional(Schema.String),\n language_probability: Schema.optional(Schema.Number),\n words: Schema.optional(Schema.Array(Word)),\n transcription_id: Schema.optional(Schema.String),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst toWordTimestamp = (w: typeof Word.Type): WordTimestamp | undefined =>\n w.start === undefined || w.end === undefined\n ? undefined\n : {\n text: w.text,\n startSeconds: w.start,\n endSeconds: w.end,\n ...(w.logprob !== undefined && { confidence: Math.exp(w.logprob) }),\n ...(w.speaker_id !== undefined && { speakerId: w.speaker_id }),\n }\n\n// ---------------------------------------------------------------------------\n// Multipart body\n// ---------------------------------------------------------------------------\n\nconst buildForm = (request: ElevenLabsTranscribeRequest) =>\n Effect.gen(function* () {\n const blob = yield* audioToBlob(request.audio)\n const fileName = request.fileName ?? defaultFileName(request.audio.mimeType ?? \"\")\n const form = new FormData()\n form.set(\"model_id\", request.model)\n form.set(\"file\", blob, fileName)\n if (request.language !== undefined) form.set(\"language_code\", request.language)\n if (request.diarization !== undefined)\n form.set(\"diarize\", request.diarization ? \"true\" : \"false\")\n if (request.wordTimestamps === true) form.set(\"timestamps_granularity\", \"word\")\n if (request.numSpeakers !== undefined) form.set(\"num_speakers\", String(request.numSpeakers))\n if (request.audioEvents !== undefined)\n form.set(\"tag_audio_events\", request.audioEvents ? \"true\" : \"false\")\n return form\n })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://api.elevenlabs.io/v1\"\n\nconst transcribeImpl = (cfg: Config) => (request: ElevenLabsTranscribeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const form = yield* buildForm(request)\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/speech-to-text`).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyFormData(form),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeWire(json).pipe(\n Effect.mapError(\n (cause) => new AiError.GenerationFailed({ provider: \"elevenlabs\", raw: cause }),\n ),\n )\n const words = wire.words\n ?.map(toWordTimestamp)\n .filter((w): w is WordTimestamp => w !== undefined)\n return {\n text: wire.text,\n ...(wire.language_code !== undefined && { languageCode: wire.language_code }),\n ...(words !== undefined && words.length > 0 && { words }),\n raw: json,\n } satisfies TranscriptResult\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const ctor = yield* Socket.WebSocketConstructor\n return {\n transcribe: (r) =>\n transcribeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamTranscriptionFrom: (audioIn, request) =>\n streamTranscription(cfg)(audioIn, request).pipe(\n Stream.provideService(HttpClient.HttpClient, client),\n Stream.provideService(Socket.WebSocketConstructor, ctor),\n ),\n } satisfies ElevenLabsTranscriberService\n })\n\n/**\n * Layer registers `ElevenLabsTranscriber`, the generic `Transcriber`,\n * **and the `SttStreaming` capability marker** — `streamTranscriptionFrom`\n * is wired to the realtime WebSocket. Provide\n * `Socket.layerWebSocketConstructorGlobal` (or a custom constructor) +\n * an `HttpClient` Layer at the call site.\n */\nexport const layer = (cfg: Config) =>\n Layer.mergeAll(\n Layer.effect(ElevenLabsTranscriber, make(cfg)),\n Layer.effect(\n Transcriber,\n Effect.map(\n make(cfg),\n (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) =>\n s.transcribe(req as ElevenLabsTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n }),\n ),\n ),\n Layer.succeed(SttStreaming, undefined),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"ElevenLabsTranscriber.mjs","names":[],"sources":["../src/ElevenLabsTranscriber.ts"],"sourcesContent":["import { Context, Effect, Layer, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { TranscriptResult, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport {\n type CommonTranscribeRequest,\n SttStreaming,\n Transcriber,\n type TranscriberService,\n} from \"@effect-uai/core/Transcriber\"\nimport { audioToBlob, defaultFileName, httpStatusError, transportFailure } from \"./codec.js\"\nimport type { ElevenLabsSttModel } from \"./models.js\"\nimport { streamTranscription } from \"./realtimeStt.js\"\nimport { type ElevenLabsRegion, resolveHost } from \"./region.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * ElevenLabs-typed transcribe request. `model` narrows to\n * `ElevenLabsSttModel`. `numSpeakers` hints diarization. `audioEvents`\n * surfaces `(laughter)` / `(music)` tags inline in `text` (provider\n * default: true). `fileName` overrides the multipart upload's\n * `filename`; ElevenLabs uses the extension to detect format.\n */\nexport type ElevenLabsTranscribeRequest = Omit<CommonTranscribeRequest, \"model\"> & {\n readonly model: ElevenLabsSttModel\n readonly numSpeakers?: number\n readonly audioEvents?: boolean\n readonly fileName?: string\n}\n\nexport type ElevenLabsTranscriberService = {\n readonly transcribe: (\n r: ElevenLabsTranscribeRequest,\n ) => Effect.Effect<TranscriptResult, AiError.AiError>\n readonly streamTranscriptionFrom: TranscriberService[\"streamTranscriptionFrom\"]\n}\n\nexport class ElevenLabsTranscriber extends Context.Service<\n ElevenLabsTranscriber,\n ElevenLabsTranscriberService\n>()(\"@betalyra/effect-uai/providers/elevenlabs/ElevenLabsTranscriber\") {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n readonly region?: ElevenLabsRegion\n}\n\n// ---------------------------------------------------------------------------\n// Response schema\n// ---------------------------------------------------------------------------\n\nconst Word = Schema.Struct({\n text: Schema.String,\n start: Schema.optional(Schema.Number),\n end: Schema.optional(Schema.Number),\n type: Schema.optional(Schema.String),\n speaker_id: Schema.optional(Schema.String),\n logprob: Schema.optional(Schema.Number),\n})\n\nconst Wire = Schema.Struct({\n text: Schema.String,\n language_code: Schema.optional(Schema.String),\n language_probability: Schema.optional(Schema.Number),\n words: Schema.optional(Schema.Array(Word)),\n transcription_id: Schema.optional(Schema.String),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst toWordTimestamp = (w: typeof Word.Type): WordTimestamp | undefined =>\n w.start === undefined || w.end === undefined\n ? undefined\n : {\n text: w.text,\n startSeconds: w.start,\n endSeconds: w.end,\n ...(w.logprob !== undefined && { confidence: Math.exp(w.logprob) }),\n ...(w.speaker_id !== undefined && { speakerId: w.speaker_id }),\n }\n\n// ---------------------------------------------------------------------------\n// Multipart body\n// ---------------------------------------------------------------------------\n\nconst buildForm = (request: ElevenLabsTranscribeRequest) =>\n Effect.gen(function* () {\n const blob = yield* audioToBlob(request.audio)\n const fileName = request.fileName ?? defaultFileName(request.audio.mimeType ?? \"\")\n const form = new FormData()\n form.set(\"model_id\", request.model)\n form.set(\"file\", blob, fileName)\n if (request.language !== undefined) form.set(\"language_code\", request.language)\n if (request.diarization !== undefined)\n form.set(\"diarize\", request.diarization ? \"true\" : \"false\")\n if (request.wordTimestamps === true) form.set(\"timestamps_granularity\", \"word\")\n if (request.numSpeakers !== undefined) form.set(\"num_speakers\", String(request.numSpeakers))\n if (request.audioEvents !== undefined)\n form.set(\"tag_audio_events\", request.audioEvents ? \"true\" : \"false\")\n return form\n })\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst transcribeImpl = (cfg: Config) => (request: ElevenLabsTranscribeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const form = yield* buildForm(request)\n const httpRequest = HttpClientRequest.post(`${resolveHost(cfg)}/speech-to-text`).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyFormData(form),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeWire(json).pipe(\n Effect.mapError(\n (cause) => new AiError.GenerationFailed({ provider: \"elevenlabs\", raw: cause }),\n ),\n )\n const words = wire.words\n ?.map(toWordTimestamp)\n .filter((w): w is WordTimestamp => w !== undefined)\n return {\n text: wire.text,\n ...(wire.language_code !== undefined && { languageCode: wire.language_code }),\n ...(words !== undefined && words.length > 0 && { words }),\n raw: json,\n } satisfies TranscriptResult\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const ctor = yield* Socket.WebSocketConstructor\n return {\n transcribe: (r) =>\n transcribeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamTranscriptionFrom: (audioIn, request) =>\n streamTranscription(cfg)(audioIn, request).pipe(\n Stream.provideService(HttpClient.HttpClient, client),\n Stream.provideService(Socket.WebSocketConstructor, ctor),\n ),\n } satisfies ElevenLabsTranscriberService\n })\n\n/**\n * Layer registers `ElevenLabsTranscriber`, the generic `Transcriber`,\n * **and the `SttStreaming` capability marker** — `streamTranscriptionFrom`\n * is wired to the realtime WebSocket. Provide\n * `Socket.layerWebSocketConstructorGlobal` (or a custom constructor) +\n * an `HttpClient` Layer at the call site.\n */\nexport const layer = (cfg: Config) =>\n Layer.mergeAll(\n Layer.effect(ElevenLabsTranscriber, make(cfg)),\n Layer.effect(\n Transcriber,\n Effect.map(\n make(cfg),\n (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) =>\n s.transcribe(req as ElevenLabsTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n }),\n ),\n ),\n Layer.succeed(SttStreaming, undefined),\n )\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as codec_d_exports } from "./codec.mjs";
2
2
  import { ElevenLabsSttModel, ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.mjs";
3
+ import { ElevenLabsRegion, resolveHost } from "./region.mjs";
3
4
  import { t as ElevenLabsSynthesizer_d_exports } from "./ElevenLabsSynthesizer.mjs";
4
5
  import { t as ElevenLabsTranscriber_d_exports } from "./ElevenLabsTranscriber.mjs";
5
- export { ElevenLabsSttModel, ElevenLabsSynthesizer_d_exports as ElevenLabsSynthesizer, ElevenLabsTranscriber_d_exports as ElevenLabsTranscriber, ElevenLabsTtsModel, ElevenLabsVoiceId, codec_d_exports as codec };
6
+ export { ElevenLabsRegion, ElevenLabsSttModel, ElevenLabsSynthesizer_d_exports as ElevenLabsSynthesizer, ElevenLabsTranscriber_d_exports as ElevenLabsTranscriber, ElevenLabsTtsModel, ElevenLabsVoiceId, codec_d_exports as codec, resolveHost };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as codec_exports } from "./codec.mjs";
2
+ import { resolveHost } from "./region.mjs";
2
3
  import { t as ElevenLabsSynthesizer_exports } from "./ElevenLabsSynthesizer.mjs";
3
4
  import { t as ElevenLabsTranscriber_exports } from "./ElevenLabsTranscriber.mjs";
4
5
  import "./models.mjs";
5
- export { ElevenLabsSynthesizer_exports as ElevenLabsSynthesizer, ElevenLabsTranscriber_exports as ElevenLabsTranscriber, codec_exports as codec };
6
+ export { ElevenLabsSynthesizer_exports as ElevenLabsSynthesizer, ElevenLabsTranscriber_exports as ElevenLabsTranscriber, codec_exports as codec, resolveHost };
@@ -1,3 +1,4 @@
1
+ import { ElevenLabsRegion } from "./region.mjs";
1
2
  import { Effect, Redacted, Schema, Stream } from "effect";
2
3
  import { HttpClient } from "effect/unstable/http";
3
4
  import * as Socket from "effect/unstable/socket/Socket";
@@ -10,9 +11,10 @@ import { TranscriptEvent } from "@effect-uai/core/Transcript";
10
11
  type Config = {
11
12
  readonly apiKey: Redacted.Redacted;
12
13
  readonly baseUrl?: string;
14
+ readonly region?: ElevenLabsRegion;
13
15
  };
14
16
  declare const inputFormatToSlug: (format: AudioFormat) => Effect.Effect<string, AiError.AiError>;
15
- declare const fetchSingleUseToken: (cfg: Config) => Effect.Effect<string, AiError.AiError, HttpClient.HttpClient>;
17
+ declare const fetchSingleUseToken: (cfg: Config) => Effect.Effect<string, any, HttpClient.HttpClient>;
16
18
  declare const ServerMessage: Schema.Union<readonly [Schema.Struct<{
17
19
  readonly message_type: Schema.Literal<"session_started">;
18
20
  readonly session_id: Schema.optional<Schema.String>;
@@ -1 +1 @@
1
- {"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;;;;KAUY,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAavD,iBAAA,GAAoB,MAAA,EAAQ,WAAA,KAAgB,MAAA,CAAO,MAAA,SAAe,OAAA,CAAQ,OAAA;AAAA,cAmB1E,mBAAA,GAAuB,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA,SAAA,OAAA,CAAA,OAAA,EAAA,UAAA,CAAA,UAAA;AAAA,cAsCzC,aAAA,EAAa,MAAA,CAAA,KAAA,WAAA,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgDN,WAAA,GACV,iBAAA,eACA,GAAA,SAAY,aAAA,CAAc,IAAA,KAAO,eAAA;AAAA,KAqCxB,eAAA;EAAA,SACD,KAAA;EAAA,SACA,YAAA;EAAA,SACA,iBAAA;EAAA,SACA,cAAA;AAAA;AAAA,cAGE,UAAA,GACX,GAAA,EAAK,MAAA,EACL,KAAA,UACA,WAAA,UACA,IAAA,EAAM,eAAA;AAAA,cAcK,gBAAA,GAAoB,KAAA,EAAO,UAAA,EAAY,UAAA;AAAA,cAqCvC,mBAAA,GACV,GAAA,EAAK,MAAA,YAEJ,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,GACtC,OAAA,EAAS,6BAAA,KACR,MAAA,CAAO,MAAA,CACR,eAAA,EACA,OAAA,CAAQ,OAAA,GAAU,CAAA,EAClB,CAAA,GAAI,UAAA,CAAW,UAAA,GAAa,MAAA,CAAO,oBAAA"}
1
+ {"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;;;;;KAWY,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,gBAAA;AAAA;AAAA,cAcP,iBAAA,GAAoB,MAAA,EAAQ,WAAA,KAAgB,MAAA,CAAO,MAAA,SAAe,OAAA,CAAQ,OAAA;AAAA,cAmB1E,mBAAA,GAAuB,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA,cAAA,UAAA,CAAA,UAAA;AAAA,cAsCzC,aAAA,EAAa,MAAA,CAAA,KAAA,WAAA,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgDN,WAAA,GACV,iBAAA,eACA,GAAA,SAAY,aAAA,CAAc,IAAA,KAAO,eAAA;AAAA,KAqCxB,eAAA;EAAA,SACD,KAAA;EAAA,SACA,YAAA;EAAA,SACA,iBAAA;EAAA,SACA,cAAA;AAAA;AAAA,cAGE,UAAA,GACX,GAAA,EAAK,MAAA,EACL,KAAA,UACA,WAAA,UACA,IAAA,EAAM,eAAA;AAAA,cAcK,gBAAA,GAAoB,KAAA,EAAO,UAAA,EAAY,UAAA;AAAA,cAqCvC,mBAAA,GACV,GAAA,EAAK,MAAA,YAEJ,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,GACtC,OAAA,EAAS,6BAAA,KACR,MAAA,CAAO,MAAA,CACR,eAAA,EACA,OAAA,CAAQ,OAAA,GAAU,CAAA,EAClB,CAAA,GAAI,UAAA,CAAW,UAAA,GAAa,MAAA,CAAO,oBAAA"}
@@ -1,4 +1,5 @@
1
1
  import { httpStatusError, transportFailure } from "./codec.mjs";
2
+ import { resolveHost } from "./region.mjs";
2
3
  import { Effect, Encoding, Match, Queue, Redacted, Schema, Stream } from "effect";
3
4
  import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
5
  import * as Socket from "effect/unstable/socket/Socket";
@@ -22,7 +23,7 @@ const TokenWire = Schema.Struct({ token: Schema.String });
22
23
  const decodeToken = Schema.decodeUnknownEffect(TokenWire);
23
24
  const fetchSingleUseToken = (cfg) => Effect.gen(function* () {
24
25
  const client = yield* HttpClient.HttpClient;
25
- const url = `${cfg.baseUrl ?? "https://api.elevenlabs.io/v1"}/single-use-token/realtime_scribe`;
26
+ const url = `${resolveHost(cfg)}/single-use-token/realtime_scribe`;
26
27
  const httpRequest = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)));
27
28
  const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
28
29
  if (response.status >= 400) {
@@ -118,7 +119,7 @@ const wireToEvent = (includeTimestamps) => (msg) => Match.value(msg).pipe(Match.
118
119
  message: typeof m.error === "string" ? m.error : JSON.stringify(m.error ?? `ElevenLabs ${m.message_type}`)
119
120
  })));
120
121
  const buildWsUrl = (cfg, token, audioFormat, opts) => {
121
- return `${(cfg.baseUrl ?? "https://api.elevenlabs.io/v1").replace(/^http/, "ws")}/speech-to-text/realtime?${new URLSearchParams({
122
+ return `${resolveHost(cfg).replace(/^http/, "ws")}/speech-to-text/realtime?${new URLSearchParams({
122
123
  token,
123
124
  audio_format: audioFormat,
124
125
  commit_strategy: opts.commitStrategy ?? "vad",
@@ -1 +1 @@
1
- {"version":3,"file":"realtimeStt.mjs","names":[],"sources":["../src/realtimeStt.ts"],"sourcesContent":["import { Cause, Effect, Encoding, Match, Queue, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport { httpStatusError, transportFailure } from \"./codec.js\"\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// AudioFormat → audio_format slug\n// ---------------------------------------------------------------------------\n\nconst unsupportedInputFormat = (format: AudioFormat) =>\n new AiError.Unsupported({\n provider: \"elevenlabs\",\n capability: \"inputFormat\",\n reason: `ElevenLabs realtime STT accepts pcm_<rate> or ulaw_8000 only. Got ${JSON.stringify(format)}.`,\n })\n\nexport const inputFormatToSlug: (format: AudioFormat) => Effect.Effect<string, AiError.AiError> =\n Match.type<AudioFormat>().pipe(\n Match.when({ container: \"raw\", encoding: \"pcm_s16le\" }, (f) =>\n Effect.succeed(`pcm_${f.sampleRate}`),\n ),\n Match.when({ container: \"raw\", encoding: \"pcm_mulaw\", sampleRate: 8000 }, () =>\n Effect.succeed(\"ulaw_8000\"),\n ),\n Match.orElse((f) => Effect.fail(unsupportedInputFormat(f))),\n )\n\n// ---------------------------------------------------------------------------\n// Single-use token — auth rides on a query param so any\n// `globalThis.WebSocket` works (browser, Node 22+, Bun, Deno).\n// ---------------------------------------------------------------------------\n\nconst TokenWire = Schema.Struct({ token: Schema.String })\nconst decodeToken = Schema.decodeUnknownEffect(TokenWire)\n\nexport const fetchSingleUseToken = (cfg: Config) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const url = `${cfg.baseUrl ?? \"https://api.elevenlabs.io/v1\"}/single-use-token/realtime_scribe`\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeToken(json).pipe(\n Effect.mapError(\n (cause) =>\n new AiError.GenerationFailed({\n provider: \"elevenlabs\",\n raw: { message: \"single-use-token response missing `token` field\", cause, json },\n }),\n ),\n )\n return wire.token\n })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst RealtimeWord = Schema.Struct({\n text: Schema.String,\n start: Schema.Number,\n end: Schema.Number,\n type: Schema.optional(Schema.String),\n speaker_id: Schema.optional(Schema.NullOr(Schema.String)),\n logprob: Schema.optional(Schema.Number),\n})\n\nconst ServerMessage = Schema.Union([\n Schema.Struct({\n message_type: Schema.Literal(\"session_started\"),\n session_id: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n message_type: Schema.Literal(\"partial_transcript\"),\n text: Schema.String,\n }),\n Schema.Struct({\n message_type: Schema.Literal(\"committed_transcript\"),\n text: Schema.String,\n }),\n Schema.Struct({\n message_type: Schema.Literal(\"committed_transcript_with_timestamps\"),\n text: Schema.String,\n language_code: Schema.optional(Schema.NullOr(Schema.String)),\n words: Schema.optional(Schema.NullOr(Schema.Array(RealtimeWord))),\n }),\n Schema.Struct({\n message_type: Schema.Literals([\"error\", \"auth_error\", \"rate_limited\", \"quota_exceeded\"]),\n error: Schema.optional(Schema.Unknown),\n }),\n])\nconst decodeServerMessage = Schema.decodeUnknownEffect(ServerMessage)\n\nconst wireToWordTimestamp = (w: typeof RealtimeWord.Type): WordTimestamp => ({\n text: w.text,\n startSeconds: w.start,\n endSeconds: w.end,\n ...(w.speaker_id != null && { speakerId: w.speaker_id }),\n ...(w.logprob !== undefined && { confidence: Math.exp(w.logprob) }),\n})\n\n/**\n * Map a server frame to a `TranscriptEvent`. The two `committed_*` variants\n * need disambiguation:\n *\n * - When `include_timestamps=true` on the URL, the server emits BOTH\n * `committed_transcript` and `committed_transcript_with_timestamps` for\n * each segment — same text, the latter is a strict superset. The bare\n * variant must be suppressed or callers see two finals per utterance.\n * - When `include_timestamps=false`, the server emits ONLY\n * `committed_transcript`. Suppressing it loses every final.\n *\n * So suppression must be conditional on `includeTimestamps`, which is what\n * `wordTimestamps` resolves to on the URL.\n */\nexport const wireToEvent =\n (includeTimestamps: boolean) =>\n (msg: typeof ServerMessage.Type): TranscriptEvent | undefined =>\n Match.value(msg).pipe(\n Match.when({ message_type: \"session_started\" }, () => undefined),\n Match.when(\n { message_type: \"partial_transcript\" },\n (m): TranscriptEvent => ({ _tag: \"partial\", text: m.text }),\n ),\n Match.when({ message_type: \"committed_transcript\" }, (m): TranscriptEvent | undefined =>\n includeTimestamps\n ? undefined // superseded by the `_with_timestamps` variant\n : { _tag: \"final\", text: m.text },\n ),\n Match.when({ message_type: \"committed_transcript_with_timestamps\" }, (m): TranscriptEvent => {\n const words = m.words == null ? undefined : m.words.map(wireToWordTimestamp)\n return {\n _tag: \"final\",\n text: m.text,\n ...(m.language_code != null && { languageCode: m.language_code }),\n ...(words !== undefined && words.length > 0 && { words }),\n }\n }),\n Match.orElse(\n (m): TranscriptEvent => ({\n _tag: \"error\",\n code: m.message_type,\n message:\n typeof m.error === \"string\"\n ? m.error\n : JSON.stringify(m.error ?? `ElevenLabs ${m.message_type}`),\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// URL + frame builders\n// ---------------------------------------------------------------------------\n\nexport type RealtimeOptions = {\n readonly model?: string\n readonly languageCode?: string\n readonly includeTimestamps?: boolean\n readonly commitStrategy?: \"manual\" | \"vad\"\n}\n\nexport const buildWsUrl = (\n cfg: Config,\n token: string,\n audioFormat: string,\n opts: RealtimeOptions,\n) => {\n const wsBase = (cfg.baseUrl ?? \"https://api.elevenlabs.io/v1\").replace(/^http/, \"ws\")\n const params = new URLSearchParams({\n token,\n audio_format: audioFormat,\n commit_strategy: opts.commitStrategy ?? \"vad\",\n ...(opts.model !== undefined && { model_id: opts.model }),\n ...(opts.languageCode !== undefined && { language_code: opts.languageCode }),\n ...(opts.includeTimestamps === true && { include_timestamps: \"true\" }),\n })\n return `${wsBase}/speech-to-text/realtime?${params.toString()}`\n}\n\nexport const encodeAudioFrame = (bytes: Uint8Array, sampleRate: number) =>\n JSON.stringify({\n message_type: \"input_audio_chunk\",\n audio_base_64: Encoding.encodeBase64(bytes),\n sample_rate: sampleRate,\n })\n\n// ---------------------------------------------------------------------------\n// Parse one server message and emit to the queue if it maps to a\n// TranscriptEvent. Invalid JSON / unknown shapes are silently dropped —\n// the connection stays open and we keep streaming subsequent messages.\n// ---------------------------------------------------------------------------\n\nconst handleServerMessage =\n (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n mapEvent: (msg: typeof ServerMessage.Type) => TranscriptEvent | undefined,\n ) =>\n (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerMessage(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n const event = mapEvent(decoded.value)\n if (event !== undefined) yield* Queue.offer(queue, event)\n })\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n//\n// Requires `HttpClient` (single-use token fetch) and\n// `Socket.WebSocketConstructor` (any `globalThis.WebSocket` works) in\n// context — `Socket.layerWebSocketConstructorGlobal` covers every\n// modern runtime.\n// ---------------------------------------------------------------------------\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<\n TranscriptEvent,\n AiError.AiError | E,\n R | HttpClient.HttpClient | Socket.WebSocketConstructor\n > =>\n Stream.unwrap(\n Effect.gen(function* () {\n const slug = yield* inputFormatToSlug(request.inputFormat)\n const token = yield* fetchSingleUseToken(cfg)\n const includeTimestamps = request.wordTimestamps === true\n const url = buildWsUrl(cfg, token, slug, {\n ...(request.model !== undefined && { model: request.model }),\n ...(request.language !== undefined && { languageCode: request.language }),\n includeTimestamps,\n })\n const socket = yield* Socket.makeWebSocket(url, {\n // Effect's Socket treats all close codes as errors by default — that\n // surfaces a clean server-side close as a stream failure. Whitelist\n // standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n })\n const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)\n const sampleRate = request.inputFormat.sampleRate\n\n const write = yield* socket.writer\n yield* Stream.runForEach(audioIn, (bytes) =>\n write(encodeAudioFrame(bytes, sampleRate)),\n ).pipe(Effect.ignore, Effect.forkScoped)\n\n // `Queue.end` flushes pending events then fails the next take with\n // `Done` — clean stream end. `Queue.shutdown` would CLEAR queued\n // items and interrupt pending takes (wrong for graceful teardown).\n yield* socket\n .runString(handleServerMessage(queue, wireToEvent(includeTimestamps)))\n .pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped)\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"realtimeStt.mjs","names":[],"sources":["../src/realtimeStt.ts"],"sourcesContent":["import { Cause, Effect, Encoding, Match, Queue, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport { httpStatusError, transportFailure } from \"./codec.js\"\nimport { type ElevenLabsRegion, resolveHost } from \"./region.js\"\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n readonly region?: ElevenLabsRegion\n}\n\n// ---------------------------------------------------------------------------\n// AudioFormat → audio_format slug\n// ---------------------------------------------------------------------------\n\nconst unsupportedInputFormat = (format: AudioFormat) =>\n new AiError.Unsupported({\n provider: \"elevenlabs\",\n capability: \"inputFormat\",\n reason: `ElevenLabs realtime STT accepts pcm_<rate> or ulaw_8000 only. Got ${JSON.stringify(format)}.`,\n })\n\nexport const inputFormatToSlug: (format: AudioFormat) => Effect.Effect<string, AiError.AiError> =\n Match.type<AudioFormat>().pipe(\n Match.when({ container: \"raw\", encoding: \"pcm_s16le\" }, (f) =>\n Effect.succeed(`pcm_${f.sampleRate}`),\n ),\n Match.when({ container: \"raw\", encoding: \"pcm_mulaw\", sampleRate: 8000 }, () =>\n Effect.succeed(\"ulaw_8000\"),\n ),\n Match.orElse((f) => Effect.fail(unsupportedInputFormat(f))),\n )\n\n// ---------------------------------------------------------------------------\n// Single-use token — auth rides on a query param so any\n// `globalThis.WebSocket` works (browser, Node 22+, Bun, Deno).\n// ---------------------------------------------------------------------------\n\nconst TokenWire = Schema.Struct({ token: Schema.String })\nconst decodeToken = Schema.decodeUnknownEffect(TokenWire)\n\nexport const fetchSingleUseToken = (cfg: Config) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const url = `${resolveHost(cfg)}/single-use-token/realtime_scribe`\n const httpRequest = HttpClientRequest.post(url).pipe(\n HttpClientRequest.setHeader(\"xi-api-key\", Redacted.value(cfg.apiKey)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeToken(json).pipe(\n Effect.mapError(\n (cause) =>\n new AiError.GenerationFailed({\n provider: \"elevenlabs\",\n raw: { message: \"single-use-token response missing `token` field\", cause, json },\n }),\n ),\n )\n return wire.token\n })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst RealtimeWord = Schema.Struct({\n text: Schema.String,\n start: Schema.Number,\n end: Schema.Number,\n type: Schema.optional(Schema.String),\n speaker_id: Schema.optional(Schema.NullOr(Schema.String)),\n logprob: Schema.optional(Schema.Number),\n})\n\nconst ServerMessage = Schema.Union([\n Schema.Struct({\n message_type: Schema.Literal(\"session_started\"),\n session_id: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n message_type: Schema.Literal(\"partial_transcript\"),\n text: Schema.String,\n }),\n Schema.Struct({\n message_type: Schema.Literal(\"committed_transcript\"),\n text: Schema.String,\n }),\n Schema.Struct({\n message_type: Schema.Literal(\"committed_transcript_with_timestamps\"),\n text: Schema.String,\n language_code: Schema.optional(Schema.NullOr(Schema.String)),\n words: Schema.optional(Schema.NullOr(Schema.Array(RealtimeWord))),\n }),\n Schema.Struct({\n message_type: Schema.Literals([\"error\", \"auth_error\", \"rate_limited\", \"quota_exceeded\"]),\n error: Schema.optional(Schema.Unknown),\n }),\n])\nconst decodeServerMessage = Schema.decodeUnknownEffect(ServerMessage)\n\nconst wireToWordTimestamp = (w: typeof RealtimeWord.Type): WordTimestamp => ({\n text: w.text,\n startSeconds: w.start,\n endSeconds: w.end,\n ...(w.speaker_id != null && { speakerId: w.speaker_id }),\n ...(w.logprob !== undefined && { confidence: Math.exp(w.logprob) }),\n})\n\n/**\n * Map a server frame to a `TranscriptEvent`. The two `committed_*` variants\n * need disambiguation:\n *\n * - When `include_timestamps=true` on the URL, the server emits BOTH\n * `committed_transcript` and `committed_transcript_with_timestamps` for\n * each segment — same text, the latter is a strict superset. The bare\n * variant must be suppressed or callers see two finals per utterance.\n * - When `include_timestamps=false`, the server emits ONLY\n * `committed_transcript`. Suppressing it loses every final.\n *\n * So suppression must be conditional on `includeTimestamps`, which is what\n * `wordTimestamps` resolves to on the URL.\n */\nexport const wireToEvent =\n (includeTimestamps: boolean) =>\n (msg: typeof ServerMessage.Type): TranscriptEvent | undefined =>\n Match.value(msg).pipe(\n Match.when({ message_type: \"session_started\" }, () => undefined),\n Match.when(\n { message_type: \"partial_transcript\" },\n (m): TranscriptEvent => ({ _tag: \"partial\", text: m.text }),\n ),\n Match.when({ message_type: \"committed_transcript\" }, (m): TranscriptEvent | undefined =>\n includeTimestamps\n ? undefined // superseded by the `_with_timestamps` variant\n : { _tag: \"final\", text: m.text },\n ),\n Match.when({ message_type: \"committed_transcript_with_timestamps\" }, (m): TranscriptEvent => {\n const words = m.words == null ? undefined : m.words.map(wireToWordTimestamp)\n return {\n _tag: \"final\",\n text: m.text,\n ...(m.language_code != null && { languageCode: m.language_code }),\n ...(words !== undefined && words.length > 0 && { words }),\n }\n }),\n Match.orElse(\n (m): TranscriptEvent => ({\n _tag: \"error\",\n code: m.message_type,\n message:\n typeof m.error === \"string\"\n ? m.error\n : JSON.stringify(m.error ?? `ElevenLabs ${m.message_type}`),\n }),\n ),\n )\n\n// ---------------------------------------------------------------------------\n// URL + frame builders\n// ---------------------------------------------------------------------------\n\nexport type RealtimeOptions = {\n readonly model?: string\n readonly languageCode?: string\n readonly includeTimestamps?: boolean\n readonly commitStrategy?: \"manual\" | \"vad\"\n}\n\nexport const buildWsUrl = (\n cfg: Config,\n token: string,\n audioFormat: string,\n opts: RealtimeOptions,\n) => {\n const wsBase = resolveHost(cfg).replace(/^http/, \"ws\")\n const params = new URLSearchParams({\n token,\n audio_format: audioFormat,\n commit_strategy: opts.commitStrategy ?? \"vad\",\n ...(opts.model !== undefined && { model_id: opts.model }),\n ...(opts.languageCode !== undefined && { language_code: opts.languageCode }),\n ...(opts.includeTimestamps === true && { include_timestamps: \"true\" }),\n })\n return `${wsBase}/speech-to-text/realtime?${params.toString()}`\n}\n\nexport const encodeAudioFrame = (bytes: Uint8Array, sampleRate: number) =>\n JSON.stringify({\n message_type: \"input_audio_chunk\",\n audio_base_64: Encoding.encodeBase64(bytes),\n sample_rate: sampleRate,\n })\n\n// ---------------------------------------------------------------------------\n// Parse one server message and emit to the queue if it maps to a\n// TranscriptEvent. Invalid JSON / unknown shapes are silently dropped —\n// the connection stays open and we keep streaming subsequent messages.\n// ---------------------------------------------------------------------------\n\nconst handleServerMessage =\n (\n queue: Queue.Queue<TranscriptEvent, Cause.Done>,\n mapEvent: (msg: typeof ServerMessage.Type) => TranscriptEvent | undefined,\n ) =>\n (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerMessage(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n const event = mapEvent(decoded.value)\n if (event !== undefined) yield* Queue.offer(queue, event)\n })\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n//\n// Requires `HttpClient` (single-use token fetch) and\n// `Socket.WebSocketConstructor` (any `globalThis.WebSocket` works) in\n// context — `Socket.layerWebSocketConstructorGlobal` covers every\n// modern runtime.\n// ---------------------------------------------------------------------------\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<\n TranscriptEvent,\n AiError.AiError | E,\n R | HttpClient.HttpClient | Socket.WebSocketConstructor\n > =>\n Stream.unwrap(\n Effect.gen(function* () {\n const slug = yield* inputFormatToSlug(request.inputFormat)\n const token = yield* fetchSingleUseToken(cfg)\n const includeTimestamps = request.wordTimestamps === true\n const url = buildWsUrl(cfg, token, slug, {\n ...(request.model !== undefined && { model: request.model }),\n ...(request.language !== undefined && { languageCode: request.language }),\n includeTimestamps,\n })\n const socket = yield* Socket.makeWebSocket(url, {\n // Effect's Socket treats all close codes as errors by default — that\n // surfaces a clean server-side close as a stream failure. Whitelist\n // standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n })\n const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)\n const sampleRate = request.inputFormat.sampleRate\n\n const write = yield* socket.writer\n yield* Stream.runForEach(audioIn, (bytes) =>\n write(encodeAudioFrame(bytes, sampleRate)),\n ).pipe(Effect.ignore, Effect.forkScoped)\n\n // `Queue.end` flushes pending events then fails the next take with\n // `Done` — clean stream end. `Queue.shutdown` would CLEAR queued\n // items and interrupt pending takes (wrong for graceful teardown).\n yield* socket\n .runString(handleServerMessage(queue, wireToEvent(includeTimestamps)))\n .pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped)\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { VoiceSettings } from "./codec.mjs";
2
2
  import { ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.mjs";
3
+ import { ElevenLabsRegion } from "./region.mjs";
3
4
  import { Redacted, Stream } from "effect";
4
5
  import * as Socket from "effect/unstable/socket/Socket";
5
6
  import * as AiError from "@effect-uai/core/AiError";
@@ -9,6 +10,7 @@ import { AudioChunk, AudioFormat } from "@effect-uai/core/Audio";
9
10
  type Config = {
10
11
  readonly apiKey: Redacted.Redacted;
11
12
  readonly baseUrl?: string;
13
+ readonly region?: ElevenLabsRegion;
12
14
  };
13
15
  /**
14
16
  * Incremental-text-in request for `/stream-input`. `voiceSettings`
@@ -1 +1 @@
1
- {"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;;;KAaY,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;;;;;;;KAUxD,uBAAA;EAAA,SACD,KAAA,GAAQ,kBAAA;EAAA,SACR,OAAA,EAAS,iBAAA;EAAA,SACT,YAAA,GAAe,WAAA;EAAA,SACf,YAAA;EAAA,SACA,aAAA,GAAgB,aAAA;EAAA,SAChB,QAAA;AAAA;AAAA,cAOE,UAAA,GAAc,GAAA,EAAK,MAAA,EAAQ,OAAA,EAAS,uBAAA,EAAyB,YAAA;AAAA,cAsE7D,eAAA,GACV,GAAA,EAAK,MAAA,YAEJ,MAAA,EAAQ,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,GACjC,OAAA,EAAS,uBAAA,KACR,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA,GAAU,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,oBAAA"}
1
+ {"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;;;;KAcY,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,gBAAA;AAAA;;;;;;;KAWR,uBAAA;EAAA,SACD,KAAA,GAAQ,kBAAA;EAAA,SACR,OAAA,EAAS,iBAAA;EAAA,SACT,YAAA,GAAe,WAAA;EAAA,SACf,YAAA;EAAA,SACA,aAAA,GAAgB,aAAA;EAAA,SAChB,QAAA;AAAA;AAAA,cAOE,UAAA,GAAc,GAAA,EAAK,MAAA,EAAQ,OAAA,EAAS,uBAAA,EAAyB,YAAA;AAAA,cAsE7D,eAAA,GACV,GAAA,EAAK,MAAA,YAEJ,MAAA,EAAQ,MAAA,CAAO,MAAA,SAAe,CAAA,EAAG,CAAA,GACjC,OAAA,EAAS,uBAAA,KACR,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA,GAAU,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,oBAAA"}
@@ -1,11 +1,12 @@
1
1
  import { defaultFormat, formatToOutputSlug, wireVoiceSettings } from "./codec.mjs";
2
+ import { resolveHost } from "./region.mjs";
2
3
  import { Effect, Encoding, Queue, Redacted, Result, Schema, Stream } from "effect";
3
4
  import * as Socket from "effect/unstable/socket/Socket";
4
5
  import * as AiError from "@effect-uai/core/AiError";
5
6
  import * as JSONL from "@effect-uai/core/JSONL";
6
7
  //#region src/realtimeTts.ts
7
8
  const buildWsUrl = (cfg, request, outputFormat) => {
8
- const wsBase = (cfg.baseUrl ?? "https://api.elevenlabs.io/v1").replace(/^http/, "ws");
9
+ const wsBase = resolveHost(cfg).replace(/^http/, "ws");
9
10
  const params = new URLSearchParams({
10
11
  output_format: outputFormat,
11
12
  auto_mode: String(request.autoMode ?? true),
@@ -1 +1 @@
1
- {"version":3,"file":"realtimeTts.mjs","names":[],"sources":["../src/realtimeTts.ts"],"sourcesContent":["import { Cause, Effect, Encoding, Queue, Redacted, Result, Schema, Stream } from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport {\n defaultFormat,\n formatToOutputSlug,\n type VoiceSettings,\n wireVoiceSettings,\n} from \"./codec.js\"\nimport type { ElevenLabsTtsModel, ElevenLabsVoiceId } from \"./models.js\"\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\nexport type { VoiceSettings } from \"./codec.js\"\n\n/**\n * Incremental-text-in request for `/stream-input`. `voiceSettings`\n * applies only on the BOS frame — mid-stream voice changes are\n * rejected. `autoMode: true` (default) lets the model pick flush\n * boundaries, which is what you want for LLM-token streams.\n */\nexport type StreamSynthesizeRequest = {\n readonly model?: ElevenLabsTtsModel\n readonly voiceId: ElevenLabsVoiceId\n readonly outputFormat?: AudioFormat\n readonly languageCode?: string\n readonly voiceSettings?: VoiceSettings\n readonly autoMode?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// URL + frame builders\n// ---------------------------------------------------------------------------\n\nexport const buildWsUrl = (cfg: Config, request: StreamSynthesizeRequest, outputFormat: string) => {\n const wsBase = (cfg.baseUrl ?? \"https://api.elevenlabs.io/v1\").replace(/^http/, \"ws\")\n const params = new URLSearchParams({\n output_format: outputFormat,\n auto_mode: String(request.autoMode ?? true),\n ...(request.model !== undefined && { model_id: request.model }),\n ...(request.languageCode !== undefined && { language_code: request.languageCode }),\n })\n return `${wsBase}/text-to-speech/${request.voiceId}/stream-input?${params.toString()}`\n}\n\nconst bosFrame = (cfg: Config, request: StreamSynthesizeRequest) => {\n const vs = wireVoiceSettings(request.voiceSettings)\n return JSON.stringify({\n text: \" \",\n \"xi-api-key\": Redacted.value(cfg.apiKey),\n ...(vs !== undefined && { voice_settings: vs }),\n })\n}\n\nconst textFrame = (text: string) => JSON.stringify({ text: text.endsWith(\" \") ? text : `${text} ` })\nconst eosFrame = JSON.stringify({ text: \"\" })\n\n// ---------------------------------------------------------------------------\n// Wire schema (server → client) + helpers\n// ---------------------------------------------------------------------------\n\nconst ServerFrame = Schema.Struct({\n audio: Schema.optional(Schema.NullOr(Schema.String)),\n isFinal: Schema.optional(Schema.NullOr(Schema.Boolean)),\n error: Schema.optional(Schema.Unknown),\n message: Schema.optional(Schema.String),\n})\nconst decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame)\n\nconst decodeAudio = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>\n Result.match(Encoding.decodeBase64(b64), {\n onSuccess: Effect.succeed,\n onFailure: (cause) =>\n Effect.fail(\n new AiError.GenerationFailed({\n provider: \"elevenlabs\",\n raw: { message: \"failed to decode audio frame\", cause },\n }),\n ),\n })\n\nconst handleServerFrame = (queue: Queue.Queue<AudioChunk, Cause.Done>) => (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerFrame(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n const frame = decoded.value\n if (frame.error !== undefined) {\n yield* Effect.logWarning(\"[elevenlabs-tts] server error frame\", {\n error: frame.error,\n message: frame.message,\n })\n return\n }\n if (frame.audio == null || frame.audio === \"\") return\n const bytes = yield* decodeAudio(frame.audio).pipe(Effect.option)\n if (bytes._tag === \"Some\") yield* Queue.offer(queue, { bytes: bytes.value })\n })\n\n// ---------------------------------------------------------------------------\n// Stream<string> → Stream<AudioChunk>. Requires `Socket.WebSocketConstructor`.\n// ---------------------------------------------------------------------------\n\nexport const streamSynthesis =\n (cfg: Config) =>\n <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: StreamSynthesizeRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R | Socket.WebSocketConstructor> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const slug = yield* formatToOutputSlug(request.outputFormat ?? defaultFormat)\n // ElevenLabs closes `/stream-input` with code 1000 after delivering the\n // final audio chunk. Effect's default treats all close codes as errors,\n // which would surface as a stream failure right after the last audio\n // arrives. Whitelist standard clean-close codes.\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request, slug), {\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n })\n const queue = yield* Queue.bounded<AudioChunk, Cause.Done>(64)\n const write = yield* socket.writer\n\n // Writer fiber: BOS → drain text stream → EOS. Socket-side failures\n // end this fiber; the reader still surfaces a clean stream end via\n // `Queue.end` (in the ensuring below) when the upstream WS closes.\n yield* Effect.gen(function* () {\n yield* write(bosFrame(cfg, request))\n yield* Stream.runForEach(textIn, (text) =>\n text.length === 0 ? Effect.void : write(textFrame(text)),\n )\n yield* write(eosFrame)\n }).pipe(Effect.ignore, Effect.forkScoped)\n\n // Reader fiber. `ensuring(Queue.end)` flushes pending chunks then\n // fails the next take with `Done`, which `Stream.fromQueue` treats\n // as a clean end. (`Queue.shutdown` would CLEAR queued items and\n // interrupt pending takes — wrong for graceful teardown.)\n yield* socket\n .runString(handleServerFrame(queue))\n .pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped)\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"realtimeTts.mjs","names":[],"sources":["../src/realtimeTts.ts"],"sourcesContent":["import { Cause, Effect, Encoding, Queue, Redacted, Result, Schema, Stream } from \"effect\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport {\n defaultFormat,\n formatToOutputSlug,\n type VoiceSettings,\n wireVoiceSettings,\n} from \"./codec.js\"\nimport type { ElevenLabsTtsModel, ElevenLabsVoiceId } from \"./models.js\"\nimport { type ElevenLabsRegion, resolveHost } from \"./region.js\"\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n readonly region?: ElevenLabsRegion\n}\n\nexport type { VoiceSettings } from \"./codec.js\"\n\n/**\n * Incremental-text-in request for `/stream-input`. `voiceSettings`\n * applies only on the BOS frame — mid-stream voice changes are\n * rejected. `autoMode: true` (default) lets the model pick flush\n * boundaries, which is what you want for LLM-token streams.\n */\nexport type StreamSynthesizeRequest = {\n readonly model?: ElevenLabsTtsModel\n readonly voiceId: ElevenLabsVoiceId\n readonly outputFormat?: AudioFormat\n readonly languageCode?: string\n readonly voiceSettings?: VoiceSettings\n readonly autoMode?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// URL + frame builders\n// ---------------------------------------------------------------------------\n\nexport const buildWsUrl = (cfg: Config, request: StreamSynthesizeRequest, outputFormat: string) => {\n const wsBase = resolveHost(cfg).replace(/^http/, \"ws\")\n const params = new URLSearchParams({\n output_format: outputFormat,\n auto_mode: String(request.autoMode ?? true),\n ...(request.model !== undefined && { model_id: request.model }),\n ...(request.languageCode !== undefined && { language_code: request.languageCode }),\n })\n return `${wsBase}/text-to-speech/${request.voiceId}/stream-input?${params.toString()}`\n}\n\nconst bosFrame = (cfg: Config, request: StreamSynthesizeRequest) => {\n const vs = wireVoiceSettings(request.voiceSettings)\n return JSON.stringify({\n text: \" \",\n \"xi-api-key\": Redacted.value(cfg.apiKey),\n ...(vs !== undefined && { voice_settings: vs }),\n })\n}\n\nconst textFrame = (text: string) => JSON.stringify({ text: text.endsWith(\" \") ? text : `${text} ` })\nconst eosFrame = JSON.stringify({ text: \"\" })\n\n// ---------------------------------------------------------------------------\n// Wire schema (server → client) + helpers\n// ---------------------------------------------------------------------------\n\nconst ServerFrame = Schema.Struct({\n audio: Schema.optional(Schema.NullOr(Schema.String)),\n isFinal: Schema.optional(Schema.NullOr(Schema.Boolean)),\n error: Schema.optional(Schema.Unknown),\n message: Schema.optional(Schema.String),\n})\nconst decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame)\n\nconst decodeAudio = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>\n Result.match(Encoding.decodeBase64(b64), {\n onSuccess: Effect.succeed,\n onFailure: (cause) =>\n Effect.fail(\n new AiError.GenerationFailed({\n provider: \"elevenlabs\",\n raw: { message: \"failed to decode audio frame\", cause },\n }),\n ),\n })\n\nconst handleServerFrame = (queue: Queue.Queue<AudioChunk, Cause.Done>) => (raw: string) =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return\n const decoded = yield* decodeServerFrame(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return\n const frame = decoded.value\n if (frame.error !== undefined) {\n yield* Effect.logWarning(\"[elevenlabs-tts] server error frame\", {\n error: frame.error,\n message: frame.message,\n })\n return\n }\n if (frame.audio == null || frame.audio === \"\") return\n const bytes = yield* decodeAudio(frame.audio).pipe(Effect.option)\n if (bytes._tag === \"Some\") yield* Queue.offer(queue, { bytes: bytes.value })\n })\n\n// ---------------------------------------------------------------------------\n// Stream<string> → Stream<AudioChunk>. Requires `Socket.WebSocketConstructor`.\n// ---------------------------------------------------------------------------\n\nexport const streamSynthesis =\n (cfg: Config) =>\n <E, R>(\n textIn: Stream.Stream<string, E, R>,\n request: StreamSynthesizeRequest,\n ): Stream.Stream<AudioChunk, AiError.AiError | E, R | Socket.WebSocketConstructor> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const slug = yield* formatToOutputSlug(request.outputFormat ?? defaultFormat)\n // ElevenLabs closes `/stream-input` with code 1000 after delivering the\n // final audio chunk. Effect's default treats all close codes as errors,\n // which would surface as a stream failure right after the last audio\n // arrives. Whitelist standard clean-close codes.\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request, slug), {\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n })\n const queue = yield* Queue.bounded<AudioChunk, Cause.Done>(64)\n const write = yield* socket.writer\n\n // Writer fiber: BOS → drain text stream → EOS. Socket-side failures\n // end this fiber; the reader still surfaces a clean stream end via\n // `Queue.end` (in the ensuring below) when the upstream WS closes.\n yield* Effect.gen(function* () {\n yield* write(bosFrame(cfg, request))\n yield* Stream.runForEach(textIn, (text) =>\n text.length === 0 ? Effect.void : write(textFrame(text)),\n )\n yield* write(eosFrame)\n }).pipe(Effect.ignore, Effect.forkScoped)\n\n // Reader fiber. `ensuring(Queue.end)` flushes pending chunks then\n // fails the next take with `Done`, which `Stream.fromQueue` treats\n // as a clean end. (`Queue.shutdown` would CLEAR queued items and\n // interrupt pending takes — wrong for graceful teardown.)\n yield* socket\n .runString(handleServerFrame(queue))\n .pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped)\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":";;;;;;;AAyCA,MAAa,cAAc,KAAa,SAAkC,iBAAyB;CACjG,MAAM,SAAS,YAAY,IAAI,CAAC,QAAQ,SAAS,KAAK;CACtD,MAAM,SAAS,IAAI,gBAAgB;EACjC,eAAe;EACf,WAAW,OAAO,QAAQ,YAAY,KAAK;EAC3C,GAAI,QAAQ,UAAU,KAAA,KAAa,EAAE,UAAU,QAAQ,OAAO;EAC9D,GAAI,QAAQ,iBAAiB,KAAA,KAAa,EAAE,eAAe,QAAQ,cAAc;EAClF,CAAC;AACF,QAAO,GAAG,OAAO,kBAAkB,QAAQ,QAAQ,gBAAgB,OAAO,UAAU;;AAGtF,MAAM,YAAY,KAAa,YAAqC;CAClE,MAAM,KAAK,kBAAkB,QAAQ,cAAc;AACnD,QAAO,KAAK,UAAU;EACpB,MAAM;EACN,cAAc,SAAS,MAAM,IAAI,OAAO;EACxC,GAAI,OAAO,KAAA,KAAa,EAAE,gBAAgB,IAAI;EAC/C,CAAC;;AAGJ,MAAM,aAAa,SAAiB,KAAK,UAAU,EAAE,MAAM,KAAK,SAAS,IAAI,GAAG,OAAO,GAAG,KAAK,IAAI,CAAC;AACpG,MAAM,WAAW,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AAM7C,MAAM,cAAc,OAAO,OAAO;CAChC,OAAO,OAAO,SAAS,OAAO,OAAO,OAAO,OAAO,CAAC;CACpD,SAAS,OAAO,SAAS,OAAO,OAAO,OAAO,QAAQ,CAAC;CACvD,OAAO,OAAO,SAAS,OAAO,QAAQ;CACtC,SAAS,OAAO,SAAS,OAAO,OAAO;CACxC,CAAC;AACF,MAAM,oBAAoB,OAAO,oBAAoB,YAAY;AAEjE,MAAM,eAAe,QACnB,OAAO,MAAM,SAAS,aAAa,IAAI,EAAE;CACvC,WAAW,OAAO;CAClB,YAAY,UACV,OAAO,KACL,IAAI,QAAQ,iBAAiB;EAC3B,UAAU;EACV,KAAK;GAAE,SAAS;GAAgC;GAAO;EACxD,CAAC,CACH;CACJ,CAAC;AAEJ,MAAM,qBAAqB,WAAgD,QACzE,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,MAAM,UAAU,IAAI;AACxC,KAAI,SAAS,KAAA,EAAW;CACxB,MAAM,UAAU,OAAO,kBAAkB,KAAK,CAAC,KAAK,OAAO,OAAO;AAClE,KAAI,QAAQ,SAAS,OAAQ;CAC7B,MAAM,QAAQ,QAAQ;AACtB,KAAI,MAAM,UAAU,KAAA,GAAW;AAC7B,SAAO,OAAO,WAAW,uCAAuC;GAC9D,OAAO,MAAM;GACb,SAAS,MAAM;GAChB,CAAC;AACF;;AAEF,KAAI,MAAM,SAAS,QAAQ,MAAM,UAAU,GAAI;CAC/C,MAAM,QAAQ,OAAO,YAAY,MAAM,MAAM,CAAC,KAAK,OAAO,OAAO;AACjE,KAAI,MAAM,SAAS,OAAQ,QAAO,MAAM,MAAM,OAAO,EAAE,OAAO,MAAM,OAAO,CAAC;EAC5E;AAMJ,MAAa,mBACV,SAEC,QACA,YAEA,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,mBAAmB,QAAQ,gBAAgB,cAAc;CAK7E,MAAM,SAAS,OAAO,OAAO,cAAc,WAAW,KAAK,SAAS,KAAK,EAAE,EACzE,mBAAmB,SAAS,SAAS,OAAQ,SAAS,QAAQ,SAAS,MACxE,CAAC;CACF,MAAM,QAAQ,OAAO,MAAM,QAAgC,GAAG;CAC9D,MAAM,QAAQ,OAAO,OAAO;AAK5B,QAAO,OAAO,IAAI,aAAa;AAC7B,SAAO,MAAM,SAAS,KAAK,QAAQ,CAAC;AACpC,SAAO,OAAO,WAAW,SAAS,SAChC,KAAK,WAAW,IAAI,OAAO,OAAO,MAAM,UAAU,KAAK,CAAC,CACzD;AACD,SAAO,MAAM,SAAS;GACtB,CAAC,KAAK,OAAO,QAAQ,OAAO,WAAW;AAMzC,QAAO,OACJ,UAAU,kBAAkB,MAAM,CAAC,CACnC,KAAK,OAAO,SAAS,MAAM,IAAI,MAAM,CAAC,EAAE,OAAO,WAAW;AAE7D,QAAO,OAAO,UAAU,MAAM;EAC9B,CACH"}
@@ -0,0 +1,29 @@
1
+ //#region src/region.d.ts
2
+ /**
3
+ * ElevenLabs regional residency endpoints. The default workspace runs
4
+ * against `api.elevenlabs.io`; EU- and IN-residency workspaces run
5
+ * against `api.{eu,in}.residency.elevenlabs.io` (REST + WSS).
6
+ *
7
+ * API keys are workspace-bound — a key minted in an EU workspace must
8
+ * be used against the EU host (and vice versa). Mixing key/host
9
+ * surfaces as `401 PERMISSION_DENIED`.
10
+ *
11
+ * Reference: https://elevenlabs.io/docs/overview/administration/data-residency
12
+ */
13
+ type ElevenLabsRegion = "default" | "eu" | "in" | (string & {});
14
+ /**
15
+ * Resolve the HTTP base URL from a Config. `baseUrl` wins when set;
16
+ * otherwise the host is computed from `region`. Unknown region strings
17
+ * pass through as `api.{region}.residency.elevenlabs.io/v1` for forward
18
+ * compat.
19
+ *
20
+ * WS code derives its `wss://` URL by substituting `http→ws` on the
21
+ * resolved host.
22
+ */
23
+ declare const resolveHost: (cfg: {
24
+ readonly baseUrl?: string;
25
+ readonly region?: ElevenLabsRegion;
26
+ }) => string;
27
+ //#endregion
28
+ export { ElevenLabsRegion, resolveHost };
29
+ //# sourceMappingURL=region.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.d.mts","names":[],"sources":["../src/region.ts"],"mappings":";;AAaA;;;;;AAaA;;;;;KAbY,gBAAA;;;;;;;;;;cAaC,WAAA,GAAe,GAAA;EAAA,SACjB,OAAA;EAAA,SACA,MAAA,GAAS,gBAAA;AAAA"}
@@ -0,0 +1,20 @@
1
+ //#region src/region.ts
2
+ const DEFAULT_HOST = "https://api.elevenlabs.io/v1";
3
+ /**
4
+ * Resolve the HTTP base URL from a Config. `baseUrl` wins when set;
5
+ * otherwise the host is computed from `region`. Unknown region strings
6
+ * pass through as `api.{region}.residency.elevenlabs.io/v1` for forward
7
+ * compat.
8
+ *
9
+ * WS code derives its `wss://` URL by substituting `http→ws` on the
10
+ * resolved host.
11
+ */
12
+ const resolveHost = (cfg) => {
13
+ if (cfg.baseUrl !== void 0) return cfg.baseUrl;
14
+ if (cfg.region === void 0 || cfg.region === "default") return DEFAULT_HOST;
15
+ return `https://api.${cfg.region}.residency.elevenlabs.io/v1`;
16
+ };
17
+ //#endregion
18
+ export { resolveHost };
19
+
20
+ //# sourceMappingURL=region.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.mjs","names":[],"sources":["../src/region.ts"],"sourcesContent":["/**\n * ElevenLabs regional residency endpoints. The default workspace runs\n * against `api.elevenlabs.io`; EU- and IN-residency workspaces run\n * against `api.{eu,in}.residency.elevenlabs.io` (REST + WSS).\n *\n * API keys are workspace-bound — a key minted in an EU workspace must\n * be used against the EU host (and vice versa). Mixing key/host\n * surfaces as `401 PERMISSION_DENIED`.\n *\n * Reference: https://elevenlabs.io/docs/overview/administration/data-residency\n */\n\n// eslint-disable-next-line @typescript-eslint/ban-types\nexport type ElevenLabsRegion = \"default\" | \"eu\" | \"in\" | (string & {})\n\nconst DEFAULT_HOST = \"https://api.elevenlabs.io/v1\"\n\n/**\n * Resolve the HTTP base URL from a Config. `baseUrl` wins when set;\n * otherwise the host is computed from `region`. Unknown region strings\n * pass through as `api.{region}.residency.elevenlabs.io/v1` for forward\n * compat.\n *\n * WS code derives its `wss://` URL by substituting `http→ws` on the\n * resolved host.\n */\nexport const resolveHost = (cfg: {\n readonly baseUrl?: string\n readonly region?: ElevenLabsRegion\n}): string => {\n if (cfg.baseUrl !== undefined) return cfg.baseUrl\n if (cfg.region === undefined || cfg.region === \"default\") return DEFAULT_HOST\n return `https://api.${cfg.region}.residency.elevenlabs.io/v1`\n}\n"],"mappings":";AAeA,MAAM,eAAe;;;;;;;;;;AAWrB,MAAa,eAAe,QAGd;AACZ,KAAI,IAAI,YAAY,KAAA,EAAW,QAAO,IAAI;AAC1C,KAAI,IAAI,WAAW,KAAA,KAAa,IAAI,WAAW,UAAW,QAAO;AACjE,QAAO,eAAe,IAAI,OAAO"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,30 @@
1
+ import { i as it, n as globalExpect, r as describe } from "./dist-DMsg8YLb.mjs";
2
+ import { resolveHost } from "./region.mjs";
3
+ //#region src/region.test.ts
4
+ describe("elevenlabs/region.resolveHost", () => {
5
+ it("returns default host when no region or baseUrl", () => {
6
+ globalExpect(resolveHost({})).toBe("https://api.elevenlabs.io/v1");
7
+ });
8
+ it("returns default host for region 'default'", () => {
9
+ globalExpect(resolveHost({ region: "default" })).toBe("https://api.elevenlabs.io/v1");
10
+ });
11
+ it("computes EU residency host for region 'eu'", () => {
12
+ globalExpect(resolveHost({ region: "eu" })).toBe("https://api.eu.residency.elevenlabs.io/v1");
13
+ });
14
+ it("computes IN residency host for region 'in'", () => {
15
+ globalExpect(resolveHost({ region: "in" })).toBe("https://api.in.residency.elevenlabs.io/v1");
16
+ });
17
+ it("passes unknown region through as residency host", () => {
18
+ globalExpect(resolveHost({ region: "ap" })).toBe("https://api.ap.residency.elevenlabs.io/v1");
19
+ });
20
+ it("baseUrl wins over region", () => {
21
+ globalExpect(resolveHost({
22
+ baseUrl: "http://localhost:8080/v1",
23
+ region: "eu"
24
+ })).toBe("http://localhost:8080/v1");
25
+ });
26
+ });
27
+ //#endregion
28
+ export {};
29
+
30
+ //# sourceMappingURL=region.test.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.test.mjs","names":[],"sources":["../src/region.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\"\nimport { resolveHost } from \"./region.js\"\n\ndescribe(\"elevenlabs/region.resolveHost\", () => {\n it(\"returns default host when no region or baseUrl\", () => {\n expect(resolveHost({})).toBe(\"https://api.elevenlabs.io/v1\")\n })\n\n it(\"returns default host for region 'default'\", () => {\n expect(resolveHost({ region: \"default\" })).toBe(\"https://api.elevenlabs.io/v1\")\n })\n\n it(\"computes EU residency host for region 'eu'\", () => {\n expect(resolveHost({ region: \"eu\" })).toBe(\"https://api.eu.residency.elevenlabs.io/v1\")\n })\n\n it(\"computes IN residency host for region 'in'\", () => {\n expect(resolveHost({ region: \"in\" })).toBe(\"https://api.in.residency.elevenlabs.io/v1\")\n })\n\n it(\"passes unknown region through as residency host\", () => {\n expect(resolveHost({ region: \"ap\" })).toBe(\"https://api.ap.residency.elevenlabs.io/v1\")\n })\n\n it(\"baseUrl wins over region\", () => {\n expect(resolveHost({ baseUrl: \"http://localhost:8080/v1\", region: \"eu\" })).toBe(\n \"http://localhost:8080/v1\",\n )\n })\n})\n"],"mappings":";;;AAGA,SAAS,uCAAuC;AAC9C,IAAG,wDAAwD;AACzD,eAAO,YAAY,EAAE,CAAC,CAAC,CAAC,KAAK,+BAA+B;GAC5D;AAEF,IAAG,mDAAmD;AACpD,eAAO,YAAY,EAAE,QAAQ,WAAW,CAAC,CAAC,CAAC,KAAK,+BAA+B;GAC/E;AAEF,IAAG,oDAAoD;AACrD,eAAO,YAAY,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,KAAK,4CAA4C;GACvF;AAEF,IAAG,oDAAoD;AACrD,eAAO,YAAY,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,KAAK,4CAA4C;GACvF;AAEF,IAAG,yDAAyD;AAC1D,eAAO,YAAY,EAAE,QAAQ,MAAM,CAAC,CAAC,CAAC,KAAK,4CAA4C;GACvF;AAEF,IAAG,kCAAkC;AACnC,eAAO,YAAY;GAAE,SAAS;GAA4B,QAAQ;GAAM,CAAC,CAAC,CAAC,KACzE,2BACD;GACD;EACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/elevenlabs",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "ElevenLabs speech-to-text and text-to-speech provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -51,7 +51,7 @@
51
51
  "devDependencies": {
52
52
  "effect": "4.0.0-beta.57",
53
53
  "typescript": "^6.0.3",
54
- "@effect-uai/core": "0.5.2"
54
+ "@effect-uai/core": "0.6.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@effect-uai/core": ">=0.2.0 <1",
@@ -1,11 +1,14 @@
1
- import { Context, Effect, Layer, Redacted, Stream } from "effect"
1
+ import { Array as Arr, Context, Effect, Layer, Redacted, Stream } from "effect"
2
2
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
3
  import * as Socket from "effect/unstable/socket/Socket"
4
4
  import type * as AiError from "@effect-uai/core/AiError"
5
5
  import type { AudioBlob, AudioChunk } from "@effect-uai/core/Audio"
6
6
  import {
7
7
  type CommonStreamSynthesizeRequest,
8
+ type CommonSynthesizeDialogueRequest,
8
9
  type CommonSynthesizeRequest,
10
+ type CustomPronunciation,
11
+ MultiSpeakerTts,
9
12
  SpeechSynthesizer,
10
13
  type SpeechSynthesizerService,
11
14
  TtsIncrementalText,
@@ -20,6 +23,7 @@ import {
20
23
  } from "./codec.js"
21
24
  import type { ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.js"
22
25
  import { streamSynthesis as realtimeStream } from "./realtimeTts.js"
26
+ import { type ElevenLabsRegion, resolveHost } from "./region.js"
23
27
 
24
28
  // ---------------------------------------------------------------------------
25
29
  // Public types
@@ -46,6 +50,8 @@ export type ElevenLabsSynthesizerService = {
46
50
  r: ElevenLabsSynthesizeRequest,
47
51
  ) => Stream.Stream<AudioChunk, AiError.AiError>
48
52
  readonly streamSynthesisFrom: SpeechSynthesizerService["streamSynthesisFrom"]
53
+ readonly synthesizeDialogue: SpeechSynthesizerService["synthesizeDialogue"]
54
+ readonly streamSynthesizeDialogue: SpeechSynthesizerService["streamSynthesizeDialogue"]
49
55
  }
50
56
 
51
57
  export class ElevenLabsSynthesizer extends Context.Service<
@@ -53,14 +59,65 @@ export class ElevenLabsSynthesizer extends Context.Service<
53
59
  ElevenLabsSynthesizerService
54
60
  >()("@betalyra/effect-uai/providers/elevenlabs/ElevenLabsSynthesizer") {}
55
61
 
56
- export type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }
62
+ export type Config = {
63
+ readonly apiKey: Redacted.Redacted
64
+ readonly baseUrl?: string
65
+ readonly region?: ElevenLabsRegion
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Pronunciations — SSML <phoneme> rewrite (gated to legacy models)
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * Models that honor inline `<phoneme>` SSML tags. Other models silently
74
+ * drop the tags server-side, so we don't bother rewriting the text for
75
+ * them — the pronunciation overrides are silently ignored.
76
+ *
77
+ * Reference: https://elevenlabs.io/docs/best-practices/prompting/controls
78
+ */
79
+ const PHONEME_SUPPORTED_MODELS: ReadonlySet<string> = new Set([
80
+ "eleven_flash_v2",
81
+ "eleven_english_v1",
82
+ "eleven_monolingual_v1",
83
+ ])
84
+
85
+ const wireAlphabet = (encoding: CustomPronunciation["encoding"]): string | undefined =>
86
+ encoding === "ipa" ? "ipa" : encoding === "cmu-arpabet" ? "cmu-arpabet" : undefined
87
+
88
+ const phonemeTag = (phrase: string, pronunciation: string, alphabet: string): string =>
89
+ `<phoneme alphabet="${alphabet}" ph="${pronunciation}">${phrase}</phoneme>`
90
+
91
+ const escapeForRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
92
+
93
+ /**
94
+ * Apply pronunciation overrides to a text string. Only applies for
95
+ * phoneme-supported models; only IPA and CMU Arpabet are honored
96
+ * (x-sampa is silently dropped). Whole-word, case-insensitive
97
+ * replacement.
98
+ */
99
+ const applyPronunciations = (
100
+ text: string,
101
+ model: string,
102
+ pronunciations: ReadonlyArray<CustomPronunciation> | undefined,
103
+ ): string => {
104
+ if (pronunciations === undefined || !PHONEME_SUPPORTED_MODELS.has(model)) return text
105
+ return Arr.reduce(pronunciations, text, (acc, p) => {
106
+ const alphabet = wireAlphabet(p.encoding)
107
+ if (alphabet === undefined) return acc
108
+ return acc.replace(
109
+ new RegExp(`\\b${escapeForRegex(p.phrase)}\\b`, "i"),
110
+ phonemeTag(p.phrase, p.pronunciation, alphabet),
111
+ )
112
+ })
113
+ }
57
114
 
58
115
  // ---------------------------------------------------------------------------
59
116
  // Codec — request → JSON body
60
117
  // ---------------------------------------------------------------------------
61
118
 
62
119
  const buildBody = (r: ElevenLabsSynthesizeRequest) => ({
63
- text: r.text,
120
+ text: applyPronunciations(r.text, r.model, r.pronunciations),
64
121
  model_id: r.model,
65
122
  ...(r.languageCode !== undefined && { language_code: r.languageCode }),
66
123
  ...(r.seed !== undefined && { seed: r.seed }),
@@ -70,16 +127,75 @@ const buildBody = (r: ElevenLabsSynthesizeRequest) => ({
70
127
  })
71
128
 
72
129
  // ---------------------------------------------------------------------------
73
- // HTTP plumbing (sync + chunked-HTTP streaming)
130
+ // Dialogue — POST /v1/text-to-dialogue (+ /stream variant)
74
131
  // ---------------------------------------------------------------------------
75
132
 
76
- const baseUrl = (cfg: Config) => cfg.baseUrl ?? "https://api.elevenlabs.io/v1"
133
+ const DEFAULT_DIALOGUE_MODEL = "eleven_v3"
134
+
135
+ const buildDialogueBody = (r: CommonSynthesizeDialogueRequest) => ({
136
+ inputs: r.turns.map((t) => ({
137
+ voice_id: t.voiceId,
138
+ text: applyPronunciations(t.text, r.model, r.pronunciations),
139
+ })),
140
+ model_id: r.model ?? DEFAULT_DIALOGUE_MODEL,
141
+ ...(r.languageCode !== undefined && { language_code: r.languageCode }),
142
+ })
143
+
144
+ const buildDialogueHttpRequest = (
145
+ cfg: Config,
146
+ r: CommonSynthesizeDialogueRequest,
147
+ path: "" | "/stream",
148
+ ) =>
149
+ Effect.gen(function* () {
150
+ const format = r.outputFormat ?? defaultFormat
151
+ const slug = yield* formatToOutputSlug(format)
152
+ const url = `${resolveHost(cfg)}/text-to-dialogue${path}?output_format=${slug}`
153
+ const httpRequest = HttpClientRequest.post(url).pipe(
154
+ HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)),
155
+ HttpClientRequest.bodyJsonUnsafe(buildDialogueBody(r)),
156
+ )
157
+ return { httpRequest, format }
158
+ })
159
+
160
+ const synthesizeDialogueImpl = (cfg: Config) => (request: CommonSynthesizeDialogueRequest) =>
161
+ Effect.gen(function* () {
162
+ const client = yield* HttpClient.HttpClient
163
+ const { httpRequest, format } = yield* buildDialogueHttpRequest(cfg, request, "")
164
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
165
+ if (response.status >= 400) {
166
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
167
+ return yield* Effect.fail(httpStatusError(response.status, text))
168
+ }
169
+ const bytes = yield* response.arrayBuffer.pipe(Effect.mapError(transportFailure))
170
+ return { format, bytes: new Uint8Array(bytes) } satisfies AudioBlob
171
+ })
172
+
173
+ const streamSynthesizeDialogueImpl = (cfg: Config) => (request: CommonSynthesizeDialogueRequest) =>
174
+ Stream.unwrap(
175
+ Effect.gen(function* () {
176
+ const client = yield* HttpClient.HttpClient
177
+ const { httpRequest } = yield* buildDialogueHttpRequest(cfg, request, "/stream")
178
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
179
+ if (response.status >= 400) {
180
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
181
+ return Stream.fail(httpStatusError(response.status, text))
182
+ }
183
+ return response.stream.pipe(
184
+ Stream.mapError(transportFailure),
185
+ Stream.map((bytes): AudioChunk => ({ bytes })),
186
+ )
187
+ }),
188
+ )
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // HTTP plumbing (sync + chunked-HTTP streaming)
192
+ // ---------------------------------------------------------------------------
77
193
 
78
194
  const buildHttpRequest = (cfg: Config, r: ElevenLabsSynthesizeRequest, path: "" | "/stream") =>
79
195
  Effect.gen(function* () {
80
196
  const format = r.outputFormat ?? defaultFormat
81
197
  const slug = yield* formatToOutputSlug(format)
82
- const url = `${baseUrl(cfg)}/text-to-speech/${r.voiceId}${path}?output_format=${slug}`
198
+ const url = `${resolveHost(cfg)}/text-to-speech/${r.voiceId}${path}?output_format=${slug}`
83
199
  const httpRequest = HttpClientRequest.post(url).pipe(
84
200
  HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)),
85
201
  HttpClientRequest.bodyJsonUnsafe(buildBody(r)),
@@ -134,15 +250,25 @@ export const make = (cfg: Config) =>
134
250
  realtimeStream(cfg)(textIn, request as ElevenLabsSynthesizeRequest).pipe(
135
251
  Stream.provideService(Socket.WebSocketConstructor, ctor),
136
252
  ),
253
+ synthesizeDialogue: (request) =>
254
+ synthesizeDialogueImpl(cfg)(request).pipe(
255
+ Effect.provideService(HttpClient.HttpClient, client),
256
+ ),
257
+ streamSynthesizeDialogue: (request) =>
258
+ streamSynthesizeDialogueImpl(cfg)(request).pipe(
259
+ Stream.provideService(HttpClient.HttpClient, client),
260
+ ),
137
261
  } satisfies ElevenLabsSynthesizerService
138
262
  })
139
263
 
140
264
  /**
141
265
  * Layer registers `ElevenLabsSynthesizer`, the generic
142
- * `SpeechSynthesizer`, **and `TtsIncrementalText`** — incremental
143
- * text-in is wired to the realtime `/stream-input` WebSocket. Provide
144
- * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer at
145
- * the call site.
266
+ * `SpeechSynthesizer`, the `TtsIncrementalText` marker (incremental
267
+ * text-in via `/stream-input` WebSocket), **and `MultiSpeakerTts`** —
268
+ * multi-speaker dialogue is wired to `POST /v1/text-to-dialogue`
269
+ * (sync) and `/v1/text-to-dialogue/stream` (chunked). Provide
270
+ * `Socket.layerWebSocketConstructorGlobal` and an `HttpClient` Layer
271
+ * at the call site.
146
272
  */
147
273
  export const layer = (cfg: Config) =>
148
274
  Layer.mergeAll(
@@ -158,8 +284,11 @@ export const layer = (cfg: Config) =>
158
284
  s.streamSynthesis(req as ElevenLabsSynthesizeRequest),
159
285
  streamSynthesisFrom: (textIn, req: CommonStreamSynthesizeRequest) =>
160
286
  s.streamSynthesisFrom(textIn, req),
287
+ synthesizeDialogue: s.synthesizeDialogue,
288
+ streamSynthesizeDialogue: s.streamSynthesizeDialogue,
161
289
  }),
162
290
  ),
163
291
  ),
164
292
  Layer.succeed(TtsIncrementalText, undefined),
293
+ Layer.succeed(MultiSpeakerTts, undefined),
165
294
  )
@@ -12,6 +12,7 @@ import {
12
12
  import { audioToBlob, defaultFileName, httpStatusError, transportFailure } from "./codec.js"
13
13
  import type { ElevenLabsSttModel } from "./models.js"
14
14
  import { streamTranscription } from "./realtimeStt.js"
15
+ import { type ElevenLabsRegion, resolveHost } from "./region.js"
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Public types
@@ -43,7 +44,11 @@ export class ElevenLabsTranscriber extends Context.Service<
43
44
  ElevenLabsTranscriberService
44
45
  >()("@betalyra/effect-uai/providers/elevenlabs/ElevenLabsTranscriber") {}
45
46
 
46
- export type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }
47
+ export type Config = {
48
+ readonly apiKey: Redacted.Redacted
49
+ readonly baseUrl?: string
50
+ readonly region?: ElevenLabsRegion
51
+ }
47
52
 
48
53
  // ---------------------------------------------------------------------------
49
54
  // Response schema
@@ -103,13 +108,11 @@ const buildForm = (request: ElevenLabsTranscribeRequest) =>
103
108
  // HTTP plumbing
104
109
  // ---------------------------------------------------------------------------
105
110
 
106
- const baseUrl = (cfg: Config) => cfg.baseUrl ?? "https://api.elevenlabs.io/v1"
107
-
108
111
  const transcribeImpl = (cfg: Config) => (request: ElevenLabsTranscribeRequest) =>
109
112
  Effect.gen(function* () {
110
113
  const client = yield* HttpClient.HttpClient
111
114
  const form = yield* buildForm(request)
112
- const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/speech-to-text`).pipe(
115
+ const httpRequest = HttpClientRequest.post(`${resolveHost(cfg)}/speech-to-text`).pipe(
113
116
  HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)),
114
117
  HttpClientRequest.bodyFormData(form),
115
118
  )
package/src/index.ts CHANGED
@@ -2,3 +2,4 @@ export * as ElevenLabsSynthesizer from "./ElevenLabsSynthesizer.js"
2
2
  export * as ElevenLabsTranscriber from "./ElevenLabsTranscriber.js"
3
3
  export * as codec from "./codec.js"
4
4
  export * from "./models.js"
5
+ export * from "./region.js"
@@ -7,8 +7,13 @@ import * as JSONL from "@effect-uai/core/JSONL"
7
7
  import type { TranscriptEvent, WordTimestamp } from "@effect-uai/core/Transcript"
8
8
  import type { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber"
9
9
  import { httpStatusError, transportFailure } from "./codec.js"
10
+ import { type ElevenLabsRegion, resolveHost } from "./region.js"
10
11
 
11
- export type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }
12
+ export type Config = {
13
+ readonly apiKey: Redacted.Redacted
14
+ readonly baseUrl?: string
15
+ readonly region?: ElevenLabsRegion
16
+ }
12
17
 
13
18
  // ---------------------------------------------------------------------------
14
19
  // AudioFormat → audio_format slug
@@ -43,7 +48,7 @@ const decodeToken = Schema.decodeUnknownEffect(TokenWire)
43
48
  export const fetchSingleUseToken = (cfg: Config) =>
44
49
  Effect.gen(function* () {
45
50
  const client = yield* HttpClient.HttpClient
46
- const url = `${cfg.baseUrl ?? "https://api.elevenlabs.io/v1"}/single-use-token/realtime_scribe`
51
+ const url = `${resolveHost(cfg)}/single-use-token/realtime_scribe`
47
52
  const httpRequest = HttpClientRequest.post(url).pipe(
48
53
  HttpClientRequest.setHeader("xi-api-key", Redacted.value(cfg.apiKey)),
49
54
  )
@@ -178,7 +183,7 @@ export const buildWsUrl = (
178
183
  audioFormat: string,
179
184
  opts: RealtimeOptions,
180
185
  ) => {
181
- const wsBase = (cfg.baseUrl ?? "https://api.elevenlabs.io/v1").replace(/^http/, "ws")
186
+ const wsBase = resolveHost(cfg).replace(/^http/, "ws")
182
187
  const params = new URLSearchParams({
183
188
  token,
184
189
  audio_format: audioFormat,
@@ -10,8 +10,13 @@ import {
10
10
  wireVoiceSettings,
11
11
  } from "./codec.js"
12
12
  import type { ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.js"
13
+ import { type ElevenLabsRegion, resolveHost } from "./region.js"
13
14
 
14
- export type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }
15
+ export type Config = {
16
+ readonly apiKey: Redacted.Redacted
17
+ readonly baseUrl?: string
18
+ readonly region?: ElevenLabsRegion
19
+ }
15
20
 
16
21
  export type { VoiceSettings } from "./codec.js"
17
22
 
@@ -35,7 +40,7 @@ export type StreamSynthesizeRequest = {
35
40
  // ---------------------------------------------------------------------------
36
41
 
37
42
  export const buildWsUrl = (cfg: Config, request: StreamSynthesizeRequest, outputFormat: string) => {
38
- const wsBase = (cfg.baseUrl ?? "https://api.elevenlabs.io/v1").replace(/^http/, "ws")
43
+ const wsBase = resolveHost(cfg).replace(/^http/, "ws")
39
44
  const params = new URLSearchParams({
40
45
  output_format: outputFormat,
41
46
  auto_mode: String(request.autoMode ?? true),
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { resolveHost } from "./region.js"
3
+
4
+ describe("elevenlabs/region.resolveHost", () => {
5
+ it("returns default host when no region or baseUrl", () => {
6
+ expect(resolveHost({})).toBe("https://api.elevenlabs.io/v1")
7
+ })
8
+
9
+ it("returns default host for region 'default'", () => {
10
+ expect(resolveHost({ region: "default" })).toBe("https://api.elevenlabs.io/v1")
11
+ })
12
+
13
+ it("computes EU residency host for region 'eu'", () => {
14
+ expect(resolveHost({ region: "eu" })).toBe("https://api.eu.residency.elevenlabs.io/v1")
15
+ })
16
+
17
+ it("computes IN residency host for region 'in'", () => {
18
+ expect(resolveHost({ region: "in" })).toBe("https://api.in.residency.elevenlabs.io/v1")
19
+ })
20
+
21
+ it("passes unknown region through as residency host", () => {
22
+ expect(resolveHost({ region: "ap" })).toBe("https://api.ap.residency.elevenlabs.io/v1")
23
+ })
24
+
25
+ it("baseUrl wins over region", () => {
26
+ expect(resolveHost({ baseUrl: "http://localhost:8080/v1", region: "eu" })).toBe(
27
+ "http://localhost:8080/v1",
28
+ )
29
+ })
30
+ })
package/src/region.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ElevenLabs regional residency endpoints. The default workspace runs
3
+ * against `api.elevenlabs.io`; EU- and IN-residency workspaces run
4
+ * against `api.{eu,in}.residency.elevenlabs.io` (REST + WSS).
5
+ *
6
+ * API keys are workspace-bound — a key minted in an EU workspace must
7
+ * be used against the EU host (and vice versa). Mixing key/host
8
+ * surfaces as `401 PERMISSION_DENIED`.
9
+ *
10
+ * Reference: https://elevenlabs.io/docs/overview/administration/data-residency
11
+ */
12
+
13
+ // eslint-disable-next-line @typescript-eslint/ban-types
14
+ export type ElevenLabsRegion = "default" | "eu" | "in" | (string & {})
15
+
16
+ const DEFAULT_HOST = "https://api.elevenlabs.io/v1"
17
+
18
+ /**
19
+ * Resolve the HTTP base URL from a Config. `baseUrl` wins when set;
20
+ * otherwise the host is computed from `region`. Unknown region strings
21
+ * pass through as `api.{region}.residency.elevenlabs.io/v1` for forward
22
+ * compat.
23
+ *
24
+ * WS code derives its `wss://` URL by substituting `http→ws` on the
25
+ * resolved host.
26
+ */
27
+ export const resolveHost = (cfg: {
28
+ readonly baseUrl?: string
29
+ readonly region?: ElevenLabsRegion
30
+ }): string => {
31
+ if (cfg.baseUrl !== undefined) return cfg.baseUrl
32
+ if (cfg.region === undefined || cfg.region === "default") return DEFAULT_HOST
33
+ return `https://api.${cfg.region}.residency.elevenlabs.io/v1`
34
+ }