@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,8 @@
1
+ import { Redacted } from "effect";
2
+ import * as Socket from "effect/unstable/socket/Socket";
3
+
4
+ //#region src/wsAuth.d.ts
5
+ declare const authedWsConstructor: (apiKey: Redacted.Redacted) => Socket.WebSocketConstructor["Service"];
6
+ //#endregion
7
+ export { authedWsConstructor };
8
+ //# sourceMappingURL=wsAuth.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wsAuth.d.mts","names":[],"sources":["../src/wsAuth.ts"],"mappings":";;;;cAea,mBAAA,GACV,MAAA,EAAQ,QAAA,CAAS,QAAA,KAAW,MAAA,CAAO,oBAAA"}
@@ -0,0 +1,8 @@
1
+ import { authHeader } from "./codec.mjs";
2
+ import { WebSocket } from "ws";
3
+ //#region src/wsAuth.ts
4
+ const authedWsConstructor = (apiKey) => (url) => new WebSocket(url, void 0, { headers: { Authorization: authHeader(apiKey) } });
5
+ //#endregion
6
+ export { authedWsConstructor };
7
+
8
+ //# sourceMappingURL=wsAuth.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wsAuth.mjs","names":["WSWebSocket"],"sources":["../src/wsAuth.ts"],"sourcesContent":["/**\n * Inworld WS auth helper. The Inworld realtime endpoints (`/tts/...` and\n * `/stt/...`) require `Authorization: Basic <API_KEY>` on the WS upgrade\n * — the docs document a `?Authorization=…` query-param variant, but in\n * practice the server returns `authentication is required` for that\n * path. Header auth is what Inworld's own samples use.\n *\n * Headers on a WS upgrade aren't settable via `globalThis.WebSocket`, so\n * we use the `ws` peer dep (Node/Bun only).\n */\nimport { Redacted } from \"effect\"\nimport type * as Socket from \"effect/unstable/socket/Socket\"\nimport { WebSocket as WSWebSocket } from \"ws\"\nimport { authHeader } from \"./codec.js\"\n\nexport const authedWsConstructor =\n (apiKey: Redacted.Redacted): Socket.WebSocketConstructor[\"Service\"] =>\n (url) =>\n new WSWebSocket(url, undefined, {\n headers: { Authorization: authHeader(apiKey) },\n }) as unknown as globalThis.WebSocket\n"],"mappings":";;;AAeA,MAAa,uBACV,YACA,QACC,IAAIA,UAAY,KAAK,KAAA,GAAW,EAC9B,SAAS,EAAE,eAAe,WAAW,OAAO,EAAE,EAC/C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@effect-uai/inworld",
3
+ "version": "0.4.0",
4
+ "description": "Inworld speech-to-text and text-to-speech provider for @effect-uai/core.",
5
+ "keywords": [
6
+ "ai",
7
+ "effect",
8
+ "inworld",
9
+ "speech",
10
+ "stt",
11
+ "transcription",
12
+ "tts"
13
+ ],
14
+ "homepage": "https://github.com/betalyra/effect-uai/tree/main/packages/providers/inworld",
15
+ "bugs": {
16
+ "url": "https://github.com/betalyra/effect-uai/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Betalyra",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/betalyra/effect-uai",
23
+ "directory": "packages/providers/inworld"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "type": "module",
32
+ "main": "./dist/index.mjs",
33
+ "types": "./dist/index.d.mts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.mts",
37
+ "import": "./dist/index.mjs"
38
+ },
39
+ "./InworldSynthesizer": {
40
+ "types": "./dist/InworldSynthesizer.d.mts",
41
+ "import": "./dist/InworldSynthesizer.mjs"
42
+ },
43
+ "./InworldRealtimeSynthesizer": {
44
+ "types": "./dist/InworldRealtimeSynthesizer.d.mts",
45
+ "import": "./dist/InworldRealtimeSynthesizer.mjs"
46
+ },
47
+ "./InworldTranscriber": {
48
+ "types": "./dist/InworldTranscriber.d.mts",
49
+ "import": "./dist/InworldTranscriber.mjs"
50
+ },
51
+ "./InworldRealtimeTranscriber": {
52
+ "types": "./dist/InworldRealtimeTranscriber.d.mts",
53
+ "import": "./dist/InworldRealtimeTranscriber.mjs"
54
+ }
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "devDependencies": {
60
+ "@types/ws": "^8.5.13",
61
+ "effect": "4.0.0-beta.57",
62
+ "typescript": "^6.0.3",
63
+ "ws": "^8.18.0",
64
+ "@effect-uai/core": "0.4.0"
65
+ },
66
+ "peerDependencies": {
67
+ "@effect-uai/core": ">=0.2.0 <1",
68
+ "effect": "4.0.0-beta.57",
69
+ "ws": "^8.0.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "ws": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "scripts": {
77
+ "build": "tsdown",
78
+ "typecheck": "tsc --noEmit"
79
+ }
80
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Realtime variant of `InworldSynthesizer`. Wires `streamSynthesisFrom` to
3
+ * `wss://api.inworld.ai/tts/v1/voice:streamBidirectional` and registers
4
+ * the `TtsIncrementalText` capability marker, so the call compiles
5
+ * against this Layer alone.
6
+ *
7
+ * Sync `synthesize` and chunked `streamSynthesis` reuse the implementations
8
+ * exported from `./InworldSynthesizer.js` — the only added surface here is
9
+ * the WS path.
10
+ */
11
+ import { Effect, Layer, Stream } from "effect"
12
+ import { HttpClient } from "effect/unstable/http"
13
+ import {
14
+ type CommonStreamSynthesizeRequest,
15
+ type CommonSynthesizeRequest,
16
+ SpeechSynthesizer,
17
+ type SpeechSynthesizerService,
18
+ TtsIncrementalText,
19
+ } from "@effect-uai/core/SpeechSynthesizer"
20
+ import {
21
+ type Config,
22
+ InworldSynthesizer,
23
+ type InworldSynthesizerService,
24
+ type InworldSynthesizeRequest,
25
+ streamSynthesisImpl,
26
+ synthesizeImpl,
27
+ } from "./InworldSynthesizer.js"
28
+ import { streamSynthesis as realtimeStream } from "./realtimeTts.js"
29
+
30
+ export type {
31
+ Config,
32
+ InworldSynthesizerService,
33
+ InworldSynthesizeRequest,
34
+ } from "./InworldSynthesizer.js"
35
+ export { InworldSynthesizer } from "./InworldSynthesizer.js"
36
+
37
+ export const make = (cfg: Config) =>
38
+ Effect.map(
39
+ HttpClient.HttpClient.asEffect(),
40
+ (client) =>
41
+ ({
42
+ synthesize: (r) =>
43
+ synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
44
+ streamSynthesis: (r) =>
45
+ streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),
46
+ streamSynthesisFrom: (textIn, request) =>
47
+ realtimeStream(cfg)(textIn, request as InworldSynthesizeRequest),
48
+ }) satisfies InworldSynthesizerService,
49
+ )
50
+
51
+ /**
52
+ * Realtime Layer. Registers `InworldSynthesizer`, the generic
53
+ * `SpeechSynthesizer`, **and** the `TtsIncrementalText` capability
54
+ * marker. The WS constructor uses the `ws` peer dep internally to set
55
+ * the `Authorization: Basic` header — no `Socket.WebSocketConstructor`
56
+ * needed at the call site.
57
+ */
58
+ export const layer = (cfg: Config) =>
59
+ Layer.mergeAll(
60
+ Layer.effect(InworldSynthesizer, make(cfg)),
61
+ Layer.effect(
62
+ SpeechSynthesizer,
63
+ Effect.map(
64
+ make(cfg),
65
+ (s): SpeechSynthesizerService => ({
66
+ synthesize: (req: CommonSynthesizeRequest) =>
67
+ s.synthesize(req as InworldSynthesizeRequest),
68
+ streamSynthesis: (req: CommonSynthesizeRequest) =>
69
+ s.streamSynthesis(req as InworldSynthesizeRequest),
70
+ streamSynthesisFrom: (textIn, req: CommonStreamSynthesizeRequest) =>
71
+ s.streamSynthesisFrom(textIn, req),
72
+ }),
73
+ ),
74
+ ),
75
+ Layer.succeed(TtsIncrementalText, undefined),
76
+ )
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Realtime variant of `InworldTranscriber`. Wires `streamTranscriptionFrom`
3
+ * to `wss://api.inworld.ai/stt/v1/transcribe:streamBidirectional` and
4
+ * registers the `SttStreaming` capability marker, so calls compile against
5
+ * this Layer alone.
6
+ *
7
+ * Sync `transcribe` reuses `transcribeImpl` from `./InworldTranscriber.js` —
8
+ * the only added surface is the WS path.
9
+ */
10
+ import { Effect, Layer } from "effect"
11
+ import { HttpClient } from "effect/unstable/http"
12
+ import {
13
+ type CommonTranscribeRequest,
14
+ SttStreaming,
15
+ Transcriber,
16
+ type TranscriberService,
17
+ } from "@effect-uai/core/Transcriber"
18
+ import {
19
+ type Config,
20
+ InworldTranscriber,
21
+ type InworldTranscriberService,
22
+ type InworldTranscribeRequest,
23
+ transcribeImpl,
24
+ } from "./InworldTranscriber.js"
25
+ import { streamTranscription } from "./realtimeStt.js"
26
+
27
+ export type {
28
+ Config,
29
+ InworldTranscriberService,
30
+ InworldTranscribeRequest,
31
+ } from "./InworldTranscriber.js"
32
+ export { InworldTranscriber } from "./InworldTranscriber.js"
33
+
34
+ export const make = (cfg: Config) =>
35
+ Effect.map(
36
+ HttpClient.HttpClient.asEffect(),
37
+ (client) =>
38
+ ({
39
+ transcribe: (r) =>
40
+ transcribeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
41
+ streamTranscriptionFrom: (audioIn, request) => streamTranscription(cfg)(audioIn, request),
42
+ }) satisfies InworldTranscriberService,
43
+ )
44
+
45
+ /**
46
+ * Realtime Layer. Registers `InworldTranscriber`, the generic
47
+ * `Transcriber`, **and** the `SttStreaming` capability marker.
48
+ */
49
+ export const layer = (cfg: Config) =>
50
+ Layer.mergeAll(
51
+ Layer.effect(InworldTranscriber, make(cfg)),
52
+ Layer.effect(
53
+ Transcriber,
54
+ Effect.map(
55
+ make(cfg),
56
+ (s): TranscriberService => ({
57
+ transcribe: (req: CommonTranscribeRequest) =>
58
+ s.transcribe(req as InworldTranscribeRequest),
59
+ streamTranscriptionFrom: s.streamTranscriptionFrom,
60
+ }),
61
+ ),
62
+ ),
63
+ Layer.succeed(SttStreaming, undefined),
64
+ )
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Sync + chunked-HTTP TTS via Inworld's REST endpoints.
3
+ *
4
+ * - `synthesize` → `POST /tts/v1/voice` (JSON body, single base64 audio in
5
+ * response).
6
+ * - `streamSynthesis` → `POST /tts/v1/voice:stream` (NDJSON response, one
7
+ * JSON object per line, each carrying a base64 audio chunk).
8
+ *
9
+ * Incremental text-in (`streamSynthesisFrom`) lives at the
10
+ * `InworldRealtimeSynthesizer` subpath, which adds the WS path and the
11
+ * `TtsIncrementalText` capability marker.
12
+ */
13
+ import { Context, Effect, Layer, Redacted, Schema, Stream } from "effect"
14
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
15
+ import * as AiError from "@effect-uai/core/AiError"
16
+ import type { AudioBlob, AudioChunk } from "@effect-uai/core/Audio"
17
+ import * as JSONL from "@effect-uai/core/JSONL"
18
+ import {
19
+ type CommonStreamSynthesizeRequest,
20
+ type CommonSynthesizeRequest,
21
+ SpeechSynthesizer,
22
+ type SpeechSynthesizerService,
23
+ } from "@effect-uai/core/SpeechSynthesizer"
24
+ import {
25
+ audioConfigFor,
26
+ authHeader,
27
+ decodeAudioContent,
28
+ defaultFormat,
29
+ httpStatusError,
30
+ transportFailure,
31
+ } from "./codec.js"
32
+ import type { InworldDeliveryMode, InworldTtsModel, InworldVoiceId } from "./models.js"
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Public types
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Inworld-typed synthesize request.
40
+ *
41
+ * - `deliveryMode` only honored by `inworld-tts-2` (`STABLE` / `BALANCED` /
42
+ * `CREATIVE` — older models ignore it).
43
+ * - `temperature` valid range is `(0, 2]` per docs.
44
+ * - `applyTextNormalization` defaults to `ON`; pass `"OFF"` to feed the
45
+ * model raw text (faster, but punctuation pacing is on you).
46
+ */
47
+ export type InworldSynthesizeRequest = Omit<CommonSynthesizeRequest, "model" | "voiceId"> & {
48
+ readonly model: InworldTtsModel
49
+ readonly voiceId: InworldVoiceId
50
+ readonly temperature?: number
51
+ readonly deliveryMode?: InworldDeliveryMode
52
+ readonly applyTextNormalization?: "ON" | "OFF"
53
+ }
54
+
55
+ export type InworldSynthesizerService = {
56
+ readonly synthesize: (r: InworldSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>
57
+ readonly streamSynthesis: (
58
+ r: InworldSynthesizeRequest,
59
+ ) => Stream.Stream<AudioChunk, AiError.AiError>
60
+ readonly streamSynthesisFrom: SpeechSynthesizerService["streamSynthesisFrom"]
61
+ }
62
+
63
+ export class InworldSynthesizer extends Context.Service<
64
+ InworldSynthesizer,
65
+ InworldSynthesizerService
66
+ >()("@betalyra/effect-uai/providers/inworld/InworldSynthesizer") {}
67
+
68
+ export type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Request body
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /** Exported for reuse by the realtime variant's BOS `create` frame. */
75
+ export const buildBody = (r: InworldSynthesizeRequest) =>
76
+ Effect.gen(function* () {
77
+ const format = r.outputFormat ?? defaultFormat
78
+ const audioConfig = yield* audioConfigFor(format, r.speed)
79
+ const body = {
80
+ text: r.text,
81
+ voiceId: r.voiceId,
82
+ modelId: r.model,
83
+ audioConfig,
84
+ ...(r.languageCode !== undefined && { language: r.languageCode }),
85
+ ...(r.deliveryMode !== undefined && { deliveryMode: r.deliveryMode }),
86
+ ...(r.temperature !== undefined && { temperature: r.temperature }),
87
+ ...(r.applyTextNormalization !== undefined && {
88
+ applyTextNormalization: r.applyTextNormalization,
89
+ }),
90
+ }
91
+ return { body, format }
92
+ })
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Wire schemas
96
+ // ---------------------------------------------------------------------------
97
+
98
+ const SyncResponse = Schema.Struct({ audioContent: Schema.String })
99
+ const decodeSyncResponse = Schema.decodeUnknownEffect(SyncResponse)
100
+
101
+ const StreamLine = Schema.Struct({
102
+ result: Schema.optional(Schema.Struct({ audioContent: Schema.optional(Schema.String) })),
103
+ error: Schema.optional(Schema.Unknown),
104
+ })
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // HTTP plumbing
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const baseUrl = (cfg: Config) => cfg.baseUrl ?? "https://api.inworld.ai"
111
+
112
+ const buildRequest = (cfg: Config, path: "/tts/v1/voice" | "/tts/v1/voice:stream", body: unknown) =>
113
+ HttpClientRequest.post(`${baseUrl(cfg)}${path}`).pipe(
114
+ HttpClientRequest.setHeader("Authorization", authHeader(cfg.apiKey)),
115
+ HttpClientRequest.bodyJsonUnsafe(body),
116
+ )
117
+
118
+ /** Exported for reuse by `InworldRealtimeSynthesizer`. */
119
+ export const synthesizeImpl = (cfg: Config) => (request: InworldSynthesizeRequest) =>
120
+ Effect.gen(function* () {
121
+ const client = yield* HttpClient.HttpClient
122
+ const { body, format } = yield* buildBody(request)
123
+ const httpRequest = buildRequest(cfg, "/tts/v1/voice", body)
124
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
125
+ if (response.status >= 400) {
126
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
127
+ return yield* Effect.fail(httpStatusError(response.status, text))
128
+ }
129
+ const json = yield* response.json.pipe(Effect.mapError(transportFailure))
130
+ const wire = yield* decodeSyncResponse(json).pipe(
131
+ Effect.mapError(
132
+ (cause) =>
133
+ new AiError.GenerationFailed({
134
+ provider: "inworld",
135
+ raw: { message: "sync TTS response missing `audioContent`", cause, json },
136
+ }),
137
+ ),
138
+ )
139
+ const bytes = yield* decodeAudioContent(wire.audioContent)
140
+ return { format, bytes } satisfies AudioBlob
141
+ })
142
+
143
+ const lineToStream = (line: typeof StreamLine.Type): Stream.Stream<AudioChunk, AiError.AiError> => {
144
+ if (line.error !== undefined) {
145
+ return Stream.fail(new AiError.GenerationFailed({ provider: "inworld", raw: line.error }))
146
+ }
147
+ const b64 = line.result?.audioContent
148
+ if (b64 === undefined) return Stream.empty
149
+ return Stream.fromEffect(
150
+ decodeAudioContent(b64).pipe(Effect.map((bytes): AudioChunk => ({ bytes }))),
151
+ )
152
+ }
153
+
154
+ /** Exported for reuse by `InworldRealtimeSynthesizer`. */
155
+ export const streamSynthesisImpl = (cfg: Config) => (request: InworldSynthesizeRequest) =>
156
+ Stream.unwrap(
157
+ Effect.gen(function* () {
158
+ const client = yield* HttpClient.HttpClient
159
+ const { body } = yield* buildBody(request)
160
+ const httpRequest = buildRequest(cfg, "/tts/v1/voice:stream", body)
161
+ const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
162
+ if (response.status >= 400) {
163
+ const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
164
+ return Stream.fail(httpStatusError(response.status, text))
165
+ }
166
+ return response.stream.pipe(
167
+ Stream.mapError(transportFailure),
168
+ JSONL.fromBytes,
169
+ JSONL.parse(StreamLine),
170
+ Stream.mapError(
171
+ (cause): AiError.AiError =>
172
+ new AiError.GenerationFailed({
173
+ provider: "inworld",
174
+ raw: { message: "NDJSON parse failed", cause },
175
+ }),
176
+ ),
177
+ Stream.flatMap(lineToStream),
178
+ )
179
+ }),
180
+ )
181
+
182
+ const streamUnsupported = <E, R>(
183
+ _textIn: Stream.Stream<string, E, R>,
184
+ _request: CommonStreamSynthesizeRequest,
185
+ ): Stream.Stream<AudioChunk, AiError.AiError | E, R> => {
186
+ const fail: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(
187
+ new AiError.Unsupported({
188
+ provider: "inworld",
189
+ capability: "streamSynthesisFrom",
190
+ reason:
191
+ "This Layer is sync-only. Import `@effect-uai/inworld/InworldRealtimeSynthesizer` to use the WS streamSynthesisFrom path.",
192
+ }),
193
+ )
194
+ return fail
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Constructors
199
+ // ---------------------------------------------------------------------------
200
+
201
+ export const make = (
202
+ cfg: Config,
203
+ ): Effect.Effect<InworldSynthesizerService, never, HttpClient.HttpClient> =>
204
+ Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
205
+ synthesize: (r) =>
206
+ synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
207
+ streamSynthesis: (r) =>
208
+ streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),
209
+ streamSynthesisFrom: streamUnsupported,
210
+ }))
211
+
212
+ /**
213
+ * Sync-only Layer. Registers `InworldSynthesizer` + the generic
214
+ * `SpeechSynthesizer`. Does **not** register `TtsIncrementalText` — for
215
+ * incremental text-in use `@effect-uai/inworld/InworldRealtimeSynthesizer`.
216
+ */
217
+ export const layer = (
218
+ cfg: Config,
219
+ ): Layer.Layer<InworldSynthesizer | SpeechSynthesizer, never, HttpClient.HttpClient> =>
220
+ Layer.merge(
221
+ Layer.effect(InworldSynthesizer, make(cfg)),
222
+ Layer.effect(
223
+ SpeechSynthesizer,
224
+ Effect.map(
225
+ make(cfg),
226
+ (s): SpeechSynthesizerService => ({
227
+ synthesize: (req: CommonSynthesizeRequest) =>
228
+ s.synthesize(req as InworldSynthesizeRequest),
229
+ streamSynthesis: (req: CommonSynthesizeRequest) =>
230
+ s.streamSynthesis(req as InworldSynthesizeRequest),
231
+ streamSynthesisFrom: s.streamSynthesisFrom,
232
+ }),
233
+ ),
234
+ ),
235
+ )