@tanstack/ai-gemini 0.29.4 → 0.31.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/README.md +42 -16
- package/dist/esm/adapters/text.d.ts +9 -1
- package/dist/esm/adapters/text.js +102 -6
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/tts.d.ts +5 -1
- package/dist/esm/adapters/tts.js +18 -4
- package/dist/esm/adapters/tts.js.map +1 -1
- package/dist/esm/experimental/text-interactions/adapter.d.ts +1 -0
- package/dist/esm/experimental/text-interactions/adapter.js +103 -3
- package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/text.ts +140 -9
- package/src/adapters/tts.ts +38 -13
- package/src/experimental/text-interactions/adapter.ts +163 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.js","names":[],"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 { buildGeminiUsage } from '../usage'\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/client'\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 readonly 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 // Surface token usage (with per-modality breakdown) when Gemini reports\n // it. Spread conditionally for exactOptionalPropertyTypes — shared by both\n // the PCM→WAV and pass-through return paths below.\n const usageField = response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}\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 ...usageField,\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format. `String.split` always returns at\n // least one element, so [0] is defined.\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 ...usageField,\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 (const byte of bytes) {\n binary += String.fromCharCode(byte)\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tts.js","names":[],"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 { buildGeminiUsage } from '../usage'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSCapabilities, TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI, SpeechConfig } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils/client'\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 /**\n * Gemini multi-speaker TTS tops out at 2 voices, and reports no timings.\n */\n override readonly capabilities: TTSCapabilities = { maxSpeakers: 2 }\n\n private readonly 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, modelOptions, voice, logger, turns } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const speechConfig: SpeechConfig = {}\n // Gemini has no turn structure on the wire: speakers are named in the\n // prompt and mapped to voices in the config. `turns` carries voice names,\n // so the voice name doubles as the speaker label.\n const text = turns\n ? turns.map((turn) => `${turn.voice}: ${turn.text}`).join('\\n')\n : options.text\n\n if (turns) {\n const voiceNames = [...new Set(turns.map((turn) => turn.voice))].map(\n toGeminiVoice,\n )\n speechConfig.multiSpeakerVoiceConfig = {\n speakerVoiceConfigs: voiceNames.map((voiceName) => ({\n speaker: voiceName,\n voiceConfig: { prebuiltVoiceConfig: { voiceName } },\n })),\n }\n } else 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 const defaultVoiceName =\n voice !== undefined ? toGeminiVoice(voice) : '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 // Surface token usage (with per-modality breakdown) when Gemini reports\n // it. Spread conditionally for exactOptionalPropertyTypes — shared by both\n // the PCM→WAV and pass-through return paths below.\n const usageField = response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}\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 ...usageField,\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format. `String.split` always returns at\n // least one element, so [0] is defined.\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 ...usageField,\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/** Narrow a caller-supplied voice string to a known Gemini voice, or throw. */\nfunction toGeminiVoice(voice: string): GeminiTTSVoice {\n const match = GEMINI_TTS_VOICES.find((known) => known === voice)\n if (!match) {\n throw new Error(\n `Invalid Gemini TTS voice \"${voice}\". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,\n )\n }\n return match\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 (const byte of bytes) {\n binary += String.fromCharCode(byte)\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"],"mappings":"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"}
|
|
@@ -78,6 +78,7 @@ export declare class GeminiTextInteractionsAdapter<TModel extends GeminiModels,
|
|
|
78
78
|
constructor(config: GeminiTextInteractionsConfig, model: TModel);
|
|
79
79
|
chatStream(options: TextOptions<GeminiTextInteractionsProviderOptions>): AsyncIterable<AdapterYieldChunk>;
|
|
80
80
|
structuredOutput(options: StructuredOutputOptions<GeminiTextInteractionsProviderOptions>): Promise<StructuredOutputResult<unknown>>;
|
|
81
|
+
structuredOutputStream(options: StructuredOutputOptions<GeminiTextInteractionsProviderOptions>): AsyncIterable<AdapterYieldChunk>;
|
|
81
82
|
}
|
|
82
83
|
/** @experimental Interactions API is in Beta. */
|
|
83
84
|
export declare function createGeminiTextInteractions<TModel extends GeminiModels>(model: TModel, apiKey: string, config?: Omit<GeminiTextInteractionsConfig, 'apiKey'>): GeminiTextInteractionsAdapter<TModel, ResolveProviderOptions<TModel>, ResolveInputModalities<TModel>, ResolveToolCapabilities<TModel>>;
|
|
@@ -70,7 +70,7 @@ var GeminiTextInteractionsAdapter = class extends BaseTextAdapter {
|
|
|
70
70
|
const stream = await this.client.interactions.create({
|
|
71
71
|
...request,
|
|
72
72
|
stream: true
|
|
73
|
-
}, { signal: options
|
|
73
|
+
}, { signal: abortSignalFromOptions(options) });
|
|
74
74
|
for await (const chunk of translateInteractionEvents(stream, options.model, runId, threadId, options.parentRunId, timestamp, this.name, logger)) {
|
|
75
75
|
if (chunk.type === EventType.CUSTOM && chunk.name === "gemini.interactionId") {
|
|
76
76
|
const value = chunk.value;
|
|
@@ -145,14 +145,14 @@ var GeminiTextInteractionsAdapter = class extends BaseTextAdapter {
|
|
|
145
145
|
model: this.model,
|
|
146
146
|
request
|
|
147
147
|
});
|
|
148
|
-
const result = await this.client.interactions.create(request, { signal: chatOptions
|
|
148
|
+
const result = await this.client.interactions.create(request, { signal: abortSignalFromOptions(chatOptions) });
|
|
149
149
|
const rawText = extractTextFromInteraction(result);
|
|
150
150
|
if (!rawText) throw new Error(`Gemini Interactions returned no text output for structured-output request (status: ${result.status}). The model may have produced only tool calls or non-text content.`);
|
|
151
151
|
let parsed;
|
|
152
152
|
try {
|
|
153
153
|
parsed = JSON.parse(rawText);
|
|
154
154
|
} catch {
|
|
155
|
-
throw new Error(
|
|
155
|
+
throw new Error(jsonContentParseError(rawText, "structured output"));
|
|
156
156
|
}
|
|
157
157
|
return {
|
|
158
158
|
data: parsed,
|
|
@@ -166,7 +166,107 @@ var GeminiTextInteractionsAdapter = class extends BaseTextAdapter {
|
|
|
166
166
|
throw new Error(error instanceof Error ? error.message : "An unknown error occurred during structured output generation.", { cause: error });
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
async *structuredOutputStream(options) {
|
|
170
|
+
const { chatOptions, outputSchema } = options;
|
|
171
|
+
const runId = chatOptions.runId ?? generateId(this.name);
|
|
172
|
+
const threadId = chatOptions.threadId ?? generateId(this.name);
|
|
173
|
+
const timestamp = Date.now();
|
|
174
|
+
const effectivePreviousInteractionId = chatOptions.modelOptions?.previous_interaction_id ?? this.interactionIdByThread.get(threadId);
|
|
175
|
+
const request = {
|
|
176
|
+
...buildInteractionsRequest({
|
|
177
|
+
...chatOptions,
|
|
178
|
+
modelOptions: {
|
|
179
|
+
...chatOptions.modelOptions,
|
|
180
|
+
previous_interaction_id: effectivePreviousInteractionId
|
|
181
|
+
}
|
|
182
|
+
}),
|
|
183
|
+
stream: true,
|
|
184
|
+
response_format: {
|
|
185
|
+
type: "text",
|
|
186
|
+
mime_type: "application/json",
|
|
187
|
+
schema: outputSchema
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
chatOptions.logger.request(`activity=structuredOutputStream provider=gemini-text-interactions model=${this.model} messages=${chatOptions.messages.length}`, {
|
|
192
|
+
provider: this.name,
|
|
193
|
+
model: this.model,
|
|
194
|
+
request
|
|
195
|
+
});
|
|
196
|
+
const stream = await this.client.interactions.create(request, { signal: abortSignalFromOptions(chatOptions) });
|
|
197
|
+
let rawText = "";
|
|
198
|
+
let finished;
|
|
199
|
+
let failed = false;
|
|
200
|
+
for await (const chunk of translateInteractionEvents(stream, chatOptions.model, runId, threadId, chatOptions.parentRunId, timestamp, this.name, chatOptions.logger)) {
|
|
201
|
+
if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) rawText += chunk.delta;
|
|
202
|
+
if (chunk.type === EventType.CUSTOM && chunk.name === "gemini.interactionId") {
|
|
203
|
+
const value = chunk.value;
|
|
204
|
+
this.interactionIdByThread.set(threadId, value.interactionId);
|
|
205
|
+
}
|
|
206
|
+
if (chunk.type === EventType.RUN_ERROR) failed = true;
|
|
207
|
+
if (chunk.type === EventType.RUN_FINISHED) finished = chunk;
|
|
208
|
+
else yield chunk;
|
|
209
|
+
}
|
|
210
|
+
if (failed) return;
|
|
211
|
+
if (!finished) {
|
|
212
|
+
yield interactionsStructuredStreamError(chatOptions, runId, "Gemini Interactions structured-output stream ended without a terminal event", "truncated-stream");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!rawText) {
|
|
216
|
+
yield interactionsStructuredStreamError(chatOptions, runId, "Gemini Interactions structured-output stream contained no content", "empty-response");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let object;
|
|
220
|
+
try {
|
|
221
|
+
object = JSON.parse(rawText);
|
|
222
|
+
} catch {
|
|
223
|
+
yield interactionsStructuredStreamError(chatOptions, runId, jsonContentParseError(rawText, "Gemini Interactions structured-output stream"), "parse-error");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
yield {
|
|
227
|
+
type: EventType.CUSTOM,
|
|
228
|
+
name: "structured-output.complete",
|
|
229
|
+
value: {
|
|
230
|
+
object,
|
|
231
|
+
raw: rawText
|
|
232
|
+
},
|
|
233
|
+
model: chatOptions.model,
|
|
234
|
+
timestamp: Date.now()
|
|
235
|
+
};
|
|
236
|
+
yield {
|
|
237
|
+
...finished,
|
|
238
|
+
timestamp: Date.now()
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const message = error instanceof Error ? error.message : "An unknown error occurred during structured output streaming.";
|
|
242
|
+
chatOptions.logger.errors("gemini-text-interactions.structuredOutputStream fatal", {
|
|
243
|
+
error,
|
|
244
|
+
source: "gemini-text-interactions.structuredOutputStream"
|
|
245
|
+
});
|
|
246
|
+
yield interactionsStructuredStreamError(chatOptions, runId, message, "provider-error");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
169
249
|
};
|
|
250
|
+
function abortSignalFromOptions(options) {
|
|
251
|
+
return options.request?.signal ?? options.abortController?.signal;
|
|
252
|
+
}
|
|
253
|
+
function jsonContentParseError(rawText, label) {
|
|
254
|
+
return `Failed to parse ${label} as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`;
|
|
255
|
+
}
|
|
256
|
+
function interactionsStructuredStreamError(options, runId, message, code) {
|
|
257
|
+
return {
|
|
258
|
+
type: EventType.RUN_ERROR,
|
|
259
|
+
runId,
|
|
260
|
+
model: options.model,
|
|
261
|
+
timestamp: Date.now(),
|
|
262
|
+
message,
|
|
263
|
+
code,
|
|
264
|
+
error: {
|
|
265
|
+
message,
|
|
266
|
+
code
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
170
270
|
/** @experimental Interactions API is in Beta. */
|
|
171
271
|
function createGeminiTextInteractions(model, apiKey, config) {
|
|
172
272
|
return new GeminiTextInteractionsAdapter({
|