@effect-uai/inworld 0.4.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/dist/InworldRealtimeSynthesizer.d.mts +30 -0
  3. package/dist/InworldRealtimeSynthesizer.d.mts.map +1 -0
  4. package/dist/InworldRealtimeSynthesizer.mjs +43 -0
  5. package/dist/InworldRealtimeSynthesizer.mjs.map +1 -0
  6. package/dist/InworldRealtimeTranscriber.d.mts +30 -0
  7. package/dist/InworldRealtimeTranscriber.d.mts.map +1 -0
  8. package/dist/InworldRealtimeTranscriber.mjs +37 -0
  9. package/dist/InworldRealtimeTranscriber.mjs.map +1 -0
  10. package/dist/InworldSynthesizer.d.mts +71 -0
  11. package/dist/InworldSynthesizer.d.mts.map +1 -0
  12. package/dist/InworldSynthesizer.mjs +131 -0
  13. package/dist/InworldSynthesizer.mjs.map +1 -0
  14. package/dist/InworldTranscriber.d.mts +48 -0
  15. package/dist/InworldTranscriber.d.mts.map +1 -0
  16. package/dist/InworldTranscriber.mjs +116 -0
  17. package/dist/InworldTranscriber.mjs.map +1 -0
  18. package/dist/chunk-CfYAbeIz.mjs +13 -0
  19. package/dist/codec-C-2ssVAz.d.mts +35 -0
  20. package/dist/codec-C-2ssVAz.d.mts.map +1 -0
  21. package/dist/codec.d.mts +2 -0
  22. package/dist/codec.mjs +100 -0
  23. package/dist/codec.mjs.map +1 -0
  24. package/dist/index.d.mts +7 -0
  25. package/dist/index.mjs +7 -0
  26. package/dist/models-CPLZDcgt.d.mts +47 -0
  27. package/dist/models-CPLZDcgt.d.mts.map +1 -0
  28. package/dist/models.d.mts +2 -0
  29. package/dist/models.mjs +1 -0
  30. package/dist/realtimeStt.d.mts +38 -0
  31. package/dist/realtimeStt.d.mts.map +1 -0
  32. package/dist/realtimeStt.mjs +145 -0
  33. package/dist/realtimeStt.mjs.map +1 -0
  34. package/dist/realtimeTts.d.mts +29 -0
  35. package/dist/realtimeTts.d.mts.map +1 -0
  36. package/dist/realtimeTts.mjs +94 -0
  37. package/dist/realtimeTts.mjs.map +1 -0
  38. package/dist/wsAuth.d.mts +8 -0
  39. package/dist/wsAuth.d.mts.map +1 -0
  40. package/dist/wsAuth.mjs +8 -0
  41. package/dist/wsAuth.mjs.map +1 -0
  42. package/package.json +80 -0
  43. package/src/InworldRealtimeSynthesizer.ts +76 -0
  44. package/src/InworldRealtimeTranscriber.ts +64 -0
  45. package/src/InworldSynthesizer.ts +235 -0
  46. package/src/InworldTranscriber.ts +225 -0
  47. package/src/codec.ts +141 -0
  48. package/src/index.ts +6 -0
  49. package/src/models.ts +69 -0
  50. package/src/realtimeStt.ts +236 -0
  51. package/src/realtimeTts.ts +167 -0
  52. package/src/wsAuth.ts +21 -0
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,35 @@
1
+ import { t as InworldAudioEncoding } from "./models-CPLZDcgt.mjs";
2
+ import { Effect, Redacted } from "effect";
3
+ import * as AiError from "@effect-uai/core/AiError";
4
+ import { AudioFormat } from "@effect-uai/core/Audio";
5
+
6
+ //#region src/codec.d.ts
7
+ declare namespace codec_d_exports {
8
+ export { AudioConfig, audioConfigFor, audioEncodingFor, authHeader, decodeAudioContent, defaultFormat, httpStatusError, transportFailure };
9
+ }
10
+ declare const authHeader: (apiKey: Redacted.Redacted) => string;
11
+ type AudioConfig = {
12
+ readonly audioEncoding: InworldAudioEncoding;
13
+ readonly sampleRateHertz?: number;
14
+ readonly bitRate?: number;
15
+ readonly speakingRate?: number;
16
+ };
17
+ /**
18
+ * Map our cross-provider `AudioFormat` to Inworld's `audioConfig.audioEncoding`.
19
+ *
20
+ * Inworld quirk: sync `LINEAR16` / `WAV` responses include a WAV header in
21
+ * the returned `audioContent`; streaming (NDJSON + WS) responses for the
22
+ * same encoding do NOT include the header. Callers consuming raw PCM via
23
+ * streaming should request `raw + pcm_s16le` (→ `PCM`) for predictable
24
+ * concatenation, and reserve `wav` for sync.
25
+ */
26
+ declare const audioEncodingFor: (format: AudioFormat) => Effect.Effect<InworldAudioEncoding, AiError.AiError>;
27
+ declare const audioConfigFor: (format: AudioFormat, speakingRate?: number) => Effect.Effect<AudioConfig, AiError.AiError>;
28
+ /** Default Inworld output: 24 kHz mono mp3. Matches Inworld's docs default. */
29
+ declare const defaultFormat: AudioFormat;
30
+ declare const decodeAudioContent: (b64: string) => Effect.Effect<Uint8Array, AiError.AiError>;
31
+ declare const httpStatusError: (status: number, body: string) => AiError.AiError;
32
+ declare const transportFailure: (cause: unknown) => AiError.AiError;
33
+ //#endregion
34
+ export { codec_d_exports as a, httpStatusError as c, authHeader as i, transportFailure as l, audioConfigFor as n, decodeAudioContent as o, audioEncodingFor as r, defaultFormat as s, AudioConfig as t };
35
+ //# sourceMappingURL=codec-C-2ssVAz.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec-C-2ssVAz.d.mts","names":[],"sources":["../src/codec.ts"],"mappings":";;;;;;;;;cAUa,UAAA,GAAc,MAAA,EAAQ,QAAA,CAAS,QAAA;AAAA,KAMhC,WAAA;EAAA,SACD,aAAA,EAAe,oBAAA;EAAA,SACf,eAAA;EAAA,SACA,OAAA;EAAA,SACA,YAAA;AAAA;;;;;;;AAVX;;;cA6Ba,gBAAA,GACX,MAAA,EAAQ,WAAA,KACL,MAAA,CAAO,MAAA,CAAO,oBAAA,EAAsB,OAAA,CAAQ,OAAA;AAAA,cAoBpC,cAAA,GACX,MAAA,EAAQ,WAAA,EACR,YAAA,cACC,MAAA,CAAO,MAAA,CAAO,WAAA,EAAa,OAAA,CAAQ,OAAA;;cASzB,aAAA,EAAe,WAAA;AAAA,cAUf,kBAAA,GAAsB,GAAA,aAAc,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;AAAA,cAgBtE,eAAA,GAAkB,MAAA,UAAgB,IAAA,aAAiB,OAAA,CAAQ,OAAA;AAAA,cAwC3D,gBAAA,GAAoB,KAAA,cAAiB,OAAA,CAAQ,OAAA"}
@@ -0,0 +1,2 @@
1
+ import { c as httpStatusError, i as authHeader, l as transportFailure, n as audioConfigFor, o as decodeAudioContent, r as audioEncodingFor, s as defaultFormat, t as AudioConfig } from "./codec-C-2ssVAz.mjs";
2
+ export { AudioConfig, audioConfigFor, audioEncodingFor, authHeader, decodeAudioContent, defaultFormat, httpStatusError, transportFailure };
package/dist/codec.mjs ADDED
@@ -0,0 +1,100 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import { Effect, Encoding, Match, Redacted, Result } from "effect";
3
+ import * as AiError from "@effect-uai/core/AiError";
4
+ //#region src/codec.ts
5
+ var codec_exports = /* @__PURE__ */ __exportAll({
6
+ audioConfigFor: () => audioConfigFor,
7
+ audioEncodingFor: () => audioEncodingFor,
8
+ authHeader: () => authHeader,
9
+ decodeAudioContent: () => decodeAudioContent,
10
+ defaultFormat: () => defaultFormat,
11
+ httpStatusError: () => httpStatusError,
12
+ transportFailure: () => transportFailure
13
+ });
14
+ const authHeader = (apiKey) => `Basic ${Redacted.value(apiKey)}`;
15
+ const unsupportedFormat = (format) => new AiError.Unsupported({
16
+ provider: "inworld",
17
+ capability: "outputFormat",
18
+ reason: `Cannot encode ${JSON.stringify(format)} as Inworld audioConfig. Supported: mp3, wav (LINEAR16), ogg+opus, flac, raw pcm_s16le/mulaw/alaw.`
19
+ });
20
+ /**
21
+ * Map our cross-provider `AudioFormat` to Inworld's `audioConfig.audioEncoding`.
22
+ *
23
+ * Inworld quirk: sync `LINEAR16` / `WAV` responses include a WAV header in
24
+ * the returned `audioContent`; streaming (NDJSON + WS) responses for the
25
+ * same encoding do NOT include the header. Callers consuming raw PCM via
26
+ * streaming should request `raw + pcm_s16le` (→ `PCM`) for predictable
27
+ * concatenation, and reserve `wav` for sync.
28
+ */
29
+ const audioEncodingFor = (format) => Match.value(format).pipe(Match.when({ container: "mp3" }, () => Effect.succeed("MP3")), Match.when({ container: "wav" }, () => Effect.succeed("LINEAR16")), Match.when({
30
+ container: "ogg",
31
+ encoding: "opus"
32
+ }, () => Effect.succeed("OGG_OPUS")), Match.when({ container: "flac" }, () => Effect.succeed("FLAC")), Match.when({
33
+ container: "raw",
34
+ encoding: "pcm_s16le"
35
+ }, () => Effect.succeed("PCM")), Match.when({
36
+ container: "raw",
37
+ encoding: "pcm_mulaw"
38
+ }, () => Effect.succeed("MULAW")), Match.when({
39
+ container: "raw",
40
+ encoding: "pcm_alaw"
41
+ }, () => Effect.succeed("ALAW")), Match.orElse((f) => Effect.fail(unsupportedFormat(f))));
42
+ const audioConfigFor = (format, speakingRate) => Effect.map(audioEncodingFor(format), (audioEncoding) => ({
43
+ audioEncoding,
44
+ sampleRateHertz: format.sampleRate,
45
+ ...format.bitRate !== void 0 && { bitRate: format.bitRate },
46
+ ...speakingRate !== void 0 && { speakingRate }
47
+ }));
48
+ /** Default Inworld output: 24 kHz mono mp3. Matches Inworld's docs default. */
49
+ const defaultFormat = {
50
+ container: "mp3",
51
+ encoding: "mp3",
52
+ sampleRate: 24e3
53
+ };
54
+ const decodeAudioContent = (b64) => Result.match(Encoding.decodeBase64(b64), {
55
+ onSuccess: Effect.succeed,
56
+ onFailure: (cause) => Effect.fail(new AiError.GenerationFailed({
57
+ provider: "inworld",
58
+ raw: {
59
+ message: "audioContent base64 decode failed",
60
+ cause
61
+ }
62
+ }))
63
+ });
64
+ const httpStatusError = (status, body) => Match.value(status).pipe(Match.when(429, () => new AiError.RateLimited({
65
+ provider: "inworld",
66
+ raw: body
67
+ })), Match.whenOr(408, 504, () => new AiError.Timeout({
68
+ provider: "inworld",
69
+ raw: body
70
+ })), Match.when(401, () => new AiError.AuthFailed({
71
+ provider: "inworld",
72
+ subtype: "auth",
73
+ raw: body
74
+ })), Match.when(403, () => new AiError.AuthFailed({
75
+ provider: "inworld",
76
+ subtype: "permission",
77
+ raw: body
78
+ })), Match.when(402, () => new AiError.AuthFailed({
79
+ provider: "inworld",
80
+ subtype: "billing",
81
+ raw: body
82
+ })), Match.when(413, () => new AiError.ContextLengthExceeded({
83
+ provider: "inworld",
84
+ raw: body
85
+ })), Match.when((n) => n >= 500, (n) => new AiError.Unavailable({
86
+ provider: "inworld",
87
+ status: n,
88
+ raw: body
89
+ })), Match.orElse(() => new AiError.InvalidRequest({
90
+ provider: "inworld",
91
+ raw: body
92
+ })));
93
+ const transportFailure = (cause) => new AiError.Unavailable({
94
+ provider: "inworld",
95
+ raw: cause
96
+ });
97
+ //#endregion
98
+ export { audioConfigFor, audioEncodingFor, authHeader, decodeAudioContent, defaultFormat, httpStatusError, codec_exports as t, transportFailure };
99
+
100
+ //# sourceMappingURL=codec.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.mjs","names":[],"sources":["../src/codec.ts"],"sourcesContent":["import { Effect, Encoding, Match, Redacted, Result } from \"effect\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioFormat } from \"@effect-uai/core/Audio\"\nimport type { InworldAudioEncoding } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Auth — `Authorization: Basic $INWORLD_API_KEY`. The portal key is already\n// base64-encoded, so we inject it verbatim. Do NOT re-encode `key:secret`.\n// ---------------------------------------------------------------------------\n\nexport const authHeader = (apiKey: Redacted.Redacted): string => `Basic ${Redacted.value(apiKey)}`\n\n// ---------------------------------------------------------------------------\n// AudioFormat → Inworld `audioConfig`\n// ---------------------------------------------------------------------------\n\nexport type AudioConfig = {\n readonly audioEncoding: InworldAudioEncoding\n readonly sampleRateHertz?: number\n readonly bitRate?: number\n readonly speakingRate?: number\n}\n\nconst unsupportedFormat = (format: AudioFormat): AiError.AiError =>\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"outputFormat\",\n reason: `Cannot encode ${JSON.stringify(format)} as Inworld audioConfig. Supported: mp3, wav (LINEAR16), ogg+opus, flac, raw pcm_s16le/mulaw/alaw.`,\n })\n\n/**\n * Map our cross-provider `AudioFormat` to Inworld's `audioConfig.audioEncoding`.\n *\n * Inworld quirk: sync `LINEAR16` / `WAV` responses include a WAV header in\n * the returned `audioContent`; streaming (NDJSON + WS) responses for the\n * same encoding do NOT include the header. Callers consuming raw PCM via\n * streaming should request `raw + pcm_s16le` (→ `PCM`) for predictable\n * concatenation, and reserve `wav` for sync.\n */\nexport const audioEncodingFor: (\n format: AudioFormat,\n) => Effect.Effect<InworldAudioEncoding, AiError.AiError> = (format) =>\n Match.value(format).pipe(\n Match.when({ container: \"mp3\" }, () => Effect.succeed<InworldAudioEncoding>(\"MP3\")),\n Match.when({ container: \"wav\" }, () => Effect.succeed<InworldAudioEncoding>(\"LINEAR16\")),\n Match.when({ container: \"ogg\", encoding: \"opus\" }, () =>\n Effect.succeed<InworldAudioEncoding>(\"OGG_OPUS\"),\n ),\n Match.when({ container: \"flac\" }, () => Effect.succeed<InworldAudioEncoding>(\"FLAC\")),\n Match.when({ container: \"raw\", encoding: \"pcm_s16le\" }, () =>\n Effect.succeed<InworldAudioEncoding>(\"PCM\"),\n ),\n Match.when({ container: \"raw\", encoding: \"pcm_mulaw\" }, () =>\n Effect.succeed<InworldAudioEncoding>(\"MULAW\"),\n ),\n Match.when({ container: \"raw\", encoding: \"pcm_alaw\" }, () =>\n Effect.succeed<InworldAudioEncoding>(\"ALAW\"),\n ),\n Match.orElse((f) => Effect.fail(unsupportedFormat(f))),\n )\n\nexport const audioConfigFor = (\n format: AudioFormat,\n speakingRate?: number,\n): Effect.Effect<AudioConfig, AiError.AiError> =>\n Effect.map(audioEncodingFor(format), (audioEncoding) => ({\n audioEncoding,\n sampleRateHertz: format.sampleRate,\n ...(format.bitRate !== undefined && { bitRate: format.bitRate }),\n ...(speakingRate !== undefined && { speakingRate }),\n }))\n\n/** Default Inworld output: 24 kHz mono mp3. Matches Inworld's docs default. */\nexport const defaultFormat: AudioFormat = {\n container: \"mp3\",\n encoding: \"mp3\",\n sampleRate: 24000,\n}\n\n// ---------------------------------------------------------------------------\n// base64 → bytes — decoding `audioContent` from sync, NDJSON, and WS responses\n// ---------------------------------------------------------------------------\n\nexport const decodeAudioContent = (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: \"inworld\",\n raw: { message: \"audioContent base64 decode failed\", cause },\n }),\n ),\n })\n\n// ---------------------------------------------------------------------------\n// HTTP status → AiError\n// ---------------------------------------------------------------------------\n\nexport const httpStatusError: (status: number, body: string) => AiError.AiError = (status, body) =>\n Match.value(status).pipe(\n Match.when(\n 429,\n (): AiError.AiError => new AiError.RateLimited({ provider: \"inworld\", raw: body }),\n ),\n Match.whenOr(\n 408,\n 504,\n (): AiError.AiError => new AiError.Timeout({ provider: \"inworld\", raw: body }),\n ),\n Match.when(\n 401,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"inworld\", subtype: \"auth\", raw: body }),\n ),\n Match.when(\n 403,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"inworld\", subtype: \"permission\", raw: body }),\n ),\n Match.when(\n 402,\n (): AiError.AiError =>\n new AiError.AuthFailed({ provider: \"inworld\", subtype: \"billing\", raw: body }),\n ),\n Match.when(\n 413,\n (): AiError.AiError => new AiError.ContextLengthExceeded({ provider: \"inworld\", raw: body }),\n ),\n Match.when(\n (n) => n >= 500,\n (n): AiError.AiError =>\n new AiError.Unavailable({ provider: \"inworld\", status: n, raw: body }),\n ),\n Match.orElse(\n (): AiError.AiError => new AiError.InvalidRequest({ provider: \"inworld\", raw: body }),\n ),\n )\n\nexport const transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"inworld\", raw: cause })\n"],"mappings":";;;;;;;;;;;;;AAUA,MAAa,cAAc,WAAsC,SAAS,SAAS,MAAM,OAAO;AAahG,MAAM,qBAAqB,WACzB,IAAI,QAAQ,YAAY;CACtB,UAAU;CACV,YAAY;CACZ,QAAQ,iBAAiB,KAAK,UAAU,OAAO,CAAC;CACjD,CAAC;;;;;;;;;;AAWJ,MAAa,oBAEgD,WAC3D,MAAM,MAAM,OAAO,CAAC,KAClB,MAAM,KAAK,EAAE,WAAW,OAAO,QAAQ,OAAO,QAA8B,MAAM,CAAC,EACnF,MAAM,KAAK,EAAE,WAAW,OAAO,QAAQ,OAAO,QAA8B,WAAW,CAAC,EACxF,MAAM,KAAK;CAAE,WAAW;CAAO,UAAU;CAAQ,QAC/C,OAAO,QAA8B,WAAW,CACjD,EACD,MAAM,KAAK,EAAE,WAAW,QAAQ,QAAQ,OAAO,QAA8B,OAAO,CAAC,EACrF,MAAM,KAAK;CAAE,WAAW;CAAO,UAAU;CAAa,QACpD,OAAO,QAA8B,MAAM,CAC5C,EACD,MAAM,KAAK;CAAE,WAAW;CAAO,UAAU;CAAa,QACpD,OAAO,QAA8B,QAAQ,CAC9C,EACD,MAAM,KAAK;CAAE,WAAW;CAAO,UAAU;CAAY,QACnD,OAAO,QAA8B,OAAO,CAC7C,EACD,MAAM,QAAQ,MAAM,OAAO,KAAK,kBAAkB,EAAE,CAAC,CAAC,CACvD;AAEH,MAAa,kBACX,QACA,iBAEA,OAAO,IAAI,iBAAiB,OAAO,GAAG,mBAAmB;CACvD;CACA,iBAAiB,OAAO;CACxB,GAAI,OAAO,YAAY,KAAA,KAAa,EAAE,SAAS,OAAO,SAAS;CAC/D,GAAI,iBAAiB,KAAA,KAAa,EAAE,cAAc;CACnD,EAAE;;AAGL,MAAa,gBAA6B;CACxC,WAAW;CACX,UAAU;CACV,YAAY;CACb;AAMD,MAAa,sBAAsB,QACjC,OAAO,MAAM,SAAS,aAAa,IAAI,EAAE;CACvC,WAAW,OAAO;CAClB,YAAY,UACV,OAAO,KACL,IAAI,QAAQ,iBAAiB;EAC3B,UAAU;EACV,KAAK;GAAE,SAAS;GAAqC;GAAO;EAC7D,CAAC,CACH;CACJ,CAAC;AAMJ,MAAa,mBAAsE,QAAQ,SACzF,MAAM,MAAM,OAAO,CAAC,KAClB,MAAM,KACJ,WACuB,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,KAAK;CAAM,CAAC,CACnF,EACD,MAAM,OACJ,KACA,WACuB,IAAI,QAAQ,QAAQ;CAAE,UAAU;CAAW,KAAK;CAAM,CAAC,CAC/E,EACD,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAQ,KAAK;CAAM,CAAC,CAC9E,EACD,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAc,KAAK;CAAM,CAAC,CACpF,EACD,MAAM,KACJ,WAEE,IAAI,QAAQ,WAAW;CAAE,UAAU;CAAW,SAAS;CAAW,KAAK;CAAM,CAAC,CACjF,EACD,MAAM,KACJ,WACuB,IAAI,QAAQ,sBAAsB;CAAE,UAAU;CAAW,KAAK;CAAM,CAAC,CAC7F,EACD,MAAM,MACH,MAAM,KAAK,MACX,MACC,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,QAAQ;CAAG,KAAK;CAAM,CAAC,CACzE,EACD,MAAM,aACmB,IAAI,QAAQ,eAAe;CAAE,UAAU;CAAW,KAAK;CAAM,CAAC,CACtF,CACF;AAEH,MAAa,oBAAoB,UAC/B,IAAI,QAAQ,YAAY;CAAE,UAAU;CAAW,KAAK;CAAO,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { a as InworldVoiceId, i as InworldTtsModel, n as InworldDeliveryMode, r as InworldSttModel, t as InworldAudioEncoding } from "./models-CPLZDcgt.mjs";
2
+ import { a as codec_d_exports } from "./codec-C-2ssVAz.mjs";
3
+ import { t as InworldSynthesizer_d_exports } from "./InworldSynthesizer.mjs";
4
+ import { t as InworldRealtimeSynthesizer_d_exports } from "./InworldRealtimeSynthesizer.mjs";
5
+ import { t as InworldTranscriber_d_exports } from "./InworldTranscriber.mjs";
6
+ import { t as InworldRealtimeTranscriber_d_exports } from "./InworldRealtimeTranscriber.mjs";
7
+ export { InworldAudioEncoding, InworldDeliveryMode, InworldRealtimeSynthesizer_d_exports as InworldRealtimeSynthesizer, InworldRealtimeTranscriber_d_exports as InworldRealtimeTranscriber, InworldSttModel, InworldSynthesizer_d_exports as InworldSynthesizer, InworldTranscriber_d_exports as InworldTranscriber, InworldTtsModel, InworldVoiceId, codec_d_exports as codec };
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import { t as codec_exports } from "./codec.mjs";
2
+ import { t as InworldSynthesizer_exports } from "./InworldSynthesizer.mjs";
3
+ import { t as InworldRealtimeSynthesizer_exports } from "./InworldRealtimeSynthesizer.mjs";
4
+ import { t as InworldTranscriber_exports } from "./InworldTranscriber.mjs";
5
+ import { t as InworldRealtimeTranscriber_exports } from "./InworldRealtimeTranscriber.mjs";
6
+ import "./models.mjs";
7
+ export { InworldRealtimeSynthesizer_exports as InworldRealtimeSynthesizer, InworldRealtimeTranscriber_exports as InworldRealtimeTranscriber, InworldSynthesizer_exports as InworldSynthesizer, InworldTranscriber_exports as InworldTranscriber, codec_exports as codec };
@@ -0,0 +1,47 @@
1
+ //#region src/models.d.ts
2
+ /**
3
+ * Inworld TTS models (current per [docs.inworld.ai/tts/tts](https://docs.inworld.ai/tts/tts)).
4
+ *
5
+ * - `inworld-tts-2` — flagship, 100+ languages, supports `deliveryMode` style
6
+ * steering (`STABLE` / `BALANCED` / `CREATIVE`). ~200ms P50.
7
+ * - `inworld-tts-1.5-max` — 15 languages, ~200ms P50.
8
+ * - `inworld-tts-1.5-mini` — 15 languages, ~120ms P50, lowest latency.
9
+ *
10
+ * `inworld-tts-1` / `inworld-tts-1-max` are not in current docs — treat as
11
+ * superseded. The `(string & {})` tail keeps autocomplete on literals while
12
+ * accepting any string so newly-released models work without an SDK update.
13
+ */
14
+ type InworldTtsModel = "inworld-tts-2" | "inworld-tts-1.5-max" | "inworld-tts-1.5-mini" | (string & {});
15
+ /**
16
+ * Inworld voice ID. Inworld voices are human-readable names ("Sarah",
17
+ * "Edward", …) but there's no public list-voices REST endpoint — voices
18
+ * are browsed via the Inworld Portal. Typing this as `string` rather than
19
+ * a hard-coded union avoids the union rotting between SDK releases.
20
+ */
21
+ type InworldVoiceId = string;
22
+ /**
23
+ * Inworld STT models. `inworld/inworld-stt-1` is the first-party model
24
+ * (marked Experimental in docs at time of writing). The rest are
25
+ * router-style passthroughs — Inworld proxies to the named provider
26
+ * behind their auth & billing.
27
+ *
28
+ * Streaming WS support varies: `inworld/inworld-stt-1` and AssemblyAI /
29
+ * Soniox variants support both sync + WS; Groq Whisper is sync-only.
30
+ */
31
+ type InworldSttModel = "inworld/inworld-stt-1" | "groq/whisper-large-v3" | "assemblyai/universal-streaming-english" | "assemblyai/universal-streaming-multilingual" | "assemblyai/u3-rt-pro" | "assemblyai/whisper-rt" | "soniox/stt-rt-v4" | (string & {});
32
+ /**
33
+ * `deliveryMode` style steering — only honored by `inworld-tts-2`. Older
34
+ * models ignore the field silently.
35
+ */
36
+ type InworldDeliveryMode = "STABLE" | "BALANCED" | "CREATIVE";
37
+ /**
38
+ * Inworld's `audioEncoding` enum (TTS audioConfig).
39
+ *
40
+ * Note: sync `LINEAR16` / `WAV` responses include a WAV header in the
41
+ * `audioContent` bytes. Streaming (NDJSON + WS) chunks do **not**. The
42
+ * codec layer surfaces this difference in `AudioFormat.container`.
43
+ */
44
+ type InworldAudioEncoding = "LINEAR16" | "MP3" | "OGG_OPUS" | "ALAW" | "MULAW" | "FLAC" | "PCM" | "WAV";
45
+ //#endregion
46
+ export { InworldVoiceId as a, InworldTtsModel as i, InworldDeliveryMode as n, InworldSttModel as r, InworldAudioEncoding as t };
47
+ //# sourceMappingURL=models-CPLZDcgt.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models-CPLZDcgt.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAYA;;;;;AAaA;;;;;AAWA;KAxBY,eAAA;;;;AAuCZ;;;KA1BY,cAAA;;AAmCZ;;;;;;;;KAxBY,eAAA;;;;;KAeA,mBAAA;;;;;;;;KASA,oBAAA"}
@@ -0,0 +1,2 @@
1
+ import { a as InworldVoiceId, i as InworldTtsModel, n as InworldDeliveryMode, r as InworldSttModel, t as InworldAudioEncoding } from "./models-CPLZDcgt.mjs";
2
+ export { InworldAudioEncoding, InworldDeliveryMode, InworldSttModel, InworldTtsModel, InworldVoiceId };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import { Redacted, Schema, Stream } from "effect";
2
+ import * as AiError from "@effect-uai/core/AiError";
3
+ import { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber";
4
+ import { TranscriptEvent } from "@effect-uai/core/Transcript";
5
+
6
+ //#region src/realtimeStt.d.ts
7
+ type Config = {
8
+ readonly apiKey: Redacted.Redacted;
9
+ readonly baseUrl?: string;
10
+ };
11
+ declare const ServerFrame: Schema.Struct<{
12
+ readonly result: Schema.optional<Schema.Struct<{
13
+ readonly transcription: Schema.optional<Schema.Struct<{
14
+ readonly transcript: Schema.String;
15
+ readonly isFinal: Schema.optional<Schema.Boolean>;
16
+ readonly wordTimestamps: Schema.optional<Schema.NullOr<Schema.$Array<Schema.Struct<{
17
+ readonly word: Schema.String;
18
+ readonly startTimeMs: Schema.optional<Schema.Number>;
19
+ readonly endTimeMs: Schema.optional<Schema.Number>;
20
+ readonly confidence: Schema.optional<Schema.Number>;
21
+ }>>>>;
22
+ }>>;
23
+ readonly speechStarted: Schema.optional<Schema.Struct<{
24
+ readonly startTimeMs: Schema.optional<Schema.Number>;
25
+ readonly confidence: Schema.optional<Schema.Number>;
26
+ }>>;
27
+ readonly speechStopped: Schema.optional<Schema.Struct<{
28
+ readonly silenceDurationMs: Schema.optional<Schema.Number>;
29
+ }>>;
30
+ readonly status: Schema.optional<Schema.Unknown>;
31
+ }>>;
32
+ readonly error: Schema.optional<Schema.Unknown>;
33
+ }>;
34
+ declare const wireToEvent: (frame: typeof ServerFrame.Type) => TranscriptEvent | undefined;
35
+ declare const streamTranscription: (cfg: Config) => <E, R>(audioIn: Stream.Stream<Uint8Array, E, R>, request: CommonStreamTranscribeRequest) => Stream.Stream<TranscriptEvent, AiError.AiError | E, R>;
36
+ //#endregion
37
+ export { Config, streamTranscription, wireToEvent };
38
+ //# sourceMappingURL=realtimeStt.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;KA6BY,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAqG9D,WAAA,EAAW,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;cAgBJ,WAAA,GAAe,KAAA,SAAc,WAAA,CAAY,IAAA,KAAO,eAAA;AAAA,cAoDhD,mBAAA,GACV,GAAA,EAAK,MAAA,YAEJ,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,CAAA,EAAG,CAAA,GACtC,OAAA,EAAS,6BAAA,KACR,MAAA,CAAO,MAAA,CAAO,eAAA,EAAiB,OAAA,CAAQ,OAAA,GAAU,CAAA,EAAG,CAAA"}
@@ -0,0 +1,145 @@
1
+ import { authedWsConstructor } from "./wsAuth.mjs";
2
+ import { Effect, Encoding, Match, Queue, Schema, Stream } from "effect";
3
+ import * as AiError from "@effect-uai/core/AiError";
4
+ import * as JSONL from "@effect-uai/core/JSONL";
5
+ import * as Socket from "effect/unstable/socket/Socket";
6
+ //#region src/realtimeStt.ts
7
+ /**
8
+ * Inworld Realtime STT — `wss://api.inworld.ai/stt/v1/transcribe:streamBidirectional`.
9
+ *
10
+ * Auth: `Authorization: Basic <API_KEY>` on the WS upgrade header (matches
11
+ * Inworld's own JS sample). Setting headers needs the `ws` peer dep
12
+ * (Node/Bun); see `./wsAuth.ts`.
13
+ *
14
+ * Wire shape (per [inworld-ai/inworld-api-examples](https://github.com/inworld-ai/inworld-api-examples)):
15
+ * client → server:
16
+ * `{ "transcribeConfig": { modelId, audioEncoding, sampleRateHertz, language, ... } }` (first frame)
17
+ * `{ "audioChunk": { "content": "<base64 audio>" } }` (×N)
18
+ * `{ "closeStream": {} }` (end-of-input)
19
+ * server → client:
20
+ * `{ "result": { "transcription": { "transcript": "...", "isFinal": true|false } } }`
21
+ * `{ "result": { "speechStarted": { "startTimeMs": ... } } }`
22
+ * `{ "result": { "speechStopped": { "silenceDurationMs": ... } } }`
23
+ *
24
+ * Inworld's STT WS sends audio as base64 inside JSON (NOT binary frames),
25
+ * matching the rest of the Inworld API style.
26
+ */
27
+ const unsupportedFormat = (format) => new AiError.Unsupported({
28
+ provider: "inworld",
29
+ capability: "inputFormat",
30
+ reason: `Inworld realtime STT accepts pcm_s16le (LINEAR16), mp3, ogg/opus, or flac. Got ${JSON.stringify(format)}.`
31
+ });
32
+ const inputFormatToWire = Match.type().pipe(Match.when({
33
+ container: "raw",
34
+ encoding: "pcm_s16le"
35
+ }, () => Effect.succeed("LINEAR16")), Match.when({
36
+ container: "wav",
37
+ encoding: "pcm_s16le"
38
+ }, () => Effect.succeed("LINEAR16")), Match.when({ container: "mp3" }, () => Effect.succeed("MP3")), Match.when({
39
+ container: "ogg",
40
+ encoding: "opus"
41
+ }, () => Effect.succeed("OGG_OPUS")), Match.when({ container: "flac" }, () => Effect.succeed("FLAC")), Match.orElse((f) => Effect.fail(unsupportedFormat(f))));
42
+ const buildWsUrl = (cfg) => {
43
+ return `${(cfg.baseUrl ?? "https://api.inworld.ai").replace(/^http/, "ws")}/stt/v1/transcribe:streamBidirectional`;
44
+ };
45
+ const promptToTerms = (prompt) => prompt === void 0 ? void 0 : typeof prompt === "string" ? [prompt] : prompt.terms;
46
+ const configFrame = (encoding, request) => {
47
+ const prompts = promptToTerms(request.prompt);
48
+ return JSON.stringify({ transcribeConfig: {
49
+ modelId: request.model,
50
+ audioEncoding: encoding,
51
+ sampleRateHertz: request.inputFormat.sampleRate,
52
+ numberOfChannels: request.inputFormat.channels ?? 1,
53
+ language: request.language ?? "en-US",
54
+ ...prompts !== void 0 && { prompts },
55
+ ...request.wordTimestamps === true && { includeWordTimestamps: true }
56
+ } });
57
+ };
58
+ const audioChunkFrame = (bytes) => JSON.stringify({ audioChunk: { content: Encoding.encodeBase64(bytes) } });
59
+ const endTurnFrame = JSON.stringify({ endTurn: {} });
60
+ const closeStreamFrame = JSON.stringify({ closeStream: {} });
61
+ const WireWord = Schema.Struct({
62
+ word: Schema.String,
63
+ startTimeMs: Schema.optional(Schema.Number),
64
+ endTimeMs: Schema.optional(Schema.Number),
65
+ confidence: Schema.optional(Schema.Number)
66
+ });
67
+ const ResultBody = Schema.Struct({
68
+ transcription: Schema.optional(Schema.Struct({
69
+ transcript: Schema.String,
70
+ isFinal: Schema.optional(Schema.Boolean),
71
+ wordTimestamps: Schema.optional(Schema.NullOr(Schema.Array(WireWord)))
72
+ })),
73
+ speechStarted: Schema.optional(Schema.Struct({
74
+ startTimeMs: Schema.optional(Schema.Number),
75
+ confidence: Schema.optional(Schema.Number)
76
+ })),
77
+ speechStopped: Schema.optional(Schema.Struct({ silenceDurationMs: Schema.optional(Schema.Number) })),
78
+ status: Schema.optional(Schema.Unknown)
79
+ });
80
+ const ServerFrame = Schema.Struct({
81
+ result: Schema.optional(ResultBody),
82
+ error: Schema.optional(Schema.Unknown)
83
+ });
84
+ const decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame);
85
+ const wireWordToCommon = (w) => w.startTimeMs === void 0 || w.endTimeMs === void 0 ? void 0 : {
86
+ text: w.word,
87
+ startSeconds: w.startTimeMs / 1e3,
88
+ endSeconds: w.endTimeMs / 1e3,
89
+ ...w.confidence !== void 0 && { confidence: w.confidence }
90
+ };
91
+ const wireToEvent = (frame) => {
92
+ if (frame.error !== void 0) return {
93
+ _tag: "error",
94
+ message: typeof frame.error === "string" ? frame.error : JSON.stringify(frame.error)
95
+ };
96
+ const result = frame.result;
97
+ if (result === void 0) return void 0;
98
+ if (result.transcription !== void 0) {
99
+ const t = result.transcription;
100
+ const words = t.wordTimestamps?.map(wireWordToCommon).filter((w) => w !== void 0);
101
+ return t.isFinal === true ? {
102
+ _tag: "final",
103
+ text: t.transcript,
104
+ ...words !== void 0 && words.length > 0 && { words }
105
+ } : {
106
+ _tag: "partial",
107
+ text: t.transcript,
108
+ ...words !== void 0 && words.length > 0 && { words }
109
+ };
110
+ }
111
+ if (result.speechStarted !== void 0) return {
112
+ _tag: "speech-started",
113
+ atSeconds: (result.speechStarted.startTimeMs ?? 0) / 1e3
114
+ };
115
+ if (result.speechStopped !== void 0) return {
116
+ _tag: "utterance-ended",
117
+ atSeconds: 0
118
+ };
119
+ };
120
+ const handleServerMessage = (queue) => (raw) => Effect.gen(function* () {
121
+ const json = yield* JSONL.parseSafe(raw);
122
+ if (json === void 0) return;
123
+ const decoded = yield* decodeServerFrame(json).pipe(Effect.option);
124
+ if (decoded._tag === "None") return;
125
+ const event = wireToEvent(decoded.value);
126
+ if (event !== void 0) yield* Queue.offer(queue, event);
127
+ });
128
+ const streamTranscription = (cfg) => (audioIn, request) => Stream.unwrap(Effect.gen(function* () {
129
+ const encoding = yield* inputFormatToWire(request.inputFormat);
130
+ const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg.apiKey)));
131
+ const queue = yield* Queue.bounded(64);
132
+ const write = yield* socket.writer;
133
+ yield* Effect.gen(function* () {
134
+ yield* write(configFrame(encoding, request));
135
+ yield* Stream.runForEach(audioIn, (bytes) => write(audioChunkFrame(bytes)));
136
+ yield* write(endTurnFrame);
137
+ yield* write(closeStreamFrame);
138
+ }).pipe(Effect.ignore, Effect.forkScoped);
139
+ yield* socket.runString(handleServerMessage(queue)).pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped);
140
+ return Stream.fromQueue(queue);
141
+ }));
142
+ //#endregion
143
+ export { streamTranscription, wireToEvent };
144
+
145
+ //# sourceMappingURL=realtimeStt.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtimeStt.mjs","names":[],"sources":["../src/realtimeStt.ts"],"sourcesContent":["/**\n * Inworld Realtime STT — `wss://api.inworld.ai/stt/v1/transcribe:streamBidirectional`.\n *\n * Auth: `Authorization: Basic <API_KEY>` on the WS upgrade header (matches\n * Inworld's own JS sample). Setting headers needs the `ws` peer dep\n * (Node/Bun); see `./wsAuth.ts`.\n *\n * Wire shape (per [inworld-ai/inworld-api-examples](https://github.com/inworld-ai/inworld-api-examples)):\n * client → server:\n * `{ \"transcribeConfig\": { modelId, audioEncoding, sampleRateHertz, language, ... } }` (first frame)\n * `{ \"audioChunk\": { \"content\": \"<base64 audio>\" } }` (×N)\n * `{ \"closeStream\": {} }` (end-of-input)\n * server → client:\n * `{ \"result\": { \"transcription\": { \"transcript\": \"...\", \"isFinal\": true|false } } }`\n * `{ \"result\": { \"speechStarted\": { \"startTimeMs\": ... } } }`\n * `{ \"result\": { \"speechStopped\": { \"silenceDurationMs\": ... } } }`\n *\n * Inworld's STT WS sends audio as base64 inside JSON (NOT binary frames),\n * matching the rest of the Inworld API style.\n */\nimport { Cause, Effect, Encoding, Match, Queue, Redacted, Schema, Stream } from \"effect\"\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 { authedWsConstructor } from \"./wsAuth.js\"\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// AudioFormat → `audioEncoding` slug for Inworld STT\n// ---------------------------------------------------------------------------\n\ntype WireEncoding = \"LINEAR16\" | \"MP3\" | \"OGG_OPUS\" | \"FLAC\"\n\nconst unsupportedFormat = (format: AudioFormat) =>\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"inputFormat\",\n reason: `Inworld realtime STT accepts pcm_s16le (LINEAR16), mp3, ogg/opus, or flac. Got ${JSON.stringify(format)}.`,\n })\n\nconst inputFormatToWire: (format: AudioFormat) => Effect.Effect<WireEncoding, AiError.AiError> =\n Match.type<AudioFormat>().pipe(\n Match.when({ container: \"raw\", encoding: \"pcm_s16le\" }, () =>\n Effect.succeed<WireEncoding>(\"LINEAR16\"),\n ),\n Match.when({ container: \"wav\", encoding: \"pcm_s16le\" }, () =>\n Effect.succeed<WireEncoding>(\"LINEAR16\"),\n ),\n Match.when({ container: \"mp3\" }, () => Effect.succeed<WireEncoding>(\"MP3\")),\n Match.when({ container: \"ogg\", encoding: \"opus\" }, () =>\n Effect.succeed<WireEncoding>(\"OGG_OPUS\"),\n ),\n Match.when({ container: \"flac\" }, () => Effect.succeed<WireEncoding>(\"FLAC\")),\n Match.orElse((f) => Effect.fail(unsupportedFormat(f))),\n )\n\n// ---------------------------------------------------------------------------\n// URL + frame builders\n// ---------------------------------------------------------------------------\n\nconst buildWsUrl = (cfg: Config) => {\n const wsBase = (cfg.baseUrl ?? \"https://api.inworld.ai\").replace(/^http/, \"ws\")\n return `${wsBase}/stt/v1/transcribe:streamBidirectional`\n}\n\nconst promptToTerms = (\n prompt: string | { readonly terms: ReadonlyArray<string> } | undefined,\n): ReadonlyArray<string> | undefined =>\n prompt === undefined ? undefined : typeof prompt === \"string\" ? [prompt] : prompt.terms\n\nconst configFrame = (encoding: WireEncoding, request: CommonStreamTranscribeRequest) => {\n const prompts = promptToTerms(request.prompt)\n return JSON.stringify({\n transcribeConfig: {\n modelId: request.model,\n audioEncoding: encoding,\n sampleRateHertz: request.inputFormat.sampleRate,\n numberOfChannels: request.inputFormat.channels ?? 1,\n // Inworld's sample includes `language` even though docs mark it optional.\n // Default to en-US to match the sample's behavior; caller can override.\n language: request.language ?? \"en-US\",\n ...(prompts !== undefined && { prompts }),\n ...(request.wordTimestamps === true && { includeWordTimestamps: true }),\n },\n })\n}\n\nconst audioChunkFrame = (bytes: Uint8Array) =>\n JSON.stringify({ audioChunk: { content: Encoding.encodeBase64(bytes) } })\n\nconst endTurnFrame = JSON.stringify({ endTurn: {} })\nconst closeStreamFrame = JSON.stringify({ closeStream: {} })\n\n// ---------------------------------------------------------------------------\n// Wire schemas (server → client)\n// ---------------------------------------------------------------------------\n\nconst WireWord = Schema.Struct({\n word: Schema.String,\n startTimeMs: Schema.optional(Schema.Number),\n endTimeMs: Schema.optional(Schema.Number),\n confidence: Schema.optional(Schema.Number),\n})\n\nconst ResultBody = Schema.Struct({\n transcription: Schema.optional(\n Schema.Struct({\n transcript: Schema.String,\n isFinal: Schema.optional(Schema.Boolean),\n wordTimestamps: Schema.optional(Schema.NullOr(Schema.Array(WireWord))),\n }),\n ),\n speechStarted: Schema.optional(\n Schema.Struct({\n startTimeMs: Schema.optional(Schema.Number),\n confidence: Schema.optional(Schema.Number),\n }),\n ),\n speechStopped: Schema.optional(\n Schema.Struct({\n silenceDurationMs: Schema.optional(Schema.Number),\n }),\n ),\n status: Schema.optional(Schema.Unknown),\n})\n\nconst ServerFrame = Schema.Struct({\n result: Schema.optional(ResultBody),\n error: Schema.optional(Schema.Unknown),\n})\nconst decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame)\n\nconst wireWordToCommon = (w: typeof WireWord.Type): WordTimestamp | undefined =>\n w.startTimeMs === undefined || w.endTimeMs === undefined\n ? undefined\n : {\n text: w.word,\n startSeconds: w.startTimeMs / 1000,\n endSeconds: w.endTimeMs / 1000,\n ...(w.confidence !== undefined && { confidence: w.confidence }),\n }\n\nexport const wireToEvent = (frame: typeof ServerFrame.Type): TranscriptEvent | undefined => {\n if (frame.error !== undefined) {\n return {\n _tag: \"error\",\n message: typeof frame.error === \"string\" ? frame.error : JSON.stringify(frame.error),\n }\n }\n const result = frame.result\n if (result === undefined) return undefined\n if (result.transcription !== undefined) {\n const t = result.transcription\n const words = t.wordTimestamps\n ?.map(wireWordToCommon)\n .filter((w): w is WordTimestamp => w !== undefined)\n return t.isFinal === true\n ? {\n _tag: \"final\",\n text: t.transcript,\n ...(words !== undefined && words.length > 0 && { words }),\n }\n : {\n _tag: \"partial\",\n text: t.transcript,\n ...(words !== undefined && words.length > 0 && { words }),\n }\n }\n if (result.speechStarted !== undefined) {\n return {\n _tag: \"speech-started\",\n atSeconds: (result.speechStarted.startTimeMs ?? 0) / 1000,\n }\n }\n if (result.speechStopped !== undefined) {\n return { _tag: \"utterance-ended\", atSeconds: 0 }\n }\n return undefined\n}\n\nconst handleServerMessage = (queue: Queue.Queue<TranscriptEvent, 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 event = wireToEvent(decoded.value)\n if (event !== undefined) yield* Queue.offer(queue, event)\n })\n\n// ---------------------------------------------------------------------------\n// Stream<Uint8Array> → Stream<TranscriptEvent>\n// ---------------------------------------------------------------------------\n\nexport const streamTranscription =\n (cfg: Config) =>\n <E, R>(\n audioIn: Stream.Stream<Uint8Array, E, R>,\n request: CommonStreamTranscribeRequest,\n ): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const encoding = yield* inputFormatToWire(request.inputFormat)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), {\n // Effect's Socket treats all close codes as errors by default —\n // whitelist standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg.apiKey)))\n const queue = yield* Queue.bounded<TranscriptEvent, Cause.Done>(64)\n const write = yield* socket.writer\n\n // Writer: config → drain audio → endTurn → closeStream. Inworld\n // emits `result.transcription` partials in real-time via VAD;\n // `endTurn` is sent on input-stream end to flush any tail audio\n // into a final transcript, then `closeStream` for graceful close.\n yield* Effect.gen(function* () {\n yield* write(configFrame(encoding, request))\n yield* Stream.runForEach(audioIn, (bytes) => write(audioChunkFrame(bytes)))\n yield* write(endTurnFrame)\n yield* write(closeStreamFrame)\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))\n .pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped)\n\n return Stream.fromQueue(queue)\n }),\n )\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { a as InworldVoiceId, i as InworldTtsModel, n as InworldDeliveryMode } from "./models-CPLZDcgt.mjs";
2
+ import { Redacted, Stream } from "effect";
3
+ import * as AiError from "@effect-uai/core/AiError";
4
+ import { AudioChunk, AudioFormat } from "@effect-uai/core/Audio";
5
+
6
+ //#region src/realtimeTts.d.ts
7
+ type Config = {
8
+ readonly apiKey: Redacted.Redacted;
9
+ readonly baseUrl?: string;
10
+ };
11
+ /**
12
+ * Incremental-text-in request. Mirrors the sync request minus `text` (which
13
+ * comes from the input stream).
14
+ */
15
+ type StreamSynthesizeRequest = {
16
+ readonly model: InworldTtsModel;
17
+ readonly voiceId: InworldVoiceId;
18
+ readonly outputFormat?: AudioFormat;
19
+ readonly languageCode?: string;
20
+ readonly temperature?: number;
21
+ readonly deliveryMode?: InworldDeliveryMode;
22
+ readonly applyTextNormalization?: "ON" | "OFF";
23
+ readonly speed?: number;
24
+ };
25
+ declare const buildWsUrl: (cfg: Config) => string;
26
+ declare const streamSynthesis: (cfg: Config) => <E, R>(textIn: Stream.Stream<string, E, R>, request: StreamSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError | E, R>;
27
+ //#endregion
28
+ export { Config, StreamSynthesizeRequest, buildWsUrl, streamSynthesis };
29
+ //# sourceMappingURL=realtimeTts.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;KA8BY,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;;;;;KAMxD,uBAAA;EAAA,SACD,KAAA,EAAO,eAAA;EAAA,SACP,OAAA,EAAS,cAAA;EAAA,SACT,YAAA,GAAe,WAAA;EAAA,SACf,YAAA;EAAA,SACA,WAAA;EAAA,SACA,YAAA,GAAe,mBAAA;EAAA,SACf,sBAAA;EAAA,SACA,KAAA;AAAA;AAAA,cAOE,UAAA,GAAc,GAAA,EAAK,MAAA;AAAA,cA8EnB,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"}
@@ -0,0 +1,94 @@
1
+ import { audioConfigFor, decodeAudioContent, defaultFormat } from "./codec.mjs";
2
+ import { authedWsConstructor } from "./wsAuth.mjs";
3
+ import { Effect, Queue, Schema, Stream } from "effect";
4
+ import * as JSONL from "@effect-uai/core/JSONL";
5
+ import * as Socket from "effect/unstable/socket/Socket";
6
+ //#region src/realtimeTts.ts
7
+ /**
8
+ * Inworld Realtime TTS — `wss://api.inworld.ai/tts/v1/voice:streamBidirectional`.
9
+ *
10
+ * Auth: `Authorization: Basic <API_KEY>` on the WS upgrade header (matches
11
+ * Inworld's own JS sample). The docs mention a `?Authorization=…` query
12
+ * variant but the server rejects it in practice. Setting headers needs the
13
+ * `ws` peer dep (Node/Bun); see `./wsAuth.ts`.
14
+ *
15
+ * Wire shape (single-context per call):
16
+ * client → server:
17
+ * `{ "create": { voiceId, modelId, audioConfig, ... } }`
18
+ * `{ "send_text": { "text": "..." } }` (repeated)
19
+ * `{ "close_context": {} }` (text stream end)
20
+ * server → client:
21
+ * `{ "result": { "contextCreated": {...} } }` (handshake ack)
22
+ * `{ "result": { "audioChunk": { "audioContent": "<b64>" } } }` (×N)
23
+ * `{ "result": { "contextClosed": {...} } }` (final)
24
+ *
25
+ * Multi-context (`contextId`) is not surfaced here — one logical
26
+ * utterance per call.
27
+ */
28
+ const buildWsUrl = (cfg) => {
29
+ return `${(cfg.baseUrl ?? "https://api.inworld.ai").replace(/^http/, "ws")}/tts/v1/voice:streamBidirectional`;
30
+ };
31
+ const audioConfigSnake = (request) => Effect.map(audioConfigFor(request.outputFormat ?? defaultFormat, request.speed), (cfg) => ({
32
+ audio_encoding: cfg.audioEncoding,
33
+ ...cfg.sampleRateHertz !== void 0 && { sample_rate_hertz: cfg.sampleRateHertz },
34
+ ...cfg.bitRate !== void 0 && { bit_rate: cfg.bitRate },
35
+ ...cfg.speakingRate !== void 0 && { speaking_rate: cfg.speakingRate }
36
+ }));
37
+ const createFrame = (request) => Effect.map(audioConfigSnake(request), (audio_config) => JSON.stringify({ create: {
38
+ voice_id: request.voiceId,
39
+ model_id: request.model,
40
+ audio_config,
41
+ ...request.languageCode !== void 0 && { language: request.languageCode },
42
+ ...request.deliveryMode !== void 0 && { delivery_mode: request.deliveryMode },
43
+ ...request.temperature !== void 0 && { temperature: request.temperature },
44
+ ...request.applyTextNormalization !== void 0 && { apply_text_normalization: request.applyTextNormalization }
45
+ } }));
46
+ const sendTextFrame = (text) => JSON.stringify({ send_text: {
47
+ text,
48
+ flush_context: {}
49
+ } });
50
+ const closeContextFrame = JSON.stringify({ close_context: {} });
51
+ const ResultBody = Schema.Struct({
52
+ audioChunk: Schema.optional(Schema.Struct({ audioContent: Schema.String })),
53
+ contextCreated: Schema.optional(Schema.Unknown),
54
+ flushCompleted: Schema.optional(Schema.Unknown),
55
+ contextClosed: Schema.optional(Schema.Unknown),
56
+ status: Schema.optional(Schema.Unknown)
57
+ });
58
+ const ServerFrame = Schema.Struct({
59
+ result: Schema.optional(ResultBody),
60
+ error: Schema.optional(Schema.Unknown)
61
+ });
62
+ const decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame);
63
+ const handleServerFrame = (queue) => (raw) => Effect.gen(function* () {
64
+ const json = yield* JSONL.parseSafe(raw);
65
+ if (json === void 0) return;
66
+ const decoded = yield* decodeServerFrame(json).pipe(Effect.option);
67
+ if (decoded._tag === "None") return;
68
+ const frame = decoded.value;
69
+ if (frame.error !== void 0) {
70
+ yield* Effect.logWarning("[inworld-tts] server error frame", { error: frame.error });
71
+ return;
72
+ }
73
+ const audio = frame.result?.audioChunk?.audioContent;
74
+ if (audio === void 0 || audio === "") return;
75
+ const bytes = yield* decodeAudioContent(audio).pipe(Effect.option);
76
+ if (bytes._tag === "Some") yield* Queue.offer(queue, { bytes: bytes.value });
77
+ });
78
+ const streamSynthesis = (cfg) => (textIn, request) => Stream.unwrap(Effect.gen(function* () {
79
+ const create = yield* createFrame(request);
80
+ const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg.apiKey)));
81
+ const queue = yield* Queue.bounded(64);
82
+ const write = yield* socket.writer;
83
+ yield* Effect.gen(function* () {
84
+ yield* write(create);
85
+ yield* Stream.runForEach(textIn, (text) => text.length === 0 ? Effect.void : write(sendTextFrame(text)));
86
+ yield* write(closeContextFrame);
87
+ }).pipe(Effect.ignore, Effect.forkScoped);
88
+ yield* socket.runString(handleServerFrame(queue)).pipe(Effect.ensuring(Queue.end(queue)), Effect.forkScoped);
89
+ return Stream.fromQueue(queue);
90
+ }));
91
+ //#endregion
92
+ export { buildWsUrl, streamSynthesis };
93
+
94
+ //# sourceMappingURL=realtimeTts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtimeTts.mjs","names":[],"sources":["../src/realtimeTts.ts"],"sourcesContent":["/**\n * Inworld Realtime TTS — `wss://api.inworld.ai/tts/v1/voice:streamBidirectional`.\n *\n * Auth: `Authorization: Basic <API_KEY>` on the WS upgrade header (matches\n * Inworld's own JS sample). The docs mention a `?Authorization=…` query\n * variant but the server rejects it in practice. Setting headers needs the\n * `ws` peer dep (Node/Bun); see `./wsAuth.ts`.\n *\n * Wire shape (single-context per call):\n * client → server:\n * `{ \"create\": { voiceId, modelId, audioConfig, ... } }`\n * `{ \"send_text\": { \"text\": \"...\" } }` (repeated)\n * `{ \"close_context\": {} }` (text stream end)\n * server → client:\n * `{ \"result\": { \"contextCreated\": {...} } }` (handshake ack)\n * `{ \"result\": { \"audioChunk\": { \"audioContent\": \"<b64>\" } } }` (×N)\n * `{ \"result\": { \"contextClosed\": {...} } }` (final)\n *\n * Multi-context (`contextId`) is not surfaced here — one logical\n * utterance per call.\n */\nimport { Cause, Effect, Queue, Redacted, 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 { audioConfigFor, decodeAudioContent, defaultFormat } from \"./codec.js\"\nimport type { InworldDeliveryMode, InworldTtsModel, InworldVoiceId } from \"./models.js\"\nimport { authedWsConstructor } from \"./wsAuth.js\"\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n/**\n * Incremental-text-in request. Mirrors the sync request minus `text` (which\n * comes from the input stream).\n */\nexport type StreamSynthesizeRequest = {\n readonly model: InworldTtsModel\n readonly voiceId: InworldVoiceId\n readonly outputFormat?: AudioFormat\n readonly languageCode?: string\n readonly temperature?: number\n readonly deliveryMode?: InworldDeliveryMode\n readonly applyTextNormalization?: \"ON\" | \"OFF\"\n readonly speed?: number\n}\n\n// ---------------------------------------------------------------------------\n// URL + frame builders\n// ---------------------------------------------------------------------------\n\nexport const buildWsUrl = (cfg: Config) => {\n const wsBase = (cfg.baseUrl ?? \"https://api.inworld.ai\").replace(/^http/, \"ws\")\n return `${wsBase}/tts/v1/voice:streamBidirectional`\n}\n\n// TTS WS uses snake_case outbound (per Inworld's own JS sample). The\n// camelCase helper from `codec.ts` is built for the REST/JSON sync paths;\n// rewire the field names here for the WS path. Server responses come back\n// camelCase regardless — see `ResultBody` below.\nconst audioConfigSnake = (request: StreamSynthesizeRequest) =>\n Effect.map(audioConfigFor(request.outputFormat ?? defaultFormat, request.speed), (cfg) => ({\n audio_encoding: cfg.audioEncoding,\n ...(cfg.sampleRateHertz !== undefined && { sample_rate_hertz: cfg.sampleRateHertz }),\n ...(cfg.bitRate !== undefined && { bit_rate: cfg.bitRate }),\n ...(cfg.speakingRate !== undefined && { speaking_rate: cfg.speakingRate }),\n }))\n\nconst createFrame = (request: StreamSynthesizeRequest) =>\n Effect.map(audioConfigSnake(request), (audio_config) =>\n JSON.stringify({\n create: {\n voice_id: request.voiceId,\n model_id: request.model,\n audio_config,\n ...(request.languageCode !== undefined && { language: request.languageCode }),\n ...(request.deliveryMode !== undefined && { delivery_mode: request.deliveryMode }),\n ...(request.temperature !== undefined && { temperature: request.temperature }),\n ...(request.applyTextNormalization !== undefined && {\n apply_text_normalization: request.applyTextNormalization,\n }),\n },\n }),\n )\n\n// `flush_context: {}` inside `send_text` nudges the server to flush\n// pending audio promptly — matches the low-latency sample.\nconst sendTextFrame = (text: string) => JSON.stringify({ send_text: { text, flush_context: {} } })\nconst closeContextFrame = JSON.stringify({ close_context: {} })\n\n// ---------------------------------------------------------------------------\n// Wire schema (server → client)\n// ---------------------------------------------------------------------------\n\nconst ResultBody = Schema.Struct({\n audioChunk: Schema.optional(Schema.Struct({ audioContent: Schema.String })),\n contextCreated: Schema.optional(Schema.Unknown),\n flushCompleted: Schema.optional(Schema.Unknown),\n contextClosed: Schema.optional(Schema.Unknown),\n status: Schema.optional(Schema.Unknown),\n})\n\nconst ServerFrame = Schema.Struct({\n result: Schema.optional(ResultBody),\n error: Schema.optional(Schema.Unknown),\n})\nconst decodeServerFrame = Schema.decodeUnknownEffect(ServerFrame)\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(\"[inworld-tts] server error frame\", { error: frame.error })\n return\n }\n const audio = frame.result?.audioChunk?.audioContent\n if (audio === undefined || audio === \"\") return\n const bytes = yield* decodeAudioContent(audio).pipe(Effect.option)\n if (bytes._tag === \"Some\") yield* Queue.offer(queue, { bytes: bytes.value })\n })\n\n// ---------------------------------------------------------------------------\n// Stream<string> → Stream<AudioChunk>\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> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const create = yield* createFrame(request)\n const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), {\n // Effect's Socket treats all close codes as errors by default —\n // whitelist standard clean-close codes (1000 / 1001 / 1005).\n closeCodeIsError: (code) => code !== 1000 && code !== 1001 && code !== 1005,\n }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg.apiKey)))\n const queue = yield* Queue.bounded<AudioChunk, Cause.Done>(64)\n const write = yield* socket.writer\n\n // Writer: BOS `create` → drain text as `send_text` → `close_context`.\n // The reader fiber surfaces the remaining audio drained by the server\n // post-close and shuts the queue when the WS upstream closes.\n yield* Effect.gen(function* () {\n yield* write(create)\n yield* Stream.runForEach(textIn, (text) =>\n text.length === 0 ? Effect.void : write(sendTextFrame(text)),\n )\n yield* write(closeContextFrame)\n }).pipe(Effect.ignore, Effect.forkScoped)\n\n // `Queue.end` flushes pending chunks then fails the next take with\n // `Done` — clean stream end. `Queue.shutdown` would CLEAR queued items\n // and 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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"}