@opencode/ai 0.0.0-dev-20048 → 0.0.0-dev-20052
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 +65 -1
- package/dist/generation.d.ts +13 -9
- package/dist/generation.js +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/promise.d.ts +164 -3
- package/dist/promise.js +11 -1
- package/dist/protocols/assemblyai-transcription.d.ts +40 -0
- package/dist/protocols/assemblyai-transcription.js +138 -0
- package/dist/protocols/deepgram-speech.js +17 -15
- package/dist/protocols/deepgram-transcription.d.ts +25 -0
- package/dist/protocols/deepgram-transcription.js +129 -0
- package/dist/protocols/gemini.d.ts +2 -2
- package/dist/protocols/gemini.js +2 -6
- package/dist/protocols/google-images.js +2 -14
- package/dist/protocols/google-speech.d.ts +2 -32
- package/dist/protocols/google-speech.js +22 -60
- package/dist/protocols/google-transcription.d.ts +173 -0
- package/dist/protocols/google-transcription.js +138 -0
- package/dist/protocols/openai-images.js +4 -7
- package/dist/protocols/openai-transcription.d.ts +207 -0
- package/dist/protocols/openai-transcription.js +190 -0
- package/dist/protocols/utils/gemini-generate-content.d.ts +65 -0
- package/dist/protocols/utils/gemini-generate-content.js +65 -0
- package/dist/protocols/utils/media-input.d.ts +4 -0
- package/dist/protocols/utils/media-input.js +14 -0
- package/dist/providers/assemblyai.d.ts +25 -0
- package/dist/providers/assemblyai.js +29 -0
- package/dist/providers/deepgram.d.ts +5 -0
- package/dist/providers/deepgram.js +5 -2
- package/dist/providers/google.d.ts +5 -0
- package/dist/providers/google.js +3 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/openai.d.ts +5 -0
- package/dist/providers/openai.js +4 -0
- package/dist/route/media-protocol.d.ts +15 -3
- package/dist/route/media-protocol.js +6 -0
- package/dist/route/media.d.ts +5 -2
- package/dist/route/media.js +62 -40
- package/dist/transcription-client.d.ts +28 -0
- package/dist/transcription-client.js +69 -0
- package/dist/transcription.d.ts +1504 -0
- package/dist/transcription.js +140 -0
- package/dist/utils/media-type.d.ts +1 -0
- package/dist/utils/media-type.js +14 -0
- package/dist/video-client.d.ts +1 -1
- package/dist/video-client.js +3 -6
- package/dist/video.d.ts +1 -15
- package/dist/video.js +2 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -766,6 +766,69 @@ for await (const event of ai.speech.stream({ model, text: "Hello from OpenCode."
|
|
|
766
766
|
}
|
|
767
767
|
```
|
|
768
768
|
|
|
769
|
+
## Transcription
|
|
770
|
+
|
|
771
|
+
Transcription (speech-to-text) is the one modality whose providers use every route kind: OpenAI and Gemini stream,
|
|
772
|
+
Deepgram answers inline, and AssemblyAI is queued. `Transcription.generate` and `Transcription.stream` work on all of
|
|
773
|
+
them; `Transcription.start` / `resume` return a `Generation` on queued routes and fail with `UnsupportedOperation`
|
|
774
|
+
elsewhere. Models come from `.transcription(...)` selectors on the `OpenAI`, `Google`, `Deepgram`, and `AssemblyAI`
|
|
775
|
+
facades. Common fields (`language`, `prompt`, `timestamps: "none" | "segment" | "word"`, `diarize`, `speakers`) lower
|
|
776
|
+
natively or fail with a typed `AIError` before any network call; a route may return more than asked.
|
|
777
|
+
|
|
778
|
+
```ts
|
|
779
|
+
import { Media, Transcription, TranscriptionEvent } from "@opencode/ai"
|
|
780
|
+
import { AssemblyAI, Deepgram, OpenAI } from "@opencode/ai/providers"
|
|
781
|
+
|
|
782
|
+
const openai = OpenAI.configure({ apiKey: process.env.OPENAI_API_KEY })
|
|
783
|
+
|
|
784
|
+
const program = Effect.gen(function* () {
|
|
785
|
+
const audio = yield* Media.file("./call.mp3")
|
|
786
|
+
|
|
787
|
+
// Speaker-labelled segments; labels are provider-native strings ("A", "0", "spk:0").
|
|
788
|
+
const response = yield* Transcription.generate({
|
|
789
|
+
model: Deepgram.configure({ apiKey }).transcription("nova-3"),
|
|
790
|
+
audio,
|
|
791
|
+
diarize: true,
|
|
792
|
+
timestamps: "word",
|
|
793
|
+
})
|
|
794
|
+
response.text // "Hello from OpenCode."
|
|
795
|
+
response.segments // [{ text, startSeconds, endSeconds, speaker: "0" }]
|
|
796
|
+
response.words // [{ text, startSeconds, endSeconds, speaker, confidence }]
|
|
797
|
+
response.language // the provider's own value, lowercased ("en", "english", "en_us")
|
|
798
|
+
|
|
799
|
+
// Text deltas as the model transcribes, then one finish carrying the whole transcript.
|
|
800
|
+
yield* Transcription.stream({ model: openai.transcription("gpt-4o-mini-transcribe"), audio }).pipe(
|
|
801
|
+
Stream.tap((event) => (TranscriptionEvent.is.textDelta(event) ? Console.log(event.delta) : Effect.void)),
|
|
802
|
+
Stream.runDrain,
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
// Queued: persist the token, resume from another process, and await.
|
|
806
|
+
const model = AssemblyAI.configure({ apiKey }).transcription("universal-3-5-pro")
|
|
807
|
+
const generation = yield* Transcription.start({ model, audio })
|
|
808
|
+
const resumed = yield* Transcription.resume(model, JSON.parse(JSON.stringify(generation.token)))
|
|
809
|
+
const transcript = yield* resumed.await({ poll: { interval: "3 seconds" } })
|
|
810
|
+
})
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Inline routes emit only `finish` from `stream` (no faked deltas); queued routes emit `generation-queued` /
|
|
814
|
+
`generation-progress` before it. `TranscriptionClient.layer` needs `RequestExecutor.Service`.
|
|
815
|
+
|
|
816
|
+
Provider notes:
|
|
817
|
+
|
|
818
|
+
- **OpenAI** takes inline audio only; `diarize` needs `gpt-4o-transcribe-diarize`, timestamps need `whisper-1`, and `whisper-1` does not stream.
|
|
819
|
+
- **Gemini** needs a transcribe model (`gemini-3.5-transcribe`); `prompt` and `speakers` fail typed.
|
|
820
|
+
- **Deepgram** detects the language unless `language` is set; vocabulary goes in `providerOptions.keyterm`.
|
|
821
|
+
- **AssemblyAI** uploads inline audio before submitting and is the only route that accepts `speakers`.
|
|
822
|
+
|
|
823
|
+
The promise client mirrors the Effect API:
|
|
824
|
+
|
|
825
|
+
```ts
|
|
826
|
+
const text = (await ai.transcription.generate({ model, audio })).text
|
|
827
|
+
for await (const event of ai.transcription.stream({ model, audio })) if (event.type === "text-delta") write(event.delta)
|
|
828
|
+
const generation = await ai.transcription.start({ model: assemblyai, audio })
|
|
829
|
+
const transcript = await generation.await({ poll: { interval: 3_000 } })
|
|
830
|
+
```
|
|
831
|
+
|
|
769
832
|
## Public API
|
|
770
833
|
|
|
771
834
|
- **`LLM.request({...})`** — build a provider-neutral `LLMRequest`. Accepts ergonomic inputs (`system: string`, `prompt: string`) that normalize into the canonical Schema classes.
|
|
@@ -778,7 +841,8 @@ for await (const event of ai.speech.stream({ model, text: "Hello from OpenCode."
|
|
|
778
841
|
- **`Media`** — the shared asset type (`Media.Asset`, `Media.Source`) and constructors used by messages, tool results, and media requests.
|
|
779
842
|
- **`Generation`** — provider-neutral handle for an in-flight media generation (`await`, `refresh`, `cancel`, `events`) used by queued media routes.
|
|
780
843
|
- **`Speech.request` / `Speech.generate` / `Speech.stream`** — text-to-speech through a provider-neutral request; `SpeechClient` is its Effect service and layer.
|
|
781
|
-
-
|
|
844
|
+
- **`Transcription.request` / `generate` / `stream` / `start` / `resume`** — speech-to-text over inline, streaming, and queued routes; `TranscriptionClient` is its Effect service and layer.
|
|
845
|
+
- **`@opencode/ai/promise`** — `AI.make({ layer? })` and a default `ai` client exposing `llm`, `image`, `video`, `speech`, and `transcription` as Promise / `AsyncIterable` APIs.
|
|
782
846
|
|
|
783
847
|
## Testing
|
|
784
848
|
|
package/dist/generation.d.ts
CHANGED
|
@@ -33,15 +33,18 @@ export interface AwaitOptions {
|
|
|
33
33
|
}
|
|
34
34
|
export declare const DEFAULT_POLL_INTERVAL: Duration.Duration;
|
|
35
35
|
export declare const DEFAULT_POLL_TIMEOUT: Duration.Duration;
|
|
36
|
-
export
|
|
37
|
-
readonly type: "generation-queued"
|
|
38
|
-
readonly id:
|
|
39
|
-
readonly position
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
44
|
-
|
|
36
|
+
export declare const QueuedEvent: Schema.Struct<{
|
|
37
|
+
readonly type: Schema.tag<"generation-queued">;
|
|
38
|
+
readonly id: Schema.String;
|
|
39
|
+
readonly position: Schema.optional<Schema.Number>;
|
|
40
|
+
}>;
|
|
41
|
+
export declare const ProgressEvent: Schema.Struct<{
|
|
42
|
+
readonly type: Schema.tag<"generation-progress">;
|
|
43
|
+
readonly id: Schema.String;
|
|
44
|
+
readonly progress: Schema.optional<Schema.Number>;
|
|
45
|
+
}>;
|
|
46
|
+
export type Observation = Schema.Schema.Type<typeof QueuedEvent> | Schema.Schema.Type<typeof ProgressEvent>;
|
|
47
|
+
export type Event = Observation | {
|
|
45
48
|
readonly type: "generation-finished";
|
|
46
49
|
readonly id: string;
|
|
47
50
|
readonly status: Status;
|
|
@@ -77,3 +80,4 @@ export declare class Generation<Response> {
|
|
|
77
80
|
private poll;
|
|
78
81
|
private schedule;
|
|
79
82
|
}
|
|
83
|
+
export declare const resultEvents: <Response, A>(generation: Generation<Response>, expand: (response: Response) => ReadonlyArray<A>, options?: AwaitOptions) => Stream.Stream<Observation | A, AIError>;
|
package/dist/generation.js
CHANGED
|
@@ -3,6 +3,16 @@ import { AIError, TimeoutError } from "./schema/errors.js";
|
|
|
3
3
|
export const Status = Schema.Literals(["queued", "running", "completed", "failed", "cancelled", "expired"]);
|
|
4
4
|
export const DEFAULT_POLL_INTERVAL = Duration.seconds(5);
|
|
5
5
|
export const DEFAULT_POLL_TIMEOUT = Duration.minutes(10);
|
|
6
|
+
export const QueuedEvent = Schema.Struct({
|
|
7
|
+
type: Schema.tag("generation-queued"),
|
|
8
|
+
id: Schema.String,
|
|
9
|
+
position: Schema.optional(Schema.Number),
|
|
10
|
+
}).annotate({ identifier: "Generation.Event.Queued" });
|
|
11
|
+
export const ProgressEvent = Schema.Struct({
|
|
12
|
+
type: Schema.tag("generation-progress"),
|
|
13
|
+
id: Schema.String,
|
|
14
|
+
progress: Schema.optional(Schema.Number),
|
|
15
|
+
}).annotate({ identifier: "Generation.Event.Progress" });
|
|
6
16
|
const TERMINAL = new Set(["completed", "failed", "cancelled", "expired"]);
|
|
7
17
|
export class Generation {
|
|
8
18
|
route;
|
|
@@ -100,3 +110,4 @@ export class Generation {
|
|
|
100
110
|
return spaced.pipe(Schedule.modifyDelay((metadata) => Effect.succeed(pollHint(metadata.input.snapshot) ?? interval)));
|
|
101
111
|
}
|
|
102
112
|
}
|
|
113
|
+
export const resultEvents = (generation, expand, options) => generation.events(options).pipe(Stream.filter((event) => event.type !== "generation-finished"), Stream.concat(Stream.fromIterableEffect(Effect.map(generation.result(), expand))));
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export { SpeechClient } from "./speech-client.js";
|
|
|
17
17
|
export { SpeechEvent, SpeechModel, SpeechModelSchema, SpeechRequest, SpeechResponse, SpeechTimestamp, SpeechVoice, } from "./speech.js";
|
|
18
18
|
export type { SpeechFormat, SpeechModelOptions, SpeechOptions, SpeechRequestFor, SpeechRequestInput, SpeechRoute, } from "./speech.js";
|
|
19
19
|
export { Speech } from "./speech.js";
|
|
20
|
+
export { TranscriptionClient } from "./transcription-client.js";
|
|
21
|
+
export { TranscriptionEvent, TranscriptionModel, TranscriptionModelSchema, TranscriptionRequest, TranscriptionResponse, TranscriptionSegment, TranscriptionTimestamps, TranscriptionWord, } from "./transcription.js";
|
|
22
|
+
export type { TranscriptionModelOptions, TranscriptionOptions, TranscriptionRequestFor, TranscriptionRequestInput, TranscriptionRoute, } from "./transcription.js";
|
|
23
|
+
export { Transcription } from "./transcription.js";
|
|
20
24
|
export { Media } from "./media.js";
|
|
21
25
|
export { Generation } from "./generation.js";
|
|
22
26
|
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
|
@@ -13,6 +13,9 @@ export { Video } from "./video.js";
|
|
|
13
13
|
export { SpeechClient } from "./speech-client.js";
|
|
14
14
|
export { SpeechEvent, SpeechModel, SpeechModelSchema, SpeechRequest, SpeechResponse, SpeechTimestamp, SpeechVoice, } from "./speech.js";
|
|
15
15
|
export { Speech } from "./speech.js";
|
|
16
|
+
export { TranscriptionClient } from "./transcription-client.js";
|
|
17
|
+
export { TranscriptionEvent, TranscriptionModel, TranscriptionModelSchema, TranscriptionRequest, TranscriptionResponse, TranscriptionSegment, TranscriptionTimestamps, TranscriptionWord, } from "./transcription.js";
|
|
18
|
+
export { Transcription } from "./transcription.js";
|
|
16
19
|
export { Media } from "./media.js";
|
|
17
20
|
export { Generation } from "./generation.js";
|
|
18
21
|
export { Tool, ToolFailure, toDefinitions } from "./tool.js";
|
package/dist/promise.d.ts
CHANGED
|
@@ -8,11 +8,14 @@ import { LanguageModel, LLMRequest } from "./schema/index.js";
|
|
|
8
8
|
import type { RequestInput } from "./llm.js";
|
|
9
9
|
import { SpeechModel, SpeechRequest, type SpeechRequestInput } from "./speech.js";
|
|
10
10
|
import { SpeechClient } from "./speech-client.js";
|
|
11
|
+
import { TranscriptionModel, TranscriptionRequest, type TranscriptionOptions, type TranscriptionRequestInput } from "./transcription.js";
|
|
12
|
+
import { TranscriptionClient } from "./transcription-client.js";
|
|
11
13
|
import { VideoModel, VideoRequest, type VideoOptions, type VideoRequestInput } from "./video.js";
|
|
12
14
|
import { VideoClient } from "./video-client.js";
|
|
13
15
|
/**
|
|
14
|
-
* Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, video,
|
|
15
|
-
*
|
|
16
|
+
* Promise-first entrypoint for scripts and non-Effect callers. One `ManagedRuntime` hosts the LLM, image, video, speech,
|
|
17
|
+
* and transcription clients over a request executor; every method runs the corresponding Effect API and rethrows
|
|
18
|
+
* `AIError` unchanged.
|
|
16
19
|
*/
|
|
17
20
|
export interface Options {
|
|
18
21
|
/** Executor layer; defaults to `RequestExecutor.fetchLayer`. Inject a recorder or middleware here. */
|
|
@@ -21,7 +24,7 @@ export interface Options {
|
|
|
21
24
|
export interface RunOptions {
|
|
22
25
|
readonly signal?: AbortSignal;
|
|
23
26
|
}
|
|
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;
|
|
27
|
+
export type Services = Layer.Success<typeof LLMClient.layer> | Layer.Success<typeof ImageClient.layer> | Layer.Success<typeof VideoClient.layer> | Layer.Success<typeof SpeechClient.layer> | Layer.Success<typeof TranscriptionClient.layer> | RequestExecutor.Service;
|
|
25
28
|
/** Promise view of a `Generation`: its snapshot plus `await`, `refresh`, and `cancel` returning promises. */
|
|
26
29
|
export type GenerationHandle<Response> = Snapshot & {
|
|
27
30
|
/** Serializable JSON; pass it back to `resume` from another process. */
|
|
@@ -403,6 +406,85 @@ export declare const make: (options?: Options) => {
|
|
|
403
406
|
}[] | undefined;
|
|
404
407
|
}>;
|
|
405
408
|
};
|
|
409
|
+
transcription: {
|
|
410
|
+
request: typeof import("./transcription.js").request;
|
|
411
|
+
generate: <const Model extends TranscriptionModel>(input: TranscriptionRequestInput<Model> | TranscriptionRequest, options?: AwaitOptions & RunOptions) => Promise<import("./transcription.js").TranscriptionResponse>;
|
|
412
|
+
stream: <const Model extends TranscriptionModel>(input: TranscriptionRequestInput<Model> | TranscriptionRequest, options?: AwaitOptions & RunOptions) => AsyncIterable<{
|
|
413
|
+
readonly id: string;
|
|
414
|
+
readonly type: "generation-queued";
|
|
415
|
+
readonly position?: number | undefined;
|
|
416
|
+
} | {
|
|
417
|
+
readonly id: string;
|
|
418
|
+
readonly type: "generation-progress";
|
|
419
|
+
readonly progress?: number | undefined;
|
|
420
|
+
} | {
|
|
421
|
+
readonly type: "text-delta";
|
|
422
|
+
readonly delta: string;
|
|
423
|
+
} | {
|
|
424
|
+
readonly type: "segment";
|
|
425
|
+
readonly segment: {
|
|
426
|
+
readonly text: string;
|
|
427
|
+
readonly startSeconds: number;
|
|
428
|
+
readonly endSeconds: number;
|
|
429
|
+
readonly speaker?: string | undefined;
|
|
430
|
+
};
|
|
431
|
+
} | {
|
|
432
|
+
readonly type: "finish";
|
|
433
|
+
readonly text: string;
|
|
434
|
+
readonly durationSeconds?: number | undefined;
|
|
435
|
+
readonly providerMetadata?: {
|
|
436
|
+
readonly [x: string]: {
|
|
437
|
+
readonly [x: string]: unknown;
|
|
438
|
+
};
|
|
439
|
+
} | undefined;
|
|
440
|
+
readonly usage?: {
|
|
441
|
+
readonly type: "tokens";
|
|
442
|
+
readonly input?: number | undefined;
|
|
443
|
+
readonly output?: number | undefined;
|
|
444
|
+
readonly total?: number | undefined;
|
|
445
|
+
readonly details?: {
|
|
446
|
+
readonly [x: string]: unknown;
|
|
447
|
+
} | undefined;
|
|
448
|
+
} | {
|
|
449
|
+
readonly type: "seconds";
|
|
450
|
+
readonly seconds: number;
|
|
451
|
+
} | {
|
|
452
|
+
readonly type: "characters";
|
|
453
|
+
readonly characters: number;
|
|
454
|
+
} | {
|
|
455
|
+
readonly type: "credits";
|
|
456
|
+
readonly credits: number;
|
|
457
|
+
} | {
|
|
458
|
+
readonly type: "compute";
|
|
459
|
+
readonly seconds: number;
|
|
460
|
+
} | undefined;
|
|
461
|
+
readonly notices?: readonly {
|
|
462
|
+
readonly type: "other" | "moderated" | "filtered";
|
|
463
|
+
readonly message: string;
|
|
464
|
+
readonly providerMetadata?: {
|
|
465
|
+
readonly [x: string]: {
|
|
466
|
+
readonly [x: string]: unknown;
|
|
467
|
+
};
|
|
468
|
+
} | undefined;
|
|
469
|
+
}[] | undefined;
|
|
470
|
+
readonly language?: string | undefined;
|
|
471
|
+
readonly segments?: readonly {
|
|
472
|
+
readonly text: string;
|
|
473
|
+
readonly startSeconds: number;
|
|
474
|
+
readonly endSeconds: number;
|
|
475
|
+
readonly speaker?: string | undefined;
|
|
476
|
+
}[] | undefined;
|
|
477
|
+
readonly words?: readonly {
|
|
478
|
+
readonly text: string;
|
|
479
|
+
readonly startSeconds: number;
|
|
480
|
+
readonly endSeconds: number;
|
|
481
|
+
readonly speaker?: string | undefined;
|
|
482
|
+
readonly confidence?: number | undefined;
|
|
483
|
+
}[] | undefined;
|
|
484
|
+
}>;
|
|
485
|
+
start: <const Model extends TranscriptionModel>(input: TranscriptionRequestInput<Model> | TranscriptionRequest, options?: RunOptions) => Promise<GenerationHandle<import("./transcription.js").TranscriptionResponse>>;
|
|
486
|
+
resume: <Options extends TranscriptionOptions>(model: TranscriptionModel<Options>, token: unknown, options?: RunOptions) => Promise<GenerationHandle<import("./transcription.js").TranscriptionResponse>>;
|
|
487
|
+
};
|
|
406
488
|
dispose: () => Promise<void>;
|
|
407
489
|
};
|
|
408
490
|
export type Client = ReturnType<typeof make>;
|
|
@@ -780,6 +862,85 @@ export declare const ai: {
|
|
|
780
862
|
}[] | undefined;
|
|
781
863
|
}>;
|
|
782
864
|
};
|
|
865
|
+
transcription: {
|
|
866
|
+
request: typeof import("./transcription.js").request;
|
|
867
|
+
generate: <const Model extends TranscriptionModel>(input: TranscriptionRequestInput<Model> | TranscriptionRequest, options?: AwaitOptions & RunOptions) => Promise<import("./transcription.js").TranscriptionResponse>;
|
|
868
|
+
stream: <const Model extends TranscriptionModel>(input: TranscriptionRequestInput<Model> | TranscriptionRequest, options?: AwaitOptions & RunOptions) => AsyncIterable<{
|
|
869
|
+
readonly id: string;
|
|
870
|
+
readonly type: "generation-queued";
|
|
871
|
+
readonly position?: number | undefined;
|
|
872
|
+
} | {
|
|
873
|
+
readonly id: string;
|
|
874
|
+
readonly type: "generation-progress";
|
|
875
|
+
readonly progress?: number | undefined;
|
|
876
|
+
} | {
|
|
877
|
+
readonly type: "text-delta";
|
|
878
|
+
readonly delta: string;
|
|
879
|
+
} | {
|
|
880
|
+
readonly type: "segment";
|
|
881
|
+
readonly segment: {
|
|
882
|
+
readonly text: string;
|
|
883
|
+
readonly startSeconds: number;
|
|
884
|
+
readonly endSeconds: number;
|
|
885
|
+
readonly speaker?: string | undefined;
|
|
886
|
+
};
|
|
887
|
+
} | {
|
|
888
|
+
readonly type: "finish";
|
|
889
|
+
readonly text: string;
|
|
890
|
+
readonly durationSeconds?: number | undefined;
|
|
891
|
+
readonly providerMetadata?: {
|
|
892
|
+
readonly [x: string]: {
|
|
893
|
+
readonly [x: string]: unknown;
|
|
894
|
+
};
|
|
895
|
+
} | undefined;
|
|
896
|
+
readonly usage?: {
|
|
897
|
+
readonly type: "tokens";
|
|
898
|
+
readonly input?: number | undefined;
|
|
899
|
+
readonly output?: number | undefined;
|
|
900
|
+
readonly total?: number | undefined;
|
|
901
|
+
readonly details?: {
|
|
902
|
+
readonly [x: string]: unknown;
|
|
903
|
+
} | undefined;
|
|
904
|
+
} | {
|
|
905
|
+
readonly type: "seconds";
|
|
906
|
+
readonly seconds: number;
|
|
907
|
+
} | {
|
|
908
|
+
readonly type: "characters";
|
|
909
|
+
readonly characters: number;
|
|
910
|
+
} | {
|
|
911
|
+
readonly type: "credits";
|
|
912
|
+
readonly credits: number;
|
|
913
|
+
} | {
|
|
914
|
+
readonly type: "compute";
|
|
915
|
+
readonly seconds: number;
|
|
916
|
+
} | undefined;
|
|
917
|
+
readonly notices?: readonly {
|
|
918
|
+
readonly type: "other" | "moderated" | "filtered";
|
|
919
|
+
readonly message: string;
|
|
920
|
+
readonly providerMetadata?: {
|
|
921
|
+
readonly [x: string]: {
|
|
922
|
+
readonly [x: string]: unknown;
|
|
923
|
+
};
|
|
924
|
+
} | undefined;
|
|
925
|
+
}[] | undefined;
|
|
926
|
+
readonly language?: string | undefined;
|
|
927
|
+
readonly segments?: readonly {
|
|
928
|
+
readonly text: string;
|
|
929
|
+
readonly startSeconds: number;
|
|
930
|
+
readonly endSeconds: number;
|
|
931
|
+
readonly speaker?: string | undefined;
|
|
932
|
+
}[] | undefined;
|
|
933
|
+
readonly words?: readonly {
|
|
934
|
+
readonly text: string;
|
|
935
|
+
readonly startSeconds: number;
|
|
936
|
+
readonly endSeconds: number;
|
|
937
|
+
readonly speaker?: string | undefined;
|
|
938
|
+
readonly confidence?: number | undefined;
|
|
939
|
+
}[] | undefined;
|
|
940
|
+
}>;
|
|
941
|
+
start: <const Model extends TranscriptionModel>(input: TranscriptionRequestInput<Model> | TranscriptionRequest, options?: RunOptions) => Promise<GenerationHandle<import("./transcription.js").TranscriptionResponse>>;
|
|
942
|
+
resume: <Options extends TranscriptionOptions>(model: TranscriptionModel<Options>, token: unknown, options?: RunOptions) => Promise<GenerationHandle<import("./transcription.js").TranscriptionResponse>>;
|
|
943
|
+
};
|
|
783
944
|
dispose: () => Promise<void>;
|
|
784
945
|
};
|
|
785
946
|
export * as AI from "./promise.js";
|
package/dist/promise.js
CHANGED
|
@@ -7,6 +7,8 @@ import { RequestExecutor } from "./route/executor.js";
|
|
|
7
7
|
import { LanguageModel, LLMRequest } from "./schema/index.js";
|
|
8
8
|
import { Speech, SpeechModel, SpeechRequest } from "./speech.js";
|
|
9
9
|
import { SpeechClient } from "./speech-client.js";
|
|
10
|
+
import { Transcription, TranscriptionModel, TranscriptionRequest, } from "./transcription.js";
|
|
11
|
+
import { TranscriptionClient } from "./transcription-client.js";
|
|
10
12
|
import { Video, VideoModel, VideoRequest } from "./video.js";
|
|
11
13
|
import { VideoClient } from "./video-client.js";
|
|
12
14
|
const abortEffect = (signal) => signal === undefined
|
|
@@ -21,7 +23,7 @@ const abortEffect = (signal) => signal === undefined
|
|
|
21
23
|
return Effect.sync(() => signal.removeEventListener("abort", onAbort));
|
|
22
24
|
});
|
|
23
25
|
export const make = (options = {}) => {
|
|
24
|
-
const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer, SpeechClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
|
|
26
|
+
const runtime = ManagedRuntime.make(Layer.mergeAll(LLMClient.layer, ImageClient.layer, VideoClient.layer, SpeechClient.layer, TranscriptionClient.layer).pipe(Layer.provideMerge(options.layer ?? RequestExecutor.fetchLayer)));
|
|
25
27
|
/** Run any package Effect (for example `asset.bytes()`) inside this runtime. */
|
|
26
28
|
const run = (effect, options) => runtime.runPromise(effect, { signal: options?.signal });
|
|
27
29
|
const iterate = (stream, options) => Stream.toAsyncIterable(Stream.unwrap(runtime.contextEffect.pipe(Effect.map((context) => stream.pipe(Stream.interruptWhen(abortEffect(options?.signal)), Stream.provideContext(context))))));
|
|
@@ -37,6 +39,7 @@ export const make = (options = {}) => {
|
|
|
37
39
|
const imageRequest = (input) => input instanceof ImageRequest ? input : Image.request(input);
|
|
38
40
|
const videoRequest = (input) => input instanceof VideoRequest ? input : Video.request(input);
|
|
39
41
|
const speechRequest = (input) => input instanceof SpeechRequest ? input : Speech.request(input);
|
|
42
|
+
const transcriptionRequest = (input) => input instanceof TranscriptionRequest ? input : Transcription.request(input);
|
|
40
43
|
return {
|
|
41
44
|
run,
|
|
42
45
|
llm: {
|
|
@@ -61,6 +64,13 @@ export const make = (options = {}) => {
|
|
|
61
64
|
generate: (input, options) => run(Speech.generate(speechRequest(input)), options),
|
|
62
65
|
stream: (input, options) => iterate(Speech.stream(speechRequest(input)), options),
|
|
63
66
|
},
|
|
67
|
+
transcription: {
|
|
68
|
+
request: Transcription.request,
|
|
69
|
+
generate: (input, options) => run(Transcription.generate(transcriptionRequest(input), { poll: options?.poll }), options),
|
|
70
|
+
stream: (input, options) => iterate(Transcription.stream(transcriptionRequest(input), { poll: options?.poll }), options),
|
|
71
|
+
start: (input, options) => run(Transcription.start(transcriptionRequest(input)), options).then(handle),
|
|
72
|
+
resume: (model, token, options) => run(Transcription.resume(model, token), options).then(handle),
|
|
73
|
+
},
|
|
64
74
|
dispose: () => runtime.dispose(),
|
|
65
75
|
};
|
|
66
76
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
3
|
+
import { MediaRoute } from "../route/media.js";
|
|
4
|
+
import { TranscriptionModel, TranscriptionResponse, type TranscriptionRequestFor } from "../transcription.js";
|
|
5
|
+
export declare const DEFAULT_BASE_URL = "https://api.assemblyai.com";
|
|
6
|
+
export declare const PATH = "/v2/transcript";
|
|
7
|
+
export declare const UPLOAD_PATH = "/v2/upload";
|
|
8
|
+
export type AssemblyAITranscriptionOptions = {
|
|
9
|
+
readonly keyterms_prompt?: ReadonlyArray<string>;
|
|
10
|
+
readonly punctuate?: boolean;
|
|
11
|
+
readonly format_text?: boolean;
|
|
12
|
+
readonly disfluencies?: boolean;
|
|
13
|
+
readonly filter_profanity?: boolean;
|
|
14
|
+
readonly temperature?: number;
|
|
15
|
+
readonly speaker_options?: {
|
|
16
|
+
readonly min_speakers_expected?: number;
|
|
17
|
+
readonly max_speakers_expected?: number;
|
|
18
|
+
};
|
|
19
|
+
readonly language_detection_options?: {
|
|
20
|
+
readonly expected_languages?: ReadonlyArray<string>;
|
|
21
|
+
readonly fallback_language?: string;
|
|
22
|
+
readonly code_switching?: boolean;
|
|
23
|
+
};
|
|
24
|
+
readonly speech_models?: ReadonlyArray<"universal-3-5-pro" | "universal-2" | (string & {})>;
|
|
25
|
+
} & Record<string, unknown>;
|
|
26
|
+
export type Request = TranscriptionRequestFor<AssemblyAITranscriptionOptions>;
|
|
27
|
+
export declare const Token: Schema.Struct<{
|
|
28
|
+
readonly transcriptID: Schema.String;
|
|
29
|
+
}>;
|
|
30
|
+
export type Token = Schema.Schema.Type<typeof Token>;
|
|
31
|
+
export declare const protocol: MediaProtocol.Queued<Request, TranscriptionResponse, {
|
|
32
|
+
readonly transcriptID: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const model: (input: MediaRoute.ModelInput) => TranscriptionModel<AssemblyAITranscriptionOptions>;
|
|
35
|
+
export declare const AssemblyAITranscription: {
|
|
36
|
+
readonly protocol: MediaProtocol.Queued<Request, TranscriptionResponse, {
|
|
37
|
+
readonly transcriptID: string;
|
|
38
|
+
}>;
|
|
39
|
+
readonly model: (input: MediaRoute.ModelInput) => TranscriptionModel<AssemblyAITranscriptionOptions>;
|
|
40
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Media } from "../media.js";
|
|
3
|
+
import { MediaProtocol } from "../route/media-protocol.js";
|
|
4
|
+
import { MediaRoute } from "../route/media.js";
|
|
5
|
+
import { ProviderID, mergeJsonRecords } from "../schema/index.js";
|
|
6
|
+
import { TranscriptionModel, TranscriptionResponse } from "../transcription.js";
|
|
7
|
+
import { ProviderShared, optionalNull } from "./shared.js";
|
|
8
|
+
import { MediaInput } from "./utils/media-input.js";
|
|
9
|
+
const ADAPTER = "assemblyai-transcription";
|
|
10
|
+
const NAME = "AssemblyAI";
|
|
11
|
+
const PROVIDER = ProviderID.make("assemblyai");
|
|
12
|
+
export const DEFAULT_BASE_URL = "https://api.assemblyai.com";
|
|
13
|
+
export const PATH = "/v2/transcript";
|
|
14
|
+
export const UPLOAD_PATH = "/v2/upload";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// 2. Token and response schemas
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export const Token = Schema.Struct({ transcriptID: Schema.String });
|
|
19
|
+
const Upload = Schema.Struct({ upload_url: Schema.String });
|
|
20
|
+
/** Word and utterance times are milliseconds. */
|
|
21
|
+
const Transcript = Schema.Struct({
|
|
22
|
+
id: Schema.String,
|
|
23
|
+
status: Schema.String,
|
|
24
|
+
text: optionalNull(Schema.String),
|
|
25
|
+
words: optionalNull(Schema.Array(Schema.Struct({
|
|
26
|
+
text: Schema.String,
|
|
27
|
+
start: Schema.Number,
|
|
28
|
+
end: Schema.Number,
|
|
29
|
+
confidence: optionalNull(Schema.Number),
|
|
30
|
+
speaker: optionalNull(Schema.String),
|
|
31
|
+
}))),
|
|
32
|
+
utterances: optionalNull(Schema.Array(Schema.Struct({
|
|
33
|
+
text: Schema.String,
|
|
34
|
+
start: Schema.Number,
|
|
35
|
+
end: Schema.Number,
|
|
36
|
+
speaker: optionalNull(Schema.String),
|
|
37
|
+
}))),
|
|
38
|
+
language_code: optionalNull(Schema.String),
|
|
39
|
+
audio_duration: optionalNull(Schema.Number),
|
|
40
|
+
speech_model_used: optionalNull(Schema.String),
|
|
41
|
+
error: optionalNull(Schema.String),
|
|
42
|
+
});
|
|
43
|
+
const STATUS = {
|
|
44
|
+
queued: "queued",
|
|
45
|
+
processing: "running",
|
|
46
|
+
completed: "completed",
|
|
47
|
+
error: "failed",
|
|
48
|
+
};
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// 5. Request body construction
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const decodeUpload = MediaProtocol.decodeJson(ADAPTER, NAME, Upload);
|
|
53
|
+
/** `/v2/transcript` only takes a URL, so inline audio is uploaded to `/v2/upload` first. */
|
|
54
|
+
const prepare = Effect.fn("AssemblyAITranscription.prepare")(function* (request, send) {
|
|
55
|
+
if (request.audio.source.type !== "bytes" && request.audio.source.type !== "base64")
|
|
56
|
+
return request;
|
|
57
|
+
const audio = yield* MediaInput.inlineBytes(ADAPTER, request.audio);
|
|
58
|
+
const uploaded = yield* send(UPLOAD_PATH, MediaProtocol.binary(audio, "application/octet-stream")).pipe(Effect.flatMap(decodeUpload));
|
|
59
|
+
return { ...request, audio: Media.url(uploaded.value.upload_url, { mediaType: request.audio.mediaType }) };
|
|
60
|
+
});
|
|
61
|
+
const fromRequest = Effect.fn("AssemblyAITranscription.fromRequest")(function* (request) {
|
|
62
|
+
const audio = yield* ProviderShared.mediaReference(request.audio, PROVIDER, NAME);
|
|
63
|
+
return MediaProtocol.json(mergeJsonRecords({
|
|
64
|
+
audio_url: audio.value,
|
|
65
|
+
speech_models: [request.model.id],
|
|
66
|
+
language_code: request.language,
|
|
67
|
+
language_detection: request.language === undefined ? true : undefined,
|
|
68
|
+
prompt: request.prompt,
|
|
69
|
+
// Turn-level `utterances`, the only segments AssemblyAI returns, require speaker labels.
|
|
70
|
+
speaker_labels: request.diarize === true || request.timestamps === "segment" ? true : undefined,
|
|
71
|
+
speakers_expected: request.speakers,
|
|
72
|
+
}, request.providerOptions, request.http?.body) ?? {});
|
|
73
|
+
});
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// 6. Response decoding
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
const decodeTranscript = MediaProtocol.decodeJson(ADAPTER, NAME, Transcript);
|
|
78
|
+
const decodeStart = Effect.fn("AssemblyAITranscription.decodeStart")(function* (response) {
|
|
79
|
+
const output = yield* decodeTranscript(response);
|
|
80
|
+
const status = yield* MediaProtocol.status(STATUS, output.value.status, output);
|
|
81
|
+
return { token: { transcriptID: output.value.id }, snapshot: { id: output.value.id, status } };
|
|
82
|
+
});
|
|
83
|
+
const decodeStatus = Effect.fn("AssemblyAITranscription.decodeStatus")(function* (response, context) {
|
|
84
|
+
const output = yield* decodeTranscript(response);
|
|
85
|
+
const status = yield* MediaProtocol.status(STATUS, output.value.status, output);
|
|
86
|
+
return { id: context.token.transcriptID, status };
|
|
87
|
+
});
|
|
88
|
+
const seconds = (milliseconds) => milliseconds / 1000;
|
|
89
|
+
const decodeResult = Effect.fn("AssemblyAITranscription.decodeResult")(function* (response, context) {
|
|
90
|
+
const output = yield* decodeTranscript(response);
|
|
91
|
+
const transcript = output.value;
|
|
92
|
+
const status = yield* MediaProtocol.status(STATUS, transcript.status, output);
|
|
93
|
+
const error = transcript.error ?? undefined;
|
|
94
|
+
if (status === "failed")
|
|
95
|
+
return yield* output.ended("failed", `${NAME} transcription failed${error === undefined ? "" : `: ${error}`}`);
|
|
96
|
+
if (status !== "completed")
|
|
97
|
+
return yield* output.invalid(`${NAME} transcript ${context.token.transcriptID} has not finished`);
|
|
98
|
+
const duration = transcript.audio_duration ?? undefined;
|
|
99
|
+
return new TranscriptionResponse({
|
|
100
|
+
text: transcript.text ?? "",
|
|
101
|
+
segments: transcript.utterances?.map((utterance) => ({
|
|
102
|
+
text: utterance.text,
|
|
103
|
+
startSeconds: seconds(utterance.start),
|
|
104
|
+
endSeconds: seconds(utterance.end),
|
|
105
|
+
speaker: utterance.speaker ?? undefined,
|
|
106
|
+
})),
|
|
107
|
+
words: transcript.words?.map((word) => ({
|
|
108
|
+
text: word.text,
|
|
109
|
+
startSeconds: seconds(word.start),
|
|
110
|
+
endSeconds: seconds(word.end),
|
|
111
|
+
speaker: word.speaker ?? undefined,
|
|
112
|
+
confidence: word.confidence ?? undefined,
|
|
113
|
+
})),
|
|
114
|
+
language: transcript.language_code?.toLowerCase(),
|
|
115
|
+
durationSeconds: duration,
|
|
116
|
+
usage: duration === undefined ? undefined : { type: "seconds", seconds: duration },
|
|
117
|
+
providerMetadata: {
|
|
118
|
+
assemblyai: { transcriptId: transcript.id, speechModel: transcript.speech_model_used ?? undefined },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// 7. Protocol and route
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
const transcriptPath = (token) => `${PATH}/${token.transcriptID}`;
|
|
126
|
+
export const protocol = MediaProtocol.queued({
|
|
127
|
+
id: ADAPTER,
|
|
128
|
+
name: NAME,
|
|
129
|
+
token: Token,
|
|
130
|
+
start: { prepare, body: { from: fromRequest }, decode: decodeStart },
|
|
131
|
+
status: { path: transcriptPath, decode: decodeStatus },
|
|
132
|
+
result: { path: transcriptPath, decode: decodeResult },
|
|
133
|
+
});
|
|
134
|
+
export const model = (input) => TranscriptionModel.fromRoute({ id: ADAPTER, provider: PROVIDER, protocol, baseURL: DEFAULT_BASE_URL, path: PATH }, input);
|
|
135
|
+
export const AssemblyAITranscription = {
|
|
136
|
+
protocol,
|
|
137
|
+
model,
|
|
138
|
+
};
|
|
@@ -3,6 +3,7 @@ import { MediaProtocol } from "../route/media-protocol.js";
|
|
|
3
3
|
import { MediaRoute } from "../route/media.js";
|
|
4
4
|
import { ProviderID, mergeJsonRecords } from "../schema/index.js";
|
|
5
5
|
import { SpeechModel } from "../speech.js";
|
|
6
|
+
import { MediaInput } from "./utils/media-input.js";
|
|
6
7
|
import { SpeechStream } from "./utils/speech-stream.js";
|
|
7
8
|
const ADAPTER = "deepgram-speech";
|
|
8
9
|
const NAME = "Deepgram";
|
|
@@ -20,29 +21,30 @@ const FORMATS = {
|
|
|
20
21
|
flac: { encoding: "flac" },
|
|
21
22
|
aac: { encoding: "aac" },
|
|
22
23
|
};
|
|
23
|
-
const
|
|
24
|
-
const queryParameters = (request) => {
|
|
24
|
+
const audioFormat = (request) => {
|
|
25
25
|
const format = request.format === undefined ? undefined : FORMATS[request.format];
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
return {
|
|
27
|
+
encoding: request.providerOptions?.encoding ?? format?.encoding,
|
|
28
|
+
container: request.providerOptions?.container ?? format?.container,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const queryParameters = (request) => {
|
|
32
|
+
const { encoding: _encoding, container: _container, sampleRate, bitRate, ...native } = request.providerOptions ?? {};
|
|
33
|
+
return MediaInput.query(ADAPTER, {
|
|
28
34
|
...native,
|
|
29
35
|
model: request.model.id,
|
|
30
|
-
|
|
31
|
-
container: container ?? format?.container,
|
|
36
|
+
...audioFormat(request),
|
|
32
37
|
sample_rate: sampleRate,
|
|
33
38
|
bit_rate: bitRate,
|
|
34
39
|
speed: request.speed,
|
|
35
|
-
})
|
|
36
|
-
const text = queryValue(value);
|
|
37
|
-
return text === undefined ? [] : [[key, text]];
|
|
38
|
-
}));
|
|
40
|
+
});
|
|
39
41
|
};
|
|
40
42
|
const fromRequest = Effect.fn("DeepgramSpeech.fromRequest")(function* (request) {
|
|
41
43
|
if (request.format !== undefined &&
|
|
42
44
|
FORMATS[request.format] === undefined &&
|
|
43
45
|
request.providerOptions?.encoding === undefined)
|
|
44
46
|
return yield* SpeechStream.unsupportedFormat(PROVIDER, ADAPTER, `${NAME} has no encoding for format "${request.format}"; pass providerOptions.encoding`);
|
|
45
|
-
return MediaProtocol.json(mergeJsonRecords({ text: request.text }, request.http?.body) ?? {}, queryParameters(request));
|
|
47
|
+
return MediaProtocol.json(mergeJsonRecords({ text: request.text }, request.http?.body) ?? {}, yield* queryParameters(request));
|
|
46
48
|
});
|
|
47
49
|
// ---------------------------------------------------------------------------
|
|
48
50
|
// 6. Stream parsing
|
|
@@ -55,15 +57,15 @@ const HEADERLESS_ENCODINGS = {
|
|
|
55
57
|
const finish = (state, context) => {
|
|
56
58
|
const headers = context.http.headers;
|
|
57
59
|
const mediaType = headers["content-type"];
|
|
58
|
-
const
|
|
59
|
-
const encoding = HEADERLESS_ENCODINGS[
|
|
60
|
+
const format = audioFormat(context.request);
|
|
61
|
+
const encoding = HEADERLESS_ENCODINGS[format.encoding ?? ""];
|
|
60
62
|
const requestID = headers["dg-request-id"];
|
|
61
63
|
const modelName = headers["dg-model-name"];
|
|
62
64
|
return SpeechStream.finish(ADAPTER, state, {
|
|
63
|
-
...(
|
|
65
|
+
...(format.container === "none" && encoding !== undefined
|
|
64
66
|
? SpeechStream.pcm(encoding, SpeechStream.sampleRate(mediaType), mediaType)
|
|
65
67
|
: // Deepgram's default encoding is MP3; WAV is a container around any encoding.
|
|
66
|
-
{ mediaType, info: { format:
|
|
68
|
+
{ mediaType, info: { format: format.container === "wav" ? "wav" : (format.encoding ?? "mp3") } }),
|
|
67
69
|
usage: SpeechStream.headerUsage("characters", headers["dg-char-count"]),
|
|
68
70
|
providerMetadata: requestID === undefined && modelName === undefined
|
|
69
71
|
? undefined
|