@effect-uai/google 0.6.0 → 0.8.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.
Files changed (44) hide show
  1. package/dist/Gemini.d.mts.map +1 -1
  2. package/dist/Gemini.mjs +7 -2
  3. package/dist/Gemini.mjs.map +1 -1
  4. package/dist/GeminiEmbedding.d.mts.map +1 -1
  5. package/dist/GeminiEmbedding.mjs +3 -3
  6. package/dist/GeminiEmbedding.mjs.map +1 -1
  7. package/dist/GeminiSynthesizer.d.mts.map +1 -1
  8. package/dist/GeminiSynthesizer.mjs +22 -4
  9. package/dist/GeminiSynthesizer.mjs.map +1 -1
  10. package/dist/GeminiSynthesizer.test.mjs +14 -48
  11. package/dist/GeminiSynthesizer.test.mjs.map +1 -1
  12. package/dist/LyriaGenerator.d.mts +18 -18
  13. package/dist/LyriaGenerator.d.mts.map +1 -1
  14. package/dist/LyriaGenerator.mjs +59 -42
  15. package/dist/LyriaGenerator.mjs.map +1 -1
  16. package/dist/LyriaGenerator.test.mjs +26 -72
  17. package/dist/LyriaGenerator.test.mjs.map +1 -1
  18. package/dist/codec.d.mts +11 -3
  19. package/dist/codec.d.mts.map +1 -1
  20. package/dist/codec.mjs +20 -3
  21. package/dist/codec.mjs.map +1 -1
  22. package/dist/index.d.mts +2 -3
  23. package/dist/index.mjs +1 -2
  24. package/dist/models.d.mts +1 -10
  25. package/dist/models.d.mts.map +1 -1
  26. package/package.json +2 -6
  27. package/src/Gemini.ts +15 -1
  28. package/src/GeminiEmbedding.ts +14 -9
  29. package/src/GeminiSynthesizer.test.ts +13 -67
  30. package/src/GeminiSynthesizer.ts +30 -4
  31. package/src/LyriaGenerator.test.ts +34 -78
  32. package/src/LyriaGenerator.ts +92 -73
  33. package/src/codec.ts +31 -1
  34. package/src/index.ts +0 -1
  35. package/src/models.ts +0 -17
  36. package/dist/GeminiTranscriber.d.mts +0 -42
  37. package/dist/GeminiTranscriber.d.mts.map +0 -1
  38. package/dist/GeminiTranscriber.mjs +0 -80
  39. package/dist/GeminiTranscriber.mjs.map +0 -1
  40. package/dist/GeminiTranscriber.test.d.mts +0 -1
  41. package/dist/GeminiTranscriber.test.mjs +0 -125
  42. package/dist/GeminiTranscriber.test.mjs.map +0 -1
  43. package/src/GeminiTranscriber.test.ts +0 -136
  44. package/src/GeminiTranscriber.ts +0 -204
