@effect-uai/elevenlabs 0.16.0 → 0.18.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.
- package/dist/ElevenLabsMusicGenerator.d.mts +1 -1
- package/dist/ElevenLabsMusicGenerator.mjs +1 -1
- package/dist/ElevenLabsMusicGenerator.mjs.map +1 -1
- package/dist/ElevenLabsMusicGenerator.test.mjs +3 -3
- package/dist/ElevenLabsMusicGenerator.test.mjs.map +1 -1
- package/dist/ElevenLabsSynthesizer.d.mts +2 -2
- package/dist/ElevenLabsSynthesizer.mjs +2 -2
- package/dist/ElevenLabsSynthesizer.mjs.map +1 -1
- package/dist/ElevenLabsTranscriber.d.mts +2 -2
- package/dist/ElevenLabsTranscriber.mjs +2 -2
- package/dist/ElevenLabsTranscriber.mjs.map +1 -1
- package/dist/ElevenLabsTranscriber.test.mjs +4 -4
- package/dist/ElevenLabsTranscriber.test.mjs.map +1 -1
- package/dist/codec.d.mts +1 -1
- package/dist/codec.d.mts.map +1 -1
- package/dist/codec.mjs +3 -2
- package/dist/codec.mjs.map +1 -1
- package/dist/codec.test.mjs +4 -3
- package/dist/codec.test.mjs.map +1 -1
- package/dist/dist-CXrdHvlJ.mjs +171 -0
- package/dist/dist-CXrdHvlJ.mjs.map +1 -0
- package/dist/dist-DHer1cql.mjs +1533 -0
- package/dist/dist-DHer1cql.mjs.map +1 -0
- package/dist/dist-DkEvSNwD.mjs +7 -0
- package/dist/dist-DkEvSNwD.mjs.map +1 -0
- package/dist/{test.DNmyFkvJ-N3QqMRwZ.mjs → index.C4tKZ0yW-DjxCdpW_.mjs} +9524 -5893
- package/dist/index.C4tKZ0yW-DjxCdpW_.mjs.map +1 -0
- package/dist/realtimeStt.d.mts +2 -2
- package/dist/realtimeStt.d.mts.map +1 -1
- package/dist/realtimeStt.mjs +19 -15
- package/dist/realtimeStt.mjs.map +1 -1
- package/dist/realtimeStt.test.d.mts +1 -0
- package/dist/realtimeStt.test.mjs +80 -0
- package/dist/realtimeStt.test.mjs.map +1 -0
- package/dist/realtimeTts.d.mts +1 -1
- package/dist/realtimeTts.d.mts.map +1 -1
- package/dist/realtimeTts.mjs +20 -18
- package/dist/realtimeTts.mjs.map +1 -1
- package/dist/realtimeTts.test.d.mts +1 -0
- package/dist/realtimeTts.test.mjs +59 -0
- package/dist/realtimeTts.test.mjs.map +1 -0
- package/dist/region.test.mjs +1 -1
- package/package.json +4 -4
- package/src/ElevenLabsMusicGenerator.test.ts +1 -1
- package/src/ElevenLabsMusicGenerator.ts +1 -1
- package/src/ElevenLabsSynthesizer.ts +2 -2
- package/src/ElevenLabsTranscriber.test.ts +2 -2
- package/src/ElevenLabsTranscriber.ts +2 -2
- package/src/codec.test.ts +3 -2
- package/src/codec.ts +4 -3
- package/src/realtimeStt.test.ts +92 -0
- package/src/realtimeStt.ts +28 -38
- package/src/realtimeTts.test.ts +72 -0
- package/src/realtimeTts.ts +38 -39
- package/dist/dist-C0AvqZTC.mjs +0 -145
- package/dist/dist-C0AvqZTC.mjs.map +0 -1
- package/dist/magic-string.es-CUAnjKYC.mjs +0 -1014
- package/dist/magic-string.es-CUAnjKYC.mjs.map +0 -1
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +0 -1
package/dist/realtimeStt.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ElevenLabsRegion } from "./region.mjs";
|
|
2
2
|
import { Effect, Redacted, Schema, Stream } from "effect";
|
|
3
|
-
import { HttpClient } from "effect/
|
|
3
|
+
import { HttpClient } from "effect/http";
|
|
4
4
|
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
-
import * as Socket from "effect/
|
|
5
|
+
import * as Socket from "effect/socket/Socket";
|
|
6
6
|
import { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber";
|
|
7
7
|
import { AudioFormat } from "@effect-uai/core/Audio";
|
|
8
8
|
import { TranscriptEvent } from "@effect-uai/core/Transcript";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;;;;KAcY;WACD,QAAQ,SAAS;WACjB;WACA,SAAS;;cAcP,oBAAoB,QAAQ,gBAAgB,OAAO,eAAe,QAAQ;cAmB1E,sBAAmB,KAAS,WAAM,OAAA,eAAA,QAAA,SAAA,WAAA;cAsCzC,eAAa,OAAA,gBAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgDN,cAAW,gCACK,YACd,cAAc,SAAO;KAmCxB;WACD;WACA;WACA;WACA;WACA,WAAW;;cAGT,aAAU,KAChB,QAAM,eACE,qBACM,MACb;cAgBK,mBAAgB,OAAW,YAAU;cA2BrC,sBAAmB,KACxB,YACL,GAAG,GAAC,SACM,OAAO,OAAO,YAAY,GAAG,IAAE,SAC/B,kCACR,OAAO,OACR,iBACA,QAAQ,UAAU,GAClB,IAAI,WAAW,aAAa,OAAO"}
|
package/dist/realtimeStt.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { httpStatusError, transportFailure } from "./codec.mjs";
|
|
2
2
|
import { resolveHost } from "./region.mjs";
|
|
3
|
-
import { Effect,
|
|
4
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
3
|
+
import { Effect, Match, Option, Predicate, Redacted, Schema, Stream } from "effect";
|
|
4
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
5
5
|
import * as AiError from "@effect-uai/core/AiError";
|
|
6
|
+
import { Base64 } from "effect/encoding";
|
|
6
7
|
import * as Capabilities from "@effect-uai/core/Capabilities";
|
|
7
|
-
import * as Socket from "effect/
|
|
8
|
+
import * as Socket from "effect/socket/Socket";
|
|
8
9
|
import * as JSONL from "@effect-uai/core/JSONL";
|
|
10
|
+
import * as WebSocketSession from "@effect-uai/core/WebSocketSession";
|
|
9
11
|
//#region src/realtimeStt.ts
|
|
10
12
|
const unsupportedInputFormat = (format) => new AiError.Unsupported({
|
|
11
13
|
provider: "elevenlabs",
|
|
@@ -134,16 +136,18 @@ const buildWsUrl = (cfg, token, audioFormat, opts) => {
|
|
|
134
136
|
};
|
|
135
137
|
const encodeAudioFrame = (bytes, sampleRate) => JSON.stringify({
|
|
136
138
|
message_type: "input_audio_chunk",
|
|
137
|
-
audio_base_64:
|
|
139
|
+
audio_base_64: Base64.encode(bytes),
|
|
138
140
|
sample_rate: sampleRate
|
|
139
141
|
});
|
|
140
|
-
|
|
142
|
+
/** One raw text frame to at most one event; unknown or malformed frames yield `undefined`. */
|
|
143
|
+
const frameToEvent = (mapEvent) => (raw) => Effect.gen(function* () {
|
|
141
144
|
const json = yield* JSONL.parseSafe(raw);
|
|
142
|
-
if (json === void 0) return;
|
|
145
|
+
if (json === void 0) return void 0;
|
|
143
146
|
const decoded = yield* decodeServerMessage(json).pipe(Effect.option);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
return Option.match(decoded, {
|
|
148
|
+
onNone: () => void 0,
|
|
149
|
+
onSome: mapEvent
|
|
150
|
+
});
|
|
147
151
|
});
|
|
148
152
|
const streamTranscription = (cfg) => (audioIn, request) => Stream.unwrap(Effect.gen(function* () {
|
|
149
153
|
const slug = yield* inputFormatToSlug(request.inputFormat);
|
|
@@ -161,13 +165,13 @@ const streamTranscription = (cfg) => (audioIn, request) => Stream.unwrap(Effect.
|
|
|
161
165
|
...request.biasingTerms !== void 0 && { keyterms: request.biasingTerms },
|
|
162
166
|
includeTimestamps
|
|
163
167
|
});
|
|
164
|
-
const socket = yield* Socket.makeWebSocket(url
|
|
165
|
-
const queue = yield* Queue.bounded(64);
|
|
168
|
+
const socket = yield* Socket.makeWebSocket(url);
|
|
166
169
|
const sampleRate = request.inputFormat.sampleRate;
|
|
167
|
-
const write = yield* socket.writer;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
const { write } = yield* socket.writer;
|
|
171
|
+
const asAiError = WebSocketSession.toAiError("elevenlabs");
|
|
172
|
+
const events = Stream.fromPull(Socket.readerString(socket)).pipe(Stream.scoped, Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty), Stream.mapError(asAiError), Stream.mapEffect(frameToEvent(wireToEvent(includeTimestamps))), Stream.filter(Predicate.isNotUndefined));
|
|
173
|
+
const outgoing = Stream.map(audioIn, (bytes) => encodeAudioFrame(bytes, sampleRate)).pipe(Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)));
|
|
174
|
+
return Stream.merge(events, Stream.drain(outgoing), { haltStrategy: "left" });
|
|
171
175
|
}));
|
|
172
176
|
//#endregion
|
|
173
177
|
export { buildWsUrl, encodeAudioFrame, fetchSingleUseToken, inputFormatToSlug, streamTranscription, wireToEvent };
|
package/dist/realtimeStt.mjs.map
CHANGED
|
@@ -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 Capabilities from \"@effect-uai/core/Capabilities\"\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({ message_type: \"partial_transcript\" }, (m): TranscriptEvent => ({\n _tag: \"partial\",\n 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((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// 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 readonly keyterms?: ReadonlyArray<string>\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 // Vocab biasing — repeated `keyterms` param (≤50 on realtime).\n for (const term of opts.keyterms ?? []) params.append(\"keyterms\", term)\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 yield* Capabilities.warnDroppedWhen(request.prompt, {\n provider: \"elevenlabs\",\n capability: \"prompt\",\n field: \"prompt\",\n reason: \"ElevenLabs Scribe has no free-form prompt field; bias via `biasingTerms`.\",\n })\n const url = buildWsUrl(cfg, token, slug, {\n ...(request.model !== undefined && { model: request.model }),\n ...(request.language !== undefined && { languageCode: request.language }),\n ...(request.biasingTerms !== undefined && { keyterms: request.biasingTerms }),\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 { Effect, Match, Option, Predicate, Redacted, Schema, Stream } from \"effect\"\nimport { Base64 } from \"effect/encoding\"\nimport { HttpClient, HttpClientRequest } from \"effect/http\"\nimport * as Socket from \"effect/socket/Socket\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\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 * as WebSocketSession from \"@effect-uai/core/WebSocketSession\"\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({ message_type: \"partial_transcript\" }, (m): TranscriptEvent => ({\n _tag: \"partial\",\n 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((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// 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 readonly keyterms?: ReadonlyArray<string>\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 // Vocab biasing — repeated `keyterms` param (≤50 on realtime).\n for (const term of opts.keyterms ?? []) params.append(\"keyterms\", term)\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: Base64.encode(bytes),\n sample_rate: sampleRate,\n })\n\n/** One raw text frame to at most one event; unknown or malformed frames yield `undefined`. */\nconst frameToEvent =\n (mapEvent: (msg: typeof ServerMessage.Type) => TranscriptEvent | undefined) =>\n (raw: string): Effect.Effect<TranscriptEvent | undefined> =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return undefined\n const decoded = yield* decodeServerMessage(json).pipe(Effect.option)\n return Option.match(decoded, { onNone: () => undefined, onSome: mapEvent })\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 yield* Capabilities.warnDroppedWhen(request.prompt, {\n provider: \"elevenlabs\",\n capability: \"prompt\",\n field: \"prompt\",\n reason: \"ElevenLabs Scribe has no free-form prompt field; bias via `biasingTerms`.\",\n })\n const url = buildWsUrl(cfg, token, slug, {\n ...(request.model !== undefined && { model: request.model }),\n ...(request.language !== undefined && { languageCode: request.language }),\n ...(request.biasingTerms !== undefined && { keyterms: request.biasingTerms }),\n includeTimestamps,\n })\n const socket = yield* Socket.makeWebSocket(url)\n const sampleRate = request.inputFormat.sampleRate\n const { write } = yield* socket.writer\n const asAiError = WebSocketSession.toAiError(\"elevenlabs\")\n\n const events = Stream.fromPull(Socket.readerString(socket)).pipe(\n Stream.scoped,\n Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty),\n Stream.mapError(asAiError),\n Stream.mapEffect(frameToEvent(wireToEvent(includeTimestamps))),\n Stream.filter(Predicate.isNotUndefined),\n )\n\n // Writes wait for the reader to connect.\n const outgoing = Stream.map(audioIn, (bytes) => encodeAudioFrame(bytes, sampleRate)).pipe(\n Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)),\n )\n\n // The server's close ends the transcript; the audio running out does not.\n return Stream.merge(events, Stream.drain(outgoing), { haltStrategy: \"left\" })\n }),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { n as globalExpect, s as describe } from "./index.C4tKZ0yW-DjxCdpW_.mjs";
|
|
2
|
+
import { streamTranscription } from "./realtimeStt.mjs";
|
|
3
|
+
import { t as it } from "./dist-CXrdHvlJ.mjs";
|
|
4
|
+
import { Deferred, Effect, Exit, Fiber, Layer, Redacted, Stream } from "effect";
|
|
5
|
+
import { HttpClient, HttpClientResponse } from "effect/http";
|
|
6
|
+
import * as FakeWebSocket from "@effect-uai/core/testing/FakeWebSocket";
|
|
7
|
+
//#region src/realtimeStt.test.ts
|
|
8
|
+
const request = {
|
|
9
|
+
model: "scribe_v2_realtime",
|
|
10
|
+
inputFormat: {
|
|
11
|
+
container: "raw",
|
|
12
|
+
encoding: "pcm_s16le",
|
|
13
|
+
sampleRate: 16e3,
|
|
14
|
+
channels: 1
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Hands out a token and signals `issued`, so the test closes only after the connect starts. */
|
|
18
|
+
const tokenClient = (issued) => Layer.succeed(HttpClient.HttpClient, HttpClient.make((req) => Deferred.succeed(issued, void 0).pipe(Effect.as(HttpClientResponse.fromWeb(req, new Response(JSON.stringify({ token: "t" })))))));
|
|
19
|
+
const transcribing = FakeWebSocket.make({ greeting: [
|
|
20
|
+
{
|
|
21
|
+
message_type: "session_started",
|
|
22
|
+
session_id: "s1"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
message_type: "partial_transcript",
|
|
26
|
+
text: "hel"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
message_type: "committed_transcript",
|
|
30
|
+
text: "hello"
|
|
31
|
+
}
|
|
32
|
+
] });
|
|
33
|
+
/** Run a transcription against `server`, closing it, if asked, once the frames are in. */
|
|
34
|
+
const transcribe = (server, audioIn, close) => Effect.gen(function* () {
|
|
35
|
+
const issued = yield* Deferred.make();
|
|
36
|
+
const run = streamTranscription({ apiKey: Redacted.make("test-key") })(audioIn, request).pipe(Stream.provide(Layer.merge(server.layer, tokenClient(issued))));
|
|
37
|
+
const collector = yield* Effect.forkChild(Effect.exit(Stream.runCollect(run)));
|
|
38
|
+
yield* Deferred.await(issued);
|
|
39
|
+
if (close !== void 0) {
|
|
40
|
+
yield* server.greeted;
|
|
41
|
+
yield* server.awaitSent(close.afterSent ?? 0);
|
|
42
|
+
yield* server.close(close.code);
|
|
43
|
+
}
|
|
44
|
+
return yield* Fiber.join(collector);
|
|
45
|
+
});
|
|
46
|
+
describe("ElevenLabs realtime STT socket", () => {
|
|
47
|
+
it.live("streams audio chunks, decodes transcripts, and ends on a clean close", () => Effect.gen(function* () {
|
|
48
|
+
const server = yield* transcribing;
|
|
49
|
+
const exit = yield* transcribe(server, Stream.make(new Uint8Array([1, 2])), {
|
|
50
|
+
code: 1e3,
|
|
51
|
+
afterSent: 1
|
|
52
|
+
});
|
|
53
|
+
globalExpect(exit).toEqual(Exit.succeed([{
|
|
54
|
+
_tag: "partial",
|
|
55
|
+
text: "hel"
|
|
56
|
+
}, {
|
|
57
|
+
_tag: "final",
|
|
58
|
+
text: "hello"
|
|
59
|
+
}]));
|
|
60
|
+
const sent = yield* server.sent;
|
|
61
|
+
globalExpect(sent).toMatchObject([{
|
|
62
|
+
message_type: "input_audio_chunk",
|
|
63
|
+
sample_rate: 16e3
|
|
64
|
+
}]);
|
|
65
|
+
}).pipe(Effect.scoped));
|
|
66
|
+
it.live("fails rather than ending when the connection drops", () => Effect.gen(function* () {
|
|
67
|
+
const server = yield* transcribing;
|
|
68
|
+
const exit = yield* transcribe(server, Stream.never, { code: 1011 });
|
|
69
|
+
globalExpect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("Unavailable");
|
|
70
|
+
}).pipe(Effect.scoped));
|
|
71
|
+
it.live("fails with the audio input's own error", () => Effect.gen(function* () {
|
|
72
|
+
const server = yield* transcribing;
|
|
73
|
+
const exit = yield* transcribe(server, Stream.fail("microphone unplugged"));
|
|
74
|
+
globalExpect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("microphone unplugged");
|
|
75
|
+
}).pipe(Effect.scoped));
|
|
76
|
+
});
|
|
77
|
+
//#endregion
|
|
78
|
+
export {};
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=realtimeStt.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtimeStt.test.mjs","names":[],"sources":["../src/realtimeStt.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Deferred, Effect, Exit, Fiber, Layer, Redacted, Stream } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"effect/http\"\nimport { expect } from \"vitest\"\nimport * as FakeWebSocket from \"@effect-uai/core/testing/FakeWebSocket\"\nimport { streamTranscription } from \"./realtimeStt.js\"\n\nconst pcm = { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: 16000, channels: 1 } as const\n\nconst request = { model: \"scribe_v2_realtime\", inputFormat: pcm }\n\n/** Hands out a token and signals `issued`, so the test closes only after the connect starts. */\nconst tokenClient = (issued: Deferred.Deferred<void>) =>\n Layer.succeed(\n HttpClient.HttpClient,\n HttpClient.make((req) =>\n Deferred.succeed(issued, undefined).pipe(\n Effect.as(HttpClientResponse.fromWeb(req, new Response(JSON.stringify({ token: \"t\" })))),\n ),\n ),\n )\n\nconst transcribing = FakeWebSocket.make({\n greeting: [\n { message_type: \"session_started\", session_id: \"s1\" },\n { message_type: \"partial_transcript\", text: \"hel\" },\n { message_type: \"committed_transcript\", text: \"hello\" },\n ],\n})\n\n/** Run a transcription against `server`, closing it, if asked, once the frames are in. */\nconst transcribe = <E>(\n server: FakeWebSocket.FakeWebSocketServer,\n audioIn: Stream.Stream<Uint8Array, E>,\n close?: { readonly code: number; readonly afterSent?: number },\n) =>\n Effect.gen(function* () {\n const issued = yield* Deferred.make<void>()\n const run = streamTranscription({ apiKey: Redacted.make(\"test-key\") })(audioIn, request).pipe(\n Stream.provide(Layer.merge(server.layer, tokenClient(issued))),\n )\n const collector = yield* Effect.forkChild(Effect.exit(Stream.runCollect(run)))\n yield* Deferred.await(issued)\n if (close !== undefined) {\n yield* server.greeted\n yield* server.awaitSent(close.afterSent ?? 0)\n yield* server.close(close.code)\n }\n return yield* Fiber.join(collector)\n })\n\ndescribe(\"ElevenLabs realtime STT socket\", () => {\n it.live(\"streams audio chunks, decodes transcripts, and ends on a clean close\", () =>\n Effect.gen(function* () {\n const server = yield* transcribing\n const exit = yield* transcribe(server, Stream.make(new Uint8Array([1, 2])), {\n code: 1000,\n afterSent: 1,\n })\n\n expect(exit).toEqual(\n Exit.succeed([\n { _tag: \"partial\", text: \"hel\" },\n { _tag: \"final\", text: \"hello\" },\n ]),\n )\n const sent = (yield* server.sent) as ReadonlyArray<{\n message_type: string\n sample_rate: number\n }>\n expect(sent).toMatchObject([{ message_type: \"input_audio_chunk\", sample_rate: 16000 }])\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails rather than ending when the connection drops\", () =>\n Effect.gen(function* () {\n const server = yield* transcribing\n const exit = yield* transcribe(server, Stream.never, { code: 1011 })\n\n expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain(\"Unavailable\")\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails with the audio input's own error\", () =>\n Effect.gen(function* () {\n const server = yield* transcribing\n const exit = yield* transcribe(server, Stream.fail(\"microphone unplugged\"))\n\n expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain(\"microphone unplugged\")\n }).pipe(Effect.scoped),\n )\n})\n"],"mappings":";;;;;;;AASA,MAAM,UAAU;CAAE,OAAO;CAAsB,aAAa;EAF9C,WAAW;EAAO,UAAU;EAAa,YAAY;EAAO,UAAU;CAEtB;AAAE;;AAGhE,MAAM,eAAe,WACnB,MAAM,QACJ,WAAW,YACX,WAAW,MAAM,QACf,SAAS,QAAQ,QAAQ,KAAA,CAAS,CAAC,CAAC,KAClC,OAAO,GAAG,mBAAmB,QAAQ,KAAK,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CACzF,CACF,CACF;AAEF,MAAM,eAAe,cAAc,KAAK,EACtC,UAAU;CACR;EAAE,cAAc;EAAmB,YAAY;CAAK;CACpD;EAAE,cAAc;EAAsB,MAAM;CAAM;CAClD;EAAE,cAAc;EAAwB,MAAM;CAAQ;AACxD,EACF,CAAC;;AAGD,MAAM,cACJ,QACA,SACA,UAEA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,SAAS,KAAW;CAC1C,MAAM,MAAM,oBAAoB,EAAE,QAAQ,SAAS,KAAK,UAAU,EAAE,CAAC,CAAC,CAAC,SAAS,OAAO,CAAC,CAAC,KACvF,OAAO,QAAQ,MAAM,MAAM,OAAO,OAAO,YAAY,MAAM,CAAC,CAAC,CAC/D;CACA,MAAM,YAAY,OAAO,OAAO,UAAU,OAAO,KAAK,OAAO,WAAW,GAAG,CAAC,CAAC;CAC7E,OAAO,SAAS,MAAM,MAAM;CAC5B,IAAI,UAAU,KAAA,GAAW;EACvB,OAAO,OAAO;EACd,OAAO,OAAO,UAAU,MAAM,aAAa,CAAC;EAC5C,OAAO,OAAO,MAAM,MAAM,IAAI;CAChC;CACA,OAAO,OAAO,MAAM,KAAK,SAAS;AACpC,CAAC;AAEH,SAAS,wCAAwC;CAC/C,GAAG,KAAK,8EACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;GAC1E,MAAM;GACN,WAAW;EACb,CAAC;EAED,aAAO,IAAI,CAAC,CAAC,QACX,KAAK,QAAQ,CACX;GAAE,MAAM;GAAW,MAAM;EAAM,GAC/B;GAAE,MAAM;GAAS,MAAM;EAAQ,CACjC,CAAC,CACH;EACA,MAAM,OAAQ,OAAO,OAAO;EAI5B,aAAO,IAAI,CAAC,CAAC,cAAc,CAAC;GAAE,cAAc;GAAqB,aAAa;EAAM,CAAC,CAAC;CACxF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,KAAK,4DACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,OAAO,EAAE,MAAM,KAAK,CAAC;EAEnE,aAAO,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,aAAa;CACpF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,KAAK,gDACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,KAAK,sBAAsB,CAAC;EAE1E,aAAO,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,sBAAsB;CAC7F,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;AACF,CAAC"}
|
package/dist/realtimeTts.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { ElevenLabsRegion } from "./region.mjs";
|
|
|
3
3
|
import { PronunciationDictionaryLocator, VoiceSettings } from "./codec.mjs";
|
|
4
4
|
import { Redacted, Stream } from "effect";
|
|
5
5
|
import * as AiError from "@effect-uai/core/AiError";
|
|
6
|
-
import * as Socket from "effect/
|
|
6
|
+
import * as Socket from "effect/socket/Socket";
|
|
7
7
|
import { CustomPronunciation } from "@effect-uai/core/SpeechSynthesizer";
|
|
8
8
|
import { AudioChunk, AudioFormat } from "@effect-uai/core/Audio";
|
|
9
9
|
//#region src/realtimeTts.d.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;;;;KAoBY;WACD,QAAQ,SAAS;WACjB;WACA,SAAS;;;;;;;;KAWR;WACD,QAAQ;WACR,SAAS;WACT,eAAe;WACf;WACA,gBAAgB;WAChB;;WAEA,kCAAkC,cAAc;;;WAGhD,iBAAiB,cAAc;;cAO7B,aAAU,KAAS,QAAM,SAAW,yBAAuB;cAwE3D,kBAAe,KACpB,YACL,GAAG,GAAC,QACK,OAAO,eAAe,GAAG,IAAE,SAC1B,4BACR,OAAO,OAAO,YAAY,QAAQ,UAAU,GAAG,IAAI,OAAO"}
|
package/dist/realtimeTts.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { defaultFormat, formatToOutputSlug, rejectInlinePronunciations, wirePronunciationLocators, wireVoiceSettings } from "./codec.mjs";
|
|
2
2
|
import { resolveHost } from "./region.mjs";
|
|
3
|
-
import { Effect,
|
|
3
|
+
import { Effect, Option, Predicate, Redacted, Result, Schema, Stream } from "effect";
|
|
4
4
|
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
-
import
|
|
5
|
+
import { Base64 } from "effect/encoding";
|
|
6
|
+
import * as Socket from "effect/socket/Socket";
|
|
6
7
|
import * as JSONL from "@effect-uai/core/JSONL";
|
|
8
|
+
import * as WebSocketSession from "@effect-uai/core/WebSocketSession";
|
|
7
9
|
//#region src/realtimeTts.ts
|
|
8
10
|
const buildWsUrl = (cfg, request, outputFormat) => {
|
|
9
11
|
const wsBase = resolveHost(cfg).replace(/^http/, "ws");
|
|
@@ -33,7 +35,7 @@ const ServerFrame = Schema.Struct({
|
|
|
33
35
|
message: Schema.optional(Schema.String)
|
|
34
36
|
});
|
|
35
37
|
const decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame);
|
|
36
|
-
const decodeAudio = (b64) => Result.match(
|
|
38
|
+
const decodeAudio = (b64) => Result.match(Base64.decode(b64), {
|
|
37
39
|
onSuccess: Effect.succeed,
|
|
38
40
|
onFailure: (cause) => Effect.fail(new AiError.GenerationFailed({
|
|
39
41
|
provider: "elevenlabs",
|
|
@@ -43,11 +45,12 @@ const decodeAudio = (b64) => Result.match(Encoding.decodeBase64(b64), {
|
|
|
43
45
|
}
|
|
44
46
|
}))
|
|
45
47
|
});
|
|
46
|
-
|
|
48
|
+
/** One raw text frame to at most one chunk; error frames are logged and dropped. */
|
|
49
|
+
const frameToChunk = (raw) => Effect.gen(function* () {
|
|
47
50
|
const json = yield* JSONL.parseSafe(raw);
|
|
48
|
-
if (json === void 0) return;
|
|
51
|
+
if (json === void 0) return void 0;
|
|
49
52
|
const decoded = yield* decodeServerFrame(json).pipe(Effect.option);
|
|
50
|
-
if (decoded._tag === "None") return;
|
|
53
|
+
if (decoded._tag === "None") return void 0;
|
|
51
54
|
const frame = decoded.value;
|
|
52
55
|
if (frame.error !== void 0) {
|
|
53
56
|
yield* Effect.logWarning("[elevenlabs-tts] server error frame", {
|
|
@@ -56,23 +59,22 @@ const handleServerFrame = (queue) => (raw) => Effect.gen(function* () {
|
|
|
56
59
|
});
|
|
57
60
|
return;
|
|
58
61
|
}
|
|
59
|
-
if (frame.audio == null || frame.audio === "") return;
|
|
62
|
+
if (frame.audio == null || frame.audio === "") return void 0;
|
|
60
63
|
const bytes = yield* decodeAudio(frame.audio).pipe(Effect.option);
|
|
61
|
-
|
|
64
|
+
return Option.match(bytes, {
|
|
65
|
+
onNone: () => void 0,
|
|
66
|
+
onSome: (b) => ({ bytes: b })
|
|
67
|
+
});
|
|
62
68
|
});
|
|
63
69
|
const streamSynthesis = (cfg) => (textIn, request) => Stream.unwrap(Effect.gen(function* () {
|
|
64
70
|
yield* rejectInlinePronunciations(request.pronunciations);
|
|
65
71
|
const slug = yield* formatToOutputSlug(request.outputFormat ?? defaultFormat);
|
|
66
|
-
const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request, slug)
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
yield* write(eosFrame);
|
|
73
|
-
}).pipe(Effect.ignore, Effect.forkScoped);
|
|
74
|
-
yield* socket.runString(handleServerFrame(queue)).pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped);
|
|
75
|
-
return Stream.fromQueue(queue);
|
|
72
|
+
const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request, slug));
|
|
73
|
+
const { write } = yield* socket.writer;
|
|
74
|
+
const asAiError = WebSocketSession.toAiError("elevenlabs");
|
|
75
|
+
const chunks = Stream.fromPull(Socket.readerString(socket)).pipe(Stream.scoped, Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty), Stream.mapError(asAiError), Stream.mapEffect(frameToChunk), Stream.filter(Predicate.isNotUndefined));
|
|
76
|
+
const outgoing = Stream.concat(Stream.make(bosFrame(cfg, request)), Stream.concat(textIn.pipe(Stream.filter((text) => text.length > 0), Stream.map(textFrame)), Stream.make(eosFrame))).pipe(Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)));
|
|
77
|
+
return Stream.merge(chunks, Stream.drain(outgoing), { haltStrategy: "left" });
|
|
76
78
|
}));
|
|
77
79
|
//#endregion
|
|
78
80
|
export { buildWsUrl, streamSynthesis };
|
package/dist/realtimeTts.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtimeTts.mjs","names":[],"sources":["../src/realtimeTts.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"realtimeTts.mjs","names":[],"sources":["../src/realtimeTts.ts"],"sourcesContent":["import { Effect, Option, Predicate, Redacted, Result, Schema, Stream } from \"effect\"\nimport { Base64 } from \"effect/encoding\"\nimport * as Socket from \"effect/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 type { CustomPronunciation } from \"@effect-uai/core/SpeechSynthesizer\"\nimport * as WebSocketSession from \"@effect-uai/core/WebSocketSession\"\nimport {\n defaultFormat,\n formatToOutputSlug,\n type PronunciationDictionaryLocator,\n rejectInlinePronunciations,\n type VoiceSettings,\n wirePronunciationLocators,\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 /** Pre-provisioned pronunciation dictionaries, sent on the BOS frame. */\n readonly pronunciationDictionaryLocators?: ReadonlyArray<PronunciationDictionaryLocator>\n /** Carried from the Common request so the WS path can reject inline\n * pronunciations (ElevenLabs has no stateless inline IPA path). */\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\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 ...wirePronunciationLocators(request.pronunciationDictionaryLocators),\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(Base64.decode(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\n/** One raw text frame to at most one chunk; error frames are logged and dropped. */\nconst frameToChunk = (raw: string): Effect.Effect<AudioChunk | undefined> =>\n Effect.gen(function* () {\n const json = yield* JSONL.parseSafe(raw)\n if (json === undefined) return undefined\n const decoded = yield* decodeServerFrame(json).pipe(Effect.option)\n if (decoded._tag === \"None\") return undefined\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 undefined\n }\n if (frame.audio == null || frame.audio === \"\") return undefined\n const bytes = yield* decodeAudio(frame.audio).pipe(Effect.option)\n return Option.match(bytes, { onNone: () => undefined, onSome: (b) => ({ bytes: b }) })\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 yield* rejectInlinePronunciations(request.pronunciations)\n const slug = yield* formatToOutputSlug(request.outputFormat ?? defaultFormat)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request, slug))\n const { write } = yield* socket.writer\n const asAiError = WebSocketSession.toAiError(\"elevenlabs\")\n\n // ElevenLabs closes `/stream-input` with 1000 after the final audio chunk.\n const chunks = Stream.fromPull(Socket.readerString(socket)).pipe(\n Stream.scoped,\n Stream.catchIf(WebSocketSession.isCleanClose, () => Stream.empty),\n Stream.mapError(asAiError),\n Stream.mapEffect(frameToChunk),\n Stream.filter(Predicate.isNotUndefined),\n )\n\n // BOS, then the text, then EOS. Writes wait for the reader to connect.\n const outgoing = Stream.concat(\n Stream.make(bosFrame(cfg, request)),\n Stream.concat(\n textIn.pipe(\n Stream.filter((text) => text.length > 0),\n Stream.map(textFrame),\n ),\n Stream.make(eosFrame),\n ),\n ).pipe(Stream.mapEffect((frame) => Effect.mapError(write(frame), asAiError)))\n\n return Stream.merge(chunks, Stream.drain(outgoing), { haltStrategy: \"left\" })\n }),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { n as globalExpect, s as describe } from "./index.C4tKZ0yW-DjxCdpW_.mjs";
|
|
2
|
+
import { streamSynthesis } from "./realtimeTts.mjs";
|
|
3
|
+
import { t as it } from "./dist-CXrdHvlJ.mjs";
|
|
4
|
+
import { Effect, Exit, Fiber, Redacted, Stream } from "effect";
|
|
5
|
+
import { Base64 } from "effect/encoding";
|
|
6
|
+
import * as FakeWebSocket from "@effect-uai/core/testing/FakeWebSocket";
|
|
7
|
+
//#region src/realtimeTts.test.ts
|
|
8
|
+
const request = { voiceId: "voice-1" };
|
|
9
|
+
const synthesizing = FakeWebSocket.make({ greeting: [{
|
|
10
|
+
audio: Base64.encode(new Uint8Array([1, 2])),
|
|
11
|
+
isFinal: null
|
|
12
|
+
}, {
|
|
13
|
+
audio: null,
|
|
14
|
+
isFinal: true
|
|
15
|
+
}] });
|
|
16
|
+
/** Run a synthesis against `server`, closing it, if asked, once the frames are in. */
|
|
17
|
+
const synthesize = (server, textIn, close) => Effect.gen(function* () {
|
|
18
|
+
const run = streamSynthesis({ apiKey: Redacted.make("test-key") })(textIn, request).pipe(Stream.provide(server.layer));
|
|
19
|
+
const collector = yield* Effect.forkChild(Effect.exit(Stream.runCollect(run)));
|
|
20
|
+
if (close !== void 0) {
|
|
21
|
+
yield* server.greeted;
|
|
22
|
+
yield* server.awaitSent(close.afterSent ?? 0);
|
|
23
|
+
yield* server.close(close.code);
|
|
24
|
+
}
|
|
25
|
+
return yield* Fiber.join(collector);
|
|
26
|
+
});
|
|
27
|
+
describe("ElevenLabs realtime TTS socket", () => {
|
|
28
|
+
it.live("sends BOS before any text and EOS after it, and ends on a clean close", () => Effect.gen(function* () {
|
|
29
|
+
const server = yield* synthesizing;
|
|
30
|
+
const exit = yield* synthesize(server, Stream.make("Hello", "", "world "), {
|
|
31
|
+
code: 1e3,
|
|
32
|
+
afterSent: 4
|
|
33
|
+
});
|
|
34
|
+
globalExpect(exit).toEqual(Exit.succeed([{ bytes: new Uint8Array([1, 2]) }]));
|
|
35
|
+
globalExpect(yield* server.sent).toEqual([
|
|
36
|
+
{
|
|
37
|
+
text: " ",
|
|
38
|
+
"xi-api-key": "test-key"
|
|
39
|
+
},
|
|
40
|
+
{ text: "Hello " },
|
|
41
|
+
{ text: "world " },
|
|
42
|
+
{ text: "" }
|
|
43
|
+
]);
|
|
44
|
+
}).pipe(Effect.scoped));
|
|
45
|
+
it.live("fails rather than ending when the connection drops", () => Effect.gen(function* () {
|
|
46
|
+
const server = yield* synthesizing;
|
|
47
|
+
const exit = yield* synthesize(server, Stream.never, { code: 1011 });
|
|
48
|
+
globalExpect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("Unavailable");
|
|
49
|
+
}).pipe(Effect.scoped));
|
|
50
|
+
it.live("fails with the text input's own error", () => Effect.gen(function* () {
|
|
51
|
+
const server = yield* synthesizing;
|
|
52
|
+
const exit = yield* synthesize(server, Stream.fail("llm stream broke"));
|
|
53
|
+
globalExpect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain("llm stream broke");
|
|
54
|
+
}).pipe(Effect.scoped));
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
export {};
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=realtimeTts.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtimeTts.test.mjs","names":[],"sources":["../src/realtimeTts.test.ts"],"sourcesContent":["import { describe, it } from \"@effect/vitest\"\nimport { Effect, Exit, Fiber, Redacted, Stream } from \"effect\"\nimport { Base64 } from \"effect/encoding\"\nimport { expect } from \"vitest\"\nimport * as FakeWebSocket from \"@effect-uai/core/testing/FakeWebSocket\"\nimport { streamSynthesis } from \"./realtimeTts.js\"\n\nconst request = { voiceId: \"voice-1\" }\n\nconst synthesizing = FakeWebSocket.make({\n greeting: [\n { audio: Base64.encode(new Uint8Array([1, 2])), isFinal: null },\n { audio: null, isFinal: true },\n ],\n})\n\n/** Run a synthesis against `server`, closing it, if asked, once the frames are in. */\nconst synthesize = <E>(\n server: FakeWebSocket.FakeWebSocketServer,\n textIn: Stream.Stream<string, E>,\n close?: { readonly code: number; readonly afterSent?: number },\n) =>\n Effect.gen(function* () {\n const run = streamSynthesis({ apiKey: Redacted.make(\"test-key\") })(textIn, request).pipe(\n Stream.provide(server.layer),\n )\n const collector = yield* Effect.forkChild(Effect.exit(Stream.runCollect(run)))\n if (close !== undefined) {\n yield* server.greeted\n yield* server.awaitSent(close.afterSent ?? 0)\n yield* server.close(close.code)\n }\n return yield* Fiber.join(collector)\n })\n\ndescribe(\"ElevenLabs realtime TTS socket\", () => {\n it.live(\"sends BOS before any text and EOS after it, and ends on a clean close\", () =>\n Effect.gen(function* () {\n const server = yield* synthesizing\n const exit = yield* synthesize(server, Stream.make(\"Hello\", \"\", \"world \"), {\n code: 1000,\n afterSent: 4,\n })\n\n expect(exit).toEqual(Exit.succeed([{ bytes: new Uint8Array([1, 2]) }]))\n expect(yield* server.sent).toEqual([\n { text: \" \", \"xi-api-key\": \"test-key\" },\n { text: \"Hello \" },\n { text: \"world \" },\n { text: \"\" },\n ])\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails rather than ending when the connection drops\", () =>\n Effect.gen(function* () {\n const server = yield* synthesizing\n const exit = yield* synthesize(server, Stream.never, { code: 1011 })\n\n expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain(\"Unavailable\")\n }).pipe(Effect.scoped),\n )\n\n it.live(\"fails with the text input's own error\", () =>\n Effect.gen(function* () {\n const server = yield* synthesizing\n const exit = yield* synthesize(server, Stream.fail(\"llm stream broke\"))\n\n expect(Exit.isFailure(exit) && JSON.stringify(exit.cause)).toContain(\"llm stream broke\")\n }).pipe(Effect.scoped),\n )\n})\n"],"mappings":";;;;;;;AAOA,MAAM,UAAU,EAAE,SAAS,UAAU;AAErC,MAAM,eAAe,cAAc,KAAK,EACtC,UAAU,CACR;CAAE,OAAO,OAAO,OAAO,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;CAAG,SAAS;AAAK,GAC9D;CAAE,OAAO;CAAM,SAAS;AAAK,CAC/B,EACF,CAAC;;AAGD,MAAM,cACJ,QACA,QACA,UAEA,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,gBAAgB,EAAE,QAAQ,SAAS,KAAK,UAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,KAClF,OAAO,QAAQ,OAAO,KAAK,CAC7B;CACA,MAAM,YAAY,OAAO,OAAO,UAAU,OAAO,KAAK,OAAO,WAAW,GAAG,CAAC,CAAC;CAC7E,IAAI,UAAU,KAAA,GAAW;EACvB,OAAO,OAAO;EACd,OAAO,OAAO,UAAU,MAAM,aAAa,CAAC;EAC5C,OAAO,OAAO,MAAM,MAAM,IAAI;CAChC;CACA,OAAO,OAAO,MAAM,KAAK,SAAS;AACpC,CAAC;AAEH,SAAS,wCAAwC;CAC/C,GAAG,KAAK,+EACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,KAAK,SAAS,IAAI,QAAQ,GAAG;GACzE,MAAM;GACN,WAAW;EACb,CAAC;EAED,aAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,OAAO,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;EACtE,aAAO,OAAO,OAAO,IAAI,CAAC,CAAC,QAAQ;GACjC;IAAE,MAAM;IAAK,cAAc;GAAW;GACtC,EAAE,MAAM,SAAS;GACjB,EAAE,MAAM,SAAS;GACjB,EAAE,MAAM,GAAG;EACb,CAAC;CACH,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,KAAK,4DACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,OAAO,EAAE,MAAM,KAAK,CAAC;EAEnE,aAAO,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,aAAa;CACpF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;CAEA,GAAG,KAAK,+CACN,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO,OAAO,WAAW,QAAQ,OAAO,KAAK,kBAAkB,CAAC;EAEtE,aAAO,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,kBAAkB;CACzF,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,CACvB;AACF,CAAC"}
|
package/dist/region.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as it, n as globalExpect, s as describe } from "./index.C4tKZ0yW-DjxCdpW_.mjs";
|
|
2
2
|
import { resolveHost } from "./region.mjs";
|
|
3
3
|
//#region src/region.test.ts
|
|
4
4
|
describe("elevenlabs/region.resolveHost", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/elevenlabs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "ElevenLabs speech-to-text, text-to-speech, and music-generation provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"effect": "4.0.
|
|
58
|
+
"effect": "4.0.1",
|
|
59
59
|
"typescript": "^7.0.2",
|
|
60
|
-
"@effect-uai/core": "0.
|
|
60
|
+
"@effect-uai/core": "0.18.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
64
|
-
"effect": ">=4.0.
|
|
64
|
+
"effect": ">=4.0.1 <5.0.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsdown",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Duration, Effect, Layer, Redacted, Stream } from "effect"
|
|
2
|
-
import { FetchHttpClient } from "effect/
|
|
2
|
+
import { FetchHttpClient } from "effect/http"
|
|
3
3
|
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
4
4
|
import type * as AiError from "@effect-uai/core/AiError"
|
|
5
5
|
import type { GenerateResult } from "@effect-uai/core/Music"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context, Duration, Effect, Layer, Redacted, Stream } from "effect"
|
|
2
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
3
|
import * as AiError from "@effect-uai/core/AiError"
|
|
4
4
|
import type { AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
|
|
5
5
|
import type {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Redacted, Stream } from "effect"
|
|
2
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
3
|
-
import * as Socket from "effect/
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
|
+
import * as Socket from "effect/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 {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Layer, Redacted, Stream } from "effect"
|
|
2
|
-
import { FetchHttpClient } from "effect/
|
|
3
|
-
import * as Socket from "effect/
|
|
2
|
+
import { FetchHttpClient } from "effect/http"
|
|
3
|
+
import * as Socket from "effect/socket/Socket"
|
|
4
4
|
import { describe, expect, expectTypeOf, it } from "vitest"
|
|
5
5
|
import type * as AiError from "@effect-uai/core/AiError"
|
|
6
6
|
import type { TranscriptResult } from "@effect-uai/core/Transcript"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Redacted, Schema, Stream } from "effect"
|
|
2
|
-
import { HttpClient, HttpClientRequest } from "effect/
|
|
3
|
-
import * as Socket from "effect/
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/http"
|
|
3
|
+
import * as Socket from "effect/socket/Socket"
|
|
4
4
|
import * as AiError from "@effect-uai/core/AiError"
|
|
5
5
|
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
6
6
|
import type { TranscriptResult, WordTimestamp } from "@effect-uai/core/Transcript"
|
package/src/codec.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Effect
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
2
3
|
import { describe, expect, it } from "vitest"
|
|
3
4
|
import type { AudioSource } from "@effect-uai/core/Audio"
|
|
4
5
|
import { audioToBlob, defaultFileName, formatToOutputSlug, httpStatusError } from "./codec.js"
|
|
@@ -74,7 +75,7 @@ describe("audioToBlob", () => {
|
|
|
74
75
|
const original = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
|
|
75
76
|
const source: AudioSource = {
|
|
76
77
|
_tag: "base64",
|
|
77
|
-
base64:
|
|
78
|
+
base64: Base64.encode(original),
|
|
78
79
|
mimeType: "audio/mpeg",
|
|
79
80
|
}
|
|
80
81
|
const blob = await Effect.runPromise(audioToBlob(source))
|
package/src/codec.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Array as Arr, Effect,
|
|
2
|
-
import {
|
|
1
|
+
import { Array as Arr, Effect, Match, Option, Result } from "effect"
|
|
2
|
+
import { Base64 } from "effect/encoding"
|
|
3
|
+
import { HttpClientRequest } from "effect/http"
|
|
3
4
|
import * as AiError from "@effect-uai/core/AiError"
|
|
4
5
|
import type { AudioFormat, AudioSource } from "@effect-uai/core/Audio"
|
|
5
6
|
import * as Multipart from "@effect-uai/core/Multipart"
|
|
@@ -139,7 +140,7 @@ const urlNotSupported: AiError.AiError = new AiError.InvalidRequest({
|
|
|
139
140
|
})
|
|
140
141
|
|
|
141
142
|
const decodeBase64ToBytes = (b64: string): Effect.Effect<Uint8Array, AiError.AiError> =>
|
|
142
|
-
Result.match(
|
|
143
|
+
Result.match(Base64.decode(b64), {
|
|
143
144
|
onSuccess: Effect.succeed,
|
|
144
145
|
onFailure: (cause) =>
|
|
145
146
|
Effect.fail(
|