@tanstack/ai-gemini 0.9.1 → 0.10.1
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/esm/adapters/audio.d.ts +92 -0
- package/dist/esm/adapters/audio.js +61 -0
- package/dist/esm/adapters/audio.js.map +1 -0
- package/dist/esm/adapters/image.js +42 -10
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/tts.d.ts +39 -3
- package/dist/esm/adapters/tts.js +114 -18
- package/dist/esm/adapters/tts.js.map +1 -1
- package/dist/esm/image/image-provider-options.d.ts +4 -2
- package/dist/esm/image/image-provider-options.js +8 -1
- package/dist/esm/image/image-provider-options.js.map +1 -1
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/model-meta.d.ts +6 -1
- package/dist/esm/model-meta.js +15 -0
- package/dist/esm/model-meta.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/audio.ts +185 -0
- package/src/adapters/image.ts +70 -9
- package/src/adapters/tts.ts +222 -23
- package/src/image/image-provider-options.ts +24 -4
- package/src/index.ts +14 -0
- package/src/model-meta.ts +87 -3
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { BaseAudioAdapter } from '@tanstack/ai/adapters';
|
|
2
|
+
import { GEMINI_AUDIO_MODELS } from '../model-meta.js';
|
|
3
|
+
import { AudioGenerationOptions, AudioGenerationResult } from '@tanstack/ai';
|
|
4
|
+
import { GeminiClientConfig } from '../utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Provider options for Gemini Lyria music generation.
|
|
7
|
+
*
|
|
8
|
+
* Notes on the Lyria 3 surface area:
|
|
9
|
+
* - `lyria-3-clip-preview` always returns MP3 (30-second clips). It does
|
|
10
|
+
* not accept `responseMimeType`, and duration is fixed at 30 seconds —
|
|
11
|
+
* the generic `duration` option on `AudioActivityOptions` is ignored.
|
|
12
|
+
* - `lyria-3-pro-preview` returns MP3 by default. Duration is controlled
|
|
13
|
+
* via the natural-language prompt, not a separate SDK field, so the
|
|
14
|
+
* generic `duration` option is similarly ignored.
|
|
15
|
+
* - `negativePrompt` is NOT accepted by `GenerateContentConfig` and has
|
|
16
|
+
* therefore been removed from this surface to avoid giving callers a
|
|
17
|
+
* silently-dropped knob.
|
|
18
|
+
*
|
|
19
|
+
* @see https://ai.google.dev/gemini-api/docs/music-generation
|
|
20
|
+
*/
|
|
21
|
+
export interface GeminiAudioProviderOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Seed for deterministic generation.
|
|
24
|
+
*/
|
|
25
|
+
seed?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface GeminiAudioConfig extends GeminiClientConfig {
|
|
28
|
+
}
|
|
29
|
+
/** Model type for Gemini Lyria audio generation */
|
|
30
|
+
export type GeminiAudioModel = (typeof GEMINI_AUDIO_MODELS)[number];
|
|
31
|
+
/**
|
|
32
|
+
* Gemini Lyria Music Generation Adapter.
|
|
33
|
+
*
|
|
34
|
+
* Tree-shakeable adapter for Google Lyria music generation via the Gemini API.
|
|
35
|
+
*
|
|
36
|
+
* Models:
|
|
37
|
+
* - `lyria-3-pro-preview` — flagship model, full-length songs with verses,
|
|
38
|
+
* choruses, and bridges. Outputs MP3 or WAV at 48 kHz stereo.
|
|
39
|
+
* - `lyria-3-clip-preview` — 30-second clips in MP3.
|
|
40
|
+
*
|
|
41
|
+
* @see https://ai.google.dev/gemini-api/docs/music-generation
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const adapter = geminiAudio('lyria-3-pro-preview')
|
|
46
|
+
* const result = await generateAudio({
|
|
47
|
+
* adapter,
|
|
48
|
+
* prompt: 'An upbeat jazz track with saxophone and drums',
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare class GeminiAudioAdapter<TModel extends GeminiAudioModel> extends BaseAudioAdapter<TModel, GeminiAudioProviderOptions> {
|
|
53
|
+
readonly name: "gemini";
|
|
54
|
+
private client;
|
|
55
|
+
constructor(config: GeminiAudioConfig, model: TModel);
|
|
56
|
+
generateAudio(options: AudioGenerationOptions<GeminiAudioProviderOptions>): Promise<AudioGenerationResult>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Creates a Gemini Lyria audio adapter with an explicit API key.
|
|
60
|
+
*
|
|
61
|
+
* @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')
|
|
62
|
+
* @param apiKey - Your Google API key
|
|
63
|
+
* @param config - Optional additional configuration
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const adapter = createGeminiAudio('lyria-3-pro-preview', 'your-api-key')
|
|
68
|
+
* const result = await generateAudio({
|
|
69
|
+
* adapter,
|
|
70
|
+
* prompt: 'Ambient electronic music with soft pads',
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function createGeminiAudio<TModel extends GeminiAudioModel>(model: TModel, apiKey: string, config?: Omit<GeminiAudioConfig, 'apiKey'>): GeminiAudioAdapter<TModel>;
|
|
75
|
+
/**
|
|
76
|
+
* Creates a Gemini Lyria audio adapter with automatic API key detection.
|
|
77
|
+
*
|
|
78
|
+
* Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in the environment.
|
|
79
|
+
*
|
|
80
|
+
* @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')
|
|
81
|
+
* @param config - Optional configuration (excluding apiKey)
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const adapter = geminiAudio('lyria-3-pro-preview')
|
|
86
|
+
* const result = await generateAudio({
|
|
87
|
+
* adapter,
|
|
88
|
+
* prompt: 'An orchestral piece with strings and brass',
|
|
89
|
+
* })
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function geminiAudio<TModel extends GeminiAudioModel>(model: TModel, config?: Omit<GeminiAudioConfig, 'apiKey'>): GeminiAudioAdapter<TModel>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseAudioAdapter } from "@tanstack/ai/adapters";
|
|
2
|
+
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
3
|
+
class GeminiAudioAdapter extends BaseAudioAdapter {
|
|
4
|
+
constructor(config, model) {
|
|
5
|
+
super(model, config);
|
|
6
|
+
this.name = "gemini";
|
|
7
|
+
this.client = createGeminiClient(config);
|
|
8
|
+
}
|
|
9
|
+
async generateAudio(options) {
|
|
10
|
+
const { model, prompt, modelOptions, logger } = options;
|
|
11
|
+
logger.request(`activity=generateAudio provider=gemini model=${model}`, {
|
|
12
|
+
provider: "gemini",
|
|
13
|
+
model
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
const response = await this.client.models.generateContent({
|
|
17
|
+
model,
|
|
18
|
+
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
19
|
+
config: {
|
|
20
|
+
responseModalities: ["AUDIO", "TEXT"],
|
|
21
|
+
...modelOptions?.seed != null ? { seed: modelOptions.seed } : {}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const parts = response.candidates?.[0]?.content?.parts ?? [];
|
|
25
|
+
const audioPart = parts.find(
|
|
26
|
+
(part) => part.inlineData?.mimeType?.startsWith("audio/")
|
|
27
|
+
);
|
|
28
|
+
if (!audioPart?.inlineData?.data) {
|
|
29
|
+
throw new Error("No audio data in Gemini Lyria response");
|
|
30
|
+
}
|
|
31
|
+
const contentType = audioPart.inlineData.mimeType;
|
|
32
|
+
return {
|
|
33
|
+
id: generateId(this.name),
|
|
34
|
+
model,
|
|
35
|
+
audio: {
|
|
36
|
+
b64Json: audioPart.inlineData.data,
|
|
37
|
+
contentType
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.errors("gemini.generateAudio fatal", {
|
|
42
|
+
error,
|
|
43
|
+
source: "gemini.generateAudio"
|
|
44
|
+
});
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function createGeminiAudio(model, apiKey, config) {
|
|
50
|
+
return new GeminiAudioAdapter({ ...config, apiKey }, model);
|
|
51
|
+
}
|
|
52
|
+
function geminiAudio(model, config) {
|
|
53
|
+
const apiKey = getGeminiApiKeyFromEnv();
|
|
54
|
+
return createGeminiAudio(model, apiKey, config);
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
GeminiAudioAdapter,
|
|
58
|
+
createGeminiAudio,
|
|
59
|
+
geminiAudio
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sources":["../../../src/adapters/audio.ts"],"sourcesContent":["import { BaseAudioAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type { GEMINI_AUDIO_MODELS } from '../model-meta'\nimport type {\n AudioGenerationOptions,\n AudioGenerationResult,\n} from '@tanstack/ai'\nimport type { GoogleGenAI } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Provider options for Gemini Lyria music generation.\n *\n * Notes on the Lyria 3 surface area:\n * - `lyria-3-clip-preview` always returns MP3 (30-second clips). It does\n * not accept `responseMimeType`, and duration is fixed at 30 seconds —\n * the generic `duration` option on `AudioActivityOptions` is ignored.\n * - `lyria-3-pro-preview` returns MP3 by default. Duration is controlled\n * via the natural-language prompt, not a separate SDK field, so the\n * generic `duration` option is similarly ignored.\n * - `negativePrompt` is NOT accepted by `GenerateContentConfig` and has\n * therefore been removed from this surface to avoid giving callers a\n * silently-dropped knob.\n *\n * @see https://ai.google.dev/gemini-api/docs/music-generation\n */\nexport interface GeminiAudioProviderOptions {\n /**\n * Seed for deterministic generation.\n */\n seed?: number\n}\n\nexport interface GeminiAudioConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Lyria audio generation */\nexport type GeminiAudioModel = (typeof GEMINI_AUDIO_MODELS)[number]\n\n/**\n * Gemini Lyria Music Generation Adapter.\n *\n * Tree-shakeable adapter for Google Lyria music generation via the Gemini API.\n *\n * Models:\n * - `lyria-3-pro-preview` — flagship model, full-length songs with verses,\n * choruses, and bridges. Outputs MP3 or WAV at 48 kHz stereo.\n * - `lyria-3-clip-preview` — 30-second clips in MP3.\n *\n * @see https://ai.google.dev/gemini-api/docs/music-generation\n *\n * @example\n * ```typescript\n * const adapter = geminiAudio('lyria-3-pro-preview')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An upbeat jazz track with saxophone and drums',\n * })\n * ```\n */\nexport class GeminiAudioAdapter<\n TModel extends GeminiAudioModel,\n> extends BaseAudioAdapter<TModel, GeminiAudioProviderOptions> {\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiAudioConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n async generateAudio(\n options: AudioGenerationOptions<GeminiAudioProviderOptions>,\n ): Promise<AudioGenerationResult> {\n const { model, prompt, modelOptions, logger } = options\n\n logger.request(`activity=generateAudio provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n try {\n // FIXME (SDK audit): Lyria 3 music generation may not belong on\n // generateContent at all — @google/genai exposes a `LiveMusicSession`\n // (`ai.live.music.connect`) with a `musicGenerationConfig` object.\n // `seed` is valid on GenerateContentConfig, and Lyria always returns\n // MP3 today, so we don't forward `responseMimeType` either.\n // The runtime test `emits only GenerateContentConfig-valid fields`\n // asserts the config shape so a later SDK audit can catch regressions.\n const response = await this.client.models.generateContent({\n model,\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\n config: {\n responseModalities: ['AUDIO', 'TEXT'],\n ...(modelOptions?.seed != null ? { seed: modelOptions.seed } : {}),\n },\n })\n\n const parts = response.candidates?.[0]?.content?.parts ?? []\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart?.inlineData?.data) {\n throw new Error('No audio data in Gemini Lyria response')\n }\n\n // audioPart was selected because mimeType.startsWith('audio/') was\n // truthy, so the mime type is guaranteed to be a string here. Trust the\n // value Gemini returned rather than inventing a non-standard\n // `audio/mp3` fallback (IANA is `audio/mpeg`).\n const contentType = audioPart.inlineData.mimeType\n\n return {\n id: generateId(this.name),\n model,\n audio: {\n b64Json: audioPart.inlineData.data,\n contentType,\n },\n }\n } catch (error) {\n logger.errors('gemini.generateAudio fatal', {\n error,\n source: 'gemini.generateAudio',\n })\n throw error\n }\n }\n}\n\n/**\n * Creates a Gemini Lyria audio adapter with an explicit API key.\n *\n * @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n *\n * @example\n * ```typescript\n * const adapter = createGeminiAudio('lyria-3-pro-preview', 'your-api-key')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'Ambient electronic music with soft pads',\n * })\n * ```\n */\nexport function createGeminiAudio<TModel extends GeminiAudioModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiAudioConfig, 'apiKey'>,\n): GeminiAudioAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiAudioAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini Lyria audio adapter with automatic API key detection.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in the environment.\n *\n * @param model - The Lyria model name (e.g., 'lyria-3-pro-preview')\n * @param config - Optional configuration (excluding apiKey)\n *\n * @example\n * ```typescript\n * const adapter = geminiAudio('lyria-3-pro-preview')\n * const result = await generateAudio({\n * adapter,\n * prompt: 'An orchestral piece with strings and brass',\n * })\n * ```\n */\nexport function geminiAudio<TModel extends GeminiAudioModel>(\n model: TModel,\n config?: Omit<GeminiAudioConfig, 'apiKey'>,\n): GeminiAudioAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiAudio(model, apiKey, config)\n}\n"],"names":[],"mappings":";;AA+DO,MAAM,2BAEH,iBAAqD;AAAA,EAK7D,YAAY,QAA2B,OAAe;AACpD,UAAM,OAAO,MAAM;AALrB,SAAS,OAAO;AAMd,SAAK,SAAS,mBAAmB,MAAM;AAAA,EACzC;AAAA,EAEA,MAAM,cACJ,SACgC;AAChC,UAAM,EAAE,OAAO,QAAQ,cAAc,WAAW;AAEhD,WAAO,QAAQ,gDAAgD,KAAK,IAAI;AAAA,MACtE,UAAU;AAAA,MACV;AAAA,IAAA,CACD;AAED,QAAI;AAQF,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO,gBAAgB;AAAA,QACxD;AAAA,QACA,UAAU,CAAC,EAAE,MAAM,QAAQ,OAAO,CAAC,EAAE,MAAM,OAAA,CAAQ,GAAG;AAAA,QACtD,QAAQ;AAAA,UACN,oBAAoB,CAAC,SAAS,MAAM;AAAA,UACpC,GAAI,cAAc,QAAQ,OAAO,EAAE,MAAM,aAAa,SAAS,CAAA;AAAA,QAAC;AAAA,MAClE,CACD;AAED,YAAM,QAAQ,SAAS,aAAa,CAAC,GAAG,SAAS,SAAS,CAAA;AAC1D,YAAM,YAAY,MAAM;AAAA,QAAK,CAAC,SAC5B,KAAK,YAAY,UAAU,WAAW,QAAQ;AAAA,MAAA;AAGhD,UAAI,CAAC,WAAW,YAAY,MAAM;AAChC,cAAM,IAAI,MAAM,wCAAwC;AAAA,MAC1D;AAMA,YAAM,cAAc,UAAU,WAAW;AAEzC,aAAO;AAAA,QACL,IAAI,WAAW,KAAK,IAAI;AAAA,QACxB;AAAA,QACA,OAAO;AAAA,UACL,SAAS,UAAU,WAAW;AAAA,UAC9B;AAAA,QAAA;AAAA,MACF;AAAA,IAEJ,SAAS,OAAO;AACd,aAAO,OAAO,8BAA8B;AAAA,QAC1C;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAkBO,SAAS,kBACd,OACA,QACA,QAC4B;AAG5B,SAAO,IAAI,mBAAmB,EAAE,GAAG,QAAQ,OAAA,GAAU,KAAK;AAC5D;AAmBO,SAAS,YACd,OACA,QAC4B;AAC5B,QAAM,SAAS,uBAAA;AACf,SAAO,kBAAkB,OAAO,QAAQ,MAAM;AAChD;"}
|
|
@@ -3,7 +3,7 @@ import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils
|
|
|
3
3
|
import { validatePrompt, validateImageSize, validateNumberOfImages, parseNativeImageSize, sizeToAspectRatio } from "../image/image-provider-options.js";
|
|
4
4
|
class GeminiImageAdapter extends BaseImageAdapter {
|
|
5
5
|
constructor(config, model) {
|
|
6
|
-
super(
|
|
6
|
+
super(model, config);
|
|
7
7
|
this.kind = "image";
|
|
8
8
|
this.name = "gemini";
|
|
9
9
|
this.client = createGeminiClient(config);
|
|
@@ -46,8 +46,15 @@ class GeminiImageAdapter extends BaseImageAdapter {
|
|
|
46
46
|
const { model, prompt, size, numberOfImages, modelOptions } = options;
|
|
47
47
|
const parsedSize = size ? parseNativeImageSize(size) : void 0;
|
|
48
48
|
const augmentedPrompt = numberOfImages && numberOfImages > 1 ? `${prompt} Generate ${numberOfImages} distinct images.` : prompt;
|
|
49
|
+
const nativeConfig = {};
|
|
50
|
+
if (modelOptions?.seed !== void 0) {
|
|
51
|
+
nativeConfig.seed = modelOptions.seed;
|
|
52
|
+
}
|
|
49
53
|
const config = {
|
|
50
|
-
|
|
54
|
+
...nativeConfig,
|
|
55
|
+
// Include TEXT so the model can interleave descriptions between images.
|
|
56
|
+
// IMPORTANT: responseModalities is a protected default — set it AFTER
|
|
57
|
+
// nativeConfig so nothing can silently disable image output.
|
|
51
58
|
responseModalities: ["TEXT", "IMAGE"],
|
|
52
59
|
...parsedSize && {
|
|
53
60
|
imageConfig: {
|
|
@@ -58,8 +65,7 @@ class GeminiImageAdapter extends BaseImageAdapter {
|
|
|
58
65
|
imageSize: parsedSize.resolution
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
|
-
}
|
|
62
|
-
...modelOptions
|
|
68
|
+
}
|
|
63
69
|
};
|
|
64
70
|
const response = await this.client.models.generateContent({
|
|
65
71
|
model,
|
|
@@ -70,12 +76,19 @@ class GeminiImageAdapter extends BaseImageAdapter {
|
|
|
70
76
|
}
|
|
71
77
|
transformGeminiResponse(model, response) {
|
|
72
78
|
const images = [];
|
|
79
|
+
const textParts = [];
|
|
73
80
|
const parts = response.candidates?.[0]?.content?.parts ?? [];
|
|
74
81
|
for (const part of parts) {
|
|
75
82
|
if (part.inlineData?.data && typeof part.inlineData.data === "string" && part.inlineData.data.length > 0) {
|
|
76
83
|
images.push({ b64Json: part.inlineData.data });
|
|
84
|
+
} else if (typeof part.text === "string" && part.text.length > 0) {
|
|
85
|
+
textParts.push(part.text);
|
|
77
86
|
}
|
|
78
87
|
}
|
|
88
|
+
if (images.length === 0) {
|
|
89
|
+
const reason = textParts.length > 0 ? `: ${textParts.join(" ").trim()}` : " (no inline image or text parts were returned).";
|
|
90
|
+
throw new Error(`Gemini ${model} returned no images${reason}`);
|
|
91
|
+
}
|
|
79
92
|
return {
|
|
80
93
|
id: generateId(this.name),
|
|
81
94
|
model,
|
|
@@ -93,12 +106,31 @@ class GeminiImageAdapter extends BaseImageAdapter {
|
|
|
93
106
|
};
|
|
94
107
|
}
|
|
95
108
|
transformImagenResponse(model, response) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
const entries = response.generatedImages ?? [];
|
|
110
|
+
const images = [];
|
|
111
|
+
const filterReasons = [];
|
|
112
|
+
for (const item of entries) {
|
|
113
|
+
const b64Json = item.image?.imageBytes;
|
|
114
|
+
if (b64Json) {
|
|
115
|
+
images.push({ b64Json, revisedPrompt: item.enhancedPrompt });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const reason = item.raiFilteredReason;
|
|
119
|
+
if (reason) {
|
|
120
|
+
filterReasons.push(reason);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (entries.length > 0 && images.length === 0) {
|
|
124
|
+
const joined = filterReasons.length > 0 ? filterReasons.join("; ") : "";
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Imagen ${model} returned no images: all ${entries.length} generated image(s) were filtered by Responsible-AI${joined ? ` (${joined})` : ""}.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (filterReasons.length > 0 && typeof console !== "undefined") {
|
|
130
|
+
console.warn(
|
|
131
|
+
`[gemini-image] ${filterReasons.length} of ${entries.length} images from ${model} were filtered by Responsible-AI: ${filterReasons.join("; ")}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
102
134
|
return {
|
|
103
135
|
id: generateId(this.name),
|
|
104
136
|
model,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport {\n parseNativeImageSize,\n sizeToAspectRatio,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type { GEMINI_IMAGE_MODELS } from '../model-meta'\nimport type {\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GenerateImagesConfig,\n GenerateImagesResponse,\n GoogleGenAI,\n} from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini image adapter\n */\nexport interface GeminiImageConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Image */\nexport type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number]\n\n/**\n * Gemini Image Generation Adapter\n *\n * Tree-shakeable adapter for Gemini image generation functionality.\n * Supports Imagen 3/4 models (via generateImages API) and Gemini native\n * image models like Nano Banana 2 (via generateContent API).\n *\n * Features:\n * - Aspect ratio-based image sizing\n * - Person generation controls\n * - Safety filtering\n * - Watermark options\n * - Extended resolution tiers (Nano Banana 2)\n */\nexport class GeminiImageAdapter<\n TModel extends GeminiImageModel,\n> extends BaseImageAdapter<\n TModel,\n GeminiImageProviderOptions,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName\n> {\n readonly kind = 'image' as const\n readonly name = 'gemini' as const\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiImageProviderOptions\n modelProviderOptionsByName: GeminiImageModelProviderOptionsByName\n modelSizeByName: GeminiImageModelSizeByName\n }\n\n private client: GoogleGenAI\n\n constructor(config: GeminiImageConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, logger } = options\n\n logger.request(\n `activity=generateImage provider=gemini model=${this.model}`,\n {\n provider: 'gemini',\n model: this.model,\n },\n )\n\n try {\n validatePrompt({ prompt, model })\n\n if (this.isGeminiImageModel(model)) {\n return await this.generateWithGeminiApi(options)\n }\n\n // Imagen models path (generateImages API)\n validateImageSize(model, options.size)\n validateNumberOfImages(model, options.numberOfImages)\n\n const config = this.buildImagenConfig(options)\n\n const response = await this.client.models.generateImages({\n model,\n prompt,\n config,\n })\n\n return this.transformImagenResponse(model, response)\n } catch (error) {\n logger.errors('gemini.generateImage fatal', {\n error,\n source: 'gemini.generateImage',\n })\n throw error\n }\n }\n\n private isGeminiImageModel(model: string): boolean {\n return model.startsWith('gemini-')\n }\n\n private async generateWithGeminiApi(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\n\n // The generateContent API has no numberOfImages parameter.\n // Instead, augment the prompt to request multiple images when needed.\n const augmentedPrompt =\n numberOfImages && numberOfImages > 1\n ? `${prompt} Generate ${numberOfImages} distinct images.`\n : prompt\n\n const config: GenerateContentConfig = {\n // Include TEXT so the model can interleave descriptions between images\n responseModalities: ['TEXT', 'IMAGE'],\n ...(parsedSize && {\n imageConfig: {\n ...(parsedSize.aspectRatio && {\n aspectRatio: parsedSize.aspectRatio,\n }),\n ...(parsedSize.resolution && {\n imageSize: parsedSize.resolution,\n }),\n },\n }),\n ...modelOptions,\n }\n\n const response = await this.client.models.generateContent({\n model,\n contents: augmentedPrompt,\n config,\n })\n\n return this.transformGeminiResponse(model, response)\n }\n\n private transformGeminiResponse(\n model: string,\n response: GenerateContentResponse,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const parts = response.candidates?.[0]?.content?.parts ?? []\n\n for (const part of parts) {\n if (\n part.inlineData?.data &&\n typeof part.inlineData.data === 'string' &&\n part.inlineData.data.length > 0\n ) {\n images.push({ b64Json: part.inlineData.data })\n }\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n usage: undefined,\n }\n }\n\n private buildImagenConfig(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): GenerateImagesConfig {\n const { size, numberOfImages, modelOptions } = options\n\n return {\n numberOfImages: numberOfImages ?? 1,\n // Map size to aspect ratio if provided (modelOptions.aspectRatio will override)\n aspectRatio: size ? sizeToAspectRatio(size) : undefined,\n ...modelOptions,\n }\n }\n\n private transformImagenResponse(\n model: string,\n response: GenerateImagesResponse,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = (response.generatedImages ?? []).map(\n (item) => ({\n b64Json: item.image?.imageBytes,\n revisedPrompt: item.enhancedPrompt,\n }),\n )\n\n return {\n id: generateId(this.name),\n model,\n images,\n usage: undefined,\n }\n }\n}\n\n/**\n * Creates a Gemini image adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'imagen-3.0-generate-002')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiImage('imagen-3.0-generate-002', \"your-api-key\");\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter'\n * });\n * ```\n */\nexport function createGeminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n return new GeminiImageAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini image adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'imagen-4.0-generate-001')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini image adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiImage('imagen-4.0-generate-001');\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains'\n * });\n * ```\n */\nexport function geminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiImage(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image.js","sources":["../../../src/adapters/image.ts"],"sourcesContent":["import { BaseImageAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport {\n parseNativeImageSize,\n sizeToAspectRatio,\n validateImageSize,\n validateNumberOfImages,\n validatePrompt,\n} from '../image/image-provider-options'\nimport type { GEMINI_IMAGE_MODELS } from '../model-meta'\nimport type {\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName,\n GeminiImageProviderOptions,\n} from '../image/image-provider-options'\nimport type {\n GeneratedImage,\n ImageGenerationOptions,\n ImageGenerationResult,\n} from '@tanstack/ai'\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GenerateImagesConfig,\n GenerateImagesResponse,\n GoogleGenAI,\n} from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for Gemini image adapter\n */\nexport interface GeminiImageConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini Image */\nexport type GeminiImageModel = (typeof GEMINI_IMAGE_MODELS)[number]\n\n/**\n * Gemini Image Generation Adapter\n *\n * Tree-shakeable adapter for Gemini image generation functionality.\n * Supports Imagen 3/4 models (via generateImages API) and Gemini native\n * image models like Nano Banana 2 (via generateContent API).\n *\n * Features:\n * - Aspect ratio-based image sizing\n * - Person generation controls\n * - Safety filtering\n * - Watermark options\n * - Extended resolution tiers (Nano Banana 2)\n */\nexport class GeminiImageAdapter<\n TModel extends GeminiImageModel,\n> extends BaseImageAdapter<\n TModel,\n GeminiImageProviderOptions,\n GeminiImageModelProviderOptionsByName,\n GeminiImageModelSizeByName\n> {\n readonly kind = 'image' as const\n readonly name = 'gemini' as const\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiImageProviderOptions\n modelProviderOptionsByName: GeminiImageModelProviderOptionsByName\n modelSizeByName: GeminiImageModelSizeByName\n }\n\n private client: GoogleGenAI\n\n constructor(config: GeminiImageConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n async generateImages(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, logger } = options\n\n logger.request(\n `activity=generateImage provider=gemini model=${this.model}`,\n {\n provider: 'gemini',\n model: this.model,\n },\n )\n\n try {\n validatePrompt({ prompt, model })\n\n if (this.isGeminiImageModel(model)) {\n return await this.generateWithGeminiApi(options)\n }\n\n // Imagen models path (generateImages API)\n validateImageSize(model, options.size)\n validateNumberOfImages(model, options.numberOfImages)\n\n const config = this.buildImagenConfig(options)\n\n const response = await this.client.models.generateImages({\n model,\n prompt,\n config,\n })\n\n return this.transformImagenResponse(model, response)\n } catch (error) {\n logger.errors('gemini.generateImage fatal', {\n error,\n source: 'gemini.generateImage',\n })\n throw error\n }\n }\n\n private isGeminiImageModel(model: string): boolean {\n return model.startsWith('gemini-')\n }\n\n private async generateWithGeminiApi(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): Promise<ImageGenerationResult> {\n const { model, prompt, size, numberOfImages, modelOptions } = options\n\n const parsedSize = size ? parseNativeImageSize(size) : undefined\n\n // The generateContent API has no numberOfImages parameter.\n // Instead, augment the prompt to request multiple images when needed.\n const augmentedPrompt =\n numberOfImages && numberOfImages > 1\n ? `${prompt} Generate ${numberOfImages} distinct images.`\n : prompt\n\n // GeminiImageProviderOptions is Imagen-shaped — most fields\n // (personGeneration, safetyFilterLevel, addWatermark, outputMimeType,\n // outputCompressionQuality, guidanceScale, enhancePrompt,\n // includeSafetyAttributes, includeRaiReason, outputGcsUri, labels,\n // negativePrompt, language) are only valid on GenerateImagesConfig and\n // would be rejected by the Gemini-native generateContent path. Pick only\n // the fields that are valid on GenerateContentConfig instead of spreading\n // the whole options object.\n const nativeConfig: GenerateContentConfig = {}\n if (modelOptions?.seed !== undefined) {\n nativeConfig.seed = modelOptions.seed\n }\n\n const config: GenerateContentConfig = {\n ...nativeConfig,\n // Include TEXT so the model can interleave descriptions between images.\n // IMPORTANT: responseModalities is a protected default — set it AFTER\n // nativeConfig so nothing can silently disable image output.\n responseModalities: ['TEXT', 'IMAGE'],\n ...(parsedSize && {\n imageConfig: {\n ...(parsedSize.aspectRatio && {\n aspectRatio: parsedSize.aspectRatio,\n }),\n ...(parsedSize.resolution && {\n imageSize: parsedSize.resolution,\n }),\n },\n }),\n }\n\n const response = await this.client.models.generateContent({\n model,\n contents: augmentedPrompt,\n config,\n })\n\n return this.transformGeminiResponse(model, response)\n }\n\n private transformGeminiResponse(\n model: string,\n response: GenerateContentResponse,\n ): ImageGenerationResult {\n const images: Array<GeneratedImage> = []\n const textParts: Array<string> = []\n const parts = response.candidates?.[0]?.content?.parts ?? []\n\n for (const part of parts) {\n if (\n part.inlineData?.data &&\n typeof part.inlineData.data === 'string' &&\n part.inlineData.data.length > 0\n ) {\n images.push({ b64Json: part.inlineData.data })\n } else if (typeof part.text === 'string' && part.text.length > 0) {\n textParts.push(part.text)\n }\n }\n\n // If the model returned only text parts (for example a safety refusal\n // or a \"can't do that\" message), surface the text instead of silently\n // resolving to an empty images array — otherwise callers can't tell a\n // generation failure apart from a genuine empty response.\n if (images.length === 0) {\n const reason =\n textParts.length > 0\n ? `: ${textParts.join(' ').trim()}`\n : ' (no inline image or text parts were returned).'\n throw new Error(`Gemini ${model} returned no images${reason}`)\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n usage: undefined,\n }\n }\n\n private buildImagenConfig(\n options: ImageGenerationOptions<GeminiImageProviderOptions>,\n ): GenerateImagesConfig {\n const { size, numberOfImages, modelOptions } = options\n\n return {\n numberOfImages: numberOfImages ?? 1,\n // Map size to aspect ratio if provided (modelOptions.aspectRatio will override)\n aspectRatio: size ? sizeToAspectRatio(size) : undefined,\n ...modelOptions,\n }\n }\n\n private transformImagenResponse(\n model: string,\n response: GenerateImagesResponse,\n ): ImageGenerationResult {\n const entries = response.generatedImages ?? []\n const images: Array<GeneratedImage> = []\n const filterReasons: Array<string> = []\n\n for (const item of entries) {\n const b64Json = item.image?.imageBytes\n if (b64Json) {\n images.push({ b64Json, revisedPrompt: item.enhancedPrompt })\n continue\n }\n // Imagen can drop individual entries with a raiFilteredReason when\n // Responsible-AI filters fire. Preserve the reason so callers can\n // surface it instead of silently getting back fewer images.\n const reason = (item as { raiFilteredReason?: string }).raiFilteredReason\n if (reason) {\n filterReasons.push(reason)\n }\n }\n\n // Every entry was filtered — no usable images to return. Throw rather\n // than resolve to an empty array so the caller is forced to handle the\n // failure mode explicitly.\n if (entries.length > 0 && images.length === 0) {\n const joined = filterReasons.length > 0 ? filterReasons.join('; ') : ''\n throw new Error(\n `Imagen ${model} returned no images: all ${entries.length} generated image(s) were filtered by Responsible-AI${joined ? ` (${joined})` : ''}.`,\n )\n }\n\n // Partial filter: surface via console.warn since ImageGenerationResult\n // has no warnings field. Callers that care can still inspect the count\n // mismatch between requested and returned images.\n if (filterReasons.length > 0 && typeof console !== 'undefined') {\n console.warn(\n `[gemini-image] ${filterReasons.length} of ${entries.length} images from ${model} were filtered by Responsible-AI: ${filterReasons.join('; ')}`,\n )\n }\n\n return {\n id: generateId(this.name),\n model,\n images,\n usage: undefined,\n }\n }\n}\n\n/**\n * Creates a Gemini image adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @param model - The model name (e.g., 'imagen-3.0-generate-002')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini image adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiImage('imagen-3.0-generate-002', \"your-api-key\");\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A cute baby sea otter'\n * });\n * ```\n */\nexport function createGeminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n return new GeminiImageAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini image adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'imagen-4.0-generate-001')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini image adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiImage('imagen-4.0-generate-001');\n *\n * const result = await generateImage({\n * adapter,\n * prompt: 'A beautiful sunset over mountains'\n * });\n * ```\n */\nexport function geminiImage<TModel extends GeminiImageModel>(\n model: TModel,\n config?: Omit<GeminiImageConfig, 'apiKey'>,\n): GeminiImageAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiImage(model, apiKey, config)\n}\n"],"names":[],"mappings":";;;AAuDO,MAAM,2BAEH,iBAKR;AAAA,EAaA,YAAY,QAA2B,OAAe;AACpD,UAAM,OAAO,MAAM;AAbrB,SAAS,OAAO;AAChB,SAAS,OAAO;AAad,SAAK,SAAS,mBAAmB,MAAM;AAAA,EACzC;AAAA,EAEA,MAAM,eACJ,SACgC;AAChC,UAAM,EAAE,OAAO,QAAQ,OAAA,IAAW;AAElC,WAAO;AAAA,MACL,gDAAgD,KAAK,KAAK;AAAA,MAC1D;AAAA,QACE,UAAU;AAAA,QACV,OAAO,KAAK;AAAA,MAAA;AAAA,IACd;AAGF,QAAI;AACF,qBAAe,EAAE,QAAQ,OAAO;AAEhC,UAAI,KAAK,mBAAmB,KAAK,GAAG;AAClC,eAAO,MAAM,KAAK,sBAAsB,OAAO;AAAA,MACjD;AAGA,wBAAkB,OAAO,QAAQ,IAAI;AACrC,6BAAuB,OAAO,QAAQ,cAAc;AAEpD,YAAM,SAAS,KAAK,kBAAkB,OAAO;AAE7C,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO,eAAe;AAAA,QACvD;AAAA,QACA;AAAA,QACA;AAAA,MAAA,CACD;AAED,aAAO,KAAK,wBAAwB,OAAO,QAAQ;AAAA,IACrD,SAAS,OAAO;AACd,aAAO,OAAO,8BAA8B;AAAA,QAC1C;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,mBAAmB,OAAwB;AACjD,WAAO,MAAM,WAAW,SAAS;AAAA,EACnC;AAAA,EAEA,MAAc,sBACZ,SACgC;AAChC,UAAM,EAAE,OAAO,QAAQ,MAAM,gBAAgB,iBAAiB;AAE9D,UAAM,aAAa,OAAO,qBAAqB,IAAI,IAAI;AAIvD,UAAM,kBACJ,kBAAkB,iBAAiB,IAC/B,GAAG,MAAM,aAAa,cAAc,sBACpC;AAUN,UAAM,eAAsC,CAAA;AAC5C,QAAI,cAAc,SAAS,QAAW;AACpC,mBAAa,OAAO,aAAa;AAAA,IACnC;AAEA,UAAM,SAAgC;AAAA,MACpC,GAAG;AAAA;AAAA;AAAA;AAAA,MAIH,oBAAoB,CAAC,QAAQ,OAAO;AAAA,MACpC,GAAI,cAAc;AAAA,QAChB,aAAa;AAAA,UACX,GAAI,WAAW,eAAe;AAAA,YAC5B,aAAa,WAAW;AAAA,UAAA;AAAA,UAE1B,GAAI,WAAW,cAAc;AAAA,YAC3B,WAAW,WAAW;AAAA,UAAA;AAAA,QACxB;AAAA,MACF;AAAA,IACF;AAGF,UAAM,WAAW,MAAM,KAAK,OAAO,OAAO,gBAAgB;AAAA,MACxD;AAAA,MACA,UAAU;AAAA,MACV;AAAA,IAAA,CACD;AAED,WAAO,KAAK,wBAAwB,OAAO,QAAQ;AAAA,EACrD;AAAA,EAEQ,wBACN,OACA,UACuB;AACvB,UAAM,SAAgC,CAAA;AACtC,UAAM,YAA2B,CAAA;AACjC,UAAM,QAAQ,SAAS,aAAa,CAAC,GAAG,SAAS,SAAS,CAAA;AAE1D,eAAW,QAAQ,OAAO;AACxB,UACE,KAAK,YAAY,QACjB,OAAO,KAAK,WAAW,SAAS,YAChC,KAAK,WAAW,KAAK,SAAS,GAC9B;AACA,eAAO,KAAK,EAAE,SAAS,KAAK,WAAW,MAAM;AAAA,MAC/C,WAAW,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,GAAG;AAChE,kBAAU,KAAK,KAAK,IAAI;AAAA,MAC1B;AAAA,IACF;AAMA,QAAI,OAAO,WAAW,GAAG;AACvB,YAAM,SACJ,UAAU,SAAS,IACf,KAAK,UAAU,KAAK,GAAG,EAAE,KAAA,CAAM,KAC/B;AACN,YAAM,IAAI,MAAM,UAAU,KAAK,sBAAsB,MAAM,EAAE;AAAA,IAC/D;AAEA,WAAO;AAAA,MACL,IAAI,WAAW,KAAK,IAAI;AAAA,MACxB;AAAA,MACA;AAAA,MACA,OAAO;AAAA,IAAA;AAAA,EAEX;AAAA,EAEQ,kBACN,SACsB;AACtB,UAAM,EAAE,MAAM,gBAAgB,aAAA,IAAiB;AAE/C,WAAO;AAAA,MACL,gBAAgB,kBAAkB;AAAA;AAAA,MAElC,aAAa,OAAO,kBAAkB,IAAI,IAAI;AAAA,MAC9C,GAAG;AAAA,IAAA;AAAA,EAEP;AAAA,EAEQ,wBACN,OACA,UACuB;AACvB,UAAM,UAAU,SAAS,mBAAmB,CAAA;AAC5C,UAAM,SAAgC,CAAA;AACtC,UAAM,gBAA+B,CAAA;AAErC,eAAW,QAAQ,SAAS;AAC1B,YAAM,UAAU,KAAK,OAAO;AAC5B,UAAI,SAAS;AACX,eAAO,KAAK,EAAE,SAAS,eAAe,KAAK,gBAAgB;AAC3D;AAAA,MACF;AAIA,YAAM,SAAU,KAAwC;AACxD,UAAI,QAAQ;AACV,sBAAc,KAAK,MAAM;AAAA,MAC3B;AAAA,IACF;AAKA,QAAI,QAAQ,SAAS,KAAK,OAAO,WAAW,GAAG;AAC7C,YAAM,SAAS,cAAc,SAAS,IAAI,cAAc,KAAK,IAAI,IAAI;AACrE,YAAM,IAAI;AAAA,QACR,UAAU,KAAK,4BAA4B,QAAQ,MAAM,sDAAsD,SAAS,KAAK,MAAM,MAAM,EAAE;AAAA,MAAA;AAAA,IAE/I;AAKA,QAAI,cAAc,SAAS,KAAK,OAAO,YAAY,aAAa;AAC9D,cAAQ;AAAA,QACN,kBAAkB,cAAc,MAAM,OAAO,QAAQ,MAAM,gBAAgB,KAAK,qCAAqC,cAAc,KAAK,IAAI,CAAC;AAAA,MAAA;AAAA,IAEjJ;AAEA,WAAO;AAAA,MACL,IAAI,WAAW,KAAK,IAAI;AAAA,MACxB;AAAA,MACA;AAAA,MACA,OAAO;AAAA,IAAA;AAAA,EAEX;AACF;AAqBO,SAAS,kBACd,OACA,QACA,QAC4B;AAC5B,SAAO,IAAI,mBAAmB,EAAE,QAAQ,GAAG,OAAA,GAAU,KAAK;AAC5D;AA0BO,SAAS,YACd,OACA,QAC4B;AAC5B,QAAM,SAAS,uBAAA;AACf,SAAO,kBAAkB,OAAO,QAAQ,MAAM;AAChD;"}
|
|
@@ -2,16 +2,33 @@ import { BaseTTSAdapter } from '@tanstack/ai/adapters';
|
|
|
2
2
|
import { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta.js';
|
|
3
3
|
import { TTSOptions, TTSResult } from '@tanstack/ai';
|
|
4
4
|
import { GeminiClientConfig } from '../utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for a single speaker in a multi-speaker dialogue.
|
|
7
|
+
* Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.
|
|
8
|
+
*/
|
|
9
|
+
export interface GeminiSpeakerVoiceConfig {
|
|
10
|
+
/** A name used in the prompt to refer to this speaker */
|
|
11
|
+
speaker: string;
|
|
12
|
+
/** Voice configuration for this speaker */
|
|
13
|
+
voiceConfig: {
|
|
14
|
+
prebuiltVoiceConfig: {
|
|
15
|
+
voiceName: GeminiTTSVoice;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
5
19
|
/**
|
|
6
20
|
* Provider-specific options for Gemini TTS
|
|
7
21
|
*
|
|
8
22
|
* @experimental Gemini TTS is an experimental feature.
|
|
9
23
|
* @see https://ai.google.dev/gemini-api/docs/speech-generation
|
|
24
|
+
* @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-tts-preview
|
|
10
25
|
*/
|
|
11
26
|
export interface GeminiTTSProviderOptions {
|
|
12
27
|
/**
|
|
13
|
-
* Voice configuration for TTS.
|
|
28
|
+
* Voice configuration for single-speaker TTS.
|
|
14
29
|
* Choose from 30 available voices with different characteristics.
|
|
30
|
+
*
|
|
31
|
+
* Use `multiSpeakerVoiceConfig` instead for dialogues.
|
|
15
32
|
*/
|
|
16
33
|
voiceConfig?: {
|
|
17
34
|
prebuiltVoiceConfig?: {
|
|
@@ -22,11 +39,30 @@ export interface GeminiTTSProviderOptions {
|
|
|
22
39
|
voiceName?: GeminiTTSVoice;
|
|
23
40
|
};
|
|
24
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Multi-speaker voice configuration (up to 2 speakers).
|
|
44
|
+
* Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.
|
|
45
|
+
*
|
|
46
|
+
* Each speaker's lines in the prompt are prefixed with the name defined
|
|
47
|
+
* here, e.g.:
|
|
48
|
+
*
|
|
49
|
+
* ```text
|
|
50
|
+
* Joe: Hey, how's it going?
|
|
51
|
+
* Jane: Not bad, you?
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
multiSpeakerVoiceConfig?: {
|
|
55
|
+
speakerVoiceConfigs: Array<GeminiSpeakerVoiceConfig>;
|
|
56
|
+
};
|
|
25
57
|
/**
|
|
26
58
|
* System instruction for controlling speech style.
|
|
27
59
|
* Use natural language to describe the desired speaking style,
|
|
28
60
|
* pace, tone, accent, or other characteristics.
|
|
29
61
|
*
|
|
62
|
+
* With Gemini 3.1 Flash TTS, you can also use inline audio tags like
|
|
63
|
+
* `[whispering]`, `[laughs]`, `[excited]` directly in the input text
|
|
64
|
+
* to control delivery.
|
|
65
|
+
*
|
|
30
66
|
* @example "Speak slowly and calmly, as if telling a bedtime story"
|
|
31
67
|
* @example "Use an upbeat, enthusiastic tone with moderate pace"
|
|
32
68
|
* @example "Speak with a British accent"
|
|
@@ -34,8 +70,8 @@ export interface GeminiTTSProviderOptions {
|
|
|
34
70
|
systemInstruction?: string;
|
|
35
71
|
/**
|
|
36
72
|
* Language code hint for the speech synthesis.
|
|
37
|
-
* Gemini TTS supports
|
|
38
|
-
*
|
|
73
|
+
* Gemini 3.1 Flash TTS supports 70+ languages with auto-detection;
|
|
74
|
+
* the 2.5 TTS models support 24 languages.
|
|
39
75
|
*
|
|
40
76
|
* @example "en-US" for American English
|
|
41
77
|
* @example "es-ES" for Spanish (Spain)
|
package/dist/esm/adapters/tts.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
2
2
|
import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../utils/client.js";
|
|
3
|
+
import { GEMINI_TTS_VOICES } from "../model-meta.js";
|
|
3
4
|
class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
4
5
|
constructor(config, model) {
|
|
5
|
-
super(
|
|
6
|
+
super(model, config);
|
|
6
7
|
this.name = "gemini";
|
|
7
8
|
this.client = createGeminiClient(config);
|
|
8
9
|
}
|
|
@@ -13,17 +14,36 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
13
14
|
* @see https://ai.google.dev/gemini-api/docs/speech-generation
|
|
14
15
|
*/
|
|
15
16
|
async generateSpeech(options) {
|
|
16
|
-
const { logger } = options;
|
|
17
|
-
const { model, text, modelOptions } = options;
|
|
17
|
+
const { model, text, modelOptions, voice, logger } = options;
|
|
18
18
|
logger.request(`activity=generateSpeech provider=gemini model=${model}`, {
|
|
19
19
|
provider: "gemini",
|
|
20
20
|
model
|
|
21
21
|
});
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const speechConfig = {};
|
|
23
|
+
if (modelOptions?.multiSpeakerVoiceConfig) {
|
|
24
|
+
const speakerConfigs = modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs;
|
|
25
|
+
if (!Array.isArray(speakerConfigs) || speakerConfigs.length < 1 || speakerConfigs.length > 2) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : "non-array"}.`
|
|
28
|
+
);
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
speechConfig.multiSpeakerVoiceConfig = modelOptions.multiSpeakerVoiceConfig;
|
|
31
|
+
} else {
|
|
32
|
+
if (voice !== void 0 && !GEMINI_TTS_VOICES.includes(voice)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Invalid Gemini TTS voice "${voice}". Valid voices are: ${GEMINI_TTS_VOICES.join(", ")}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const defaultVoiceName = voice ?? "Kore";
|
|
38
|
+
const supplied = modelOptions?.voiceConfig;
|
|
39
|
+
const resolvedVoiceName = supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName;
|
|
40
|
+
speechConfig.voiceConfig = {
|
|
41
|
+
prebuiltVoiceConfig: { voiceName: resolvedVoiceName }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (modelOptions?.languageCode) {
|
|
45
|
+
speechConfig.languageCode = modelOptions.languageCode;
|
|
46
|
+
}
|
|
27
47
|
try {
|
|
28
48
|
const response = await this.client.models.generateContent({
|
|
29
49
|
model,
|
|
@@ -35,15 +55,12 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
35
55
|
],
|
|
36
56
|
config: {
|
|
37
57
|
responseModalities: ["AUDIO"],
|
|
38
|
-
speechConfig
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
speechConfig,
|
|
59
|
+
// systemInstruction belongs inside `config` per the @google/genai
|
|
60
|
+
// contract — matches sibling Gemini adapters (summarize, text).
|
|
61
|
+
...modelOptions?.systemInstruction && {
|
|
62
|
+
systemInstruction: modelOptions.systemInstruction
|
|
43
63
|
}
|
|
44
|
-
},
|
|
45
|
-
...modelOptions?.systemInstruction && {
|
|
46
|
-
systemInstruction: modelOptions.systemInstruction
|
|
47
64
|
}
|
|
48
65
|
});
|
|
49
66
|
const candidate = response.candidates?.[0];
|
|
@@ -58,8 +75,24 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
58
75
|
throw new Error("No audio data in Gemini TTS response");
|
|
59
76
|
}
|
|
60
77
|
const audioBase64 = audioPart.inlineData.data;
|
|
61
|
-
const mimeType = audioPart.inlineData.mimeType
|
|
62
|
-
const
|
|
78
|
+
const mimeType = audioPart.inlineData.mimeType;
|
|
79
|
+
const pcm = parsePcmMimeType(mimeType);
|
|
80
|
+
if (pcm) {
|
|
81
|
+
const wavBase64 = wrapPcmBase64AsWav(
|
|
82
|
+
audioBase64,
|
|
83
|
+
pcm.sampleRate,
|
|
84
|
+
pcm.channels,
|
|
85
|
+
pcm.bitsPerSample
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
id: generateId(this.name),
|
|
89
|
+
model,
|
|
90
|
+
audio: wavBase64,
|
|
91
|
+
format: "wav",
|
|
92
|
+
contentType: "audio/wav"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const format = mimeType.split(";")[0].split("/")[1] || "wav";
|
|
63
96
|
return {
|
|
64
97
|
id: generateId(this.name),
|
|
65
98
|
model,
|
|
@@ -76,8 +109,71 @@ class GeminiTTSAdapter extends BaseTTSAdapter {
|
|
|
76
109
|
}
|
|
77
110
|
}
|
|
78
111
|
}
|
|
112
|
+
function parsePcmMimeType(mimeType) {
|
|
113
|
+
const normalized = mimeType.toLowerCase();
|
|
114
|
+
const subtype = normalized.split(";")[0].split("/")[1] ?? "";
|
|
115
|
+
if (subtype.includes("wav")) return void 0;
|
|
116
|
+
const bitDepthMatch = /^audio\/l(\d+)/.exec(normalized);
|
|
117
|
+
const isPcm = bitDepthMatch !== null || normalized.startsWith("audio/pcm") || normalized.startsWith("audio/x-pcm") || normalized.includes("codec=pcm");
|
|
118
|
+
if (!isPcm) return void 0;
|
|
119
|
+
const rateMatch = /rate=(\d+)/.exec(normalized);
|
|
120
|
+
const channelsMatch = /channels=(\d+)/.exec(normalized);
|
|
121
|
+
const bitsPerSample = bitDepthMatch ? Number(bitDepthMatch[1]) : 16;
|
|
122
|
+
return {
|
|
123
|
+
sampleRate: rateMatch ? Number(rateMatch[1]) : 24e3,
|
|
124
|
+
channels: channelsMatch ? Number(channelsMatch[1]) : 1,
|
|
125
|
+
bitsPerSample
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function wrapPcmBase64AsWav(pcmBase64, sampleRate, channels = 1, bitsPerSample = 16) {
|
|
129
|
+
if (bitsPerSample !== 16) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Unsupported PCM bit depth ${bitsPerSample}: only 16-bit PCM can be wrapped as WAV.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const pcmBytes = typeof Buffer !== "undefined" ? new Uint8Array(Buffer.from(pcmBase64, "base64")) : decodeBase64(pcmBase64);
|
|
135
|
+
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
136
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
137
|
+
const dataSize = pcmBytes.byteLength;
|
|
138
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
139
|
+
const view = new DataView(buffer);
|
|
140
|
+
writeAscii(view, 0, "RIFF");
|
|
141
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
142
|
+
writeAscii(view, 8, "WAVE");
|
|
143
|
+
writeAscii(view, 12, "fmt ");
|
|
144
|
+
view.setUint32(16, 16, true);
|
|
145
|
+
view.setUint16(20, 1, true);
|
|
146
|
+
view.setUint16(22, channels, true);
|
|
147
|
+
view.setUint32(24, sampleRate, true);
|
|
148
|
+
view.setUint32(28, byteRate, true);
|
|
149
|
+
view.setUint16(32, blockAlign, true);
|
|
150
|
+
view.setUint16(34, bitsPerSample, true);
|
|
151
|
+
writeAscii(view, 36, "data");
|
|
152
|
+
view.setUint32(40, dataSize, true);
|
|
153
|
+
new Uint8Array(buffer, 44).set(pcmBytes);
|
|
154
|
+
if (typeof Buffer !== "undefined") {
|
|
155
|
+
return Buffer.from(buffer).toString("base64");
|
|
156
|
+
}
|
|
157
|
+
let binary = "";
|
|
158
|
+
const bytes = new Uint8Array(buffer);
|
|
159
|
+
for (let i = 0; i < bytes.byteLength; i += 1) {
|
|
160
|
+
binary += String.fromCharCode(bytes[i]);
|
|
161
|
+
}
|
|
162
|
+
return btoa(binary);
|
|
163
|
+
}
|
|
164
|
+
function decodeBase64(b64) {
|
|
165
|
+
const binary = atob(b64);
|
|
166
|
+
const out = new Uint8Array(binary.length);
|
|
167
|
+
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
function writeAscii(view, offset, text) {
|
|
171
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
172
|
+
view.setUint8(offset + i, text.charCodeAt(i));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
79
175
|
function createGeminiSpeech(model, apiKey, config) {
|
|
80
|
-
return new GeminiTTSAdapter({
|
|
176
|
+
return new GeminiTTSAdapter({ ...config, apiKey }, model);
|
|
81
177
|
}
|
|
82
178
|
function geminiSpeech(model, config) {
|
|
83
179
|
const apiKey = getGeminiApiKeyFromEnv();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.js","sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Provider-specific options for Gemini TTS\n *\n * @experimental Gemini TTS is an experimental feature.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\nexport interface GeminiTTSProviderOptions {\n /**\n * Voice configuration for TTS.\n * Choose from 30 available voices with different characteristics.\n */\n voiceConfig?: {\n prebuiltVoiceConfig?: {\n /**\n * The voice name to use for speech synthesis.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation#voices\n */\n voiceName?: GeminiTTSVoice\n }\n }\n\n /**\n * System instruction for controlling speech style.\n * Use natural language to describe the desired speaking style,\n * pace, tone, accent, or other characteristics.\n *\n * @example \"Speak slowly and calmly, as if telling a bedtime story\"\n * @example \"Use an upbeat, enthusiastic tone with moderate pace\"\n * @example \"Speak with a British accent\"\n */\n systemInstruction?: string\n\n /**\n * Language code hint for the speech synthesis.\n * Gemini TTS supports 24 languages and can auto-detect,\n * but you can provide a hint for better results.\n *\n * @example \"en-US\" for American English\n * @example \"es-ES\" for Spanish (Spain)\n * @example \"ja-JP\" for Japanese\n */\n languageCode?: string\n}\n\n/**\n * Configuration for Gemini TTS adapter\n *\n * @experimental Gemini TTS is an experimental feature.\n */\nexport interface GeminiTTSConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini TTS */\nexport type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number]\n\n/**\n * Gemini Text-to-Speech Adapter\n *\n * Tree-shakeable adapter for Gemini TTS functionality.\n *\n * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires\n * different handling than traditional REST APIs. This adapter provides a\n * simplified interface but may have limitations.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Models:\n * - gemini-2.5-flash-preview-tts\n */\nexport class GeminiTTSAdapter<\n TModel extends GeminiTTSModel,\n> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiTTSConfig, model: TModel) {\n super(config, model)\n this.client = createGeminiClient(config)\n }\n\n /**\n * Generate speech from text using Gemini's TTS model.\n *\n * @experimental This implementation is experimental and may change.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\n async generateSpeech(\n options: TTSOptions<GeminiTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n const { model, text, modelOptions } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const voiceConfig = modelOptions?.voiceConfig || {\n prebuiltVoiceConfig: {\n voiceName: 'Kore',\n },\n }\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [{ text }],\n },\n ],\n config: {\n responseModalities: ['AUDIO'],\n speechConfig: {\n voiceConfig,\n ...(modelOptions?.languageCode && {\n languageCode: modelOptions.languageCode,\n }),\n },\n },\n ...(modelOptions?.systemInstruction && {\n systemInstruction: modelOptions.systemInstruction,\n }),\n })\n\n // Extract audio data from response\n const candidate = response.candidates?.[0]\n const parts = candidate?.content?.parts\n\n if (!parts || parts.length === 0) {\n throw new Error('No audio output received from Gemini TTS')\n }\n\n // Look for inline data (audio)\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart || !audioPart.inlineData || !audioPart.inlineData.data) {\n throw new Error('No audio data in Gemini TTS response')\n }\n\n const audioBase64 = audioPart.inlineData.data\n const mimeType = audioPart.inlineData.mimeType || 'audio/wav'\n const format = mimeType.split('/')[1] || 'wav'\n\n return {\n id: generateId(this.name),\n model,\n audio: audioBase64,\n format,\n contentType: mimeType,\n }\n } catch (error) {\n logger.errors('gemini.generateSpeech fatal', {\n error,\n source: 'gemini.generateSpeech',\n })\n throw error\n }\n }\n}\n\n/**\n * Creates a Gemini TTS adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini TTS adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiSpeech('gemini-2.5-flash-preview-tts', \"your-api-key\");\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n return new GeminiTTSAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini speech adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini speech adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiSpeech('gemini-2.5-flash-preview-tts');\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Welcome to TanStack AI!'\n * });\n * ```\n */\nexport function geminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiSpeech(model, apiKey, config)\n}\n"],"names":[],"mappings":";;AA+EO,MAAM,yBAEH,eAAiD;AAAA,EAKzD,YAAY,QAAyB,OAAe;AAClD,UAAM,QAAQ,KAAK;AALrB,SAAS,OAAO;AAMd,SAAK,SAAS,mBAAmB,MAAM;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,eACJ,SACoB;AACpB,UAAM,EAAE,WAAW;AACnB,UAAM,EAAE,OAAO,MAAM,aAAA,IAAiB;AAEtC,WAAO,QAAQ,iDAAiD,KAAK,IAAI;AAAA,MACvE,UAAU;AAAA,MACV;AAAA,IAAA,CACD;AAED,UAAM,cAAc,cAAc,eAAe;AAAA,MAC/C,qBAAqB;AAAA,QACnB,WAAW;AAAA,MAAA;AAAA,IACb;AAGF,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,OAAO,gBAAgB;AAAA,QACxD;AAAA,QACA,UAAU;AAAA,UACR;AAAA,YACE,MAAM;AAAA,YACN,OAAO,CAAC,EAAE,KAAA,CAAM;AAAA,UAAA;AAAA,QAClB;AAAA,QAEF,QAAQ;AAAA,UACN,oBAAoB,CAAC,OAAO;AAAA,UAC5B,cAAc;AAAA,YACZ;AAAA,YACA,GAAI,cAAc,gBAAgB;AAAA,cAChC,cAAc,aAAa;AAAA,YAAA;AAAA,UAC7B;AAAA,QACF;AAAA,QAEF,GAAI,cAAc,qBAAqB;AAAA,UACrC,mBAAmB,aAAa;AAAA,QAAA;AAAA,MAClC,CACD;AAGD,YAAM,YAAY,SAAS,aAAa,CAAC;AACzC,YAAM,QAAQ,WAAW,SAAS;AAElC,UAAI,CAAC,SAAS,MAAM,WAAW,GAAG;AAChC,cAAM,IAAI,MAAM,0CAA0C;AAAA,MAC5D;AAGA,YAAM,YAAY,MAAM;AAAA,QAAK,CAAC,SAC5B,KAAK,YAAY,UAAU,WAAW,QAAQ;AAAA,MAAA;AAGhD,UAAI,CAAC,aAAa,CAAC,UAAU,cAAc,CAAC,UAAU,WAAW,MAAM;AACrE,cAAM,IAAI,MAAM,sCAAsC;AAAA,MACxD;AAEA,YAAM,cAAc,UAAU,WAAW;AACzC,YAAM,WAAW,UAAU,WAAW,YAAY;AAClD,YAAM,SAAS,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK;AAEzC,aAAO;AAAA,QACL,IAAI,WAAW,KAAK,IAAI;AAAA,QACxB;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA,aAAa;AAAA,MAAA;AAAA,IAEjB,SAAS,OAAO;AACd,aAAO,OAAO,+BAA+B;AAAA,QAC3C;AAAA,QACA,QAAQ;AAAA,MAAA,CACT;AACD,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAuBO,SAAS,mBACd,OACA,QACA,QAC0B;AAC1B,SAAO,IAAI,iBAAiB,EAAE,QAAQ,GAAG,OAAA,GAAU,KAAK;AAC1D;AA4BO,SAAS,aACd,OACA,QAC0B;AAC1B,QAAM,SAAS,uBAAA;AACf,SAAO,mBAAmB,OAAO,QAAQ,MAAM;AACjD;"}
|
|
1
|
+
{"version":3,"file":"tts.js","sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_TTS_VOICES } from '../model-meta'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI, SpeechConfig } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\n * Configuration for a single speaker in a multi-speaker dialogue.\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n */\nexport interface GeminiSpeakerVoiceConfig {\n /** A name used in the prompt to refer to this speaker */\n speaker: string\n /** Voice configuration for this speaker */\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: GeminiTTSVoice\n }\n }\n}\n\n/**\n * Provider-specific options for Gemini TTS\n *\n * @experimental Gemini TTS is an experimental feature.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n * @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-tts-preview\n */\nexport interface GeminiTTSProviderOptions {\n /**\n * Voice configuration for single-speaker TTS.\n * Choose from 30 available voices with different characteristics.\n *\n * Use `multiSpeakerVoiceConfig` instead for dialogues.\n */\n voiceConfig?: {\n prebuiltVoiceConfig?: {\n /**\n * The voice name to use for speech synthesis.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation#voices\n */\n voiceName?: GeminiTTSVoice\n }\n }\n\n /**\n * Multi-speaker voice configuration (up to 2 speakers).\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n *\n * Each speaker's lines in the prompt are prefixed with the name defined\n * here, e.g.:\n *\n * ```text\n * Joe: Hey, how's it going?\n * Jane: Not bad, you?\n * ```\n */\n multiSpeakerVoiceConfig?: {\n speakerVoiceConfigs: Array<GeminiSpeakerVoiceConfig>\n }\n\n /**\n * System instruction for controlling speech style.\n * Use natural language to describe the desired speaking style,\n * pace, tone, accent, or other characteristics.\n *\n * With Gemini 3.1 Flash TTS, you can also use inline audio tags like\n * `[whispering]`, `[laughs]`, `[excited]` directly in the input text\n * to control delivery.\n *\n * @example \"Speak slowly and calmly, as if telling a bedtime story\"\n * @example \"Use an upbeat, enthusiastic tone with moderate pace\"\n * @example \"Speak with a British accent\"\n */\n systemInstruction?: string\n\n /**\n * Language code hint for the speech synthesis.\n * Gemini 3.1 Flash TTS supports 70+ languages with auto-detection;\n * the 2.5 TTS models support 24 languages.\n *\n * @example \"en-US\" for American English\n * @example \"es-ES\" for Spanish (Spain)\n * @example \"ja-JP\" for Japanese\n */\n languageCode?: string\n}\n\n/**\n * Configuration for Gemini TTS adapter\n *\n * @experimental Gemini TTS is an experimental feature.\n */\nexport interface GeminiTTSConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini TTS */\nexport type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number]\n\n/**\n * Gemini Text-to-Speech Adapter\n *\n * Tree-shakeable adapter for Gemini TTS functionality.\n *\n * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires\n * different handling than traditional REST APIs. This adapter provides a\n * simplified interface but may have limitations.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Models:\n * - gemini-2.5-flash-preview-tts\n */\nexport class GeminiTTSAdapter<\n TModel extends GeminiTTSModel,\n> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiTTSConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n /**\n * Generate speech from text using Gemini's TTS model.\n *\n * @experimental This implementation is experimental and may change.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\n async generateSpeech(\n options: TTSOptions<GeminiTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { model, text, modelOptions, voice, logger } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const speechConfig: SpeechConfig = {}\n\n if (modelOptions?.multiSpeakerVoiceConfig) {\n // Validate multi-speaker config: 1 or 2 speakers allowed.\n const speakerConfigs =\n modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs\n if (\n !Array.isArray(speakerConfigs) ||\n speakerConfigs.length < 1 ||\n speakerConfigs.length > 2\n ) {\n throw new Error(\n `Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : 'non-array'}.`,\n )\n }\n speechConfig.multiSpeakerVoiceConfig =\n modelOptions.multiSpeakerVoiceConfig\n } else {\n // Honor the standard TTSOptions.voice (used by every other TTS adapter)\n // as a fallback for the prebuilt voice name. If an explicit\n // modelOptions.voiceConfig is supplied its values win — but we still\n // fall back to `voice` / 'Kore' if the supplied voiceConfig is missing\n // prebuiltVoiceConfig.voiceName.\n if (\n voice !== undefined &&\n !(GEMINI_TTS_VOICES as ReadonlyArray<string>).includes(voice)\n ) {\n throw new Error(\n `Invalid Gemini TTS voice \"${voice}\". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,\n )\n }\n const defaultVoiceName = (voice as GeminiTTSVoice | undefined) ?? 'Kore'\n const supplied = modelOptions?.voiceConfig\n const resolvedVoiceName =\n supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName\n speechConfig.voiceConfig = {\n prebuiltVoiceConfig: { voiceName: resolvedVoiceName },\n }\n }\n\n if (modelOptions?.languageCode) {\n speechConfig.languageCode = modelOptions.languageCode\n }\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [{ text }],\n },\n ],\n config: {\n responseModalities: ['AUDIO'],\n speechConfig,\n // systemInstruction belongs inside `config` per the @google/genai\n // contract — matches sibling Gemini adapters (summarize, text).\n ...(modelOptions?.systemInstruction && {\n systemInstruction: modelOptions.systemInstruction,\n }),\n },\n })\n\n // Extract audio data from response\n const candidate = response.candidates?.[0]\n const parts = candidate?.content?.parts\n\n if (!parts || parts.length === 0) {\n throw new Error('No audio output received from Gemini TTS')\n }\n\n // Look for inline data (audio)\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart || !audioPart.inlineData || !audioPart.inlineData.data) {\n throw new Error('No audio data in Gemini TTS response')\n }\n\n const audioBase64 = audioPart.inlineData.data\n // mime is guaranteed by the `startsWith('audio/')` find predicate above.\n const mimeType = audioPart.inlineData.mimeType as string\n\n // Gemini TTS models return raw 16-bit LE PCM with a mime type like\n // `audio/L16;codec=pcm;rate=24000`. That isn't playable in an <audio>\n // element and the bare string isn't a usable file extension, so we\n // prepend a RIFF/WAV header here and normalize the result to audio/wav.\n const pcm = parsePcmMimeType(mimeType)\n if (pcm) {\n const wavBase64 = wrapPcmBase64AsWav(\n audioBase64,\n pcm.sampleRate,\n pcm.channels,\n pcm.bitsPerSample,\n )\n return {\n id: generateId(this.name),\n model,\n audio: wavBase64,\n format: 'wav',\n contentType: 'audio/wav',\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format.\n const format = mimeType.split(';')[0]!.split('/')[1] || 'wav'\n\n return {\n id: generateId(this.name),\n model,\n audio: audioBase64,\n format,\n contentType: mimeType,\n }\n } catch (error) {\n logger.errors('gemini.generateSpeech fatal', {\n error,\n source: 'gemini.generateSpeech',\n })\n throw error\n }\n }\n}\n\nfunction parsePcmMimeType(\n mimeType: string,\n): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {\n const normalized = mimeType.toLowerCase()\n const subtype = normalized.split(';')[0]!.split('/')[1] ?? ''\n // Exclude containerized wav (e.g. `audio/wav;codec=pcm`) — those already\n // carry a RIFF header and must not be re-wrapped.\n if (subtype.includes('wav')) return undefined\n\n // Accept the variants Gemini and other providers actually emit:\n // - audio/L16;codec=pcm;rate=24000 (IANA PCM with bit depth in the type)\n // - audio/L24 and friends\n // - audio/pcm and audio/x-pcm\n // - anything else that explicitly tags codec=pcm and isn't wav-containered\n const bitDepthMatch = /^audio\\/l(\\d+)/.exec(normalized)\n const isPcm =\n bitDepthMatch !== null ||\n normalized.startsWith('audio/pcm') ||\n normalized.startsWith('audio/x-pcm') ||\n normalized.includes('codec=pcm')\n if (!isPcm) return undefined\n\n const rateMatch = /rate=(\\d+)/.exec(normalized)\n const channelsMatch = /channels=(\\d+)/.exec(normalized)\n // Default to 16-bit when the mime type doesn't specify — matches Gemini's\n // audio/L16;codec=pcm;rate=24000 response.\n const bitsPerSample = bitDepthMatch ? Number(bitDepthMatch[1]) : 16\n return {\n sampleRate: rateMatch ? Number(rateMatch[1]) : 24000,\n channels: channelsMatch ? Number(channelsMatch[1]) : 1,\n bitsPerSample,\n }\n}\n\nfunction wrapPcmBase64AsWav(\n pcmBase64: string,\n sampleRate: number,\n channels = 1,\n bitsPerSample = 16,\n): string {\n // The WAV writer below emits a 16-bit PCM fmt chunk. If the source claims a\n // different bit depth we'd be lying about the payload, so bail out loudly\n // rather than producing a corrupt file.\n if (bitsPerSample !== 16) {\n throw new Error(\n `Unsupported PCM bit depth ${bitsPerSample}: only 16-bit PCM can be wrapped as WAV.`,\n )\n }\n\n const pcmBytes =\n typeof Buffer !== 'undefined'\n ? new Uint8Array(Buffer.from(pcmBase64, 'base64'))\n : decodeBase64(pcmBase64)\n\n const byteRate = (sampleRate * channels * bitsPerSample) / 8\n const blockAlign = (channels * bitsPerSample) / 8\n const dataSize = pcmBytes.byteLength\n const buffer = new ArrayBuffer(44 + dataSize)\n const view = new DataView(buffer)\n\n writeAscii(view, 0, 'RIFF')\n view.setUint32(4, 36 + dataSize, true)\n writeAscii(view, 8, 'WAVE')\n writeAscii(view, 12, 'fmt ')\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, channels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n writeAscii(view, 36, 'data')\n view.setUint32(40, dataSize, true)\n new Uint8Array(buffer, 44).set(pcmBytes)\n\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64')\n }\n let binary = ''\n const bytes = new Uint8Array(buffer)\n for (let i = 0; i < bytes.byteLength; i += 1) {\n binary += String.fromCharCode(bytes[i]!)\n }\n return btoa(binary)\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i)\n return out\n}\n\nfunction writeAscii(view: DataView, offset: number, text: string): void {\n for (let i = 0; i < text.length; i += 1) {\n view.setUint8(offset + i, text.charCodeAt(i))\n }\n}\n\n/**\n * Creates a Gemini TTS adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini TTS adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiSpeech('gemini-2.5-flash-preview-tts', \"your-api-key\");\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiTTSAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini speech adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini speech adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiSpeech('gemini-2.5-flash-preview-tts');\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Welcome to TanStack AI!'\n * });\n * ```\n */\nexport function geminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiSpeech(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|