@effect-uai/elevenlabs 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/ElevenLabsMusicGenerator.d.mts +75 -0
  2. package/dist/ElevenLabsMusicGenerator.d.mts.map +1 -0
  3. package/dist/ElevenLabsMusicGenerator.mjs +191 -0
  4. package/dist/ElevenLabsMusicGenerator.mjs.map +1 -0
  5. package/dist/ElevenLabsMusicGenerator.test.mjs +98 -0
  6. package/dist/ElevenLabsMusicGenerator.test.mjs.map +1 -0
  7. package/dist/ElevenLabsSynthesizer.d.mts +10 -4
  8. package/dist/ElevenLabsSynthesizer.d.mts.map +1 -1
  9. package/dist/ElevenLabsSynthesizer.mjs +9 -33
  10. package/dist/ElevenLabsSynthesizer.mjs.map +1 -1
  11. package/dist/ElevenLabsTranscriber.d.mts +4 -4
  12. package/dist/ElevenLabsTranscriber.d.mts.map +1 -1
  13. package/dist/ElevenLabsTranscriber.mjs +9 -1
  14. package/dist/ElevenLabsTranscriber.mjs.map +1 -1
  15. package/dist/codec.d.mts +25 -2
  16. package/dist/codec.d.mts.map +1 -1
  17. package/dist/codec.mjs +18 -1
  18. package/dist/codec.mjs.map +1 -1
  19. package/dist/index.d.mts +4 -3
  20. package/dist/index.mjs +2 -1
  21. package/dist/models.d.mts +13 -1
  22. package/dist/models.d.mts.map +1 -1
  23. package/dist/musicCodec-CZni1gAQ.d.mts +42 -0
  24. package/dist/musicCodec-CZni1gAQ.d.mts.map +1 -0
  25. package/dist/musicCodec.d.mts +2 -0
  26. package/dist/musicCodec.mjs +38 -0
  27. package/dist/musicCodec.mjs.map +1 -0
  28. package/dist/realtimeStt.d.mts +2 -1
  29. package/dist/realtimeStt.d.mts.map +1 -1
  30. package/dist/realtimeStt.mjs +14 -3
  31. package/dist/realtimeStt.mjs.map +1 -1
  32. package/dist/realtimeTts.d.mts +8 -3
  33. package/dist/realtimeTts.d.mts.map +1 -1
  34. package/dist/realtimeTts.mjs +5 -3
  35. package/dist/realtimeTts.mjs.map +1 -1
  36. package/package.json +9 -3
  37. package/src/ElevenLabsMusicGenerator.test.ts +116 -0
  38. package/src/ElevenLabsMusicGenerator.ts +353 -0
  39. package/src/ElevenLabsSynthesizer.ts +17 -51
  40. package/src/ElevenLabsTranscriber.ts +11 -0
  41. package/src/codec.ts +47 -0
  42. package/src/index.ts +1 -0
  43. package/src/models.ts +17 -0
  44. package/src/musicCodec.ts +86 -0
  45. package/src/realtimeStt.ts +11 -0
  46. package/src/realtimeTts.ts +11 -0
  47. package/dist/ElevenLabsSynthesizer.test.mjs +0 -32
  48. package/dist/ElevenLabsSynthesizer.test.mjs.map +0 -1
  49. package/src/ElevenLabsSynthesizer.test.ts +0 -49
  50. /package/dist/{ElevenLabsSynthesizer.test.d.mts → ElevenLabsMusicGenerator.test.d.mts} +0 -0
