@effect-uai/inworld 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/InworldRealtimeTranscriber.d.mts +1 -1
- package/dist/InworldSynthesizer.d.mts.map +1 -1
- package/dist/InworldSynthesizer.mjs +4 -4
- package/dist/InworldSynthesizer.mjs.map +1 -1
- package/dist/InworldTranscriber.d.mts +1 -1
- package/dist/InworldTranscriber.d.mts.map +1 -1
- package/dist/InworldTranscriber.mjs +8 -3
- package/dist/InworldTranscriber.mjs.map +1 -1
- package/dist/realtimeStt.d.mts.map +1 -1
- package/dist/realtimeStt.mjs +16 -13
- package/dist/realtimeStt.mjs.map +1 -1
- package/dist/realtimeTts.d.mts +4 -0
- package/dist/realtimeTts.d.mts.map +1 -1
- package/dist/realtimeTts.mjs +6 -0
- package/dist/realtimeTts.mjs.map +1 -1
- package/package.json +2 -2
- package/src/InworldSynthesizer.ts +4 -6
- package/src/InworldTranscriber.ts +10 -7
- package/src/realtimeStt.ts +11 -10
- package/src/realtimeTts.ts +17 -0
|
@@ -8,7 +8,7 @@ declare namespace InworldRealtimeTranscriber_d_exports {
|
|
|
8
8
|
export { Config, InworldTranscribeRequest, InworldTranscriber, InworldTranscriberService, layer, make };
|
|
9
9
|
}
|
|
10
10
|
declare const make: (cfg: Config) => Effect.Effect<{
|
|
11
|
-
transcribe: (r: InworldTranscribeRequest) => Effect.Effect<any, any,
|
|
11
|
+
transcribe: (r: InworldTranscribeRequest) => Effect.Effect<any, any, unknown>;
|
|
12
12
|
streamTranscriptionFrom: (audioIn: any, request: any) => _$effect_Stream0.Stream<TranscriptEvent, any, unknown>;
|
|
13
13
|
}, never, HttpClient.HttpClient>;
|
|
14
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InworldSynthesizer.d.mts","names":[],"sources":["../src/InworldSynthesizer.ts"],"mappings":";;;;;;;;;;;;;;;;;AA+CA;;;;KAAY,wBAAA,GAA2B,IAAA,CAAK,uBAAA;EAAA,SACjC,KAAA,EAAO,eAAA;EAAA,SACP,OAAA,EAAS,cAAA;EAAA,SACT,WAAA;EAAA,SACA,YAAA,GAAe,mBAAA;EAAA,SACf,sBAAA;AAAA;AAAA,KAGC,yBAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,wBAAA,KAA6B,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SAC9E,eAAA,GACP,CAAA,EAAG,wBAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;EAAA,SACrB,kBAAA,EAAoB,wBAAA;EAAA,SACpB,wBAAA,EAA0B,wBAAA;AAAA;AAAA,cACpC,uBAAA;cAEY,kBAAA,SAA2B,uBAAA;AAAA,KAK5B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;;
|
|
1
|
+
{"version":3,"file":"InworldSynthesizer.d.mts","names":[],"sources":["../src/InworldSynthesizer.ts"],"mappings":";;;;;;;;;;;;;;;;;AA+CA;;;;KAAY,wBAAA,GAA2B,IAAA,CAAK,uBAAA;EAAA,SACjC,KAAA,EAAO,eAAA;EAAA,SACP,OAAA,EAAS,cAAA;EAAA,SACT,WAAA;EAAA,SACA,YAAA,GAAe,mBAAA;EAAA,SACf,sBAAA;AAAA;AAAA,KAGC,yBAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,wBAAA,KAA6B,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SAC9E,eAAA,GACP,CAAA,EAAG,wBAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;EAAA,SACrB,kBAAA,EAAoB,wBAAA;EAAA,SACpB,wBAAA,EAA0B,wBAAA;AAAA;AAAA,cACpC,uBAAA;cAEY,kBAAA,SAA2B,uBAAA;AAAA,KAK5B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;;cAyBvD,SAAA,GAAa,CAAA,EAAG,wBAAA,KAAwB,MAAA,CAAA,MAAA;;;;;;;;;;;;;;cA4CxC,cAAA,GAAkB,GAAA,EAAK,MAAA,MAAY,OAAA,EAAS,wBAAA,KAAwB,MAAA,CAAA,MAAA,WAAA,UAAA,CAAA,UAAA;;cAoCpE,mBAAA,GAAuB,GAAA,EAAK,MAAA,MAAY,OAAA,EAAS,wBAAA,KAAwB,MAAA,CAAA,MAAA,aAAA,UAAA,CAAA,UAAA;;cA2CzE,uBAAA,EAAyB,wBAAA;;cAUzB,6BAAA,EAA+B,wBAAA;AAAA,cAc/B,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,yBAAA,SAAkC,UAAA,CAAW,UAAA;;;;;;cAgBjD,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,kBAAA,GAAqB,iBAAA,SAA0B,UAAA,CAAW,UAAA"}
|
|
@@ -31,14 +31,14 @@ var InworldSynthesizer_exports = /* @__PURE__ */ __exportAll({
|
|
|
31
31
|
var InworldSynthesizer = class extends Context.Service()("@betalyra/effect-uai/providers/inworld/InworldSynthesizer") {};
|
|
32
32
|
/**
|
|
33
33
|
* Inworld accepts inline phoneme overrides as `/ipa/` tokens directly
|
|
34
|
-
* in the text
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* in the text, no separate field. `CustomPronunciation.pronunciation`
|
|
35
|
+
* is always IPA, so every entry is inlined. Whole-word,
|
|
36
|
+
* case-insensitive replacement.
|
|
37
37
|
*/
|
|
38
38
|
const escapeForRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
39
|
const applyPronunciations = (text, pronunciations) => {
|
|
40
40
|
if (pronunciations === void 0) return text;
|
|
41
|
-
return Array.reduce(pronunciations, text, (acc, p) =>
|
|
41
|
+
return Array.reduce(pronunciations, text, (acc, p) => acc.replace(new RegExp(`\\b${escapeForRegex(p.phrase)}\\b`, "i"), `/${p.pronunciation}/`));
|
|
42
42
|
};
|
|
43
43
|
/** Exported for reuse by the realtime variant's BOS `create` frame. */
|
|
44
44
|
const buildBody = (r) => Effect.gen(function* () {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InworldSynthesizer.mjs","names":["Arr"],"sources":["../src/InworldSynthesizer.ts"],"sourcesContent":["/**\n * Sync + chunked-HTTP TTS via Inworld's REST endpoints.\n *\n * - `synthesize` → `POST /tts/v1/voice` (JSON body, single base64 audio in\n * response).\n * - `streamSynthesis` → `POST /tts/v1/voice:stream` (NDJSON response, one\n * JSON object per line, each carrying a base64 audio chunk).\n *\n * Incremental text-in (`streamSynthesisFrom`) lives at the\n * `InworldRealtimeSynthesizer` subpath, which adds the WS path and the\n * `TtsIncrementalText` capability marker.\n */\nimport { Array as Arr, Context, Effect, Layer, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport {\n type CommonStreamSynthesizeRequest,\n type CommonSynthesizeRequest,\n type CustomPronunciation,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n audioConfigFor,\n authHeader,\n decodeAudioContent,\n defaultFormat,\n httpStatusError,\n transportFailure,\n} from \"./codec.js\"\nimport type { InworldDeliveryMode, InworldTtsModel, InworldVoiceId } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Inworld-typed synthesize request.\n *\n * - `deliveryMode` only honored by `inworld-tts-2` (`STABLE` / `BALANCED` /\n * `CREATIVE` — older models ignore it).\n * - `temperature` valid range is `(0, 2]` per docs.\n * - `applyTextNormalization` defaults to `ON`; pass `\"OFF\"` to feed the\n * model raw text (faster, but punctuation pacing is on you).\n */\nexport type InworldSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: InworldTtsModel\n readonly voiceId: InworldVoiceId\n readonly temperature?: number\n readonly deliveryMode?: InworldDeliveryMode\n readonly applyTextNormalization?: \"ON\" | \"OFF\"\n}\n\nexport type InworldSynthesizerService = {\n readonly synthesize: (r: InworldSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: InworldSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n readonly synthesizeDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"]\n readonly streamSynthesizeDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"]\n}\n\nexport class InworldSynthesizer extends Context.Service<\n InworldSynthesizer,\n InworldSynthesizerService\n>()(\"@betalyra/effect-uai/providers/inworld/InworldSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\n/**\n * Inworld accepts inline phoneme overrides as `/ipa/` tokens directly\n * in the text — no separate field. Only IPA is supported; entries with\n * other encodings are silently dropped (audio still renders with the\n * default pronunciation). Whole-word, case-insensitive replacement.\n */\nconst escapeForRegex = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\nconst applyPronunciations = (\n text: string,\n pronunciations: ReadonlyArray<CustomPronunciation> | undefined,\n): string => {\n if (pronunciations === undefined) return text\n return Arr.reduce(pronunciations, text, (acc, p) =>\n p.encoding === \"ipa\"\n ? acc.replace(new RegExp(`\\\\b${escapeForRegex(p.phrase)}\\\\b`, \"i\"), `/${p.pronunciation}/`)\n : acc,\n )\n}\n\n/** Exported for reuse by the realtime variant's BOS `create` frame. */\nexport const buildBody = (r: InworldSynthesizeRequest) =>\n Effect.gen(function* () {\n const format = r.outputFormat ?? defaultFormat\n const audioConfig = yield* audioConfigFor(format, r.speed)\n const body = {\n text: applyPronunciations(r.text, r.pronunciations),\n voiceId: r.voiceId,\n modelId: r.model,\n audioConfig,\n ...(r.languageCode !== undefined && { language: r.languageCode }),\n ...(r.deliveryMode !== undefined && { deliveryMode: r.deliveryMode }),\n ...(r.temperature !== undefined && { temperature: r.temperature }),\n ...(r.applyTextNormalization !== undefined && {\n applyTextNormalization: r.applyTextNormalization,\n }),\n }\n return { body, format }\n })\n\n// ---------------------------------------------------------------------------\n// Wire schemas\n// ---------------------------------------------------------------------------\n\nconst SyncResponse = Schema.Struct({ audioContent: Schema.String })\nconst decodeSyncResponse = Schema.decodeUnknownEffect(SyncResponse)\n\nconst StreamLine = Schema.Struct({\n result: Schema.optional(Schema.Struct({ audioContent: Schema.optional(Schema.String) })),\n error: Schema.optional(Schema.Unknown),\n})\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://api.inworld.ai\"\n\nconst buildRequest = (cfg: Config, path: \"/tts/v1/voice\" | \"/tts/v1/voice:stream\", body: unknown) =>\n HttpClientRequest.post(`${baseUrl(cfg)}${path}`).pipe(\n HttpClientRequest.setHeader(\"Authorization\", authHeader(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const synthesizeImpl = (cfg: Config) => (request: InworldSynthesizeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { body, format } = yield* buildBody(request)\n const httpRequest = buildRequest(cfg, \"/tts/v1/voice\", body)\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeSyncResponse(json).pipe(\n Effect.mapError(\n (cause) =>\n new AiError.GenerationFailed({\n provider: \"inworld\",\n raw: { message: \"sync TTS response missing `audioContent`\", cause, json },\n }),\n ),\n )\n const bytes = yield* decodeAudioContent(wire.audioContent)\n return { format, bytes } satisfies AudioBlob\n })\n\nconst lineToStream = (line: typeof StreamLine.Type): Stream.Stream<AudioChunk, AiError.AiError> => {\n if (line.error !== undefined) {\n return Stream.fail(new AiError.GenerationFailed({ provider: \"inworld\", raw: line.error }))\n }\n const b64 = line.result?.audioContent\n if (b64 === undefined) return Stream.empty\n return Stream.fromEffect(\n decodeAudioContent(b64).pipe(Effect.map((bytes): AudioChunk => ({ bytes }))),\n )\n}\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const streamSynthesisImpl = (cfg: Config) => (request: InworldSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { body } = yield* buildBody(request)\n const httpRequest = buildRequest(cfg, \"/tts/v1/voice:stream\", body)\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n return response.stream.pipe(\n Stream.mapError(transportFailure),\n JSONL.fromBytes,\n JSONL.parse(StreamLine),\n Stream.mapError(\n (cause): AiError.AiError =>\n new AiError.GenerationFailed({\n provider: \"inworld\",\n raw: { message: \"NDJSON parse failed\", cause },\n }),\n ),\n Stream.flatMap(lineToStream),\n )\n }),\n )\n\nconst streamUnsupported = <E, R>(\n _textIn: Stream.Stream<string, E, R>,\n _request: CommonStreamSynthesizeRequest,\n): Stream.Stream<AudioChunk, AiError.AiError | E, R> => {\n const fail: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"streamSynthesisFrom\",\n reason:\n \"This Layer is sync-only. Import `@effect-uai/inworld/InworldRealtimeSynthesizer` to use the WS streamSynthesisFrom path.\",\n }),\n )\n return fail\n}\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const dialogueUnsupportedImpl: SpeechSynthesizerService[\"synthesizeDialogue\"] = () =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"synthesizeDialogue\",\n reason: \"Inworld TTS has no multi-speaker endpoint.\",\n }),\n )\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const streamDialogueUnsupportedImpl: SpeechSynthesizerService[\"streamSynthesizeDialogue\"] =\n () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"streamSynthesizeDialogue\",\n reason: \"Inworld TTS has no multi-speaker endpoint.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<InworldSynthesizerService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),\n streamSynthesisFrom: streamUnsupported,\n synthesizeDialogue: dialogueUnsupportedImpl,\n streamSynthesizeDialogue: streamDialogueUnsupportedImpl,\n }))\n\n/**\n * Sync-only Layer. Registers `InworldSynthesizer` + the generic\n * `SpeechSynthesizer`. Does **not** register `TtsIncrementalText` — for\n * incremental text-in use `@effect-uai/inworld/InworldRealtimeSynthesizer`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<InworldSynthesizer | SpeechSynthesizer, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(InworldSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as InworldSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as InworldSynthesizeRequest),\n streamSynthesisFrom: s.streamSynthesisFrom,\n synthesizeDialogue: s.synthesizeDialogue,\n streamSynthesizeDialogue: s.streamSynthesizeDialogue,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InworldSynthesizer.mjs","names":["Arr"],"sources":["../src/InworldSynthesizer.ts"],"sourcesContent":["/**\n * Sync + chunked-HTTP TTS via Inworld's REST endpoints.\n *\n * - `synthesize` → `POST /tts/v1/voice` (JSON body, single base64 audio in\n * response).\n * - `streamSynthesis` → `POST /tts/v1/voice:stream` (NDJSON response, one\n * JSON object per line, each carrying a base64 audio chunk).\n *\n * Incremental text-in (`streamSynthesisFrom`) lives at the\n * `InworldRealtimeSynthesizer` subpath, which adds the WS path and the\n * `TtsIncrementalText` capability marker.\n */\nimport { Array as Arr, Context, Effect, Layer, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk } from \"@effect-uai/core/Audio\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport {\n type CommonStreamSynthesizeRequest,\n type CommonSynthesizeRequest,\n type CustomPronunciation,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n audioConfigFor,\n authHeader,\n decodeAudioContent,\n defaultFormat,\n httpStatusError,\n transportFailure,\n} from \"./codec.js\"\nimport type { InworldDeliveryMode, InworldTtsModel, InworldVoiceId } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Inworld-typed synthesize request.\n *\n * - `deliveryMode` only honored by `inworld-tts-2` (`STABLE` / `BALANCED` /\n * `CREATIVE` — older models ignore it).\n * - `temperature` valid range is `(0, 2]` per docs.\n * - `applyTextNormalization` defaults to `ON`; pass `\"OFF\"` to feed the\n * model raw text (faster, but punctuation pacing is on you).\n */\nexport type InworldSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: InworldTtsModel\n readonly voiceId: InworldVoiceId\n readonly temperature?: number\n readonly deliveryMode?: InworldDeliveryMode\n readonly applyTextNormalization?: \"ON\" | \"OFF\"\n}\n\nexport type InworldSynthesizerService = {\n readonly synthesize: (r: InworldSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: InworldSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n readonly synthesizeDialogue: SpeechSynthesizerService[\"synthesizeDialogue\"]\n readonly streamSynthesizeDialogue: SpeechSynthesizerService[\"streamSynthesizeDialogue\"]\n}\n\nexport class InworldSynthesizer extends Context.Service<\n InworldSynthesizer,\n InworldSynthesizerService\n>()(\"@betalyra/effect-uai/providers/inworld/InworldSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\n/**\n * Inworld accepts inline phoneme overrides as `/ipa/` tokens directly\n * in the text, no separate field. `CustomPronunciation.pronunciation`\n * is always IPA, so every entry is inlined. Whole-word,\n * case-insensitive replacement.\n */\nconst escapeForRegex = (s: string): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")\n\nconst applyPronunciations = (\n text: string,\n pronunciations: ReadonlyArray<CustomPronunciation> | undefined,\n): string => {\n if (pronunciations === undefined) return text\n return Arr.reduce(pronunciations, text, (acc, p) =>\n acc.replace(new RegExp(`\\\\b${escapeForRegex(p.phrase)}\\\\b`, \"i\"), `/${p.pronunciation}/`),\n )\n}\n\n/** Exported for reuse by the realtime variant's BOS `create` frame. */\nexport const buildBody = (r: InworldSynthesizeRequest) =>\n Effect.gen(function* () {\n const format = r.outputFormat ?? defaultFormat\n const audioConfig = yield* audioConfigFor(format, r.speed)\n const body = {\n text: applyPronunciations(r.text, r.pronunciations),\n voiceId: r.voiceId,\n modelId: r.model,\n audioConfig,\n ...(r.languageCode !== undefined && { language: r.languageCode }),\n ...(r.deliveryMode !== undefined && { deliveryMode: r.deliveryMode }),\n ...(r.temperature !== undefined && { temperature: r.temperature }),\n ...(r.applyTextNormalization !== undefined && {\n applyTextNormalization: r.applyTextNormalization,\n }),\n }\n return { body, format }\n })\n\n// ---------------------------------------------------------------------------\n// Wire schemas\n// ---------------------------------------------------------------------------\n\nconst SyncResponse = Schema.Struct({ audioContent: Schema.String })\nconst decodeSyncResponse = Schema.decodeUnknownEffect(SyncResponse)\n\nconst StreamLine = Schema.Struct({\n result: Schema.optional(Schema.Struct({ audioContent: Schema.optional(Schema.String) })),\n error: Schema.optional(Schema.Unknown),\n})\n\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://api.inworld.ai\"\n\nconst buildRequest = (cfg: Config, path: \"/tts/v1/voice\" | \"/tts/v1/voice:stream\", body: unknown) =>\n HttpClientRequest.post(`${baseUrl(cfg)}${path}`).pipe(\n HttpClientRequest.setHeader(\"Authorization\", authHeader(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const synthesizeImpl = (cfg: Config) => (request: InworldSynthesizeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { body, format } = yield* buildBody(request)\n const httpRequest = buildRequest(cfg, \"/tts/v1/voice\", body)\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeSyncResponse(json).pipe(\n Effect.mapError(\n (cause) =>\n new AiError.GenerationFailed({\n provider: \"inworld\",\n raw: { message: \"sync TTS response missing `audioContent`\", cause, json },\n }),\n ),\n )\n const bytes = yield* decodeAudioContent(wire.audioContent)\n return { format, bytes } satisfies AudioBlob\n })\n\nconst lineToStream = (line: typeof StreamLine.Type): Stream.Stream<AudioChunk, AiError.AiError> => {\n if (line.error !== undefined) {\n return Stream.fail(new AiError.GenerationFailed({ provider: \"inworld\", raw: line.error }))\n }\n const b64 = line.result?.audioContent\n if (b64 === undefined) return Stream.empty\n return Stream.fromEffect(\n decodeAudioContent(b64).pipe(Effect.map((bytes): AudioChunk => ({ bytes }))),\n )\n}\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const streamSynthesisImpl = (cfg: Config) => (request: InworldSynthesizeRequest) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const { body } = yield* buildBody(request)\n const httpRequest = buildRequest(cfg, \"/tts/v1/voice:stream\", body)\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return Stream.fail(httpStatusError(response.status, text))\n }\n return response.stream.pipe(\n Stream.mapError(transportFailure),\n JSONL.fromBytes,\n JSONL.parse(StreamLine),\n Stream.mapError(\n (cause): AiError.AiError =>\n new AiError.GenerationFailed({\n provider: \"inworld\",\n raw: { message: \"NDJSON parse failed\", cause },\n }),\n ),\n Stream.flatMap(lineToStream),\n )\n }),\n )\n\nconst streamUnsupported = <E, R>(\n _textIn: Stream.Stream<string, E, R>,\n _request: CommonStreamSynthesizeRequest,\n): Stream.Stream<AudioChunk, AiError.AiError | E, R> => {\n const fail: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"streamSynthesisFrom\",\n reason:\n \"This Layer is sync-only. Import `@effect-uai/inworld/InworldRealtimeSynthesizer` to use the WS streamSynthesisFrom path.\",\n }),\n )\n return fail\n}\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const dialogueUnsupportedImpl: SpeechSynthesizerService[\"synthesizeDialogue\"] = () =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"synthesizeDialogue\",\n reason: \"Inworld TTS has no multi-speaker endpoint.\",\n }),\n )\n\n/** Exported for reuse by `InworldRealtimeSynthesizer`. */\nexport const streamDialogueUnsupportedImpl: SpeechSynthesizerService[\"streamSynthesizeDialogue\"] =\n () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"streamSynthesizeDialogue\",\n reason: \"Inworld TTS has no multi-speaker endpoint.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<InworldSynthesizerService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n streamSynthesisImpl(cfg)(r).pipe(Stream.provideService(HttpClient.HttpClient, client)),\n streamSynthesisFrom: streamUnsupported,\n synthesizeDialogue: dialogueUnsupportedImpl,\n streamSynthesizeDialogue: streamDialogueUnsupportedImpl,\n }))\n\n/**\n * Sync-only Layer. Registers `InworldSynthesizer` + the generic\n * `SpeechSynthesizer`. Does **not** register `TtsIncrementalText` — for\n * incremental text-in use `@effect-uai/inworld/InworldRealtimeSynthesizer`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<InworldSynthesizer | SpeechSynthesizer, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(InworldSynthesizer, make(cfg)),\n Layer.effect(\n SpeechSynthesizer,\n Effect.map(\n make(cfg),\n (s): SpeechSynthesizerService => ({\n synthesize: (req: CommonSynthesizeRequest) =>\n s.synthesize(req as InworldSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as InworldSynthesizeRequest),\n streamSynthesisFrom: s.streamSynthesisFrom,\n synthesizeDialogue: s.synthesizeDialogue,\n streamSynthesizeDialogue: s.streamSynthesizeDialogue,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
|
@@ -31,7 +31,7 @@ type Config = {
|
|
|
31
31
|
readonly baseUrl?: string;
|
|
32
32
|
};
|
|
33
33
|
/** Exported for reuse by `InworldRealtimeTranscriber`. */
|
|
34
|
-
declare const transcribeImpl: (cfg: Config) => (request: InworldTranscribeRequest) => Effect.Effect<any, any,
|
|
34
|
+
declare const transcribeImpl: (cfg: Config) => (request: InworldTranscribeRequest) => Effect.Effect<any, any, unknown>;
|
|
35
35
|
declare const make: (cfg: Config) => Effect.Effect<InworldTranscriberService, never, HttpClient.HttpClient>;
|
|
36
36
|
/**
|
|
37
37
|
* Sync-only Layer. Registers `InworldTranscriber` + the generic
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InworldTranscriber.d.mts","names":[],"sources":["../src/InworldTranscriber.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"InworldTranscriber.d.mts","names":[],"sources":["../src/InworldTranscriber.ts"],"mappings":";;;;;;;;;;;;;AAkCA;;;KAAY,wBAAA,GAA2B,IAAA,CAAK,uBAAA;EAAA,SACjC,KAAA,EAAO,eAAA;EAAA,SACP,aAAA;EAAA,SACA,eAAA;EAAA,SACA,gBAAA;AAAA;AAAA,KAGC,yBAAA;EAAA,SACD,UAAA,GACP,CAAA,EAAG,wBAAA,KACA,MAAA,CAAO,MAAA,CAAO,gBAAA,EAAkB,OAAA,CAAQ,OAAA;EAAA,SACpC,uBAAA,EAAyB,kBAAA;AAAA;AAAA,cACnC,uBAAA;cAEY,kBAAA,SAA2B,uBAAA;AAAA,KAK5B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;;cAwFvD,cAAA,GAAkB,GAAA,EAAK,MAAA,MAAY,OAAA,EAAS,wBAAA,KAAwB,MAAA,CAAA,MAAA;AAAA,cAwDpE,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,yBAAA,SAAkC,UAAA,CAAW,UAAA;;;;;;cAYjD,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,kBAAA,GAAqB,WAAA,SAAoB,UAAA,CAAW,UAAA"}
|
|
@@ -4,6 +4,7 @@ import { Context, Effect, Encoding, Layer, Match, Schema, Stream } from "effect"
|
|
|
4
4
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
5
|
import * as AiError from "@effect-uai/core/AiError";
|
|
6
6
|
import { Transcriber } from "@effect-uai/core/Transcriber";
|
|
7
|
+
import * as Capabilities from "@effect-uai/core/Capabilities";
|
|
7
8
|
//#region src/InworldTranscriber.ts
|
|
8
9
|
/**
|
|
9
10
|
* Sync transcription via Inworld's REST endpoint.
|
|
@@ -28,10 +29,14 @@ const urlNotSupported = new AiError.InvalidRequest({
|
|
|
28
29
|
raw: "Inworld accepts only inline base64. Fetch the URL yourself and pass `{ _tag: \"bytes\", bytes, mimeType }`."
|
|
29
30
|
});
|
|
30
31
|
const audioToBase64 = Match.type().pipe(Match.tag("bytes", (a) => Effect.succeed(Encoding.encodeBase64(a.bytes))), Match.tag("base64", (a) => Effect.succeed(a.base64)), Match.tag("url", () => Effect.fail(urlNotSupported)), Match.exhaustive);
|
|
31
|
-
const promptToTerms = (prompt) => prompt === void 0 ? void 0 : typeof prompt === "string" ? [prompt] : prompt.terms;
|
|
32
32
|
const buildBody = (request) => Effect.gen(function* () {
|
|
33
33
|
const content = yield* audioToBase64(request.audio);
|
|
34
|
-
|
|
34
|
+
yield* Capabilities.warnDroppedWhen(request.prompt, {
|
|
35
|
+
provider: "inworld",
|
|
36
|
+
capability: "prompt",
|
|
37
|
+
field: "prompt",
|
|
38
|
+
reason: "Inworld STT has no free-form prompt field; bias via `biasingTerms`."
|
|
39
|
+
});
|
|
35
40
|
return {
|
|
36
41
|
transcribeConfig: {
|
|
37
42
|
modelId: request.model,
|
|
@@ -39,7 +44,7 @@ const buildBody = (request) => Effect.gen(function* () {
|
|
|
39
44
|
...request.sampleRateHertz !== void 0 && { sampleRateHertz: request.sampleRateHertz },
|
|
40
45
|
numberOfChannels: request.numberOfChannels ?? 1,
|
|
41
46
|
...request.language !== void 0 && { language: request.language },
|
|
42
|
-
...
|
|
47
|
+
...request.biasingTerms !== void 0 && { prompts: request.biasingTerms },
|
|
43
48
|
...request.wordTimestamps === true && { includeWordTimestamps: true }
|
|
44
49
|
},
|
|
45
50
|
audioData: { content }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InworldTranscriber.mjs","names":[],"sources":["../src/InworldTranscriber.ts"],"sourcesContent":["/**\n * Sync transcription via Inworld's REST endpoint.\n *\n * - `transcribe` → `POST /stt/v1/transcribe` with audio as base64 in\n * `audioData.content`. URL input is not accepted by Inworld — the\n * adapter rejects `_tag: \"url\"` audio sources with `InvalidRequest`.\n *\n * Streaming transcription lives at the `InworldRealtimeTranscriber`\n * subpath, which adds the WS path and the `SttStreaming` capability marker.\n */\nimport { Context, Effect, Encoding, Layer, Match, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioSource } from \"@effect-uai/core/Audio\"\nimport type { TranscriptEvent, TranscriptResult, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport {\n type CommonStreamTranscribeRequest,\n type CommonTranscribeRequest,\n Transcriber,\n type TranscriberService,\n} from \"@effect-uai/core/Transcriber\"\nimport { authHeader, httpStatusError, transportFailure } from \"./codec.js\"\nimport type { InworldSttModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Inworld-typed transcribe request. `numberOfChannels` defaults to 1.\n * `audioEncoding` is autodetected from the audio MIME by default; pass\n * an explicit value to override (useful for raw PCM bytes).\n */\nexport type InworldTranscribeRequest = Omit<CommonTranscribeRequest, \"model\"> & {\n readonly model: InworldSttModel\n readonly audioEncoding?: \"AUTO_DETECT\" | \"LINEAR16\" | \"MP3\" | \"OGG_OPUS\" | \"FLAC\"\n readonly sampleRateHertz?: number\n readonly numberOfChannels?: number\n}\n\nexport type InworldTranscriberService = {\n readonly transcribe: (\n r: InworldTranscribeRequest,\n ) => Effect.Effect<TranscriptResult, AiError.AiError>\n readonly streamTranscriptionFrom: TranscriberService[\"streamTranscriptionFrom\"]\n}\n\nexport class InworldTranscriber extends Context.Service<\n InworldTranscriber,\n InworldTranscriberService\n>()(\"@betalyra/effect-uai/providers/inworld/InworldTranscriber\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// AudioSource → base64. Inworld's sync endpoint takes inline base64 only.\n// ---------------------------------------------------------------------------\n\nconst urlNotSupported: AiError.AiError = new AiError.InvalidRequest({\n provider: \"inworld\",\n param: \"audio\",\n raw: 'Inworld accepts only inline base64. Fetch the URL yourself and pass `{ _tag: \"bytes\", bytes, mimeType }`.',\n})\n\nconst audioToBase64: (audio: AudioSource) => Effect.Effect<string, AiError.AiError> =\n Match.type<AudioSource>().pipe(\n Match.tag(\"bytes\", (a) => Effect.succeed(Encoding.encodeBase64(a.bytes))),\n Match.tag(\"base64\", (a) => Effect.succeed(a.base64)),\n Match.tag(\"url\", () => Effect.fail(urlNotSupported)),\n Match.exhaustive,\n )\n\n// ---------------------------------------------------------------------------\n// Request body\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 buildBody = (request: InworldTranscribeRequest) =>\n Effect.gen(function* () {\n const content = yield* audioToBase64(request.audio)\n const prompts = promptToTerms(request.prompt)\n return {\n transcribeConfig: {\n modelId: request.model,\n audioEncoding: request.audioEncoding ?? \"AUTO_DETECT\",\n ...(request.sampleRateHertz !== undefined && {\n sampleRateHertz: request.sampleRateHertz,\n }),\n numberOfChannels: request.numberOfChannels ?? 1,\n ...(request.language !== undefined && { language: request.language }),\n ...(prompts !== undefined && { prompts }),\n ...(request.wordTimestamps === true && { includeWordTimestamps: true }),\n },\n audioData: { content },\n }\n })\n\n// ---------------------------------------------------------------------------\n// Wire schema\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 WireResponse = Schema.Struct({\n transcription: Schema.Struct({\n transcript: Schema.String,\n isFinal: Schema.optional(Schema.Boolean),\n wordTimestamps: Schema.optional(Schema.NullOr(Schema.Array(WireWord))),\n }),\n})\nconst decodeResponse = Schema.decodeUnknownEffect(WireResponse)\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\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://api.inworld.ai\"\n\n/** Exported for reuse by `InworldRealtimeTranscriber`. */\nexport const transcribeImpl = (cfg: Config) => (request: InworldTranscribeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const body = yield* buildBody(request)\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/stt/v1/transcribe`).pipe(\n HttpClientRequest.setHeader(\"Authorization\", authHeader(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeResponse(json).pipe(\n Effect.mapError(\n (cause) =>\n new AiError.GenerationFailed({\n provider: \"inworld\",\n raw: { message: \"STT response missing `transcription.transcript`\", cause, json },\n }),\n ),\n )\n const words = wire.transcription.wordTimestamps\n ?.map(wireWordToCommon)\n .filter((w): w is WordTimestamp => w !== undefined)\n return {\n text: wire.transcription.transcript,\n ...(words !== undefined && words.length > 0 && { words }),\n raw: json,\n } satisfies TranscriptResult\n })\n\n// ---------------------------------------------------------------------------\n// streamTranscriptionFrom Unsupported on this Layer\n// ---------------------------------------------------------------------------\n\nconst streamUnsupported = <E, R>(\n _audioIn: Stream.Stream<Uint8Array, E, R>,\n _request: CommonStreamTranscribeRequest,\n): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> => {\n const fail: Stream.Stream<TranscriptEvent, AiError.AiError | E, R> = Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"streamTranscriptionFrom\",\n reason:\n \"This Layer is sync-only. Import `@effect-uai/inworld/InworldRealtimeTranscriber` to use the WS streamTranscriptionFrom path.\",\n }),\n )\n return fail\n}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<InworldTranscriberService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n transcribe: (request) =>\n transcribeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamTranscriptionFrom: streamUnsupported,\n }))\n\n/**\n * Sync-only Layer. Registers `InworldTranscriber` + the generic\n * `Transcriber`. Does **not** register `SttStreaming` — for live\n * transcription use `@effect-uai/inworld/InworldRealtimeTranscriber`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<InworldTranscriber | Transcriber, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(InworldTranscriber, make(cfg)),\n Layer.effect(\n Transcriber,\n Effect.map(\n make(cfg),\n (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) =>\n s.transcribe(req as InworldTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n }),\n ),\n ),\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,qBAAb,cAAwC,QAAQ,SAG7C,CAAC,4DAA4D,CAAC;AAQjE,MAAM,kBAAmC,IAAI,QAAQ,eAAe;CAClE,UAAU;CACV,OAAO;CACP,KAAK;CACN,CAAC;AAEF,MAAM,gBACJ,MAAM,MAAmB,CAAC,KACxB,MAAM,IAAI,UAAU,MAAM,OAAO,QAAQ,SAAS,aAAa,EAAE,MAAM,CAAC,CAAC,EACzE,MAAM,IAAI,WAAW,MAAM,OAAO,QAAQ,EAAE,OAAO,CAAC,EACpD,MAAM,IAAI,aAAa,OAAO,KAAK,gBAAgB,CAAC,EACpD,MAAM,WACP;AAMH,MAAM,iBACJ,WAEA,WAAW,KAAA,IAAY,KAAA,IAAY,OAAO,WAAW,WAAW,CAAC,OAAO,GAAG,OAAO;AAEpF,MAAM,aAAa,YACjB,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,OAAO,cAAc,QAAQ,MAAM;CACnD,MAAM,UAAU,cAAc,QAAQ,OAAO;AAC7C,QAAO;EACL,kBAAkB;GAChB,SAAS,QAAQ;GACjB,eAAe,QAAQ,iBAAiB;GACxC,GAAI,QAAQ,oBAAoB,KAAA,KAAa,EAC3C,iBAAiB,QAAQ,iBAC1B;GACD,kBAAkB,QAAQ,oBAAoB;GAC9C,GAAI,QAAQ,aAAa,KAAA,KAAa,EAAE,UAAU,QAAQ,UAAU;GACpE,GAAI,YAAY,KAAA,KAAa,EAAE,SAAS;GACxC,GAAI,QAAQ,mBAAmB,QAAQ,EAAE,uBAAuB,MAAM;GACvE;EACD,WAAW,EAAE,SAAS;EACvB;EACD;AAMJ,MAAM,WAAW,OAAO,OAAO;CAC7B,MAAM,OAAO;CACb,aAAa,OAAO,SAAS,OAAO,OAAO;CAC3C,WAAW,OAAO,SAAS,OAAO,OAAO;CACzC,YAAY,OAAO,SAAS,OAAO,OAAO;CAC3C,CAAC;AAEF,MAAM,eAAe,OAAO,OAAO,EACjC,eAAe,OAAO,OAAO;CAC3B,YAAY,OAAO;CACnB,SAAS,OAAO,SAAS,OAAO,QAAQ;CACxC,gBAAgB,OAAO,SAAS,OAAO,OAAO,OAAO,MAAM,SAAS,CAAC,CAAC;CACvE,CAAC,EACH,CAAC;AACF,MAAM,iBAAiB,OAAO,oBAAoB,aAAa;AAE/D,MAAM,oBAAoB,MACxB,EAAE,gBAAgB,KAAA,KAAa,EAAE,cAAc,KAAA,IAC3C,KAAA,IACA;CACE,MAAM,EAAE;CACR,cAAc,EAAE,cAAc;CAC9B,YAAY,EAAE,YAAY;CAC1B,GAAI,EAAE,eAAe,KAAA,KAAa,EAAE,YAAY,EAAE,YAAY;CAC/D;AAMP,MAAM,WAAW,QAAgB,IAAI,WAAW;;AAGhD,MAAa,kBAAkB,SAAiB,YAC9C,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,WAAW;CACjC,MAAM,OAAO,OAAO,UAAU,QAAQ;CACtC,MAAM,cAAc,kBAAkB,KAAK,GAAG,QAAQ,IAAI,CAAC,oBAAoB,CAAC,KAC9E,kBAAkB,UAAU,iBAAiB,WAAW,IAAI,OAAO,CAAC,EACpE,kBAAkB,eAAe,KAAK,CACvC;CACD,MAAM,WAAW,OAAO,OAAO,QAAQ,YAAY,CAAC,KAAK,OAAO,SAAS,iBAAiB,CAAC;AAC3F,KAAI,SAAS,UAAU,KAAK;EAC1B,MAAM,OAAO,OAAO,SAAS,KAAK,KAAK,OAAO,oBAAoB,GAAG,CAAC;AACtE,SAAO,OAAO,OAAO,KAAK,gBAAgB,SAAS,QAAQ,KAAK,CAAC;;CAEnE,MAAM,OAAO,OAAO,SAAS,KAAK,KAAK,OAAO,SAAS,iBAAiB,CAAC;CACzE,MAAM,OAAO,OAAO,eAAe,KAAK,CAAC,KACvC,OAAO,UACJ,UACC,IAAI,QAAQ,iBAAiB;EAC3B,UAAU;EACV,KAAK;GAAE,SAAS;GAAmD;GAAO;GAAM;EACjF,CAAC,CACL,CACF;CACD,MAAM,QAAQ,KAAK,cAAc,gBAC7B,IAAI,iBAAiB,CACtB,QAAQ,MAA0B,MAAM,KAAA,EAAU;AACrD,QAAO;EACL,MAAM,KAAK,cAAc;EACzB,GAAI,UAAU,KAAA,KAAa,MAAM,SAAS,KAAK,EAAE,OAAO;EACxD,KAAK;EACN;EACD;AAMJ,MAAM,qBACJ,UACA,aAC2D;AAS3D,QARqE,OAAO,KAC1E,IAAI,QAAQ,YAAY;EACtB,UAAU;EACV,YAAY;EACZ,QACE;EACH,CAAC,CAEO;;AAOb,MAAa,QACX,QAEA,OAAO,IAAI,WAAW,WAAW,UAAU,GAAG,YAAY;CACxD,aAAa,YACX,eAAe,IAAI,CAAC,QAAQ,CAAC,KAAK,OAAO,eAAe,WAAW,YAAY,OAAO,CAAC;CACzF,yBAAyB;CAC1B,EAAE;;;;;;AAOL,MAAa,SACX,QAEA,MAAM,MACJ,MAAM,OAAO,oBAAoB,KAAK,IAAI,CAAC,EAC3C,MAAM,OACJ,aACA,OAAO,IACL,KAAK,IAAI,GACR,OAA2B;CAC1B,aAAa,QACX,EAAE,WAAW,IAAgC;CAC/C,yBAAyB,EAAE;CAC5B,EACF,CACF,CACF"}
|
|
1
|
+
{"version":3,"file":"InworldTranscriber.mjs","names":[],"sources":["../src/InworldTranscriber.ts"],"sourcesContent":["/**\n * Sync transcription via Inworld's REST endpoint.\n *\n * - `transcribe` → `POST /stt/v1/transcribe` with audio as base64 in\n * `audioData.content`. URL input is not accepted by Inworld — the\n * adapter rejects `_tag: \"url\"` audio sources with `InvalidRequest`.\n *\n * Streaming transcription lives at the `InworldRealtimeTranscriber`\n * subpath, which adds the WS path and the `SttStreaming` capability marker.\n */\nimport { Context, Effect, Encoding, Layer, Match, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioSource } from \"@effect-uai/core/Audio\"\nimport * as Capabilities from \"@effect-uai/core/Capabilities\"\nimport type { TranscriptEvent, TranscriptResult, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport {\n type CommonStreamTranscribeRequest,\n type CommonTranscribeRequest,\n Transcriber,\n type TranscriberService,\n} from \"@effect-uai/core/Transcriber\"\nimport { authHeader, httpStatusError, transportFailure } from \"./codec.js\"\nimport type { InworldSttModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Inworld-typed transcribe request. `numberOfChannels` defaults to 1.\n * `audioEncoding` is autodetected from the audio MIME by default; pass\n * an explicit value to override (useful for raw PCM bytes).\n */\nexport type InworldTranscribeRequest = Omit<CommonTranscribeRequest, \"model\"> & {\n readonly model: InworldSttModel\n readonly audioEncoding?: \"AUTO_DETECT\" | \"LINEAR16\" | \"MP3\" | \"OGG_OPUS\" | \"FLAC\"\n readonly sampleRateHertz?: number\n readonly numberOfChannels?: number\n}\n\nexport type InworldTranscriberService = {\n readonly transcribe: (\n r: InworldTranscribeRequest,\n ) => Effect.Effect<TranscriptResult, AiError.AiError>\n readonly streamTranscriptionFrom: TranscriberService[\"streamTranscriptionFrom\"]\n}\n\nexport class InworldTranscriber extends Context.Service<\n InworldTranscriber,\n InworldTranscriberService\n>()(\"@betalyra/effect-uai/providers/inworld/InworldTranscriber\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// AudioSource → base64. Inworld's sync endpoint takes inline base64 only.\n// ---------------------------------------------------------------------------\n\nconst urlNotSupported: AiError.AiError = new AiError.InvalidRequest({\n provider: \"inworld\",\n param: \"audio\",\n raw: 'Inworld accepts only inline base64. Fetch the URL yourself and pass `{ _tag: \"bytes\", bytes, mimeType }`.',\n})\n\nconst audioToBase64: (audio: AudioSource) => Effect.Effect<string, AiError.AiError> =\n Match.type<AudioSource>().pipe(\n Match.tag(\"bytes\", (a) => Effect.succeed(Encoding.encodeBase64(a.bytes))),\n Match.tag(\"base64\", (a) => Effect.succeed(a.base64)),\n Match.tag(\"url\", () => Effect.fail(urlNotSupported)),\n Match.exhaustive,\n )\n\n// ---------------------------------------------------------------------------\n// Request body\n// ---------------------------------------------------------------------------\n\nconst buildBody = (request: InworldTranscribeRequest) =>\n Effect.gen(function* () {\n const content = yield* audioToBase64(request.audio)\n // Inworld's `prompts` is a vocab-biasing term list — maps from\n // `biasingTerms`. There's no free-form prompt field, so `prompt` warns.\n yield* Capabilities.warnDroppedWhen(request.prompt, {\n provider: \"inworld\",\n capability: \"prompt\",\n field: \"prompt\",\n reason: \"Inworld STT has no free-form prompt field; bias via `biasingTerms`.\",\n })\n return {\n transcribeConfig: {\n modelId: request.model,\n audioEncoding: request.audioEncoding ?? \"AUTO_DETECT\",\n ...(request.sampleRateHertz !== undefined && {\n sampleRateHertz: request.sampleRateHertz,\n }),\n numberOfChannels: request.numberOfChannels ?? 1,\n ...(request.language !== undefined && { language: request.language }),\n ...(request.biasingTerms !== undefined && { prompts: request.biasingTerms }),\n ...(request.wordTimestamps === true && { includeWordTimestamps: true }),\n },\n audioData: { content },\n }\n })\n\n// ---------------------------------------------------------------------------\n// Wire schema\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 WireResponse = Schema.Struct({\n transcription: Schema.Struct({\n transcript: Schema.String,\n isFinal: Schema.optional(Schema.Boolean),\n wordTimestamps: Schema.optional(Schema.NullOr(Schema.Array(WireWord))),\n }),\n})\nconst decodeResponse = Schema.decodeUnknownEffect(WireResponse)\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\n// ---------------------------------------------------------------------------\n// HTTP plumbing\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://api.inworld.ai\"\n\n/** Exported for reuse by `InworldRealtimeTranscriber`. */\nexport const transcribeImpl = (cfg: Config) => (request: InworldTranscribeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const body = yield* buildBody(request)\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/stt/v1/transcribe`).pipe(\n HttpClientRequest.setHeader(\"Authorization\", authHeader(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* Effect.fail(httpStatusError(response.status, text))\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeResponse(json).pipe(\n Effect.mapError(\n (cause) =>\n new AiError.GenerationFailed({\n provider: \"inworld\",\n raw: { message: \"STT response missing `transcription.transcript`\", cause, json },\n }),\n ),\n )\n const words = wire.transcription.wordTimestamps\n ?.map(wireWordToCommon)\n .filter((w): w is WordTimestamp => w !== undefined)\n return {\n text: wire.transcription.transcript,\n ...(words !== undefined && words.length > 0 && { words }),\n raw: json,\n } satisfies TranscriptResult\n })\n\n// ---------------------------------------------------------------------------\n// streamTranscriptionFrom Unsupported on this Layer\n// ---------------------------------------------------------------------------\n\nconst streamUnsupported = <E, R>(\n _audioIn: Stream.Stream<Uint8Array, E, R>,\n _request: CommonStreamTranscribeRequest,\n): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> => {\n const fail: Stream.Stream<TranscriptEvent, AiError.AiError | E, R> = Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"streamTranscriptionFrom\",\n reason:\n \"This Layer is sync-only. Import `@effect-uai/inworld/InworldRealtimeTranscriber` to use the WS streamTranscriptionFrom path.\",\n }),\n )\n return fail\n}\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<InworldTranscriberService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n transcribe: (request) =>\n transcribeImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamTranscriptionFrom: streamUnsupported,\n }))\n\n/**\n * Sync-only Layer. Registers `InworldTranscriber` + the generic\n * `Transcriber`. Does **not** register `SttStreaming` — for live\n * transcription use `@effect-uai/inworld/InworldRealtimeTranscriber`.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<InworldTranscriber | Transcriber, never, HttpClient.HttpClient> =>\n Layer.merge(\n Layer.effect(InworldTranscriber, make(cfg)),\n Layer.effect(\n Transcriber,\n Effect.map(\n make(cfg),\n (s): TranscriberService => ({\n transcribe: (req: CommonTranscribeRequest) =>\n s.transcribe(req as InworldTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"realtimeStt.d.mts","names":[],"sources":["../src/realtimeStt.ts"],"mappings":";;;;;;KA8BY,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cA+F9D,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"}
|
package/dist/realtimeStt.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { Effect, Encoding, Match, Queue, Schema, Stream } from "effect";
|
|
|
3
3
|
import * as AiError from "@effect-uai/core/AiError";
|
|
4
4
|
import * as JSONL from "@effect-uai/core/JSONL";
|
|
5
5
|
import * as Socket from "effect/unstable/socket/Socket";
|
|
6
|
+
import * as Capabilities from "@effect-uai/core/Capabilities";
|
|
6
7
|
//#region src/realtimeStt.ts
|
|
7
8
|
/**
|
|
8
9
|
* Inworld Realtime STT — `wss://api.inworld.ai/stt/v1/transcribe:streamBidirectional`.
|
|
@@ -42,19 +43,15 @@ const inputFormatToWire = Match.type().pipe(Match.when({
|
|
|
42
43
|
const buildWsUrl = (cfg) => {
|
|
43
44
|
return `${(cfg.baseUrl ?? "https://api.inworld.ai").replace(/^http/, "ws")}/stt/v1/transcribe:streamBidirectional`;
|
|
44
45
|
};
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
...prompts !== void 0 && { prompts },
|
|
55
|
-
...request.wordTimestamps === true && { includeWordTimestamps: true }
|
|
56
|
-
} });
|
|
57
|
-
};
|
|
46
|
+
const configFrame = (encoding, request) => JSON.stringify({ transcribeConfig: {
|
|
47
|
+
modelId: request.model,
|
|
48
|
+
audioEncoding: encoding,
|
|
49
|
+
sampleRateHertz: request.inputFormat.sampleRate,
|
|
50
|
+
numberOfChannels: request.inputFormat.channels ?? 1,
|
|
51
|
+
language: request.language ?? "en-US",
|
|
52
|
+
...request.biasingTerms !== void 0 && { prompts: request.biasingTerms },
|
|
53
|
+
...request.wordTimestamps === true && { includeWordTimestamps: true }
|
|
54
|
+
} });
|
|
58
55
|
const audioChunkFrame = (bytes) => JSON.stringify({ audioChunk: { content: Encoding.encodeBase64(bytes) } });
|
|
59
56
|
const endTurnFrame = JSON.stringify({ endTurn: {} });
|
|
60
57
|
const closeStreamFrame = JSON.stringify({ closeStream: {} });
|
|
@@ -126,6 +123,12 @@ const handleServerMessage = (queue) => (raw) => Effect.gen(function* () {
|
|
|
126
123
|
if (event !== void 0) yield* Queue.offer(queue, event);
|
|
127
124
|
});
|
|
128
125
|
const streamTranscription = (cfg) => (audioIn, request) => Stream.unwrap(Effect.gen(function* () {
|
|
126
|
+
yield* Capabilities.warnDroppedWhen(request.prompt, {
|
|
127
|
+
provider: "inworld",
|
|
128
|
+
capability: "prompt",
|
|
129
|
+
field: "prompt",
|
|
130
|
+
reason: "Inworld STT has no free-form prompt field; bias via `biasingTerms`."
|
|
131
|
+
});
|
|
129
132
|
const encoding = yield* inputFormatToWire(request.inputFormat);
|
|
130
133
|
const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg.apiKey)));
|
|
131
134
|
const queue = yield* Queue.bounded(64);
|
package/dist/realtimeStt.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
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 Capabilities from \"@effect-uai/core/Capabilities\"\nimport * as JSONL from \"@effect-uai/core/JSONL\"\nimport type { TranscriptEvent, WordTimestamp } from \"@effect-uai/core/Transcript\"\nimport type { CommonStreamTranscribeRequest } from \"@effect-uai/core/Transcriber\"\nimport { 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 configFrame = (encoding: WireEncoding, request: CommonStreamTranscribeRequest) =>\n 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` is a vocab-biasing term list — maps from `biasingTerms`.\n ...(request.biasingTerms !== undefined && { prompts: request.biasingTerms }),\n ...(request.wordTimestamps === true && { includeWordTimestamps: true }),\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 yield* Capabilities.warnDroppedWhen(request.prompt, {\n provider: \"inworld\",\n capability: \"prompt\",\n field: \"prompt\",\n reason: \"Inworld STT has no free-form prompt field; bias via `biasingTerms`.\",\n })\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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"}
|
package/dist/realtimeTts.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { a as InworldVoiceId, i as InworldTtsModel, n as InworldDeliveryMode } from "./models-CPLZDcgt.mjs";
|
|
2
2
|
import { Redacted, Stream } from "effect";
|
|
3
|
+
import { CustomPronunciation } from "@effect-uai/core/SpeechSynthesizer";
|
|
3
4
|
import * as AiError from "@effect-uai/core/AiError";
|
|
4
5
|
import { AudioChunk, AudioFormat } from "@effect-uai/core/Audio";
|
|
5
6
|
|
|
@@ -21,6 +22,9 @@ type StreamSynthesizeRequest = {
|
|
|
21
22
|
readonly deliveryMode?: InworldDeliveryMode;
|
|
22
23
|
readonly applyTextNormalization?: "ON" | "OFF";
|
|
23
24
|
readonly speed?: number;
|
|
25
|
+
/** Carried from the Common request so the WS path can reject inline
|
|
26
|
+
* pronunciations (no place to rewrite into a chunked text stream). */
|
|
27
|
+
readonly pronunciations?: ReadonlyArray<CustomPronunciation>;
|
|
24
28
|
};
|
|
25
29
|
declare const buildWsUrl: (cfg: Config) => string;
|
|
26
30
|
declare const streamSynthesis: (cfg: Config) => <E, R>(textIn: Stream.Stream<string, E, R>, request: StreamSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError | E, R>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"realtimeTts.d.mts","names":[],"sources":["../src/realtimeTts.ts"],"mappings":";;;;;;;KA+BY,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;EAGkD;;EAAA,SAAlD,cAAA,GAAiB,aAAA,CAAc,mBAAA;AAAA;AAAA,cAO7B,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"}
|
package/dist/realtimeTts.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { audioConfigFor, decodeAudioContent, defaultFormat } from "./codec.mjs";
|
|
2
2
|
import { authedWsConstructor } from "./wsAuth.mjs";
|
|
3
3
|
import { Effect, Queue, Schema, Stream } from "effect";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
4
5
|
import * as JSONL from "@effect-uai/core/JSONL";
|
|
5
6
|
import * as Socket from "effect/unstable/socket/Socket";
|
|
6
7
|
//#region src/realtimeTts.ts
|
|
@@ -76,6 +77,11 @@ const handleServerFrame = (queue) => (raw) => Effect.gen(function* () {
|
|
|
76
77
|
if (bytes._tag === "Some") yield* Queue.offer(queue, { bytes: bytes.value });
|
|
77
78
|
});
|
|
78
79
|
const streamSynthesis = (cfg) => (textIn, request) => Stream.unwrap(Effect.gen(function* () {
|
|
80
|
+
if (request.pronunciations !== void 0 && request.pronunciations.length > 0) return Stream.fail(new AiError.Unsupported({
|
|
81
|
+
provider: "inworld",
|
|
82
|
+
capability: "pronunciations",
|
|
83
|
+
reason: "Inworld applies inline /ipa/ pronunciations on sync `synthesize` only; the incremental WS path streams text in chunks with no place to rewrite. Use `synthesize` for pronunciation overrides."
|
|
84
|
+
}));
|
|
79
85
|
const create = yield* createFrame(request);
|
|
80
86
|
const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), { closeCodeIsError: (code) => code !== 1e3 && code !== 1001 && code !== 1005 }).pipe(Effect.provideService(Socket.WebSocketConstructor, authedWsConstructor(cfg.apiKey)));
|
|
81
87
|
const queue = yield* Queue.bounded(64);
|
package/dist/realtimeTts.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
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 type { CustomPronunciation } from \"@effect-uai/core/SpeechSynthesizer\"\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 /** Carried from the Common request so the WS path can reject inline\n * pronunciations (no place to rewrite into a chunked text stream). */\n readonly pronunciations?: ReadonlyArray<CustomPronunciation>\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 // Inline pronunciations are load-bearing (bucket 1) but there is no\n // single text to rewrite `/ipa/` into on a chunked stream. Reject\n // rather than silently drop; use sync `synthesize` for overrides.\n if (request.pronunciations !== undefined && request.pronunciations.length > 0) {\n return Stream.fail(\n new AiError.Unsupported({\n provider: \"inworld\",\n capability: \"pronunciations\",\n reason:\n \"Inworld applies inline /ipa/ pronunciations on sync `synthesize` only; the incremental WS path streams text in chunks with no place to rewrite. Use `synthesize` for pronunciation overrides.\",\n }),\n )\n }\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/inworld",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Inworld speech-to-text and text-to-speech provider for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"effect": "4.0.0-beta.57",
|
|
62
62
|
"typescript": "^6.0.3",
|
|
63
63
|
"ws": "^8.18.0",
|
|
64
|
-
"@effect-uai/core": "0.
|
|
64
|
+
"@effect-uai/core": "0.7.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
@@ -76,9 +76,9 @@ export type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: st
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Inworld accepts inline phoneme overrides as `/ipa/` tokens directly
|
|
79
|
-
* in the text
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* in the text, no separate field. `CustomPronunciation.pronunciation`
|
|
80
|
+
* is always IPA, so every entry is inlined. Whole-word,
|
|
81
|
+
* case-insensitive replacement.
|
|
82
82
|
*/
|
|
83
83
|
const escapeForRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
84
84
|
|
|
@@ -88,9 +88,7 @@ const applyPronunciations = (
|
|
|
88
88
|
): string => {
|
|
89
89
|
if (pronunciations === undefined) return text
|
|
90
90
|
return Arr.reduce(pronunciations, text, (acc, p) =>
|
|
91
|
-
p.
|
|
92
|
-
? acc.replace(new RegExp(`\\b${escapeForRegex(p.phrase)}\\b`, "i"), `/${p.pronunciation}/`)
|
|
93
|
-
: acc,
|
|
91
|
+
acc.replace(new RegExp(`\\b${escapeForRegex(p.phrase)}\\b`, "i"), `/${p.pronunciation}/`),
|
|
94
92
|
)
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -12,6 +12,7 @@ import { Context, Effect, Encoding, Layer, Match, Redacted, Schema, Stream } fro
|
|
|
12
12
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
13
13
|
import * as AiError from "@effect-uai/core/AiError"
|
|
14
14
|
import type { AudioSource } from "@effect-uai/core/Audio"
|
|
15
|
+
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
15
16
|
import type { TranscriptEvent, TranscriptResult, WordTimestamp } from "@effect-uai/core/Transcript"
|
|
16
17
|
import {
|
|
17
18
|
type CommonStreamTranscribeRequest,
|
|
@@ -74,15 +75,17 @@ const audioToBase64: (audio: AudioSource) => Effect.Effect<string, AiError.AiErr
|
|
|
74
75
|
// Request body
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
76
77
|
|
|
77
|
-
const promptToTerms = (
|
|
78
|
-
prompt: string | { readonly terms: ReadonlyArray<string> } | undefined,
|
|
79
|
-
): ReadonlyArray<string> | undefined =>
|
|
80
|
-
prompt === undefined ? undefined : typeof prompt === "string" ? [prompt] : prompt.terms
|
|
81
|
-
|
|
82
78
|
const buildBody = (request: InworldTranscribeRequest) =>
|
|
83
79
|
Effect.gen(function* () {
|
|
84
80
|
const content = yield* audioToBase64(request.audio)
|
|
85
|
-
|
|
81
|
+
// Inworld's `prompts` is a vocab-biasing term list — maps from
|
|
82
|
+
// `biasingTerms`. There's no free-form prompt field, so `prompt` warns.
|
|
83
|
+
yield* Capabilities.warnDroppedWhen(request.prompt, {
|
|
84
|
+
provider: "inworld",
|
|
85
|
+
capability: "prompt",
|
|
86
|
+
field: "prompt",
|
|
87
|
+
reason: "Inworld STT has no free-form prompt field; bias via `biasingTerms`.",
|
|
88
|
+
})
|
|
86
89
|
return {
|
|
87
90
|
transcribeConfig: {
|
|
88
91
|
modelId: request.model,
|
|
@@ -92,7 +95,7 @@ const buildBody = (request: InworldTranscribeRequest) =>
|
|
|
92
95
|
}),
|
|
93
96
|
numberOfChannels: request.numberOfChannels ?? 1,
|
|
94
97
|
...(request.language !== undefined && { language: request.language }),
|
|
95
|
-
...(
|
|
98
|
+
...(request.biasingTerms !== undefined && { prompts: request.biasingTerms }),
|
|
96
99
|
...(request.wordTimestamps === true && { includeWordTimestamps: true }),
|
|
97
100
|
},
|
|
98
101
|
audioData: { content },
|
package/src/realtimeStt.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { Cause, Effect, Encoding, Match, Queue, Redacted, Schema, Stream } from
|
|
|
22
22
|
import * as Socket from "effect/unstable/socket/Socket"
|
|
23
23
|
import * as AiError from "@effect-uai/core/AiError"
|
|
24
24
|
import type { AudioFormat } from "@effect-uai/core/Audio"
|
|
25
|
+
import * as Capabilities from "@effect-uai/core/Capabilities"
|
|
25
26
|
import * as JSONL from "@effect-uai/core/JSONL"
|
|
26
27
|
import type { TranscriptEvent, WordTimestamp } from "@effect-uai/core/Transcript"
|
|
27
28
|
import type { CommonStreamTranscribeRequest } from "@effect-uai/core/Transcriber"
|
|
@@ -67,14 +68,8 @@ const buildWsUrl = (cfg: Config) => {
|
|
|
67
68
|
return `${wsBase}/stt/v1/transcribe:streamBidirectional`
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
): ReadonlyArray<string> | undefined =>
|
|
73
|
-
prompt === undefined ? undefined : typeof prompt === "string" ? [prompt] : prompt.terms
|
|
74
|
-
|
|
75
|
-
const configFrame = (encoding: WireEncoding, request: CommonStreamTranscribeRequest) => {
|
|
76
|
-
const prompts = promptToTerms(request.prompt)
|
|
77
|
-
return JSON.stringify({
|
|
71
|
+
const configFrame = (encoding: WireEncoding, request: CommonStreamTranscribeRequest) =>
|
|
72
|
+
JSON.stringify({
|
|
78
73
|
transcribeConfig: {
|
|
79
74
|
modelId: request.model,
|
|
80
75
|
audioEncoding: encoding,
|
|
@@ -83,11 +78,11 @@ const configFrame = (encoding: WireEncoding, request: CommonStreamTranscribeRequ
|
|
|
83
78
|
// Inworld's sample includes `language` even though docs mark it optional.
|
|
84
79
|
// Default to en-US to match the sample's behavior; caller can override.
|
|
85
80
|
language: request.language ?? "en-US",
|
|
86
|
-
|
|
81
|
+
// `prompts` is a vocab-biasing term list — maps from `biasingTerms`.
|
|
82
|
+
...(request.biasingTerms !== undefined && { prompts: request.biasingTerms }),
|
|
87
83
|
...(request.wordTimestamps === true && { includeWordTimestamps: true }),
|
|
88
84
|
},
|
|
89
85
|
})
|
|
90
|
-
}
|
|
91
86
|
|
|
92
87
|
const audioChunkFrame = (bytes: Uint8Array) =>
|
|
93
88
|
JSON.stringify({ audioChunk: { content: Encoding.encodeBase64(bytes) } })
|
|
@@ -204,6 +199,12 @@ export const streamTranscription =
|
|
|
204
199
|
): Stream.Stream<TranscriptEvent, AiError.AiError | E, R> =>
|
|
205
200
|
Stream.unwrap(
|
|
206
201
|
Effect.gen(function* () {
|
|
202
|
+
yield* Capabilities.warnDroppedWhen(request.prompt, {
|
|
203
|
+
provider: "inworld",
|
|
204
|
+
capability: "prompt",
|
|
205
|
+
field: "prompt",
|
|
206
|
+
reason: "Inworld STT has no free-form prompt field; bias via `biasingTerms`.",
|
|
207
|
+
})
|
|
207
208
|
const encoding = yield* inputFormatToWire(request.inputFormat)
|
|
208
209
|
const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), {
|
|
209
210
|
// Effect's Socket treats all close codes as errors by default —
|
package/src/realtimeTts.ts
CHANGED
|
@@ -24,6 +24,7 @@ import * as Socket from "effect/unstable/socket/Socket"
|
|
|
24
24
|
import * as AiError from "@effect-uai/core/AiError"
|
|
25
25
|
import type { AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
|
|
26
26
|
import * as JSONL from "@effect-uai/core/JSONL"
|
|
27
|
+
import type { CustomPronunciation } from "@effect-uai/core/SpeechSynthesizer"
|
|
27
28
|
import { audioConfigFor, decodeAudioContent, defaultFormat } from "./codec.js"
|
|
28
29
|
import type { InworldDeliveryMode, InworldTtsModel, InworldVoiceId } from "./models.js"
|
|
29
30
|
import { authedWsConstructor } from "./wsAuth.js"
|
|
@@ -43,6 +44,9 @@ export type StreamSynthesizeRequest = {
|
|
|
43
44
|
readonly deliveryMode?: InworldDeliveryMode
|
|
44
45
|
readonly applyTextNormalization?: "ON" | "OFF"
|
|
45
46
|
readonly speed?: number
|
|
47
|
+
/** Carried from the Common request so the WS path can reject inline
|
|
48
|
+
* pronunciations (no place to rewrite into a chunked text stream). */
|
|
49
|
+
readonly pronunciations?: ReadonlyArray<CustomPronunciation>
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
// ---------------------------------------------------------------------------
|
|
@@ -135,6 +139,19 @@ export const streamSynthesis =
|
|
|
135
139
|
): Stream.Stream<AudioChunk, AiError.AiError | E, R> =>
|
|
136
140
|
Stream.unwrap(
|
|
137
141
|
Effect.gen(function* () {
|
|
142
|
+
// Inline pronunciations are load-bearing (bucket 1) but there is no
|
|
143
|
+
// single text to rewrite `/ipa/` into on a chunked stream. Reject
|
|
144
|
+
// rather than silently drop; use sync `synthesize` for overrides.
|
|
145
|
+
if (request.pronunciations !== undefined && request.pronunciations.length > 0) {
|
|
146
|
+
return Stream.fail(
|
|
147
|
+
new AiError.Unsupported({
|
|
148
|
+
provider: "inworld",
|
|
149
|
+
capability: "pronunciations",
|
|
150
|
+
reason:
|
|
151
|
+
"Inworld applies inline /ipa/ pronunciations on sync `synthesize` only; the incremental WS path streams text in chunks with no place to rewrite. Use `synthesize` for pronunciation overrides.",
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
138
155
|
const create = yield* createFrame(request)
|
|
139
156
|
const socket = yield* Socket.makeWebSocket(buildWsUrl(cfg), {
|
|
140
157
|
// Effect's Socket treats all close codes as errors by default —
|