@effect-uai/google 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Gemini.mjs +1 -1
- package/dist/GeminiEmbedding.mjs +1 -1
- package/dist/GeminiSynthesizer.d.mts +48 -0
- package/dist/GeminiSynthesizer.d.mts.map +1 -0
- package/dist/GeminiSynthesizer.mjs +95 -0
- package/dist/GeminiSynthesizer.mjs.map +1 -0
- package/dist/GeminiSynthesizer.test.d.mts +1 -0
- package/dist/GeminiSynthesizer.test.mjs +84 -0
- package/dist/GeminiSynthesizer.test.mjs.map +1 -0
- package/dist/GeminiTranscriber.d.mts +42 -0
- package/dist/GeminiTranscriber.d.mts.map +1 -0
- package/dist/GeminiTranscriber.mjs +80 -0
- package/dist/GeminiTranscriber.mjs.map +1 -0
- package/dist/GeminiTranscriber.test.d.mts +1 -0
- package/dist/GeminiTranscriber.test.mjs +125 -0
- package/dist/GeminiTranscriber.test.mjs.map +1 -0
- package/dist/LyriaGenerator.d.mts +57 -0
- package/dist/LyriaGenerator.d.mts.map +1 -0
- package/dist/LyriaGenerator.mjs +202 -0
- package/dist/LyriaGenerator.mjs.map +1 -0
- package/dist/LyriaGenerator.test.d.mts +1 -0
- package/dist/LyriaGenerator.test.mjs +138 -0
- package/dist/LyriaGenerator.test.mjs.map +1 -0
- package/dist/{chunk-CfYAbeIz.mjs → chunk-uyGKjUfl.mjs} +2 -1
- package/dist/codec.mjs +1 -1
- package/dist/dist-DMsg8YLb.mjs +13782 -0
- package/dist/dist-DMsg8YLb.mjs.map +1 -0
- package/dist/geminiSpeechCodec.d.mts +37 -0
- package/dist/geminiSpeechCodec.d.mts.map +1 -0
- package/dist/geminiSpeechCodec.mjs +110 -0
- package/dist/geminiSpeechCodec.mjs.map +1 -0
- package/dist/geminiSpeechCodec.test.d.mts +1 -0
- package/dist/geminiSpeechCodec.test.mjs +78 -0
- package/dist/geminiSpeechCodec.test.mjs.map +1 -0
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +4 -1
- package/dist/magic-string.es-CaWgF-3p.mjs +1013 -0
- package/dist/magic-string.es-CaWgF-3p.mjs.map +1 -0
- package/dist/models.d.mts +35 -1
- package/dist/models.d.mts.map +1 -1
- package/package.json +14 -2
- package/src/GeminiSynthesizer.test.ts +93 -0
- package/src/GeminiSynthesizer.ts +214 -0
- package/src/GeminiTranscriber.test.ts +136 -0
- package/src/GeminiTranscriber.ts +204 -0
- package/src/LyriaGenerator.test.ts +162 -0
- package/src/LyriaGenerator.ts +394 -0
- package/src/geminiSpeechCodec.test.ts +56 -0
- package/src/geminiSpeechCodec.ts +137 -0
- package/src/index.ts +3 -0
- package/src/models.ts +84 -0
package/dist/Gemini.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
|
|
2
2
|
import { WireChunk, accumulatorToTurn, buildRequestBody, emptyAccumulator, ingestChunk } from "./codec.mjs";
|
|
3
3
|
import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
|
|
4
4
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
package/dist/GeminiEmbedding.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
|
|
2
2
|
import { Context, Effect, Encoding, Layer, Match, Redacted, Schema } from "effect";
|
|
3
3
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
4
|
import * as AiError from "@effect-uai/core/AiError";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { GeminiTtsModel, GeminiVoiceName } from "./models.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Redacted, Stream } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { CommonSynthesizeRequest, SpeechSynthesizer, SpeechSynthesizerService } from "@effect-uai/core/SpeechSynthesizer";
|
|
6
|
+
import { AudioBlob, AudioChunk } from "@effect-uai/core/Audio";
|
|
7
|
+
|
|
8
|
+
//#region src/GeminiSynthesizer.d.ts
|
|
9
|
+
declare namespace GeminiSynthesizer_d_exports {
|
|
10
|
+
export { Config, GeminiSynthesizeRequest, GeminiSynthesizer, GeminiSynthesizerService, layer, make, realizeOutput };
|
|
11
|
+
}
|
|
12
|
+
type GeminiSynthesizeRequest = Omit<CommonSynthesizeRequest, "model" | "voiceId"> & {
|
|
13
|
+
readonly model: GeminiTtsModel;
|
|
14
|
+
readonly voiceId: GeminiVoiceName;
|
|
15
|
+
};
|
|
16
|
+
type GeminiSynthesizerService = {
|
|
17
|
+
readonly synthesize: (r: GeminiSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>;
|
|
18
|
+
readonly streamSynthesis: (r: GeminiSynthesizeRequest) => Stream.Stream<AudioChunk, AiError.AiError>;
|
|
19
|
+
readonly streamSynthesisFrom: SpeechSynthesizerService["streamSynthesisFrom"];
|
|
20
|
+
};
|
|
21
|
+
declare const GeminiSynthesizer_base: Context.ServiceClass<GeminiSynthesizer, "@betalyra/effect-uai/providers/google/GeminiSynthesizer", GeminiSynthesizerService>;
|
|
22
|
+
declare class GeminiSynthesizer extends GeminiSynthesizer_base {}
|
|
23
|
+
type Config = {
|
|
24
|
+
readonly apiKey: Redacted.Redacted;
|
|
25
|
+
readonly baseUrl?: string;
|
|
26
|
+
};
|
|
27
|
+
declare const realizeOutput: (u: "mp3" | "wav" | "ogg" | "opus" | "flac" | "aac" | "webm" | "raw") => Effect.Effect<readonly [{
|
|
28
|
+
readonly container: "raw";
|
|
29
|
+
readonly encoding: "pcm_s16le";
|
|
30
|
+
readonly sampleRate: 24000;
|
|
31
|
+
readonly channels: 1;
|
|
32
|
+
}, (b: Uint8Array) => Uint8Array<ArrayBufferLike>] | readonly [{
|
|
33
|
+
readonly container: "wav";
|
|
34
|
+
readonly encoding: "pcm_s16le";
|
|
35
|
+
readonly sampleRate: 24000;
|
|
36
|
+
readonly channels: 1;
|
|
37
|
+
}, (b: Uint8Array) => Uint8Array<ArrayBufferLike>], AiError.Unsupported, never>;
|
|
38
|
+
declare const make: (cfg: Config) => Effect.Effect<GeminiSynthesizerService, never, HttpClient.HttpClient>;
|
|
39
|
+
/**
|
|
40
|
+
* Layer registers both `GeminiSynthesizer` and the generic
|
|
41
|
+
* `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini
|
|
42
|
+
* TTS is sync-only at the wire level, so `streamSynthesisFrom` against
|
|
43
|
+
* this Layer alone is a compile-time error.
|
|
44
|
+
*/
|
|
45
|
+
declare const layer: (cfg: Config) => Layer.Layer<GeminiSynthesizer | SpeechSynthesizer, never, HttpClient.HttpClient>;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { Config, GeminiSynthesizeRequest, GeminiSynthesizer, GeminiSynthesizerService, layer, make, realizeOutput, GeminiSynthesizer_d_exports as t };
|
|
48
|
+
//# sourceMappingURL=GeminiSynthesizer.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiSynthesizer.d.mts","names":[],"sources":["../src/GeminiSynthesizer.ts"],"mappings":";;;;;;;;;;;KAsBY,uBAAA,GAA0B,IAAA,CAAK,uBAAA;EAAA,SAChC,KAAA,EAAO,cAAA;EAAA,SACP,OAAA,EAAS,eAAA;AAAA;AAAA,KAGR,wBAAA;EAAA,SACD,UAAA,GAAa,CAAA,EAAG,uBAAA,KAA4B,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,OAAA,CAAQ,OAAA;EAAA,SAC7E,eAAA,GACP,CAAA,EAAG,uBAAA,KACA,MAAA,CAAO,MAAA,CAAO,UAAA,EAAY,OAAA,CAAQ,OAAA;EAAA,SAC9B,mBAAA,EAAqB,wBAAA;AAAA;AAAA,cAC/B,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,KAK3B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAUvD,aAAA,GAAa,CAAA,uEAAA,MAAA,CAAA,MAAA;EAAA;;;;OAIhB,UAAA,KAAU,UAAA,CAAA,eAAA;EAAA;;;;OAMV,UAAA,KAAU,UAAA,CAAA,eAAA,IAAA,OAAA,CAAA,WAAA;AAAA,cAoHP,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA,CAAA,wBAAA,SAAA,UAAA,CAAA,UAAA;;AArJhC;;;;;cA0Ka,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,CAAA,iBAAA,GAAA,iBAAA,SAAA,UAAA,CAAA,UAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
|
|
2
|
+
import { decodeBase64Audio, httpStatusError, transportFailure, wrapPcmAsWav } from "./geminiSpeechCodec.mjs";
|
|
3
|
+
import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from "effect";
|
|
4
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
6
|
+
import { SpeechSynthesizer } from "@effect-uai/core/SpeechSynthesizer";
|
|
7
|
+
//#region src/GeminiSynthesizer.ts
|
|
8
|
+
var GeminiSynthesizer_exports = /* @__PURE__ */ __exportAll({
|
|
9
|
+
GeminiSynthesizer: () => GeminiSynthesizer,
|
|
10
|
+
layer: () => layer,
|
|
11
|
+
make: () => make,
|
|
12
|
+
realizeOutput: () => realizeOutput
|
|
13
|
+
});
|
|
14
|
+
var GeminiSynthesizer = class extends Context.Service()("@betalyra/effect-uai/providers/google/GeminiSynthesizer") {};
|
|
15
|
+
const PCM_RATE = 24e3;
|
|
16
|
+
const PCM_CHANNELS = 1;
|
|
17
|
+
const realizeOutput = Match.type().pipe(Match.when("raw", () => Effect.succeed([{
|
|
18
|
+
container: "raw",
|
|
19
|
+
encoding: "pcm_s16le",
|
|
20
|
+
sampleRate: PCM_RATE,
|
|
21
|
+
channels: PCM_CHANNELS
|
|
22
|
+
}, (b) => b])), Match.when("wav", () => Effect.succeed([{
|
|
23
|
+
container: "wav",
|
|
24
|
+
encoding: "pcm_s16le",
|
|
25
|
+
sampleRate: PCM_RATE,
|
|
26
|
+
channels: PCM_CHANNELS
|
|
27
|
+
}, (b) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS)])), Match.whenOr("mp3", "opus", "aac", "flac", "ogg", "webm", (c) => Effect.fail(new AiError.Unsupported({
|
|
28
|
+
provider: "gemini",
|
|
29
|
+
capability: "outputFormat",
|
|
30
|
+
reason: `Gemini TTS emits raw 16-bit PCM at 24 kHz. Request "raw" or "wav"; ${c} is unavailable.`
|
|
31
|
+
}))), Match.exhaustive);
|
|
32
|
+
const Wire = Schema.Struct({ candidates: Schema.optional(Schema.Array(Schema.Struct({ content: Schema.optional(Schema.Struct({ parts: Schema.optional(Schema.Array(Schema.Struct({ inlineData: Schema.optional(Schema.Struct({
|
|
33
|
+
mimeType: Schema.optional(Schema.String),
|
|
34
|
+
data: Schema.String
|
|
35
|
+
})) }))) })) }))) });
|
|
36
|
+
const decodeWire = Schema.decodeUnknownEffect(Wire);
|
|
37
|
+
const findInline = (wire) => (wire.candidates ?? []).flatMap((c) => c.content?.parts ?? []).find((p) => p.inlineData !== void 0)?.inlineData;
|
|
38
|
+
const ttsBody = (r) => ({
|
|
39
|
+
contents: [{ parts: [{ text: r.text }] }],
|
|
40
|
+
generationConfig: {
|
|
41
|
+
responseModalities: ["AUDIO"],
|
|
42
|
+
speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: r.voiceId } } }
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const baseUrl = (cfg) => cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
46
|
+
const synthesizeImpl = (cfg) => (request) => Effect.gen(function* () {
|
|
47
|
+
const client = yield* HttpClient.HttpClient;
|
|
48
|
+
const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? "raw");
|
|
49
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/models/${request.model}:generateContent`).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe(ttsBody(request)));
|
|
50
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
51
|
+
if (response.status >= 400) {
|
|
52
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
53
|
+
return yield* Effect.fail(httpStatusError(response.status, text));
|
|
54
|
+
}
|
|
55
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure));
|
|
56
|
+
return {
|
|
57
|
+
format,
|
|
58
|
+
bytes: wrap(yield* decodeBase64Audio((yield* decodeWire(json).pipe(Effect.map(findInline), Effect.mapError((cause) => new AiError.GenerationFailed({
|
|
59
|
+
provider: "gemini",
|
|
60
|
+
raw: cause
|
|
61
|
+
})), Effect.flatMap((found) => found === void 0 ? Effect.fail(new AiError.GenerationFailed({
|
|
62
|
+
provider: "gemini",
|
|
63
|
+
raw: {
|
|
64
|
+
message: "Gemini TTS response had no inlineData audio part.",
|
|
65
|
+
hint: "Likely a prompt-safety rejection or the model didn't honour responseModalities.",
|
|
66
|
+
response: json
|
|
67
|
+
}
|
|
68
|
+
})) : Effect.succeed(found)))).data))
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const unsupportedStreamFrom = () => Stream.fail(new AiError.Unsupported({
|
|
72
|
+
provider: "gemini",
|
|
73
|
+
capability: "streamSynthesisFrom",
|
|
74
|
+
reason: "Gemini TTS via generateContent is sync-only. Use Cloud Text-to-Speech (Chirp 3 HD) for incremental text-in."
|
|
75
|
+
}));
|
|
76
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
|
|
77
|
+
synthesize: (r) => synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
78
|
+
streamSynthesis: (r) => Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(Stream.map((b) => ({ bytes: b.bytes })), Stream.provideService(HttpClient.HttpClient, client)),
|
|
79
|
+
streamSynthesisFrom: unsupportedStreamFrom
|
|
80
|
+
}));
|
|
81
|
+
/**
|
|
82
|
+
* Layer registers both `GeminiSynthesizer` and the generic
|
|
83
|
+
* `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini
|
|
84
|
+
* TTS is sync-only at the wire level, so `streamSynthesisFrom` against
|
|
85
|
+
* this Layer alone is a compile-time error.
|
|
86
|
+
*/
|
|
87
|
+
const layer = (cfg) => Layer.merge(Layer.effect(GeminiSynthesizer, make(cfg)), Layer.effect(SpeechSynthesizer, Effect.map(make(cfg), (s) => ({
|
|
88
|
+
synthesize: (req) => s.synthesize(req),
|
|
89
|
+
streamSynthesis: (req) => s.streamSynthesis(req),
|
|
90
|
+
streamSynthesisFrom: s.streamSynthesisFrom
|
|
91
|
+
}))));
|
|
92
|
+
//#endregion
|
|
93
|
+
export { GeminiSynthesizer, layer, make, realizeOutput, GeminiSynthesizer_exports as t };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=GeminiSynthesizer.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiSynthesizer.mjs","names":[],"sources":["../src/GeminiSynthesizer.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob, AudioChunk, AudioFormat } from \"@effect-uai/core/Audio\"\nimport {\n type CommonStreamSynthesizeRequest,\n type CommonSynthesizeRequest,\n SpeechSynthesizer,\n type SpeechSynthesizerService,\n} from \"@effect-uai/core/SpeechSynthesizer\"\nimport {\n decodeBase64Audio,\n httpStatusError,\n transportFailure,\n wrapPcmAsWav,\n} from \"./geminiSpeechCodec.js\"\nimport type { GeminiTtsModel, GeminiVoiceName } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type GeminiSynthesizeRequest = Omit<CommonSynthesizeRequest, \"model\" | \"voiceId\"> & {\n readonly model: GeminiTtsModel\n readonly voiceId: GeminiVoiceName\n}\n\nexport type GeminiSynthesizerService = {\n readonly synthesize: (r: GeminiSynthesizeRequest) => Effect.Effect<AudioBlob, AiError.AiError>\n readonly streamSynthesis: (\n r: GeminiSynthesizeRequest,\n ) => Stream.Stream<AudioChunk, AiError.AiError>\n readonly streamSynthesisFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"]\n}\n\nexport class GeminiSynthesizer extends Context.Service<\n GeminiSynthesizer,\n GeminiSynthesizerService\n>()(\"@betalyra/effect-uai/providers/google/GeminiSynthesizer\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Codec — Gemini TTS always emits 24 kHz mono signed-16 PCM. Caller picks\n// raw bytes or WAV-wrapped.\n// ---------------------------------------------------------------------------\n\nconst PCM_RATE = 24000\nconst PCM_CHANNELS = 1\n\nexport const realizeOutput = Match.type<AudioFormat[\"container\"]>().pipe(\n Match.when(\"raw\", () =>\n Effect.succeed([\n { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => b,\n ] as const),\n ),\n Match.when(\"wav\", () =>\n Effect.succeed([\n { container: \"wav\", encoding: \"pcm_s16le\", sampleRate: PCM_RATE, channels: PCM_CHANNELS },\n (b: Uint8Array) => wrapPcmAsWav(b, PCM_RATE, PCM_CHANNELS),\n ] as const),\n ),\n Match.whenOr(\"mp3\", \"opus\", \"aac\", \"flac\", \"ogg\", \"webm\", (c) =>\n Effect.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"outputFormat\",\n reason: `Gemini TTS emits raw 16-bit PCM at 24 kHz. Request \"raw\" or \"wav\"; ${c} is unavailable.`,\n }),\n ),\n ),\n Match.exhaustive,\n)\n\n// ---------------------------------------------------------------------------\n// Wire\n// ---------------------------------------------------------------------------\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(\n Schema.Array(\n Schema.Struct({\n content: Schema.optional(\n Schema.Struct({\n parts: Schema.optional(\n Schema.Array(\n Schema.Struct({\n inlineData: Schema.optional(\n Schema.Struct({\n mimeType: Schema.optional(Schema.String),\n data: Schema.String,\n }),\n ),\n }),\n ),\n ),\n }),\n ),\n }),\n ),\n ),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst findInline = (wire: typeof Wire.Type) =>\n (wire.candidates ?? [])\n .flatMap((c) => c.content?.parts ?? [])\n .find((p) => p.inlineData !== undefined)?.inlineData\n\nconst ttsBody = (r: GeminiSynthesizeRequest) => ({\n contents: [{ parts: [{ text: r.text }] }],\n generationConfig: {\n responseModalities: [\"AUDIO\"],\n speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: r.voiceId } } },\n },\n})\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst synthesizeImpl = (cfg: Config) => (request: GeminiSynthesizeRequest) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const [format, wrap] = yield* realizeOutput(request.outputFormat?.container ?? \"raw\")\n const httpRequest = HttpClientRequest.post(\n `${baseUrl(cfg)}/models/${request.model}:generateContent`,\n ).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe(ttsBody(request)),\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 inline = yield* decodeWire(json).pipe(\n Effect.map(findInline),\n Effect.mapError((cause) => new AiError.GenerationFailed({ provider: \"gemini\", raw: cause })),\n Effect.flatMap((found) =>\n found === undefined\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: \"gemini\",\n raw: {\n message: \"Gemini TTS response had no inlineData audio part.\",\n hint: \"Likely a prompt-safety rejection or the model didn't honour responseModalities.\",\n response: json,\n },\n }),\n )\n : Effect.succeed(found),\n ),\n )\n const pcm = yield* decodeBase64Audio(inline.data)\n return { format, bytes: wrap(pcm) } satisfies AudioBlob\n })\n\nconst unsupportedStreamFrom: SpeechSynthesizerService[\"streamSynthesisFrom\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamSynthesisFrom\",\n reason:\n \"Gemini TTS via generateContent is sync-only. Use Cloud Text-to-Speech (Chirp 3 HD) for incremental text-in.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.map(\n HttpClient.HttpClient.asEffect(),\n (client): GeminiSynthesizerService => ({\n synthesize: (r) =>\n synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamSynthesis: (r) =>\n Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(\n Stream.map((b): AudioChunk => ({ bytes: b.bytes })),\n Stream.provideService(HttpClient.HttpClient, client),\n ),\n streamSynthesisFrom: unsupportedStreamFrom,\n }),\n )\n\n/**\n * Layer registers both `GeminiSynthesizer` and the generic\n * `SpeechSynthesizer`. Does NOT register `TtsIncrementalText` — Gemini\n * TTS is sync-only at the wire level, so `streamSynthesisFrom` against\n * this Layer alone is a compile-time error.\n */\nexport const layer = (cfg: Config) =>\n Layer.merge(\n Layer.effect(GeminiSynthesizer, 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 GeminiSynthesizeRequest),\n streamSynthesis: (req: CommonSynthesizeRequest) =>\n s.streamSynthesis(req as GeminiSynthesizeRequest),\n streamSynthesisFrom: s.streamSynthesisFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { i as it, n as globalExpect, r as describe, t as import_dist } from "./dist-DMsg8YLb.mjs";
|
|
2
|
+
import { GeminiSynthesizer, layer, realizeOutput } from "./GeminiSynthesizer.mjs";
|
|
3
|
+
import { Effect, Layer, Redacted, Stream } from "effect";
|
|
4
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
5
|
+
import * as SpeechSynthesizer$1 from "@effect-uai/core/SpeechSynthesizer";
|
|
6
|
+
//#region src/GeminiSynthesizer.test.ts
|
|
7
|
+
const cfg = { apiKey: Redacted.make("test-key") };
|
|
8
|
+
const live = Layer.provide(layer(cfg), FetchHttpClient.layer);
|
|
9
|
+
describe("GeminiSynthesizer.realizeOutput", () => {
|
|
10
|
+
it("maps raw to native PCM 24 kHz mono with an identity wrapper", async () => {
|
|
11
|
+
const [format, wrap] = await Effect.runPromise(realizeOutput("raw"));
|
|
12
|
+
globalExpect(format).toEqual({
|
|
13
|
+
container: "raw",
|
|
14
|
+
encoding: "pcm_s16le",
|
|
15
|
+
sampleRate: 24e3,
|
|
16
|
+
channels: 1
|
|
17
|
+
});
|
|
18
|
+
const pcm = new Uint8Array([
|
|
19
|
+
1,
|
|
20
|
+
2,
|
|
21
|
+
3,
|
|
22
|
+
4
|
|
23
|
+
]);
|
|
24
|
+
globalExpect(wrap(pcm)).toBe(pcm);
|
|
25
|
+
});
|
|
26
|
+
it("maps wav to a RIFF/WAVE wrapper that prepends a 44-byte header", async () => {
|
|
27
|
+
const [format, wrap] = await Effect.runPromise(realizeOutput("wav"));
|
|
28
|
+
globalExpect(format.container).toBe("wav");
|
|
29
|
+
const out = wrap(new Uint8Array([
|
|
30
|
+
0,
|
|
31
|
+
1,
|
|
32
|
+
2,
|
|
33
|
+
3
|
|
34
|
+
]));
|
|
35
|
+
globalExpect(out.length).toBe(48);
|
|
36
|
+
const ascii = (start, len) => String.fromCharCode(...Array.from(out.slice(start, start + len)));
|
|
37
|
+
globalExpect(ascii(0, 4)).toBe("RIFF");
|
|
38
|
+
globalExpect(ascii(8, 4)).toBe("WAVE");
|
|
39
|
+
globalExpect(ascii(12, 4)).toBe("fmt ");
|
|
40
|
+
globalExpect(ascii(36, 4)).toBe("data");
|
|
41
|
+
});
|
|
42
|
+
it("fails Unsupported for mp3 / opus / aac / flac / ogg / webm", async () => {
|
|
43
|
+
const exit = await Effect.runPromiseExit(realizeOutput("mp3"));
|
|
44
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
45
|
+
if (exit._tag === "Failure") {
|
|
46
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
|
|
47
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("outputFormat");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("GeminiSynthesizer capability guards (runtime)", () => {
|
|
52
|
+
it("streamSynthesisFrom returns an Unsupported stream", async () => {
|
|
53
|
+
const program = GeminiSynthesizer.use((s) => Stream.runDrain(s.streamSynthesisFrom(Stream.fromIterable(["hi"]), {
|
|
54
|
+
model: "gemini-2.5-flash-preview-tts",
|
|
55
|
+
voiceId: "Kore"
|
|
56
|
+
})));
|
|
57
|
+
const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)));
|
|
58
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
59
|
+
if (exit._tag === "Failure") {
|
|
60
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
|
|
61
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("streamSynthesisFrom");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("GeminiSynthesizer Layer (compile-time)", () => {
|
|
66
|
+
it("leaves `TtsIncrementalText` unsatisfied when using `streamSynthesisFrom` against this Layer", () => {
|
|
67
|
+
const audio = Stream.fromIterable(["a"]).pipe(SpeechSynthesizer$1.streamSynthesisFrom({
|
|
68
|
+
model: "gemini-2.5-flash-preview-tts",
|
|
69
|
+
voiceId: "Kore"
|
|
70
|
+
}));
|
|
71
|
+
(0, import_dist.expectTypeOf)(Stream.runDrain(audio).pipe(Effect.provide(live))).toEqualTypeOf();
|
|
72
|
+
});
|
|
73
|
+
it("sync `synthesize` requires no marker", () => {
|
|
74
|
+
(0, import_dist.expectTypeOf)(SpeechSynthesizer$1.synthesize({
|
|
75
|
+
text: "hi",
|
|
76
|
+
model: "gemini-2.5-flash-preview-tts",
|
|
77
|
+
voiceId: "Kore"
|
|
78
|
+
}).pipe(Effect.provide(live))).toEqualTypeOf();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
//#endregion
|
|
82
|
+
export {};
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=GeminiSynthesizer.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiSynthesizer.test.mjs","names":["GeminiSynthesizer.layer","GeminiSynthesizer.realizeOutput","SpeechSynthesizer"],"sources":["../src/GeminiSynthesizer.test.ts"],"sourcesContent":["import { Effect, Layer, Redacted, Stream } from \"effect\"\nimport { FetchHttpClient } from \"effect/unstable/http\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport type * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioBlob } from \"@effect-uai/core/Audio\"\nimport * as SpeechSynthesizer from \"@effect-uai/core/SpeechSynthesizer\"\nimport * as GeminiSynthesizer from \"./GeminiSynthesizer.js\"\n\nconst cfg: GeminiSynthesizer.Config = { apiKey: Redacted.make(\"test-key\") }\n// FetchHttpClient is required for `make`, but these tests only exercise\n// the codec and the compile-time / runtime Unsupported branches — no\n// real HTTP call is made.\nconst live = Layer.provide(GeminiSynthesizer.layer(cfg), FetchHttpClient.layer)\n\ndescribe(\"GeminiSynthesizer.realizeOutput\", () => {\n it(\"maps raw to native PCM 24 kHz mono with an identity wrapper\", async () => {\n const [format, wrap] = await Effect.runPromise(GeminiSynthesizer.realizeOutput(\"raw\"))\n expect(format).toEqual({\n container: \"raw\",\n encoding: \"pcm_s16le\",\n sampleRate: 24000,\n channels: 1,\n })\n const pcm = new Uint8Array([1, 2, 3, 4])\n expect(wrap(pcm)).toBe(pcm)\n })\n\n it(\"maps wav to a RIFF/WAVE wrapper that prepends a 44-byte header\", async () => {\n const [format, wrap] = await Effect.runPromise(GeminiSynthesizer.realizeOutput(\"wav\"))\n expect(format.container).toBe(\"wav\")\n const out = wrap(new Uint8Array([0, 1, 2, 3]))\n expect(out.length).toBe(44 + 4)\n const ascii = (start: number, len: number) =>\n String.fromCharCode(...Array.from(out.slice(start, start + len)))\n expect(ascii(0, 4)).toBe(\"RIFF\")\n expect(ascii(8, 4)).toBe(\"WAVE\")\n expect(ascii(12, 4)).toBe(\"fmt \")\n expect(ascii(36, 4)).toBe(\"data\")\n })\n\n it(\"fails Unsupported for mp3 / opus / aac / flac / ogg / webm\", async () => {\n const exit = await Effect.runPromiseExit(GeminiSynthesizer.realizeOutput(\"mp3\"))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"outputFormat\")\n }\n })\n})\n\ndescribe(\"GeminiSynthesizer capability guards (runtime)\", () => {\n it(\"streamSynthesisFrom returns an Unsupported stream\", async () => {\n const program = GeminiSynthesizer.GeminiSynthesizer.use((s) =>\n Stream.runDrain(\n s.streamSynthesisFrom(Stream.fromIterable([\"hi\"]), {\n model: \"gemini-2.5-flash-preview-tts\",\n voiceId: \"Kore\",\n }),\n ),\n )\n const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"streamSynthesisFrom\")\n }\n })\n})\n\ndescribe(\"GeminiSynthesizer Layer (compile-time)\", () => {\n it(\"leaves `TtsIncrementalText` unsatisfied when using `streamSynthesisFrom` against this Layer\", () => {\n const tokens: Stream.Stream<string> = Stream.fromIterable([\"a\"])\n const audio = tokens.pipe(\n SpeechSynthesizer.streamSynthesisFrom({\n model: \"gemini-2.5-flash-preview-tts\",\n voiceId: \"Kore\",\n }),\n )\n const provided = Stream.runDrain(audio).pipe(Effect.provide(live))\n expectTypeOf(provided).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, SpeechSynthesizer.TtsIncrementalText>\n >()\n })\n\n it(\"sync `synthesize` requires no marker\", () => {\n const synth = SpeechSynthesizer.synthesize({\n text: \"hi\",\n model: \"gemini-2.5-flash-preview-tts\",\n voiceId: \"Kore\",\n }).pipe(Effect.provide(live))\n expectTypeOf(synth).toEqualTypeOf<Effect.Effect<AudioBlob, AiError.AiError, never>>()\n })\n})\n"],"mappings":";;;;;;AAQA,MAAM,MAAgC,EAAE,QAAQ,SAAS,KAAK,WAAW,EAAE;AAI3E,MAAM,OAAO,MAAM,QAAQA,MAAwB,IAAI,EAAE,gBAAgB,MAAM;AAE/E,SAAS,yCAAyC;AAChD,IAAG,+DAA+D,YAAY;EAC5E,MAAM,CAAC,QAAQ,QAAQ,MAAM,OAAO,WAAWC,cAAgC,MAAM,CAAC;AACtF,eAAO,OAAO,CAAC,QAAQ;GACrB,WAAW;GACX,UAAU;GACV,YAAY;GACZ,UAAU;GACX,CAAC;EACF,MAAM,MAAM,IAAI,WAAW;GAAC;GAAG;GAAG;GAAG;GAAE,CAAC;AACxC,eAAO,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI;GAC3B;AAEF,IAAG,kEAAkE,YAAY;EAC/E,MAAM,CAAC,QAAQ,QAAQ,MAAM,OAAO,WAAWA,cAAgC,MAAM,CAAC;AACtF,eAAO,OAAO,UAAU,CAAC,KAAK,MAAM;EACpC,MAAM,MAAM,KAAK,IAAI,WAAW;GAAC;GAAG;GAAG;GAAG;GAAE,CAAC,CAAC;AAC9C,eAAO,IAAI,OAAO,CAAC,KAAK,GAAO;EAC/B,MAAM,SAAS,OAAe,QAC5B,OAAO,aAAa,GAAG,MAAM,KAAK,IAAI,MAAM,OAAO,QAAQ,IAAI,CAAC,CAAC;AACnE,eAAO,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,OAAO;AAChC,eAAO,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,OAAO;AAChC,eAAO,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,OAAO;AACjC,eAAO,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,OAAO;GACjC;AAEF,IAAG,8DAA8D,YAAY;EAC3E,MAAM,OAAO,MAAM,OAAO,eAAeA,cAAgC,MAAM,CAAC;AAChF,eAAO,KAAK,KAAK,CAAC,KAAK,UAAU;AACjC,MAAI,KAAK,SAAS,WAAW;AAC3B,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,cAAc;AAC3D,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,eAAe;;GAE9D;EACF;AAEF,SAAS,uDAAuD;AAC9D,IAAG,qDAAqD,YAAY;EAClE,MAAM,UAAA,kBAA8C,KAAK,MACvD,OAAO,SACL,EAAE,oBAAoB,OAAO,aAAa,CAAC,KAAK,CAAC,EAAE;GACjD,OAAO;GACP,SAAS;GACV,CAAC,CACH,CACF;EACD,MAAM,OAAO,MAAM,OAAO,eAAe,QAAQ,KAAK,OAAO,QAAQ,KAAK,CAAC,CAAC;AAC5E,eAAO,KAAK,KAAK,CAAC,KAAK,UAAU;AACjC,MAAI,KAAK,SAAS,WAAW;AAC3B,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,cAAc;AAC3D,gBAAO,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,UAAU,sBAAsB;;GAErE;EACF;AAEF,SAAS,gDAAgD;AACvD,IAAG,qGAAqG;EAEtG,MAAM,QADgC,OAAO,aAAa,CAAC,IAAI,CAC3C,CAAC,KACnBC,oBAAkB,oBAAoB;GACpC,OAAO;GACP,SAAS;GACV,CAAC,CACH;AAED,GAAA,GAAA,YAAA,cADiB,OAAO,SAAS,MAAM,CAAC,KAAK,OAAO,QAAQ,KAAK,CAC5C,CAAC,CAAC,eAEpB;GACH;AAEF,IAAG,8CAA8C;AAM/C,GAAA,GAAA,YAAA,cALcA,oBAAkB,WAAW;GACzC,MAAM;GACN,OAAO;GACP,SAAS;GACV,CAAC,CAAC,KAAK,OAAO,QAAQ,KAAK,CACV,CAAC,CAAC,eAAiE;GACrF;EACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { GeminiSttModel } from "./models.mjs";
|
|
2
|
+
import { Context, Effect, Layer, Redacted } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { CommonTranscribeRequest, Transcriber, TranscriberService } from "@effect-uai/core/Transcriber";
|
|
6
|
+
import { TranscriptResult } from "@effect-uai/core/Transcript";
|
|
7
|
+
|
|
8
|
+
//#region src/GeminiTranscriber.d.ts
|
|
9
|
+
declare namespace GeminiTranscriber_d_exports {
|
|
10
|
+
export { Config, GeminiTranscribeRequest, GeminiTranscriber, GeminiTranscriberService, buildPrompt, layer, make };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Gemini transcription rides on `:generateContent` — the model receives
|
|
14
|
+
* audio plus a textual "transcribe verbatim" prompt and returns plain
|
|
15
|
+
* text. There is no native structured timestamp / diarization channel,
|
|
16
|
+
* so `wordTimestamps` / `diarization` short-circuit with `Unsupported`.
|
|
17
|
+
*/
|
|
18
|
+
type GeminiTranscribeRequest = Omit<CommonTranscribeRequest, "model"> & {
|
|
19
|
+
readonly model: GeminiSttModel;
|
|
20
|
+
};
|
|
21
|
+
type GeminiTranscriberService = {
|
|
22
|
+
readonly transcribe: (r: GeminiTranscribeRequest) => Effect.Effect<TranscriptResult, AiError.AiError>;
|
|
23
|
+
readonly streamTranscriptionFrom: TranscriberService["streamTranscriptionFrom"];
|
|
24
|
+
};
|
|
25
|
+
declare const GeminiTranscriber_base: Context.ServiceClass<GeminiTranscriber, "@betalyra/effect-uai/providers/google/GeminiTranscriber", GeminiTranscriberService>;
|
|
26
|
+
declare class GeminiTranscriber extends GeminiTranscriber_base {}
|
|
27
|
+
type Config = {
|
|
28
|
+
readonly apiKey: Redacted.Redacted;
|
|
29
|
+
readonly baseUrl?: string;
|
|
30
|
+
};
|
|
31
|
+
declare const buildPrompt: (r: GeminiTranscribeRequest) => string;
|
|
32
|
+
declare const make: (cfg: Config) => Effect.Effect<GeminiTranscriberService, never, HttpClient.HttpClient>;
|
|
33
|
+
/**
|
|
34
|
+
* Layer registers both `GeminiTranscriber` and the generic
|
|
35
|
+
* `Transcriber`. Does NOT register `SttStreaming` — Gemini's transcription
|
|
36
|
+
* is sync-only, so `streamTranscriptionFrom` against this Layer alone
|
|
37
|
+
* is a compile-time error.
|
|
38
|
+
*/
|
|
39
|
+
declare const layer: (cfg: Config) => Layer.Layer<GeminiTranscriber | Transcriber, never, HttpClient.HttpClient>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { Config, GeminiTranscribeRequest, GeminiTranscriber, GeminiTranscriberService, buildPrompt, layer, make, GeminiTranscriber_d_exports as t };
|
|
42
|
+
//# sourceMappingURL=GeminiTranscriber.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiTranscriber.d.mts","names":[],"sources":["../src/GeminiTranscriber.ts"],"mappings":";;;;;;;;;;;;;;;;;KAsBY,uBAAA,GAA0B,IAAA,CAAK,uBAAA;EAAA,SAChC,KAAA,EAAO,cAAA;AAAA;AAAA,KAGN,wBAAA;EAAA,SACD,UAAA,GACP,CAAA,EAAG,uBAAA,KACA,MAAA,CAAO,MAAA,CAAO,gBAAA,EAAkB,OAAA,CAAQ,OAAA;EAAA,SACpC,uBAAA,EAAyB,kBAAA;AAAA;AAAA,cACnC,sBAAA;cAEY,iBAAA,SAA0B,sBAAA;AAAA,KAK3B,MAAA;EAAA,SAAoB,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SAAmB,OAAA;AAAA;AAAA,cAqCvD,WAAA,GAAe,CAAA,EAAG,uBAAA;AAAA,cAkGlB,IAAA,GAAQ,GAAA,EAAK,MAAA,KAAM,MAAA,CAAA,MAAA,CAAA,wBAAA,SAAA,UAAA,CAAA,UAAA;;;;;AAnJhC;;cAmKa,KAAA,GAAS,GAAA,EAAK,MAAA,KAAM,KAAA,CAAA,KAAA,CAAA,iBAAA,GAAA,WAAA,SAAA,UAAA,CAAA,UAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { n as __exportAll } from "./chunk-uyGKjUfl.mjs";
|
|
2
|
+
import { audioSourceToInlineData, httpStatusError, transportFailure } from "./geminiSpeechCodec.mjs";
|
|
3
|
+
import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from "effect";
|
|
4
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
6
|
+
import { Transcriber } from "@effect-uai/core/Transcriber";
|
|
7
|
+
//#region src/GeminiTranscriber.ts
|
|
8
|
+
var GeminiTranscriber_exports = /* @__PURE__ */ __exportAll({
|
|
9
|
+
GeminiTranscriber: () => GeminiTranscriber,
|
|
10
|
+
buildPrompt: () => buildPrompt,
|
|
11
|
+
layer: () => layer,
|
|
12
|
+
make: () => make
|
|
13
|
+
});
|
|
14
|
+
var GeminiTranscriber = class extends Context.Service()("@betalyra/effect-uai/providers/google/GeminiTranscriber") {};
|
|
15
|
+
const unsupported = (capability, reason) => new AiError.Unsupported({
|
|
16
|
+
provider: "gemini",
|
|
17
|
+
capability,
|
|
18
|
+
reason
|
|
19
|
+
});
|
|
20
|
+
const ensureSupported = Match.type().pipe(Match.when({ wordTimestamps: true }, () => Effect.fail(unsupported("wordTimestamps", "Gemini's prompt-driven transcription returns plain text — no per-word timing. Use Cloud Speech-to-Text (Chirp 2) for word timestamps."))), Match.when({ diarization: true }, () => Effect.fail(unsupported("diarization", "Gemini's prompt-driven transcription does not surface speaker IDs. Use Cloud Speech-to-Text (Chirp 2) for diarization."))), Match.orElse(() => Effect.void));
|
|
21
|
+
const biasingHint = (r) => {
|
|
22
|
+
if (r.prompt === void 0) return "";
|
|
23
|
+
if (typeof r.prompt === "string") return `\nContext: ${r.prompt}`;
|
|
24
|
+
return r.prompt.terms.length === 0 ? "" : `\nThe following terms / names appear in the audio: ${r.prompt.terms.join(", ")}.`;
|
|
25
|
+
};
|
|
26
|
+
const buildPrompt = (r) => (r.language !== void 0 ? `Transcribe the audio verbatim in ${r.language}. Return only the transcript text, no preamble.` : "Transcribe the audio verbatim. Return only the transcript text, no preamble.") + biasingHint(r);
|
|
27
|
+
const Wire = Schema.Struct({ candidates: Schema.optional(Schema.Array(Schema.Struct({ content: Schema.optional(Schema.Struct({ parts: Schema.optional(Schema.Array(Schema.Struct({ text: Schema.optional(Schema.String) }))) })) }))) });
|
|
28
|
+
const decodeWire = Schema.decodeUnknownEffect(Wire);
|
|
29
|
+
const collectText = (wire) => (wire.candidates ?? []).flatMap((c) => c.content?.parts ?? []).flatMap((p) => p.text !== void 0 ? [p.text] : []).join("\n").trim();
|
|
30
|
+
const baseUrl = (cfg) => cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
31
|
+
const transcribeImpl = (cfg) => (request) => Effect.gen(function* () {
|
|
32
|
+
yield* ensureSupported(request);
|
|
33
|
+
const client = yield* HttpClient.HttpClient;
|
|
34
|
+
const inline = yield* audioSourceToInlineData(request.audio);
|
|
35
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/models/${request.model}:generateContent`).pipe(HttpClientRequest.setHeader("x-goog-api-key", Redacted.value(cfg.apiKey)), HttpClientRequest.bodyJsonUnsafe({ contents: [{ parts: [{ text: buildPrompt(request) }, { inlineData: inline }] }] }));
|
|
36
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
37
|
+
if (response.status >= 400) {
|
|
38
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
39
|
+
return yield* Effect.fail(httpStatusError(response.status, text));
|
|
40
|
+
}
|
|
41
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure));
|
|
42
|
+
return {
|
|
43
|
+
text: yield* decodeWire(json).pipe(Effect.map(collectText), Effect.mapError((cause) => new AiError.GenerationFailed({
|
|
44
|
+
provider: "gemini",
|
|
45
|
+
raw: cause
|
|
46
|
+
})), Effect.flatMap((t) => t.length === 0 ? Effect.fail(new AiError.GenerationFailed({
|
|
47
|
+
provider: "gemini",
|
|
48
|
+
raw: {
|
|
49
|
+
message: "Gemini transcription response had no text part.",
|
|
50
|
+
hint: "Likely a prompt-safety rejection or the model returned only metadata.",
|
|
51
|
+
response: json
|
|
52
|
+
}
|
|
53
|
+
})) : Effect.succeed(t))),
|
|
54
|
+
...request.language !== void 0 && { languageCode: request.language },
|
|
55
|
+
raw: json
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const unsupportedStreamFrom = () => Stream.fail(new AiError.Unsupported({
|
|
59
|
+
provider: "gemini",
|
|
60
|
+
capability: "streamTranscriptionFrom",
|
|
61
|
+
reason: "Gemini transcription via generateContent is sync-only. Use Cloud Speech-to-Text (StreamingRecognize) for live transcription."
|
|
62
|
+
}));
|
|
63
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
|
|
64
|
+
transcribe: (r) => transcribeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
65
|
+
streamTranscriptionFrom: unsupportedStreamFrom
|
|
66
|
+
}));
|
|
67
|
+
/**
|
|
68
|
+
* Layer registers both `GeminiTranscriber` and the generic
|
|
69
|
+
* `Transcriber`. Does NOT register `SttStreaming` — Gemini's transcription
|
|
70
|
+
* is sync-only, so `streamTranscriptionFrom` against this Layer alone
|
|
71
|
+
* is a compile-time error.
|
|
72
|
+
*/
|
|
73
|
+
const layer = (cfg) => Layer.merge(Layer.effect(GeminiTranscriber, make(cfg)), Layer.effect(Transcriber, Effect.map(make(cfg), (s) => ({
|
|
74
|
+
transcribe: (req) => s.transcribe(req),
|
|
75
|
+
streamTranscriptionFrom: s.streamTranscriptionFrom
|
|
76
|
+
}))));
|
|
77
|
+
//#endregion
|
|
78
|
+
export { GeminiTranscriber, buildPrompt, layer, make, GeminiTranscriber_exports as t };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=GeminiTranscriber.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiTranscriber.mjs","names":[],"sources":["../src/GeminiTranscriber.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Schema, Stream } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport type { TranscriptResult } from \"@effect-uai/core/Transcript\"\nimport {\n type CommonTranscribeRequest,\n Transcriber,\n type TranscriberService,\n} from \"@effect-uai/core/Transcriber\"\nimport { audioSourceToInlineData, httpStatusError, transportFailure } from \"./geminiSpeechCodec.js\"\nimport type { GeminiSttModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Gemini transcription rides on `:generateContent` — the model receives\n * audio plus a textual \"transcribe verbatim\" prompt and returns plain\n * text. There is no native structured timestamp / diarization channel,\n * so `wordTimestamps` / `diarization` short-circuit with `Unsupported`.\n */\nexport type GeminiTranscribeRequest = Omit<CommonTranscribeRequest, \"model\"> & {\n readonly model: GeminiSttModel\n}\n\nexport type GeminiTranscriberService = {\n readonly transcribe: (\n r: GeminiTranscribeRequest,\n ) => Effect.Effect<TranscriptResult, AiError.AiError>\n readonly streamTranscriptionFrom: TranscriberService[\"streamTranscriptionFrom\"]\n}\n\nexport class GeminiTranscriber extends Context.Service<\n GeminiTranscriber,\n GeminiTranscriberService\n>()(\"@betalyra/effect-uai/providers/google/GeminiTranscriber\") {}\n\nexport type Config = { readonly apiKey: Redacted.Redacted; readonly baseUrl?: string }\n\n// ---------------------------------------------------------------------------\n// Prompt builder + capability guards\n// ---------------------------------------------------------------------------\n\nconst unsupported = (capability: string, reason: string) =>\n new AiError.Unsupported({ provider: \"gemini\", capability, reason })\n\nconst ensureSupported = Match.type<GeminiTranscribeRequest>().pipe(\n Match.when({ wordTimestamps: true }, () =>\n Effect.fail(\n unsupported(\n \"wordTimestamps\",\n \"Gemini's prompt-driven transcription returns plain text — no per-word timing. Use Cloud Speech-to-Text (Chirp 2) for word timestamps.\",\n ),\n ),\n ),\n Match.when({ diarization: true }, () =>\n Effect.fail(\n unsupported(\n \"diarization\",\n \"Gemini's prompt-driven transcription does not surface speaker IDs. Use Cloud Speech-to-Text (Chirp 2) for diarization.\",\n ),\n ),\n ),\n Match.orElse(() => Effect.void),\n)\n\nconst biasingHint = (r: GeminiTranscribeRequest) => {\n if (r.prompt === undefined) return \"\"\n if (typeof r.prompt === \"string\") return `\\nContext: ${r.prompt}`\n return r.prompt.terms.length === 0\n ? \"\"\n : `\\nThe following terms / names appear in the audio: ${r.prompt.terms.join(\", \")}.`\n}\n\nexport const buildPrompt = (r: GeminiTranscribeRequest) =>\n (r.language !== undefined\n ? `Transcribe the audio verbatim in ${r.language}. Return only the transcript text, no preamble.`\n : \"Transcribe the audio verbatim. Return only the transcript text, no preamble.\") +\n biasingHint(r)\n\n// ---------------------------------------------------------------------------\n// Wire\n// ---------------------------------------------------------------------------\n\nconst Wire = Schema.Struct({\n candidates: Schema.optional(\n Schema.Array(\n Schema.Struct({\n content: Schema.optional(\n Schema.Struct({\n parts: Schema.optional(\n Schema.Array(Schema.Struct({ text: Schema.optional(Schema.String) })),\n ),\n }),\n ),\n }),\n ),\n ),\n})\nconst decodeWire = Schema.decodeUnknownEffect(Wire)\n\nconst collectText = (wire: typeof Wire.Type) =>\n (wire.candidates ?? [])\n .flatMap((c) => c.content?.parts ?? [])\n .flatMap((p) => (p.text !== undefined ? [p.text] : []))\n .join(\"\\n\")\n .trim()\n\n// ---------------------------------------------------------------------------\n// Service implementation\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config) => cfg.baseUrl ?? \"https://generativelanguage.googleapis.com/v1beta\"\n\nconst transcribeImpl = (cfg: Config) => (request: GeminiTranscribeRequest) =>\n Effect.gen(function* () {\n yield* ensureSupported(request)\n const client = yield* HttpClient.HttpClient\n const inline = yield* audioSourceToInlineData(request.audio)\n const httpRequest = HttpClientRequest.post(\n `${baseUrl(cfg)}/models/${request.model}:generateContent`,\n ).pipe(\n HttpClientRequest.setHeader(\"x-goog-api-key\", Redacted.value(cfg.apiKey)),\n HttpClientRequest.bodyJsonUnsafe({\n contents: [{ parts: [{ text: buildPrompt(request) }, { inlineData: inline }] }],\n }),\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 text = yield* decodeWire(json).pipe(\n Effect.map(collectText),\n Effect.mapError((cause) => new AiError.GenerationFailed({ provider: \"gemini\", raw: cause })),\n Effect.flatMap((t) =>\n t.length === 0\n ? Effect.fail(\n new AiError.GenerationFailed({\n provider: \"gemini\",\n raw: {\n message: \"Gemini transcription response had no text part.\",\n hint: \"Likely a prompt-safety rejection or the model returned only metadata.\",\n response: json,\n },\n }),\n )\n : Effect.succeed(t),\n ),\n )\n return {\n text,\n ...(request.language !== undefined && { languageCode: request.language }),\n raw: json,\n } satisfies TranscriptResult\n })\n\nconst unsupportedStreamFrom: TranscriberService[\"streamTranscriptionFrom\"] = () =>\n Stream.fail(\n new AiError.Unsupported({\n provider: \"gemini\",\n capability: \"streamTranscriptionFrom\",\n reason:\n \"Gemini transcription via generateContent is sync-only. Use Cloud Speech-to-Text (StreamingRecognize) for live transcription.\",\n }),\n )\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nexport const make = (cfg: Config) =>\n Effect.map(\n HttpClient.HttpClient.asEffect(),\n (client): GeminiTranscriberService => ({\n transcribe: (r) =>\n transcribeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n streamTranscriptionFrom: unsupportedStreamFrom,\n }),\n )\n\n/**\n * Layer registers both `GeminiTranscriber` and the generic\n * `Transcriber`. Does NOT register `SttStreaming` — Gemini's transcription\n * is sync-only, so `streamTranscriptionFrom` against this Layer alone\n * is a compile-time error.\n */\nexport const layer = (cfg: Config) =>\n Layer.merge(\n Layer.effect(GeminiTranscriber, 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 GeminiTranscribeRequest),\n streamTranscriptionFrom: s.streamTranscriptionFrom,\n }),\n ),\n ),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { i as it, n as globalExpect, r as describe, t as import_dist } from "./dist-DMsg8YLb.mjs";
|
|
2
|
+
import { GeminiTranscriber, buildPrompt, layer } from "./GeminiTranscriber.mjs";
|
|
3
|
+
import { Effect, Layer, Redacted, Stream } from "effect";
|
|
4
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
5
|
+
import * as Transcriber$1 from "@effect-uai/core/Transcriber";
|
|
6
|
+
//#region src/GeminiTranscriber.test.ts
|
|
7
|
+
const cfg = { apiKey: Redacted.make("test-key") };
|
|
8
|
+
const live = Layer.provide(layer(cfg), FetchHttpClient.layer);
|
|
9
|
+
const dummyAudio = {
|
|
10
|
+
_tag: "bytes",
|
|
11
|
+
bytes: new Uint8Array([
|
|
12
|
+
0,
|
|
13
|
+
1,
|
|
14
|
+
2,
|
|
15
|
+
3
|
|
16
|
+
]),
|
|
17
|
+
mimeType: "audio/wav"
|
|
18
|
+
};
|
|
19
|
+
describe("GeminiTranscriber.buildPrompt", () => {
|
|
20
|
+
it("uses the language-agnostic prompt when no language is set", () => {
|
|
21
|
+
globalExpect(buildPrompt({
|
|
22
|
+
audio: dummyAudio,
|
|
23
|
+
model: "gemini-2.5-flash"
|
|
24
|
+
})).toContain("Transcribe the audio verbatim. Return only the transcript text");
|
|
25
|
+
});
|
|
26
|
+
it("includes the language when set", () => {
|
|
27
|
+
globalExpect(buildPrompt({
|
|
28
|
+
audio: dummyAudio,
|
|
29
|
+
model: "gemini-2.5-flash",
|
|
30
|
+
language: "pt-BR"
|
|
31
|
+
})).toContain("in pt-BR");
|
|
32
|
+
});
|
|
33
|
+
it("appends a free-text prompt as context", () => {
|
|
34
|
+
globalExpect(buildPrompt({
|
|
35
|
+
audio: dummyAudio,
|
|
36
|
+
model: "gemini-2.5-flash",
|
|
37
|
+
prompt: "Engineering meeting; expect technical jargon."
|
|
38
|
+
})).toContain("Context: Engineering meeting");
|
|
39
|
+
});
|
|
40
|
+
it("appends a terms list as biasing vocabulary", () => {
|
|
41
|
+
globalExpect(buildPrompt({
|
|
42
|
+
audio: dummyAudio,
|
|
43
|
+
model: "gemini-2.5-flash",
|
|
44
|
+
prompt: { terms: [
|
|
45
|
+
"Effect",
|
|
46
|
+
"Lyria",
|
|
47
|
+
"MusicGenerator"
|
|
48
|
+
] }
|
|
49
|
+
})).toContain("Effect, Lyria, MusicGenerator");
|
|
50
|
+
});
|
|
51
|
+
it("omits the biasing block when the terms list is empty", () => {
|
|
52
|
+
globalExpect(buildPrompt({
|
|
53
|
+
audio: dummyAudio,
|
|
54
|
+
model: "gemini-2.5-flash",
|
|
55
|
+
prompt: { terms: [] }
|
|
56
|
+
})).not.toContain("terms / names");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("GeminiTranscriber capability guards (runtime)", () => {
|
|
60
|
+
it("fails Unsupported for wordTimestamps", async () => {
|
|
61
|
+
const program = GeminiTranscriber.use((s) => s.transcribe({
|
|
62
|
+
audio: dummyAudio,
|
|
63
|
+
model: "gemini-2.5-flash",
|
|
64
|
+
wordTimestamps: true
|
|
65
|
+
}));
|
|
66
|
+
const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)));
|
|
67
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
68
|
+
if (exit._tag === "Failure") {
|
|
69
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
|
|
70
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("wordTimestamps");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
it("fails Unsupported for diarization", async () => {
|
|
74
|
+
const program = GeminiTranscriber.use((s) => s.transcribe({
|
|
75
|
+
audio: dummyAudio,
|
|
76
|
+
model: "gemini-2.5-flash",
|
|
77
|
+
diarization: true
|
|
78
|
+
}));
|
|
79
|
+
const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)));
|
|
80
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
81
|
+
if (exit._tag === "Failure") {
|
|
82
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
|
|
83
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("diarization");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("streamTranscriptionFrom returns an Unsupported stream", async () => {
|
|
87
|
+
const program = GeminiTranscriber.use((s) => Stream.runDrain(s.streamTranscriptionFrom(Stream.fromIterable([new Uint8Array([0])]), {
|
|
88
|
+
model: "gemini-2.5-flash",
|
|
89
|
+
inputFormat: {
|
|
90
|
+
container: "raw",
|
|
91
|
+
encoding: "pcm_s16le",
|
|
92
|
+
sampleRate: 16e3
|
|
93
|
+
}
|
|
94
|
+
})));
|
|
95
|
+
const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)));
|
|
96
|
+
globalExpect(exit._tag).toBe("Failure");
|
|
97
|
+
if (exit._tag === "Failure") {
|
|
98
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("Unsupported");
|
|
99
|
+
globalExpect(JSON.stringify(exit.cause)).toContain("streamTranscriptionFrom");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("GeminiTranscriber Layer (compile-time)", () => {
|
|
104
|
+
it("leaves `SttStreaming` unsatisfied when using `streamTranscriptionFrom` against this Layer", () => {
|
|
105
|
+
const events = Stream.fromIterable([new Uint8Array([0])]).pipe(Transcriber$1.streamTranscriptionFrom({
|
|
106
|
+
model: "gemini-2.5-flash",
|
|
107
|
+
inputFormat: {
|
|
108
|
+
container: "raw",
|
|
109
|
+
encoding: "pcm_s16le",
|
|
110
|
+
sampleRate: 16e3
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
(0, import_dist.expectTypeOf)(Stream.runDrain(events).pipe(Effect.provide(live))).toEqualTypeOf();
|
|
114
|
+
});
|
|
115
|
+
it("sync `transcribe` requires no marker", () => {
|
|
116
|
+
(0, import_dist.expectTypeOf)(Transcriber$1.transcribe({
|
|
117
|
+
audio: dummyAudio,
|
|
118
|
+
model: "gemini-2.5-flash"
|
|
119
|
+
}).pipe(Effect.provide(live))).toEqualTypeOf();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
//#endregion
|
|
123
|
+
export {};
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=GeminiTranscriber.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GeminiTranscriber.test.mjs","names":["GeminiTranscriber.layer","GeminiTranscriber.buildPrompt","Transcriber"],"sources":["../src/GeminiTranscriber.test.ts"],"sourcesContent":["import { Effect, Layer, Redacted, Stream } from \"effect\"\nimport { FetchHttpClient } from \"effect/unstable/http\"\nimport { describe, expect, expectTypeOf, it } from \"vitest\"\nimport type * as AiError from \"@effect-uai/core/AiError\"\nimport type { AudioSource } from \"@effect-uai/core/Audio\"\nimport type { TranscriptResult } from \"@effect-uai/core/Transcript\"\nimport * as Transcriber from \"@effect-uai/core/Transcriber\"\nimport * as GeminiTranscriber from \"./GeminiTranscriber.js\"\n\nconst cfg: GeminiTranscriber.Config = { apiKey: Redacted.make(\"test-key\") }\nconst live = Layer.provide(GeminiTranscriber.layer(cfg), FetchHttpClient.layer)\n\nconst dummyAudio: AudioSource = {\n _tag: \"bytes\",\n bytes: new Uint8Array([0, 1, 2, 3]),\n mimeType: \"audio/wav\",\n}\n\ndescribe(\"GeminiTranscriber.buildPrompt\", () => {\n it(\"uses the language-agnostic prompt when no language is set\", () => {\n expect(\n GeminiTranscriber.buildPrompt({ audio: dummyAudio, model: \"gemini-2.5-flash\" }),\n ).toContain(\"Transcribe the audio verbatim. Return only the transcript text\")\n })\n\n it(\"includes the language when set\", () => {\n expect(\n GeminiTranscriber.buildPrompt({\n audio: dummyAudio,\n model: \"gemini-2.5-flash\",\n language: \"pt-BR\",\n }),\n ).toContain(\"in pt-BR\")\n })\n\n it(\"appends a free-text prompt as context\", () => {\n const out = GeminiTranscriber.buildPrompt({\n audio: dummyAudio,\n model: \"gemini-2.5-flash\",\n prompt: \"Engineering meeting; expect technical jargon.\",\n })\n expect(out).toContain(\"Context: Engineering meeting\")\n })\n\n it(\"appends a terms list as biasing vocabulary\", () => {\n const out = GeminiTranscriber.buildPrompt({\n audio: dummyAudio,\n model: \"gemini-2.5-flash\",\n prompt: { terms: [\"Effect\", \"Lyria\", \"MusicGenerator\"] },\n })\n expect(out).toContain(\"Effect, Lyria, MusicGenerator\")\n })\n\n it(\"omits the biasing block when the terms list is empty\", () => {\n const out = GeminiTranscriber.buildPrompt({\n audio: dummyAudio,\n model: \"gemini-2.5-flash\",\n prompt: { terms: [] },\n })\n expect(out).not.toContain(\"terms / names\")\n })\n})\n\ndescribe(\"GeminiTranscriber capability guards (runtime)\", () => {\n it(\"fails Unsupported for wordTimestamps\", async () => {\n const program = GeminiTranscriber.GeminiTranscriber.use((s) =>\n s.transcribe({\n audio: dummyAudio,\n model: \"gemini-2.5-flash\",\n wordTimestamps: true,\n }),\n )\n const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"wordTimestamps\")\n }\n })\n\n it(\"fails Unsupported for diarization\", async () => {\n const program = GeminiTranscriber.GeminiTranscriber.use((s) =>\n s.transcribe({\n audio: dummyAudio,\n model: \"gemini-2.5-flash\",\n diarization: true,\n }),\n )\n const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"diarization\")\n }\n })\n\n it(\"streamTranscriptionFrom returns an Unsupported stream\", async () => {\n const program = GeminiTranscriber.GeminiTranscriber.use((s) =>\n Stream.runDrain(\n s.streamTranscriptionFrom(Stream.fromIterable([new Uint8Array([0])]), {\n model: \"gemini-2.5-flash\",\n inputFormat: { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: 16000 },\n }),\n ),\n )\n const exit = await Effect.runPromiseExit(program.pipe(Effect.provide(live)))\n expect(exit._tag).toBe(\"Failure\")\n if (exit._tag === \"Failure\") {\n expect(JSON.stringify(exit.cause)).toContain(\"Unsupported\")\n expect(JSON.stringify(exit.cause)).toContain(\"streamTranscriptionFrom\")\n }\n })\n})\n\ndescribe(\"GeminiTranscriber Layer (compile-time)\", () => {\n it(\"leaves `SttStreaming` unsatisfied when using `streamTranscriptionFrom` against this Layer\", () => {\n const audio: Stream.Stream<Uint8Array> = Stream.fromIterable([new Uint8Array([0])])\n const events = audio.pipe(\n Transcriber.streamTranscriptionFrom({\n model: \"gemini-2.5-flash\",\n inputFormat: { container: \"raw\", encoding: \"pcm_s16le\", sampleRate: 16000 },\n }),\n )\n const provided = Stream.runDrain(events).pipe(Effect.provide(live))\n expectTypeOf(provided).toEqualTypeOf<\n Effect.Effect<void, AiError.AiError, Transcriber.SttStreaming>\n >()\n })\n\n it(\"sync `transcribe` requires no marker\", () => {\n const t = Transcriber.transcribe({ audio: dummyAudio, model: \"gemini-2.5-flash\" }).pipe(\n Effect.provide(live),\n )\n expectTypeOf(t).toEqualTypeOf<Effect.Effect<TranscriptResult, AiError.AiError, never>>()\n })\n})\n"],"mappings":"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"}
|