@@ -1,11 +1,10 @@
1
1
  import {
2
- Array as Arr,
3
2
  Context,
3
+ Duration,
4
4
  Effect,
5
5
  Encoding,
6
6
  Layer,
7
7
  Match,
8
- Option,
9
8
  Redacted,
10
9
  Result,
11
10
  Schema,
@@ -14,13 +13,16 @@ import {
14
13
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
15
14
  import * as AiError from "@effect-uai/core/AiError"
16
15
  import type { AudioChunk, AudioFormat } from "@effect-uai/core/Audio"
16
+ import { warnDroppedWhen } from "@effect-uai/core/Capabilities"
17
17
  import type {
18
18
  CommonGenerateMusicRequest,
19
19
  CommonStreamGenerateMusicRequest,
20
+ GenerateResult,
20
21
  MusicResult,
21
22
  MusicSessionInput,
22
- WeightedPrompt,
23
+ MusicStreamEvent,
23
24
  } from "@effect-uai/core/Music"
25
+ import { singleVariant } from "@effect-uai/core/Music"
24
26
  import { MusicGenerator, type MusicGeneratorService } from "@effect-uai/core/MusicGenerator"
25
27
  import type { LyriaModel } from "./models.js"
26
28
 
@@ -29,25 +31,34 @@ import type { LyriaModel } from "./models.js"
29
31
  // ---------------------------------------------------------------------------
30
32
 
31
33
  /**
32
- * Lyria-typed generate request. Narrows `model` to `LyriaModel`. The
33
- * Lyria sync API (`generateContent`) accepts only `audio/mp3` and
34
- * `audio/wav` output; `bpm`, `scale`, `instrumental` etc. on the
35
- * `CommonGenerateMusicRequest` are flattened into the prompt text
36
- * because the public API does not expose them as structured fields.
34
+ * Lyria-typed generate request. Narrows `model` to `LyriaModel`.
35
+ *
36
+ * Lyria 3 sync `generateContent` has no structured wire fields for
37
+ * `lyrics` / `duration` / `seed` / `instrumental` / `bpm` / `scale` /
38
+ * etc. Those fields on the Common request are logged as
39
+ * dropped-bucket-2 hints; the adapter never rewrites or augments the
40
+ * caller's prompt on their behalf. If you want lyrics or vocal
41
+ * suppression, include them in your `prompt` text yourself.
42
+ *
43
+ * Output format: only `audio/mp3` and `audio/wav` are accepted; other
44
+ * containers fail `Unsupported`. The Clip model variant is fixed at
45
+ * `audio/mp3`.
37
46
  */
38
47
  export type LyriaGenerateRequest = Omit<CommonGenerateMusicRequest, "model"> & {
39
48
  readonly model: LyriaModel
40
49
  }
41
50
 
42
51
  export type LyriaGeneratorService = {
43
- readonly generate: (request: LyriaGenerateRequest) => Effect.Effect<MusicResult, AiError.AiError>
52
+ readonly generate: (
53
+ request: LyriaGenerateRequest,
54
+ ) => Effect.Effect<GenerateResult, AiError.AiError>
44
55
  readonly streamGeneration: (
45
56
  request: LyriaGenerateRequest,
46
57
  ) => Stream.Stream<AudioChunk, AiError.AiError>
47
58
  readonly streamGenerationFrom: <E, R>(
48
59
  input: Stream.Stream<MusicSessionInput, E, R>,
49
60
  request: CommonStreamGenerateMusicRequest,
50
- ) => Stream.Stream<AudioChunk, AiError.AiError | E, R>
61
+ ) => Stream.Stream<MusicStreamEvent, AiError.AiError | E, R>
51
62
  }
52
63
 
53
64
  export class LyriaGenerator extends Context.Service<LyriaGenerator, LyriaGeneratorService>()(
@@ -60,7 +71,7 @@ export type Config = {
60
71
  }
61
72
 
62
73
  // ---------------------------------------------------------------------------
63
- // Codec — request → prompt text + response_format
74
+ // Codec: AudioFormat → Lyria mimeType
64
75
  // ---------------------------------------------------------------------------
65
76
 
66
77
  export type LyriaResponseMimeType = "audio/mp3" | "audio/wav"
@@ -85,10 +96,10 @@ export const containerToMimeType: (
85
96
  )
86
97
 
87
98
  /**
88
- * Is this a Lyria 3 Clip model variant? Clip is fixed at 30 s MP3 — it
99
+ * Is this a Lyria 3 Clip model variant? Clip is fixed at 30 s MP3, it
89
100
  * does not accept a `responseFormat` on `generationConfig` (the live
90
- * API rejects unknown fields, despite what the docs imply). Pro
91
- * accepts the documented `responseFormat` knob.
101
+ * API rejects unknown fields). Pro accepts the documented
102
+ * `responseFormat` knob.
92
103
  */
93
104
  const isClipModel = (model: string): boolean => model.includes("clip")
94
105
 
@@ -110,38 +121,51 @@ export const realizedFormat: (mime: LyriaResponseMimeType) => AudioFormat =
110
121
  Match.exhaustive,
111
122
  )
112
123
 
124
+ // ---------------------------------------------------------------------------
125
+ // Bucket-2 warn-and-drop for Common fields Lyria 3 can't honor
126
+ // ---------------------------------------------------------------------------
127
+
113
128
  /**
114
- * Collapse `prompts | WeightedPrompt[]` into a single prompt string and
115
- * splice in non-structural hints (`lyrics`, `bpm`, `scale`,
116
- * `instrumental`, `durationSeconds`). The Lyria 3 sync API only takes
117
- * one text part — every condition lives inline.
129
+ * Warn (don't fail) when the caller passed Common fields Lyria 3 has
130
+ * no wire field for. Bucket-2 per capabilities policy: provider has
131
+ * no structured interpretation; the output is still valid music, just
132
+ * less aligned with the caller's hint. We never embed these into the
133
+ * prompt on the caller's behalf, prompt construction is the
134
+ * developer's job.
118
135
  */
119
- export const buildPrompt = (request: CommonGenerateMusicRequest): string => {
120
- const promptText = Match.value(request.prompts).pipe(
121
- Match.when(Match.string, (s) => s),
122
- Match.orElse((ws: ReadonlyArray<WeightedPrompt>) =>
123
- Arr.map(ws, (w) =>
124
- w.weight !== undefined && w.weight !== 1 ? `${w.text} (weight ${w.weight})` : w.text,
125
- ).join(". "),
126
- ),
136
+ const warnDroppedHints = (request: LyriaGenerateRequest): Effect.Effect<void> =>
137
+ Effect.all(
138
+ [
139
+ warnDroppedWhen(request.lyrics, {
140
+ provider: "lyria",
141
+ capability: "lyrics",
142
+ field: "lyrics",
143
+ reason:
144
+ "Lyria 3 sync has no `lyrics` wire field. If you want vocals to follow specific lyrics, embed them in your prompt yourself (e.g. with `[Verse]` / `[Chorus]` tags).",
145
+ }),
146
+ warnDroppedWhen(
147
+ request.duration !== undefined ? Duration.toMillis(request.duration) : undefined,
148
+ {
149
+ provider: "lyria",
150
+ capability: "duration",
151
+ field: "duration",
152
+ reason:
153
+ "Lyria 3 clip is fixed at 30 s; pro derives duration from prompt content. There is no wire field for duration. If you want to steer length, mention it in your prompt.",
154
+ },
155
+ ),
156
+ warnDroppedWhen(request.seed, {
157
+ provider: "lyria",
158
+ capability: "seed",
159
+ field: "seed",
160
+ reason:
161
+ "Lyria 3 (Gemini surface) does not expose a seed parameter. Output is non-deterministic across identical prompts. Use Lyria 2 (Vertex) for seeded generation.",
162
+ }),
163
+ ],
164
+ { discard: true },
127
165
  )
128
- const hints = Arr.getSomes([
129
- request.instrumental === true ? Option.some("Instrumental only — no vocals.") : Option.none(),
130
- request.bpm !== undefined ? Option.some(`BPM: ${request.bpm}.`) : Option.none(),
131
- request.scale !== undefined ? Option.some(`Key/scale: ${request.scale}.`) : Option.none(),
132
- request.durationSeconds !== undefined
133
- ? Option.some(`Target duration: ${request.durationSeconds}s.`)
134
- : Option.none(),
135
- ])
136
- const lyricsBlock =
137
- request.lyrics !== undefined && request.instrumental !== true
138
- ? `\n\nLyrics:\n${request.lyrics}`
139
- : ""
140
- return [promptText, ...hints].join(" ") + lyricsBlock
141
- }
142
166
 
143
167
  // ---------------------------------------------------------------------------
144
- // Wire response schema — just enough to find audio + optional lyrics text
168
+ // Wire response schema
145
169
  // ---------------------------------------------------------------------------
146
170
 
147
171
  const InlineData = Schema.Struct({
@@ -245,26 +269,23 @@ const generateImpl =
245
269
  (cfg: Config) =>
246
270
  (
247
271
  request: LyriaGenerateRequest,
248
- ): Effect.Effect<MusicResult, AiError.AiError, HttpClient.HttpClient> =>
272
+ ): Effect.Effect<GenerateResult, AiError.AiError, HttpClient.HttpClient> =>
249
273
  Effect.gen(function* () {
250
274
  const client = yield* HttpClient.HttpClient
251
275
  const requestedContainer = (request.outputFormat?.container ??
252
276
  "mp3") satisfies AudioFormat["container"]
253
- const mimeType = yield* containerToMimeType(requestedContainer)
254
- // Clip is fixed 30 s mp3 — reject wav up front rather than sending
255
- // a `responseFormat` that the live API ignores or rejects.
256
- if (isClipModel(request.model) && mimeType === "audio/wav") {
257
- return yield* new AiError.Unsupported({
258
- provider: "lyria",
259
- capability: "outputFormat",
260
- reason: `lyria-3-clip-preview is fixed at audio/mp3. Use lyria-3-pro-preview for audio/wav output.`,
261
- })
262
- }
263
- const promptText = buildPrompt(request)
277
+ const requestedMime = yield* containerToMimeType(requestedContainer)
278
+ // lyria-3-clip-preview has no `responseFormat` wire field and always
279
+ // emits mp3, so there is no format to send and no 400 to surface. Rather
280
+ // than keep a per-model table that rejects `wav`, report the format the
281
+ // model actually produces (mp3) so the result is never mislabeled.
282
+ // lyria-3-pro honours the requested container.
283
+ const mimeType = isClipModel(request.model) ? ("audio/mp3" as const) : requestedMime
284
+ yield* warnDroppedHints(request)
264
285
  const body = isClipModel(request.model)
265
- ? { contents: [{ parts: [{ text: promptText }] }] }
286
+ ? { contents: [{ parts: [{ text: request.prompt }] }] }
266
287
  : {
267
- contents: [{ parts: [{ text: promptText }] }],
288
+ contents: [{ parts: [{ text: request.prompt }] }],
268
289
  generationConfig: {
269
290
  responseModalities: ["AUDIO", "TEXT"],
270
291
  responseFormat: { audio: { mimeType } },
@@ -290,10 +311,6 @@ const generateImpl =
290
311
  )
291
312
  const { inline, text } = extractParts(wire)
292
313
  if (inline === undefined) {
293
- // No audio came back. Most common cause: Lyria filtered the prompt
294
- // (named-artist references, copyrighted lyrics, etc.). Surface the
295
- // full response body — text parts, finish reasons, prompt-feedback
296
- // blocks — so the caller can see why.
297
314
  return yield* new AiError.GenerationFailed({
298
315
  provider: "lyria",
299
316
  raw: {
@@ -305,12 +322,13 @@ const generateImpl =
305
322
  })
306
323
  }
307
324
  const bytes = yield* decodeBase64ToBytes(inline.data)
308
- return {
309
- format: realizedFormat(mimeType),
310
- bytes,
325
+ const result: MusicResult = {
326
+ audio: { format: realizedFormat(mimeType), bytes },
327
+ provider: "lyria",
311
328
  ...(text !== undefined && { lyrics: text }),
312
- watermark: { kind: "synthid" },
313
- } satisfies MusicResult
329
+ watermark: "synthid",
330
+ }
331
+ return singleVariant(result)
314
332
  })
315
333
 
316
334
  const streamGenerationImpl =
@@ -320,22 +338,22 @@ const streamGenerationImpl =
320
338
  ): Stream.Stream<AudioChunk, AiError.AiError, HttpClient.HttpClient> =>
321
339
  Stream.unwrap(
322
340
  Effect.map(generateImpl(cfg)(request), (result) =>
323
- Stream.succeed<AudioChunk>({ bytes: result.bytes }),
341
+ Stream.succeed<AudioChunk>({ bytes: result.primary.audio.bytes }),
324
342
  ),
325
343
  )
326
344
 
327
345
  /**
328
- * Lyria 3 sync has no bidirectional session at the wire level — that's
329
- * Lyria RealTime (Phase 1b, separate Layer). The provider Layer here
330
- * also does NOT register `MusicInteractiveSession`, so callers using
346
+ * Lyria 3 sync has no bidirectional session at the wire level, that's
347
+ * Lyria RealTime (separate Layer). The provider Layer here also does
348
+ * NOT register `MusicInteractiveSession`, so callers using
331
349
  * `MusicGenerator.streamGenerationFrom` against only this Layer get a
332
350
  * compile-time error before this runtime fallback ever fires.
333
351
  */
334
352
  const streamGenerationFromUnsupported = <E, R>(
335
353
  _input: Stream.Stream<MusicSessionInput, E, R>,
336
354
  _request: CommonStreamGenerateMusicRequest,
337
- ): Stream.Stream<AudioChunk, AiError.AiError | E, R> => {
338
- const fail: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(
355
+ ): Stream.Stream<MusicStreamEvent, AiError.AiError | E, R> => {
356
+ const fail: Stream.Stream<MusicStreamEvent, AiError.AiError | E, R> = Stream.fail(
339
357
  new AiError.Unsupported({
340
358
  provider: "lyria",
341
359
  capability: "streamGenerationFrom",
@@ -365,7 +383,7 @@ export const make = (
365
383
  * Layer that registers both the provider-specific `LyriaGenerator` tag
366
384
  * and the generic `MusicGenerator` tag.
367
385
  *
368
- * Does NOT register the `MusicInteractiveSession` capability marker —
386
+ * Does NOT register the `MusicInteractiveSession` capability marker,
369
387
  * Lyria 3 sync has no bidirectional session at the wire level. Code
370
388
  * calling `MusicGenerator.streamGenerationFrom` will fail to typecheck
371
389
  * against this Layer alone, which is the intended UX.
@@ -380,9 +398,10 @@ export const layer = (
380
398
  Effect.map(
381
399
  make(cfg),
382
400
  (s): MusicGeneratorService => ({
383
- generate: (req: CommonGenerateMusicRequest) => s.generate(req as LyriaGenerateRequest),
401
+ generate: (req: CommonGenerateMusicRequest) =>
402
+ s.generate({ ...req, model: req.model as LyriaModel }),
384
403
  streamGeneration: (req: CommonStreamGenerateMusicRequest) =>
385
- s.streamGeneration(req as LyriaGenerateRequest),
404
+ s.streamGeneration({ ...req, model: req.model as LyriaModel }),
386
405
  streamGenerationFrom: s.streamGenerationFrom,
387
406
  }),
388
407
  ),
package/src/codec.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  } from "@effect-uai/core/Items"
9
9
  import type { ToolDescriptor } from "@effect-uai/core/Tool"
10
10
  import type { Turn } from "@effect-uai/core/Turn"
11
+ import type { CommonRequest } from "@effect-uai/core/LanguageModel"
11
12
 
12
13
  // ---------------------------------------------------------------------------
13
14
  // Wire schemas - the subset of Gemini's streamGenerateContent payload we
@@ -341,10 +342,39 @@ const toolDescriptorsToTools = (tools: ReadonlyArray<ToolDescriptor>): ReadonlyA
341
342
  },
342
343
  ]
343
344
 
345
+ /**
346
+ * Map our cross-provider `toolChoice` onto Gemini's `functionCallingConfig`.
347
+ * Gemini honours the same three modes plus a function allow-list, so the
348
+ * choice is mapped, not dropped. `undefined` keeps the API default (`AUTO`).
349
+ */
350
+ const toolChoiceToFunctionCallingConfig = (
351
+ toolChoice: CommonRequest["toolChoice"],
352
+ ): RequestToolConfig["functionCallingConfig"] =>
353
+ Match.value(toolChoice ?? "auto").pipe(
354
+ Match.when("auto", () => ({ mode: "AUTO" as const })),
355
+ Match.when("required", () => ({ mode: "ANY" as const })),
356
+ Match.when("none", () => ({ mode: "NONE" as const })),
357
+ Match.orElse((tc) => ({ mode: "ANY" as const, allowedFunctionNames: [tc.name] })),
358
+ )
359
+
360
+ /**
361
+ * URL-form images need Gemini's Files API (upload, then `fileData`); we
362
+ * don't pre-upload, so such a part would otherwise vanish from the request.
363
+ * The caller fails the request with `Unsupported` instead of silently
364
+ * dropping the image.
365
+ */
366
+ export const hasUrlImageSource = (history: ReadonlyArray<HistoryItem>): boolean =>
367
+ history.some(
368
+ (item) =>
369
+ item.type === "message" &&
370
+ item.content.some((b) => b.type === "input_image" && b.source._tag === "url"),
371
+ )
372
+
344
373
  export const buildRequestBody = (
345
374
  history: ReadonlyArray<HistoryItem>,
346
375
  generationConfig: Option.Option<GenerationConfig>,
347
376
  tools: ReadonlyArray<ToolDescriptor> = [],
377
+ toolChoice: CommonRequest["toolChoice"] = undefined,
348
378
  ): RequestBody => {
349
379
  const systemTexts = pipe(allMessages(history), Arr.filterMap(systemMessageText))
350
380
  const contents = pipe(history, Arr.filterMap(itemToContent(history)))
@@ -360,7 +390,7 @@ export const buildRequestBody = (
360
390
  }),
361
391
  ...(requestTools.length > 0 && {
362
392
  tools: requestTools,
363
- toolConfig: { functionCallingConfig: { mode: "AUTO" as const } },
393
+ toolConfig: { functionCallingConfig: toolChoiceToFunctionCallingConfig(toolChoice) },
364
394
  }),
365
395
  }
366
396
  }
package/src/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  export * as Gemini from "./Gemini.js"
2
2
  export * as GeminiEmbedding from "./GeminiEmbedding.js"
3
3
  export * as GeminiSynthesizer from "./GeminiSynthesizer.js"
4
- export * as GeminiTranscriber from "./GeminiTranscriber.js"
5
4
  export * as LyriaGenerator from "./LyriaGenerator.js"
6
5
  export * as codec from "./codec.js"
7
6
  export * from "./models.js"
package/src/models.ts CHANGED
@@ -44,23 +44,6 @@ export type GeminiTtsModel =
44
44
  // eslint-disable-next-line @typescript-eslint/ban-types
45
45
  | (string & {})
46
46
 
47
- /**
48
- * Known Gemini models that accept audio input via `inlineData` /
49
- * `fileData` on `generateContent`. Used by `GeminiTranscriber` —
50
- * transcription is prompt-driven on top of these models, not a
51
- * dedicated endpoint.
52
- *
53
- * Reference: https://ai.google.dev/gemini-api/docs/audio
54
- */
55
- export type GeminiSttModel =
56
- | "gemini-3-flash-preview"
57
- | "gemini-3.1-pro-preview"
58
- | "gemini-3.1-flash-lite-preview"
59
- | "gemini-2.5-flash"
60
- | "gemini-2.5-pro"
61
- // eslint-disable-next-line @typescript-eslint/ban-types
62
- | (string & {})
63
-
64
47
  /**
65
48
  * 30 prebuilt voice names for Gemini TTS. No custom-voice / cloning path
66
49
  * is exposed on this surface, so the tail is stock-only (no
@@ -1,42 +0,0 @@
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, 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<unknown, 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
@@ -1 +0,0 @@
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,iBAAA,UAAA,CAAA,UAAA"}
@@ -1,80 +0,0 @@
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* 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
@@ -1 +0,0 @@
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* 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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"}
@@ -1 +0,0 @@
1
- export { };