@opencode/ai 0.0.0-dev-20039 → 0.0.0-dev-20041
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/README.md +86 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/media-model.d.ts +3 -3
- package/dist/media-model.js +3 -3
- package/dist/promise.d.ts +111 -3
- package/dist/promise.js +9 -1
- package/dist/protocols/bedrock-converse.js +2 -8
- package/dist/protocols/cartesia-speech.d.ts +127 -0
- package/dist/protocols/cartesia-speech.js +126 -0
- package/dist/protocols/deepgram-speech.d.ts +119 -0
- package/dist/protocols/deepgram-speech.js +90 -0
- package/dist/protocols/elevenlabs-speech.d.ts +122 -0
- package/dist/protocols/elevenlabs-speech.js +115 -0
- package/dist/protocols/google-speech.d.ts +160 -0
- package/dist/protocols/google-speech.js +122 -0
- package/dist/protocols/openai-speech.d.ts +116 -0
- package/dist/protocols/openai-speech.js +98 -0
- package/dist/protocols/utils/speech-stream.d.ts +49 -0
- package/dist/protocols/utils/speech-stream.js +67 -0
- package/dist/providers/cartesia.d.ts +24 -0
- package/dist/providers/cartesia.js +22 -0
- package/dist/providers/deepgram.d.ts +24 -0
- package/dist/providers/deepgram.js +28 -0
- package/dist/providers/elevenlabs.d.ts +24 -0
- package/dist/providers/elevenlabs.js +28 -0
- package/dist/providers/google.d.ts +5 -0
- package/dist/providers/google.js +3 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/openai.d.ts +5 -0
- package/dist/providers/openai.js +6 -1
- package/dist/route/framing.d.ts +5 -1
- package/dist/route/framing.js +9 -0
- package/dist/route/media-protocol.d.ts +35 -2
- package/dist/route/media-protocol.js +22 -1
- package/dist/route/media.d.ts +15 -1
- package/dist/route/media.js +41 -7
- package/dist/speech-client.d.ts +21 -0
- package/dist/speech-client.js +25 -0
- package/dist/speech.d.ts +1150 -0
- package/dist/speech.js +119 -0
- package/dist/utils/bytes.d.ts +1 -0
- package/dist/utils/bytes.js +10 -0
- package/dist/utils/media-type.js +8 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -682,6 +682,90 @@ const generation = await ai.video.start({ model, prompt })
|
|
|
682
682
|
const video = await generation.await({ poll: { interval: 10_000 }, signal })
|
|
683
683
|
```
|
|
684
684
|
|
|
685
|
+
## Speech generation
|
|
686
|
+
|
|
687
|
+
Speech (text-to-speech) is one request whose response is parsed incrementally, so every route supports both
|
|
688
|
+
`Speech.generate` (the whole file) and `Speech.stream` (audio chunks as they arrive). Models come from `.speech(...)`
|
|
689
|
+
selectors on the `OpenAI`, `Google` (Gemini TTS), `ElevenLabs`, `Cartesia`, and `Deepgram` facades. Common fields
|
|
690
|
+
(`voice`, `format`, `speed`, `language`, `instructions`, `timestamps`) lower natively or fail with a typed `AIError`
|
|
691
|
+
before any network call; provider-native controls live under `providerOptions`, inferred from the selected model.
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
import { Media, Speech, SpeechClient, SpeechEvent } from "@opencode/ai"
|
|
695
|
+
import { ElevenLabs, OpenAI } from "@opencode/ai/providers"
|
|
696
|
+
|
|
697
|
+
const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY })
|
|
698
|
+
|
|
699
|
+
// The whole file, written to disk.
|
|
700
|
+
const program = Effect.gen(function* () {
|
|
701
|
+
const response = yield* Speech.generate({
|
|
702
|
+
model: openai.speech("gpt-4o-mini-tts"),
|
|
703
|
+
text: "Hello from OpenCode.",
|
|
704
|
+
voice: "coral",
|
|
705
|
+
format: "mp3",
|
|
706
|
+
instructions: "Warm and unhurried.",
|
|
707
|
+
})
|
|
708
|
+
response.audio // Media.Asset with bytes; headerless PCM carries info.encoding / sampleRate / channels
|
|
709
|
+
response.usage // undefined: OpenAI reports tokens only on SSE streams (Gemini: tokens; ElevenLabs: credits; Deepgram: characters)
|
|
710
|
+
yield* Media.write(response.audio, "hello.mp3")
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
// Chunks as they arrive: audio-delta* (interleaved with timestamps) then one finish carrying the assembled asset.
|
|
714
|
+
const events = Speech.stream({
|
|
715
|
+
model: ElevenLabs.configure({ apiKey }).speech("eleven_flash_v2_5"),
|
|
716
|
+
text: "Hello from OpenCode.",
|
|
717
|
+
voice: "JBFqnCBsd6RMkjVDRZzb",
|
|
718
|
+
format: "pcm",
|
|
719
|
+
timestamps: true,
|
|
720
|
+
}).pipe(
|
|
721
|
+
Stream.tap((event) => {
|
|
722
|
+
if (SpeechEvent.is.audioDelta(event)) return play(event.chunk)
|
|
723
|
+
if (SpeechEvent.is.timestamps(event)) return highlight(event.items) // { text, startSeconds, endSeconds }[]
|
|
724
|
+
return Effect.void
|
|
725
|
+
}),
|
|
726
|
+
)
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
`voice` is the provider's own identifier — a name on OpenAI and Gemini (`"coral"`, `"Kore"`), a voice id on
|
|
730
|
+
ElevenLabs and Cartesia. `{ id }` selects an OpenAI custom voice (`{ id: "voice_1234" }`) and means the same as the
|
|
731
|
+
plain string elsewhere. There is no cross-provider voice catalog. `format` is the container-level word (`mp3`, `wav`,
|
|
732
|
+
`pcm`, `opus`, `aac`, `flac`); sample rates and bitrates live under `providerOptions`, and a value the route cannot
|
|
733
|
+
produce fails as `UnsupportedOperation`. Streams buffer every chunk so `finish` can carry the whole clip.
|
|
734
|
+
`SpeechClient.layer` needs `RequestExecutor.Service`.
|
|
735
|
+
|
|
736
|
+
Provider notes:
|
|
737
|
+
|
|
738
|
+
- **OpenAI** streams over SSE (`stream_format: "sse"`), which is also the only place it reports token usage; `tts-1`
|
|
739
|
+
and `tts-1-hd` do not support SSE and stream the raw audio body instead. `pcm` is 24 kHz 16-bit mono. `language`
|
|
740
|
+
and `timestamps` are not supported.
|
|
741
|
+
- **Gemini TTS** returns raw 16-bit PCM only (`audio/L16;codec=pcm;rate=24000`), so any `format` other than `pcm`
|
|
742
|
+
fails typed; wrap the samples yourself. Style is directed in the text, so `instructions` and `speed` fail typed.
|
|
743
|
+
Only `gemini-3.1-flash-tts-preview` and later support streaming. Two-speaker audio goes through
|
|
744
|
+
`providerOptions.speechConfig.multiSpeakerVoiceConfig`.
|
|
745
|
+
- **ElevenLabs** requires `voice` (the path voice id) and authenticates with `xi-api-key`. `format` maps to the
|
|
746
|
+
`output_format` query parameter (`mp3_44100_128`, `pcm_24000`, `wav_24000`, `opus_48000_64`);
|
|
747
|
+
`providerOptions.outputFormat` sets the exact string. WAV is only available from `generate`. `timestamps: true`
|
|
748
|
+
selects the `with-timestamps` endpoints and yields character-level alignment. `instructions` is not supported.
|
|
749
|
+
- **Cartesia** requires `voice` and pins `Cartesia-Version`. `generate` defaults to MP3 from `/tts/bytes`; streams
|
|
750
|
+
and `timestamps: true` (word-level) use `/tts/sse`, which only serves raw PCM. `providerOptions.sampleRate`,
|
|
751
|
+
`bitRate`, and `encoding` complete `output_format`. No usage is reported.
|
|
752
|
+
- **Deepgram** Aura's voice is the model id (`aura-2-thalia-en`), so `voice` and `language` fail typed. `format`
|
|
753
|
+
and `providerOptions` lower to query parameters (`encoding`, `container`, `sample_rate`, `bit_rate`); `pcm` is
|
|
754
|
+
`linear16` without a container. Auth is `Authorization: Token <DEEPGRAM_API_KEY>`.
|
|
755
|
+
|
|
756
|
+
The promise client mirrors the Effect API; `ai.speech.stream` is an `AsyncIterable`.
|
|
757
|
+
|
|
758
|
+
```ts
|
|
759
|
+
import { ai } from "@opencode/ai/promise"
|
|
760
|
+
|
|
761
|
+
const response = await ai.speech.generate({ model, text: "Hello from OpenCode.", voice: "coral" })
|
|
762
|
+
await Bun.write("hello.mp3", await ai.run(response.audio.bytes()))
|
|
763
|
+
|
|
764
|
+
for await (const event of ai.speech.stream({ model, text: "Hello from OpenCode.", voice: "coral" })) {
|
|
765
|
+
if (event.type === "audio-delta") player.write(event.chunk)
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
685
769
|
## Public API
|
|
686
770
|
|
|
687
771
|
- **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
|
|
@@ -693,7 +777,8 @@ const video = await generation.await({ poll: { interval: 10_000 }, signal })
|
|
|
693
777
|
- **`ImageClient`** — Effect service and layer for image execution, parallel to `LLMClient`.
|
|
694
778
|
- **`Media`** — the shared asset type (`Media.Asset`, `Media.Source`) and constructors used by messages, tool results, and media requests.
|
|
695
779
|
- **`Generation`** — provider-neutral handle for an in-flight media generation (`await`, `refresh`, `cancel`, `events`) used by queued media routes.
|
|
696
|
-
-
|
|
780
|
+
- **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
|
|
781
|
+
- **`@opencode/ai/promise`** — `AI.make({ layer? })` and a default `ai` client exposing `llm`, `image`, `video`, and `speech` as Promise / `AsyncIterable` APIs.
|
|
697
782
|
|
|
698
783
|
## Testing
|
|
699
784
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export { VideoClient } from "./video-client.js";
|
|
|
13
13
|
export { VideoAspectRatio, VideoEvent, VideoFrames, VideoModel, VideoModelSchema, VideoRequest, VideoResponse, } from "./video.js";
|
|
14
14
|
export type { VideoModelOptions, VideoOptions, VideoRequestFor, VideoRequestInput, VideoResolution, VideoRoute, } from "./video.js";
|
|
15
15
|
export { Video } from "./video.js";
|
|
16
|
+
export { SpeechClient } from "./speech-client.js";
|
|
17
|
+
export { SpeechEvent, SpeechModel, SpeechModelSchema, SpeechRequest, SpeechResponse, SpeechTimestamp, SpeechVoice, } from "./speech.js";
|
|
18
|
+
export type { SpeechFormat, SpeechModelOptions, SpeechOptions, SpeechRequestFor, SpeechRequestInput, SpeechRoute, } from "./speech.js";
|
|
19
|
+
export { Speech } from "./speech.js";
|
|
16
20
|
export { Media } from "./media.js";
|
|
17
21
|
export { Generation } from "./generation.js";
|
|
18
22
|
export type { AwaitOptions as GenerationAwaitOptions, Event as GenerationEvent, Poll, Route as GenerationRoute, Snapshot as GenerationSnapshot, Status as GenerationStatus, } from "./generation.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,9 @@ export { Image } from "./image.js";
|
|
|
10
10
|
export { VideoClient } from "./video-client.js";
|
|
11
11
|
export { VideoAspectRatio, VideoEvent, VideoFrames, VideoModel, VideoModelSchema, VideoRequest, VideoResponse, } from "./video.js";
|
|
12
12
|
export { Video } from "./video.js";
|
|
13
|
+
export { SpeechClient } from "./speech-client.js";
|
|
14
|
+
export { SpeechEvent, SpeechModel, SpeechModelSchema, SpeechRequest, SpeechResponse, SpeechTimestamp, SpeechVoice, } from "./speech.js";
|
|
15
|
+
export { Speech } from "./speech.js";
|
|
13
16
|
export { Media } from "./media.js";
|
|
14
17
|
export { Generation } from "./generation.js";
|
|
15
18
|
export { Tool, ToolFailure, toDefinitions } from "./tool.js";
|
package/dist/media-model.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { MediaProtocol } from "./route/media-protocol.js";
|
|
|
5
5
|
import { AIError, HttpOptions, ModelID, ProviderID } from "./schema/index.js";
|
|
6
6
|
/**
|
|
7
7
|
* What every media model carries: ids, the configured route, and deployment `http` overlays. Modality classes
|
|
8
|
-
* (`ImageModel`, `VideoModel`) extend it with their route type and a nominal marker so one cannot stand
|
|
9
|
-
* other in requests.
|
|
8
|
+
* (`ImageModel`, `VideoModel`, `SpeechModel`) extend it with their route type and a nominal marker so one cannot stand
|
|
9
|
+
* in for the other in requests.
|
|
10
10
|
*/
|
|
11
11
|
export declare class MediaModel<Route, Options> {
|
|
12
12
|
protected readonly _Options: (options: Options) => Options;
|
|
@@ -34,7 +34,7 @@ export declare namespace MediaModel {
|
|
|
34
34
|
readonly headers?: Record<string, string>;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
/** Compose a protocol route input with one deployment through `MediaRoute.inline` or `
|
|
37
|
+
/** Compose a protocol route input with one deployment through `MediaRoute.inline`, `queued`, or `stream`. */
|
|
38
38
|
export declare const composeRoute: <Request extends MediaRoute.MediaRequest, Protocol, Route>(compose: (input: MediaRoute.Composition<Request> & {
|
|
39
39
|
readonly protocol: Protocol;
|
|
40
40
|
}) => Route, route: MediaModel.RouteInput<Request, Protocol>, input: MediaRoute.ModelInput) => Route;
|
package/dist/media-model.js
CHANGED
|
@@ -3,8 +3,8 @@ import { Endpoint } from "./route/endpoint.js";
|
|
|
3
3
|
import { AIError, HttpOptions, InvalidRequestError, ModelID, ProviderID } from "./schema/index.js";
|
|
4
4
|
/**
|
|
5
5
|
* What every media model carries: ids, the configured route, and deployment `http` overlays. Modality classes
|
|
6
|
-
* (`ImageModel`, `VideoModel`) extend it with their route type and a nominal marker so one cannot stand
|
|
7
|
-
* other in requests.
|
|
6
|
+
* (`ImageModel`, `VideoModel`, `SpeechModel`) extend it with their route type and a nominal marker so one cannot stand
|
|
7
|
+
* in for the other in requests.
|
|
8
8
|
*/
|
|
9
9
|
export class MediaModel {
|
|
10
10
|
id;
|
|
@@ -18,7 +18,7 @@ export class MediaModel {
|
|
|
18
18
|
this.http = input.http;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
/** Compose a protocol route input with one deployment through `MediaRoute.inline` or `
|
|
21
|
+
/** Compose a protocol route input with one deployment through `MediaRoute.inline`, `queued`, or `stream`. */
|
|
22
22
|
export const composeRoute = (compose, route, input) => compose({
|
|
23
23
|
id: route.id,
|
|
24
24
|
provider: route.provider,
|
package/dist/promise.d.ts
CHANGED
|
@@ -6,11 +6,13 @@ import { LLMClient } from "./route/client.js";
|
|
|
6
6
|
import { RequestExecutor } from "./route/executor.js";
|
|
7
7
|
import { LanguageModel, LLMRequest } from "./schema/index.js";
|
|
8
8
|
import type { RequestInput } from "./llm.js";
|
|
9
|
+
import { SpeechModel, SpeechRequest, type SpeechRequestInput } from "./speech.js";
|
|
10
|
+
import { SpeechClient } from "./speech-client.js";
|
|
9
11
|
import { VideoModel, VideoRequest, type VideoOptions, type VideoRequestInput } from "./video.js";
|
|
10
12
|
import { VideoClient } from "./video-client.js";
|
|
11
13
|
/**
|
|
12
|
-
* Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, and
|
|
13
|
-
* clients over a request executor; every method runs the corresponding Effect API and rethrows `AIError` unchanged.
|
|
14
|
+
* Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, video, and
|
|
15
|
+
* speech clients over a request executor; every method runs the corresponding Effect API and rethrows `AIError` unchanged.
|
|
14
16
|
*/
|
|
15
17
|
export interface Options {
|
|
16
18
|
/** Executor layer; defaults to `RequestExecutor.fetchLayer`. Inject a recorder or middleware here. */
|
|
@@ -19,7 +21,7 @@ export interface Options {
|
|
|
19
21
|
export interface RunOptions {
|
|
20
22
|
readonly signal?: AbortSignal;
|
|
21
23
|
}
|
|
22
|
-
export type Services = Layer.Success<typeof LLMClient.layer> | Layer.Success<typeof ImageClient.layer> | Layer.Success<typeof VideoClient.layer> | RequestExecutor.Service;
|
|
24
|
+
export type Services = Layer.Success<typeof LLMClient.layer> | Layer.Success<typeof ImageClient.layer> | Layer.Success<typeof VideoClient.layer> | Layer.Success<typeof SpeechClient.layer> | RequestExecutor.Service;
|
|
23
25
|
/** Promise view of a `Generation`: its snapshot plus `await`, `refresh`, and `cancel` returning promises. */
|
|
24
26
|
export type GenerationHandle<Response> = Snapshot & {
|
|
25
27
|
/** Serializable JSON; pass it back to `resume` from another process. */
|
|
@@ -348,6 +350,59 @@ export declare const make: (options?: Options) => {
|
|
|
348
350
|
}[] | undefined;
|
|
349
351
|
}>;
|
|
350
352
|
};
|
|
353
|
+
speech: {
|
|
354
|
+
request: typeof import("./speech.js").request;
|
|
355
|
+
generate: <const Model extends SpeechModel>(input: SpeechRequestInput<Model> | SpeechRequest, options?: RunOptions) => Promise<import("./speech.js").SpeechResponse>;
|
|
356
|
+
stream: <const Model extends SpeechModel>(input: SpeechRequestInput<Model> | SpeechRequest, options?: RunOptions) => AsyncIterable<{
|
|
357
|
+
readonly type: "audio-delta";
|
|
358
|
+
readonly chunk: Uint8Array<ArrayBufferLike>;
|
|
359
|
+
} | {
|
|
360
|
+
readonly type: "timestamps";
|
|
361
|
+
readonly items: readonly {
|
|
362
|
+
readonly text: string;
|
|
363
|
+
readonly startSeconds: number;
|
|
364
|
+
readonly endSeconds: number;
|
|
365
|
+
}[];
|
|
366
|
+
} | {
|
|
367
|
+
readonly type: "finish";
|
|
368
|
+
readonly audio: import("./media.js").Asset;
|
|
369
|
+
readonly providerMetadata?: {
|
|
370
|
+
readonly [x: string]: {
|
|
371
|
+
readonly [x: string]: unknown;
|
|
372
|
+
};
|
|
373
|
+
} | undefined;
|
|
374
|
+
readonly usage?: {
|
|
375
|
+
readonly type: "tokens";
|
|
376
|
+
readonly input?: number | undefined;
|
|
377
|
+
readonly output?: number | undefined;
|
|
378
|
+
readonly total?: number | undefined;
|
|
379
|
+
readonly details?: {
|
|
380
|
+
readonly [x: string]: unknown;
|
|
381
|
+
} | undefined;
|
|
382
|
+
} | {
|
|
383
|
+
readonly type: "seconds";
|
|
384
|
+
readonly seconds: number;
|
|
385
|
+
} | {
|
|
386
|
+
readonly type: "characters";
|
|
387
|
+
readonly characters: number;
|
|
388
|
+
} | {
|
|
389
|
+
readonly type: "credits";
|
|
390
|
+
readonly credits: number;
|
|
391
|
+
} | {
|
|
392
|
+
readonly type: "compute";
|
|
393
|
+
readonly seconds: number;
|
|
394
|
+
} | undefined;
|
|
395
|
+
readonly notices?: readonly {
|
|
396
|
+
readonly type: "other" | "moderated" | "filtered";
|
|
397
|
+
readonly message: string;
|
|
398
|
+
readonly providerMetadata?: {
|
|
399
|
+
readonly [x: string]: {
|
|
400
|
+
readonly [x: string]: unknown;
|
|
401
|
+
};
|
|
402
|
+
} | undefined;
|
|
403
|
+
}[] | undefined;
|
|
404
|
+
}>;
|
|
405
|
+
};
|
|
351
406
|
dispose: () => Promise<void>;
|
|
352
407
|
};
|
|
353
408
|
export type Client = ReturnType<typeof make>;
|
|
@@ -672,6 +727,59 @@ export declare const ai: {
|
|
|
672
727
|
}[] | undefined;
|
|
673
728
|
}>;
|
|
674
729
|
};
|
|
730
|
+
speech: {
|
|
731
|
+
request: typeof import("./speech.js").request;
|
|
732
|
+
generate: <const Model extends SpeechModel>(input: SpeechRequestInput<Model> | SpeechRequest, options?: RunOptions) => Promise<import("./speech.js").SpeechResponse>;
|
|
733
|
+
stream: <const Model extends SpeechModel>(input: SpeechRequestInput<Model> | SpeechRequest, options?: RunOptions) => AsyncIterable<{
|
|
734
|
+
readonly type: "audio-delta";
|
|
735
|
+
readonly chunk: Uint8Array<ArrayBufferLike>;
|
|
736
|
+
} | {
|
|
737
|
+
readonly type: "timestamps";
|
|
738
|
+
readonly items: readonly {
|
|
739
|
+
readonly text: string;
|
|
740
|
+
readonly startSeconds: number;
|
|
741
|
+
readonly endSeconds: number;
|
|
742
|
+
}[];
|
|
743
|
+
} | {
|
|
744
|
+
readonly type: "finish";
|
|
745
|
+
readonly audio: import("./media.js").Asset;
|
|
746
|
+
readonly providerMetadata?: {
|
|
747
|
+
readonly [x: string]: {
|
|
748
|
+
readonly [x: string]: unknown;
|
|
749
|
+
};
|
|
750
|
+
} | undefined;
|
|
751
|
+
readonly usage?: {
|
|
752
|
+
readonly type: "tokens";
|
|
753
|
+
readonly input?: number | undefined;
|
|
754
|
+
readonly output?: number | undefined;
|
|
755
|
+
readonly total?: number | undefined;
|
|
756
|
+
readonly details?: {
|
|
757
|
+
readonly [x: string]: unknown;
|
|
758
|
+
} | undefined;
|
|
759
|
+
} | {
|
|
760
|
+
readonly type: "seconds";
|
|
761
|
+
readonly seconds: number;
|
|
762
|
+
} | {
|
|
763
|
+
readonly type: "characters";
|
|
764
|
+
readonly characters: number;
|
|
765
|
+
} | {
|
|
766
|
+
readonly type: "credits";
|
|
767
|
+
readonly credits: number;
|
|
768
|
+
} | {
|
|
769
|
+
readonly type: "compute";
|
|
770
|
+
readonly seconds: number;
|
|
771
|
+
} | undefined;
|
|
772
|
+
readonly notices?: readonly {
|
|
773
|
+
readonly type: "other" | "moderated" | "filtered";
|
|
774
|
+
readonly message: string;
|
|
775
|
+
readonly providerMetadata?: {
|
|
776
|
+
readonly [x: string]: {
|
|
777
|
+
readonly [x: string]: unknown;
|
|
778
|
+
};
|
|
779
|
+
} | undefined;
|
|
780
|
+
}[] | undefined;
|
|
781
|
+
}>;
|
|
782
|
+
};
|
|
675
783
|
dispose: () => Promise<void>;
|
|
676
784
|
};
|
|
677
785
|
export * as AI from "./promise.js";
|
package/dist/promise.js
CHANGED
|
@@ -5,6 +5,8 @@ import { LLM } from "./index.js";
|
|
|
5
5
|
import { LLMClient } from "./route/client.js";
|
|
6
6
|
import { RequestExecutor } from "./route/executor.js";
|
|
7
7
|
import { LanguageModel, LLMRequest } from "./schema/index.js";
|
|
8
|
+
import { Speech, SpeechModel, SpeechRequest } from "./speech.js";
|
|
9
|
+
import { SpeechClient } from "./speech-client.js";
|
|
8
10
|
import { Video, VideoModel, VideoRequest } from "./video.js";
|
|
9
11
|
import { VideoClient } from "./video-client.js";
|
|
10
12
|
const abortEffect = (signal) => signal === undefined
|
|
@@ -19,7 +21,7 @@ const abortEffect = (signal) => signal === undefined
|
|
|
19
21
|
return Effect.sync(() => signal.removeEventListener("abort", onAbort));
|
|
20
22
|
});
|
|
21
23
|
export const make = (options = {}) => {
|
|
22
|
-
const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
|
|
24
|
+
const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer, SpeechClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
|
|
23
25
|
/** Run any package Effect (for example `asset.bytes()`) inside this runtime. */
|
|
24
26
|
const run = (effect, options) => runtime.runPromise(effect, { signal: options?.signal });
|
|
25
27
|
const iterate = (stream, options) => Stream.toAsyncIterable(Stream.unwrap(runtime.contextEffect.pipe(Effect.map((context) => stream.pipe(Stream.interruptWhen(abortEffect(options?.signal)), Stream.provideContext(context))))));
|
|
@@ -34,6 +36,7 @@ export const make = (options = {}) => {
|
|
|
34
36
|
const llmRequest = (input) => (input instanceof LLMRequest ? input : LLM.request(input));
|
|
35
37
|
const imageRequest = (input) => input instanceof ImageRequest ? input : Image.request(input);
|
|
36
38
|
const videoRequest = (input) => input instanceof VideoRequest ? input : Video.request(input);
|
|
39
|
+
const speechRequest = (input) => input instanceof SpeechRequest ? input : Speech.request(input);
|
|
37
40
|
return {
|
|
38
41
|
run,
|
|
39
42
|
llm: {
|
|
@@ -53,6 +56,11 @@ export const make = (options = {}) => {
|
|
|
53
56
|
resume: (model, token, options) => run(Video.resume(model, token), options).then(handle),
|
|
54
57
|
stream: (input, options) => iterate(Video.stream(videoRequest(input), { poll: options?.poll }), options),
|
|
55
58
|
},
|
|
59
|
+
speech: {
|
|
60
|
+
request: Speech.request,
|
|
61
|
+
generate: (input, options) => run(Speech.generate(speechRequest(input)), options),
|
|
62
|
+
stream: (input, options) => iterate(Speech.stream(speechRequest(input)), options),
|
|
63
|
+
},
|
|
56
64
|
dispose: () => runtime.dispose(),
|
|
57
65
|
};
|
|
58
66
|
};
|
|
@@ -13,6 +13,7 @@ import { Lifecycle } from "./utils/lifecycle.js";
|
|
|
13
13
|
import { MistralToolID } from "./utils/mistral-tool-id.js";
|
|
14
14
|
import { ToolSchemaProjection } from "./utils/tool-schema.js";
|
|
15
15
|
import { ToolStream } from "./utils/tool-stream.js";
|
|
16
|
+
import { concatBytes } from "../utils/bytes.js";
|
|
16
17
|
const ADAPTER = "bedrock-converse";
|
|
17
18
|
// =============================================================================
|
|
18
19
|
// Request Body Schema
|
|
@@ -413,14 +414,7 @@ const mapUsage = (usage, providerMetadataKey) => {
|
|
|
413
414
|
providerMetadata: { [providerMetadataKey]: usage },
|
|
414
415
|
});
|
|
415
416
|
};
|
|
416
|
-
const encodeRedactedContent = (chunks) =>
|
|
417
|
-
const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
418
|
-
chunks.reduce((offset, chunk) => {
|
|
419
|
-
bytes.set(chunk, offset);
|
|
420
|
-
return offset + chunk.length;
|
|
421
|
-
}, 0);
|
|
422
|
-
return Encoding.encodeBase64(bytes);
|
|
423
|
-
};
|
|
417
|
+
const encodeRedactedContent = (chunks) => Encoding.encodeBase64(concatBytes(chunks));
|
|
424
418
|
const step = (state, event) => Effect.gen(function* () {
|
|
425
419
|
if (event.contentBlockStart?.start?.toolUse) {
|
|
426
420
|
const index = event.contentBlockStart.contentBlockIndex;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
2
|
+
import { MediaRoute } from "../route/media.js";
|
|
3
|
+
import { SpeechModel, type SpeechRequestFor } from "../speech.js";
|
|
4
|
+
import { SpeechStream } from "./utils/speech-stream.js";
|
|
5
|
+
export declare const DEFAULT_BASE_URL = "https://api.cartesia.ai";
|
|
6
|
+
export declare const API_VERSION = "2026-08-14";
|
|
7
|
+
export declare const BYTES_PATH = "/tts/bytes";
|
|
8
|
+
export declare const SSE_PATH = "/tts/sse";
|
|
9
|
+
export type CartesiaSpeechString<Known extends string> = Known | (string & {});
|
|
10
|
+
export type CartesiaEncoding = SpeechStream.PcmEncoding;
|
|
11
|
+
export type CartesiaSpeechOptions = {
|
|
12
|
+
readonly sampleRate?: 8000 | 16000 | 22050 | 24000 | 44100 | 48000;
|
|
13
|
+
readonly bitRate?: 32000 | 64000 | 96000 | 128000 | 192000;
|
|
14
|
+
readonly encoding?: CartesiaEncoding;
|
|
15
|
+
readonly generation_config?: {
|
|
16
|
+
readonly volume?: number;
|
|
17
|
+
readonly emotion?: CartesiaSpeechString<"neutral" | "calm" | "angry" | "content" | "sad" | "scared">;
|
|
18
|
+
};
|
|
19
|
+
readonly pronunciation_dict_id?: string;
|
|
20
|
+
} & Record<string, unknown>;
|
|
21
|
+
export type Request = SpeechRequestFor<CartesiaSpeechOptions>;
|
|
22
|
+
interface State extends SpeechStream.Audio {
|
|
23
|
+
readonly done: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare const protocol: MediaProtocol.Streamed<Request, {
|
|
26
|
+
readonly type: "audio-delta";
|
|
27
|
+
readonly chunk: Uint8Array<ArrayBufferLike>;
|
|
28
|
+
} | {
|
|
29
|
+
readonly type: "timestamps";
|
|
30
|
+
readonly items: readonly {
|
|
31
|
+
readonly text: string;
|
|
32
|
+
readonly startSeconds: number;
|
|
33
|
+
readonly endSeconds: number;
|
|
34
|
+
}[];
|
|
35
|
+
} | {
|
|
36
|
+
readonly type: "finish";
|
|
37
|
+
readonly audio: import("../media.js").Asset;
|
|
38
|
+
readonly providerMetadata?: {
|
|
39
|
+
readonly [x: string]: {
|
|
40
|
+
readonly [x: string]: unknown;
|
|
41
|
+
};
|
|
42
|
+
} | undefined;
|
|
43
|
+
readonly usage?: {
|
|
44
|
+
readonly type: "tokens";
|
|
45
|
+
readonly input?: number | undefined;
|
|
46
|
+
readonly output?: number | undefined;
|
|
47
|
+
readonly total?: number | undefined;
|
|
48
|
+
readonly details?: {
|
|
49
|
+
readonly [x: string]: unknown;
|
|
50
|
+
} | undefined;
|
|
51
|
+
} | {
|
|
52
|
+
readonly type: "seconds";
|
|
53
|
+
readonly seconds: number;
|
|
54
|
+
} | {
|
|
55
|
+
readonly type: "characters";
|
|
56
|
+
readonly characters: number;
|
|
57
|
+
} | {
|
|
58
|
+
readonly type: "credits";
|
|
59
|
+
readonly credits: number;
|
|
60
|
+
} | {
|
|
61
|
+
readonly type: "compute";
|
|
62
|
+
readonly seconds: number;
|
|
63
|
+
} | undefined;
|
|
64
|
+
readonly notices?: readonly {
|
|
65
|
+
readonly type: "other" | "moderated" | "filtered";
|
|
66
|
+
readonly message: string;
|
|
67
|
+
readonly providerMetadata?: {
|
|
68
|
+
readonly [x: string]: {
|
|
69
|
+
readonly [x: string]: unknown;
|
|
70
|
+
};
|
|
71
|
+
} | undefined;
|
|
72
|
+
}[] | undefined;
|
|
73
|
+
}, string | Uint8Array<ArrayBufferLike>, State>;
|
|
74
|
+
export declare const model: (input: MediaRoute.ModelInput) => SpeechModel<CartesiaSpeechOptions>;
|
|
75
|
+
export declare const CartesiaSpeech: {
|
|
76
|
+
readonly protocol: MediaProtocol.Streamed<Request, {
|
|
77
|
+
readonly type: "audio-delta";
|
|
78
|
+
readonly chunk: Uint8Array<ArrayBufferLike>;
|
|
79
|
+
} | {
|
|
80
|
+
readonly type: "timestamps";
|
|
81
|
+
readonly items: readonly {
|
|
82
|
+
readonly text: string;
|
|
83
|
+
readonly startSeconds: number;
|
|
84
|
+
readonly endSeconds: number;
|
|
85
|
+
}[];
|
|
86
|
+
} | {
|
|
87
|
+
readonly type: "finish";
|
|
88
|
+
readonly audio: import("../media.js").Asset;
|
|
89
|
+
readonly providerMetadata?: {
|
|
90
|
+
readonly [x: string]: {
|
|
91
|
+
readonly [x: string]: unknown;
|
|
92
|
+
};
|
|
93
|
+
} | undefined;
|
|
94
|
+
readonly usage?: {
|
|
95
|
+
readonly type: "tokens";
|
|
96
|
+
readonly input?: number | undefined;
|
|
97
|
+
readonly output?: number | undefined;
|
|
98
|
+
readonly total?: number | undefined;
|
|
99
|
+
readonly details?: {
|
|
100
|
+
readonly [x: string]: unknown;
|
|
101
|
+
} | undefined;
|
|
102
|
+
} | {
|
|
103
|
+
readonly type: "seconds";
|
|
104
|
+
readonly seconds: number;
|
|
105
|
+
} | {
|
|
106
|
+
readonly type: "characters";
|
|
107
|
+
readonly characters: number;
|
|
108
|
+
} | {
|
|
109
|
+
readonly type: "credits";
|
|
110
|
+
readonly credits: number;
|
|
111
|
+
} | {
|
|
112
|
+
readonly type: "compute";
|
|
113
|
+
readonly seconds: number;
|
|
114
|
+
} | undefined;
|
|
115
|
+
readonly notices?: readonly {
|
|
116
|
+
readonly type: "other" | "moderated" | "filtered";
|
|
117
|
+
readonly message: string;
|
|
118
|
+
readonly providerMetadata?: {
|
|
119
|
+
readonly [x: string]: {
|
|
120
|
+
readonly [x: string]: unknown;
|
|
121
|
+
};
|
|
122
|
+
} | undefined;
|
|
123
|
+
}[] | undefined;
|
|
124
|
+
}, string | Uint8Array<ArrayBufferLike>, State>;
|
|
125
|
+
readonly model: (input: MediaRoute.ModelInput) => SpeechModel<CartesiaSpeechOptions>;
|
|
126
|
+
};
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { classifyProviderFailure } from "../provider-error.js";
|
|
3
|
+
import { Framing } from "../route/framing.js";
|
|
4
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
5
|
+
import { MediaRoute } from "../route/media.js";
|
|
6
|
+
import { AIError, ProviderID, mergeJsonRecords } from "../schema/index.js";
|
|
7
|
+
import { SpeechModel } from "../speech.js";
|
|
8
|
+
import { ProviderShared, optionalNull } from "./shared.js";
|
|
9
|
+
import { SpeechStream } from "./utils/speech-stream.js";
|
|
10
|
+
const ADAPTER = "cartesia-speech";
|
|
11
|
+
const NAME = "Cartesia";
|
|
12
|
+
const PROVIDER = ProviderID.make("cartesia");
|
|
13
|
+
export const DEFAULT_BASE_URL = "https://api.cartesia.ai";
|
|
14
|
+
export const API_VERSION = "2026-08-14";
|
|
15
|
+
export const BYTES_PATH = "/tts/bytes";
|
|
16
|
+
export const SSE_PATH = "/tts/sse";
|
|
17
|
+
const DEFAULT_SAMPLE_RATE = 44100;
|
|
18
|
+
const DEFAULT_BIT_RATE = 128000;
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// 3. Streaming event schema
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** `phoneme_timestamps` and future record types are ignored. */
|
|
23
|
+
const SseEvent = Schema.Struct({
|
|
24
|
+
type: Schema.String,
|
|
25
|
+
data: Schema.optional(Schema.Uint8ArrayFromBase64),
|
|
26
|
+
word_timestamps: Schema.optional(Schema.Struct({
|
|
27
|
+
words: Schema.Array(Schema.String),
|
|
28
|
+
start: Schema.Array(Schema.Number),
|
|
29
|
+
end: Schema.Array(Schema.Number),
|
|
30
|
+
})),
|
|
31
|
+
status_code: Schema.optional(Schema.Number),
|
|
32
|
+
title: Schema.optional(Schema.String),
|
|
33
|
+
message: Schema.optional(Schema.String),
|
|
34
|
+
error_code: optionalNull(Schema.String),
|
|
35
|
+
});
|
|
36
|
+
const decodeEvent = MediaProtocol.decodeFrame(ADAPTER, NAME, SseEvent);
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// 5. Request body construction
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/** Timestamps exist only on the SSE endpoint, so a `generate` that asks for them collects an SSE stream. */
|
|
41
|
+
const usesSse = (request) => request.mode === "stream" || request.timestamps === true;
|
|
42
|
+
const CONTAINERS = { pcm: "raw", wav: "wav", mp3: "mp3" };
|
|
43
|
+
const outputFormat = Effect.fn("CartesiaSpeech.outputFormat")(function* (request) {
|
|
44
|
+
const sse = usesSse(request);
|
|
45
|
+
const format = request.format ?? (sse ? "pcm" : "mp3");
|
|
46
|
+
const container = CONTAINERS[format];
|
|
47
|
+
if (container === undefined)
|
|
48
|
+
return yield* SpeechStream.unsupportedFormat(PROVIDER, ADAPTER, `${NAME} supports the pcm, wav, and mp3 formats, not "${format}"`);
|
|
49
|
+
if (sse && container !== "raw")
|
|
50
|
+
return yield* SpeechStream.unsupportedFormat(PROVIDER, ADAPTER, `${NAME} streams and timestamps only raw PCM; request format "pcm" instead of "${format}"`);
|
|
51
|
+
const sampleRate = request.providerOptions?.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
52
|
+
if (container === "mp3")
|
|
53
|
+
return { container, sample_rate: sampleRate, bit_rate: request.providerOptions?.bitRate ?? DEFAULT_BIT_RATE };
|
|
54
|
+
return { container, encoding: request.providerOptions?.encoding ?? "pcm_s16le", sample_rate: sampleRate };
|
|
55
|
+
});
|
|
56
|
+
const fromRequest = Effect.fn("CartesiaSpeech.fromRequest")(function* (request) {
|
|
57
|
+
const voice = SpeechStream.voiceID(request.voice);
|
|
58
|
+
if (voice === undefined)
|
|
59
|
+
return yield* ProviderShared.invalidRequest(`${NAME} requires a voice id; pass it as \`voice\``);
|
|
60
|
+
const { sampleRate: _sampleRate, bitRate: _bitRate, encoding: _encoding, ...native } = request.providerOptions ?? {};
|
|
61
|
+
return MediaProtocol.json(mergeJsonRecords({
|
|
62
|
+
model_id: request.model.id,
|
|
63
|
+
transcript: request.text,
|
|
64
|
+
voice,
|
|
65
|
+
output_format: yield* outputFormat(request),
|
|
66
|
+
language: request.language,
|
|
67
|
+
generation_config: request.speed === undefined ? undefined : { speed: request.speed },
|
|
68
|
+
add_timestamps: request.timestamps === true ? true : undefined,
|
|
69
|
+
}, native, request.http?.body) ?? {});
|
|
70
|
+
});
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// 6. Stream parsing
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
const onEvent = Effect.fn("CartesiaSpeech.onEvent")(function* (state, frame) {
|
|
75
|
+
const event = yield* decodeEvent(frame);
|
|
76
|
+
if (event.type === "chunk" && event.data !== undefined)
|
|
77
|
+
return SpeechStream.delta(state, event.data);
|
|
78
|
+
if (event.type === "timestamps" && event.word_timestamps !== undefined) {
|
|
79
|
+
const words = event.word_timestamps;
|
|
80
|
+
return [state, SpeechStream.timestamps(words.words, words.start, words.end)];
|
|
81
|
+
}
|
|
82
|
+
if (event.type === "done")
|
|
83
|
+
return [{ ...state, done: true }, []];
|
|
84
|
+
if (event.type === "error")
|
|
85
|
+
return yield* new AIError({
|
|
86
|
+
reason: classifyProviderFailure({
|
|
87
|
+
message: `${NAME} stream failed${event.title === undefined ? "" : ` (${event.title})`}: ${event.message ?? "unknown error"}`,
|
|
88
|
+
status: event.status_code,
|
|
89
|
+
rawBody: frame,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
return [state, []];
|
|
93
|
+
});
|
|
94
|
+
const finish = Effect.fn("CartesiaSpeech.finish")(function* (state, context) {
|
|
95
|
+
if (usesSse(context.request) && !state.done)
|
|
96
|
+
return yield* MediaProtocol.incomplete(ADAPTER);
|
|
97
|
+
const format = yield* outputFormat(context.request);
|
|
98
|
+
return yield* SpeechStream.finish(ADAPTER, state, format.container === "raw"
|
|
99
|
+
? SpeechStream.pcm(format.encoding, format.sample_rate)
|
|
100
|
+
: SpeechStream.container(format.container, format.sample_rate));
|
|
101
|
+
});
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// 7. Protocol and route
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
export const protocol = MediaProtocol.stream({
|
|
106
|
+
id: ADAPTER,
|
|
107
|
+
name: NAME,
|
|
108
|
+
unsupported: ["instructions"],
|
|
109
|
+
body: { from: fromRequest },
|
|
110
|
+
frames: (bytes, context) => (usesSse(context.request) ? Framing.sse.frame(bytes) : bytes),
|
|
111
|
+
initial: () => ({ chunks: [], done: false }),
|
|
112
|
+
step: SpeechStream.step(onEvent),
|
|
113
|
+
finish,
|
|
114
|
+
});
|
|
115
|
+
export const model = (input) => SpeechModel.fromRoute({
|
|
116
|
+
id: ADAPTER,
|
|
117
|
+
provider: PROVIDER,
|
|
118
|
+
protocol,
|
|
119
|
+
baseURL: DEFAULT_BASE_URL,
|
|
120
|
+
headers: { "Cartesia-Version": API_VERSION },
|
|
121
|
+
path: ({ request }) => (usesSse(request) ? SSE_PATH : BYTES_PATH),
|
|
122
|
+
}, input);
|
|
123
|
+
export const CartesiaSpeech = {
|
|
124
|
+
protocol,
|
|
125
|
+
model,
|
|
126
|
+
};
|