@@ -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\"\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
+ {"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(\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 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,9 +1,10 @@
1
- import { VoiceSettings } from "./codec.mjs";
2
1
  import { ElevenLabsTtsModel, ElevenLabsVoiceId } from "./models.mjs";
3
2
  import { ElevenLabsRegion } from "./region.mjs";
3
+ import { PronunciationDictionaryLocator, VoiceSettings } from "./codec.mjs";
4
4
  import { Redacted, Stream } from "effect";
5
- import * as Socket from "effect/unstable/socket/Socket";
6
5
  import * as AiError from "@effect-uai/core/AiError";
6
+ import * as Socket from "effect/unstable/socket/Socket";
7
+ import { CustomPronunciation } from "@effect-uai/core/SpeechSynthesizer";
7
8
  import { AudioChunk, AudioFormat } from "@effect-uai/core/Audio";
8
9
 
9
10
  //#region src/realtimeTts.d.ts
@@ -24,7 +25,11 @@ type StreamSynthesizeRequest = {
24
25
  readonly outputFormat?: AudioFormat;
25
26
  readonly languageCode?: string;
26
27
  readonly voiceSettings?: VoiceSettings;
27
- readonly autoMode?: boolean;
28
+ readonly autoMode?: boolean; /** Pre-provisioned pronunciation dictionaries, sent on the BOS frame. */
29
+ readonly pronunciationDictionaryLocators?: ReadonlyArray<PronunciationDictionaryLocator>;
30
+ /** Carried from the Common request so the WS path can reject inline
31
+ * pronunciations (ElevenLabs has no stateless inline IPA path). */
32
+ readonly pronunciations?: ReadonlyArray<CustomPronunciation>;
28
33
  };
29
34
  declare const buildWsUrl: (cfg: Config, request: StreamSynthesizeRequest, outputFormat: string) => string;
30
35
  declare const streamSynthesis: (cfg: Config) => <E, R>(textIn: Stream.Stream<string, E, R>, request: StreamSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError | E, R | Socket.WebSocketConstructor>;
@@ -1 +1 @@
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
+ {"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;;;;;KAkBY,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,YAHe;EAAA,SAKf,+BAAA,GAAkC,aAAA,CAAc,8BAAA;EAAA;;EAAA,SAGhD,cAAA,GAAiB,aAAA,CAAc,mBAAA;AAAA;AAAA,cAO7B,UAAA,GAAc,GAAA,EAAK,MAAA,EAAQ,OAAA,EAAS,uBAAA,EAAyB,YAAA;AAAA,cAuE7D,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,8 +1,8 @@
1
- import { defaultFormat, formatToOutputSlug, wireVoiceSettings } from "./codec.mjs";
1
+ import { defaultFormat, formatToOutputSlug, rejectInlinePronunciations, wirePronunciationLocators, wireVoiceSettings } from "./codec.mjs";
2
2
  import { resolveHost } from "./region.mjs";
3
3
  import { Effect, Encoding, Queue, Redacted, Result, Schema, Stream } from "effect";
4
- import * as Socket from "effect/unstable/socket/Socket";
5
4
  import * as AiError from "@effect-uai/core/AiError";
5
+ import * as Socket from "effect/unstable/socket/Socket";
6
6
  import * as JSONL from "@effect-uai/core/JSONL";
7
7
  //#region src/realtimeTts.ts
8
8
  const buildWsUrl = (cfg, request, outputFormat) => {
@@ -20,7 +20,8 @@ const bosFrame = (cfg, request) => {
20
20
  return JSON.stringify({
21
21
  text: " ",
22
22
  "xi-api-key": Redacted.value(cfg.apiKey),
23
- ...vs !== void 0 && { voice_settings: vs }
23
+ ...vs !== void 0 && { voice_settings: vs },
24
+ ...wirePronunciationLocators(request.pronunciationDictionaryLocators)
24
25
  });
25
26
  };
26
27
  const textFrame = (text) => JSON.stringify({ text: text.endsWith(" ") ? text : `${text} ` });
@@ -60,6 +61,7 @@ const handleServerFrame = (queue) => (raw) => Effect.gen(function* () {
60
61
  if (bytes._tag === "Some") yield* Queue.offer(queue, { bytes: bytes.value });
61
62
  });
62
63
  const streamSynthesis = (cfg) => (textIn, request) => Stream.unwrap(Effect.gen(function* () {
64
+ yield* rejectInlinePronunciations(request.pronunciations);
63
65
  const slug = yield* formatToOutputSlug(request.outputFormat ?? defaultFormat);
64
66
  const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg, request, slug), { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 });
65
67
  const queue = yield* Queue.bounded(64);
@@ -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\"\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":"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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 type { CustomPronunciation } from \"@effect-uai/core/SpeechSynthesizer\"\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(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 yield* rejectInlinePronunciations(request.pronunciations)\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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"}
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@effect-uai/elevenlabs",
3
- "version": "0.6.0",
4
- "description": "ElevenLabs speech-to-text and text-to-speech provider for @effect-uai/core.",
3
+ "version": "0.8.0",
4
+ "description": "ElevenLabs speech-to-text, text-to-speech, and music-generation provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "effect",
8
8
  "elevenlabs",
9
+ "music",
10
+ "music-generation",
9
11
  "speech",
10
12
  "stt",
11
13
  "transcription",
@@ -36,6 +38,10 @@
36
38
  "types": "./dist/index.d.mts",
37
39
  "import": "./dist/index.mjs"
38
40
  },
41
+ "./ElevenLabsMusicGenerator": {
42
+ "types": "./dist/ElevenLabsMusicGenerator.d.mts",
43
+ "import": "./dist/ElevenLabsMusicGenerator.mjs"
44
+ },
39
45
  "./ElevenLabsSynthesizer": {
40
46
  "types": "./dist/ElevenLabsSynthesizer.d.mts",
41
47
  "import": "./dist/ElevenLabsSynthesizer.mjs"
@@ -51,7 +57,7 @@
51
57
  "devDependencies": {
52
58
  "effect": "4.0.0-beta.57",
53
59
  "typescript": "^6.0.3",
54
- "@effect-uai/core": "0.6.0"
60
+ "@effect-uai/core": "0.8.0"
55
61
  },
56
62
  "peerDependencies": {
57
63
  "@effect-uai/core": ">=0.2.0 <1",
@@ -0,0 +1,116 @@
1
+ import { Duration, Effect, Layer, Redacted, Stream } from "effect"
2
+ import { FetchHttpClient } from "effect/unstable/http"
3
+ import { describe, expect, expectTypeOf, it } from "vitest"
4
+ import type * as AiError from "@effect-uai/core/AiError"
5
+ import type { GenerateResult } from "@effect-uai/core/Music"
6
+ import { promptsInput } from "@effect-uai/core/Music"
7
+ import * as MusicGenerator from "@effect-uai/core/MusicGenerator"
8
+ import * as ElevenLabsMusicGenerator from "./ElevenLabsMusicGenerator.js"
9
+
10
+ const cfg: ElevenLabsMusicGenerator.Config = { apiKey: Redacted.make("test-key") }
11
+ const live = Layer.provide(ElevenLabsMusicGenerator.layer(cfg), FetchHttpClient.layer)
12
+
13
+ describe("ElevenLabsMusicGenerator capability guards (runtime)", () => {
14
+ it("streamGenerationFrom returns an Unsupported stream", async () => {
15
+ const program = ElevenLabsMusicGenerator.ElevenLabsMusicGenerator.use((s) =>
16
+ Stream.runDrain(
17
+ s.streamGenerationFrom(Stream.fromIterable([promptsInput([{ text: "x" }])]), {
18
+ model: "music_v1",
19
+ prompt: "",
20
+ }),
21
+ ),
22
+ )
23
+ const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))
24
+ expect(exit._tag).toBe("Failure")
25
+ if (exit._tag === "Failure") {
26
+ expect(JSON.stringify(exit.cause)).toContain("Unsupported")
27
+ expect(JSON.stringify(exit.cause)).toContain("streamGenerationFrom")
28
+ }
29
+ })
30
+ })
31
+
32
+ describe("ElevenLabsMusicGenerator Layer (compile-time)", () => {
33
+ it("leaves `MusicInteractiveSession` unsatisfied via the generic surface", () => {
34
+ const inputs = Stream.fromIterable([promptsInput([{ text: "techno" }])])
35
+ const events = inputs.pipe(
36
+ MusicGenerator.streamGenerationFrom({ model: "music_v1", prompt: "" }),
37
+ )
38
+ const provided = Stream.runDrain(events).pipe(Effect.provide(live))
39
+ expectTypeOf(provided).toEqualTypeOf<
40
+ Effect.Effect<void, AiError.AiError, MusicGenerator.MusicInteractiveSession>
41
+ >()
42
+ })
43
+
44
+ it("sync `generate` returns GenerateResult and requires no marker", () => {
45
+ const gen = MusicGenerator.generate({ model: "music_v1", prompt: "house" }).pipe(
46
+ Effect.provide(live),
47
+ )
48
+ expectTypeOf(gen).toEqualTypeOf<Effect.Effect<GenerateResult, AiError.AiError, never>>()
49
+ })
50
+
51
+ it("typed request accepts compositionPlan, forceInstrumental, signWithC2pa", () => {
52
+ const _req: ElevenLabsMusicGenerator.ElevenLabsMusicGenerateRequest = {
53
+ model: "music_v1",
54
+ prompt: "",
55
+ compositionPlan: {
56
+ positiveGlobalStyles: ["lo-fi", "warm"],
57
+ negativeGlobalStyles: ["distorted"],
58
+ sections: [
59
+ {
60
+ sectionName: "Intro",
61
+ positiveLocalStyles: ["piano"],
62
+ negativeLocalStyles: [],
63
+ duration: Duration.seconds(15),
64
+ lines: [],
65
+ },
66
+ ],
67
+ },
68
+ forceInstrumental: true,
69
+ signWithC2pa: true,
70
+ }
71
+ expect(_req).toBeDefined()
72
+ })
73
+ })
74
+
75
+ describe("ElevenLabsMusicGenerator codec validation (runtime, no HTTP)", () => {
76
+ it("rejects compositionPlan + non-empty prompt", async () => {
77
+ const program = ElevenLabsMusicGenerator.ElevenLabsMusicGenerator.use((s) =>
78
+ s.generate({
79
+ model: "music_v1",
80
+ prompt: "house",
81
+ compositionPlan: {
82
+ positiveGlobalStyles: [],
83
+ negativeGlobalStyles: [],
84
+ sections: [],
85
+ },
86
+ }),
87
+ )
88
+ const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))
89
+ expect(exit._tag).toBe("Failure")
90
+ if (exit._tag === "Failure") {
91
+ expect(JSON.stringify(exit.cause)).toContain("InvalidRequest")
92
+ expect(JSON.stringify(exit.cause)).toContain("mutually exclusive")
93
+ }
94
+ })
95
+
96
+ it("rejects compositionPlan + duration", async () => {
97
+ const program = ElevenLabsMusicGenerator.ElevenLabsMusicGenerator.use((s) =>
98
+ s.generate({
99
+ model: "music_v1",
100
+ prompt: "",
101
+ duration: Duration.seconds(30),
102
+ compositionPlan: {
103
+ positiveGlobalStyles: [],
104
+ negativeGlobalStyles: [],
105
+ sections: [],
106
+ },
107
+ }),
108
+ )
109
+ const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))
110
+ expect(exit._tag).toBe("Failure")
111
+ if (exit._tag === "Failure") {
112
+ expect(JSON.stringify(exit.cause)).toContain("InvalidRequest")
113
+ expect(JSON.stringify(exit.cause)).toContain("`duration` is ignored")
114
+ }
115
+ })
116
+ })