@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.
- package/LICENSE +21 -0
- package/dist/InworldRealtimeSynthesizer.d.mts +30 -0
- package/dist/InworldRealtimeSynthesizer.d.mts.map +1 -0
- package/dist/InworldRealtimeSynthesizer.mjs +43 -0
- package/dist/InworldRealtimeSynthesizer.mjs.map +1 -0
- package/dist/InworldRealtimeTranscriber.d.mts +30 -0
- package/dist/InworldRealtimeTranscriber.d.mts.map +1 -0
- package/dist/InworldRealtimeTranscriber.mjs +37 -0
- package/dist/InworldRealtimeTranscriber.mjs.map +1 -0
- package/dist/InworldSynthesizer.d.mts +71 -0
- package/dist/InworldSynthesizer.d.mts.map +1 -0
- package/dist/InworldSynthesizer.mjs +131 -0
- package/dist/InworldSynthesizer.mjs.map +1 -0
- package/dist/InworldTranscriber.d.mts +48 -0
- package/dist/InworldTranscriber.d.mts.map +1 -0
- package/dist/InworldTranscriber.mjs +116 -0
- package/dist/InworldTranscriber.mjs.map +1 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/codec-C-2ssVAz.d.mts +35 -0
- package/dist/codec-C-2ssVAz.d.mts.map +1 -0
- package/dist/codec.d.mts +2 -0
- package/dist/codec.mjs +100 -0
- package/dist/codec.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/models-CPLZDcgt.d.mts +47 -0
- package/dist/models-CPLZDcgt.d.mts.map +1 -0
- package/dist/models.d.mts +2 -0
- package/dist/models.mjs +1 -0
- package/dist/realtimeStt.d.mts +38 -0
- package/dist/realtimeStt.d.mts.map +1 -0
- package/dist/realtimeStt.mjs +145 -0
- package/dist/realtimeStt.mjs.map +1 -0
- package/dist/realtimeTts.d.mts +29 -0
- package/dist/realtimeTts.d.mts.map +1 -0
- package/dist/realtimeTts.mjs +94 -0
- package/dist/realtimeTts.mjs.map +1 -0
- package/dist/wsAuth.d.mts +8 -0
- package/dist/wsAuth.d.mts.map +1 -0
- package/dist/wsAuth.mjs +8 -0
- package/dist/wsAuth.mjs.map +1 -0
- package/package.json +80 -0
- package/src/InworldRealtimeSynthesizer.ts +76 -0
- package/src/InworldRealtimeTranscriber.ts +64 -0
- package/src/InworldSynthesizer.ts +235 -0
- package/src/InworldTranscriber.ts +225 -0
- package/src/codec.ts +141 -0
- package/src/index.ts +6 -0
- package/src/models.ts +69 -0
- package/src/realtimeStt.ts +236 -0
- package/src/realtimeTts.ts +167 -0
- 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"}
|
package/dist/wsAuth.mjs
ADDED
|
@@ -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
|
+
)
|