diff --git a/.changeset/blue-pets-sin.md b/.changeset/blue-pets-sin.md new file mode 100644 index 00000000000..6e27789956c --- /dev/null +++ b/.changeset/blue-pets-sin.md @@ -0,0 +1,6 @@ +--- +'firebase': minor +'@firebase/ai': minor +--- + +Add support for the Gemini Live API. diff --git a/common/api-review/ai.api.md b/common/api-review/ai.api.md index 9210a76f20d..6d70313a3af 100644 --- a/common/api-review/ai.api.md +++ b/common/api-review/ai.api.md @@ -41,6 +41,7 @@ export const AIErrorCode: { readonly REQUEST_ERROR: "request-error"; readonly RESPONSE_ERROR: "response-error"; readonly FETCH_ERROR: "fetch-error"; + readonly SESSION_CLOSED: "session-closed"; readonly INVALID_CONTENT: "invalid-content"; readonly API_NOT_ENABLED: "api-not-enabled"; readonly INVALID_SCHEMA: "invalid-schema"; @@ -94,6 +95,11 @@ export class ArraySchema extends Schema { toJSON(): SchemaRequest; } +// @beta +export interface AudioConversationController { + stop: () => Promise; +} + // @public export abstract class Backend { protected constructor(type: BackendType); @@ -290,6 +296,7 @@ export type FinishReason = (typeof FinishReason)[keyof typeof FinishReason]; export interface FunctionCall { // (undocumented) args: object; + id?: string; // (undocumented) name: string; } @@ -342,6 +349,7 @@ export interface FunctionDeclarationsTool { // @public export interface FunctionResponse { + id?: string; // (undocumented) name: string; // (undocumented) @@ -480,6 +488,9 @@ export function getGenerativeModel(ai: AI, modelParams: ModelParams | HybridPara // @beta export function getImagenModel(ai: AI, modelParams: ImagenModelParams, requestOptions?: RequestOptions): ImagenModel; +// @beta +export function getLiveGenerativeModel(ai: AI, modelParams: LiveModelParams): LiveGenerativeModel; + // @public export class GoogleAIBackend extends Backend { constructor(); @@ -813,6 +824,96 @@ export interface LanguageModelPromptOptions { responseConstraint?: object; } +// @beta +export interface LiveGenerationConfig { + frequencyPenalty?: number; + maxOutputTokens?: number; + presencePenalty?: number; + responseModalities?: ResponseModality[]; + speechConfig?: SpeechConfig; + temperature?: number; + topK?: number; + topP?: number; +} + +// @beta +export class LiveGenerativeModel extends AIModel { + // Warning: (ae-forgotten-export) The symbol "WebSocketHandler" needs to be exported by the entry point index.d.ts + // + // @internal + constructor(ai: AI, modelParams: LiveModelParams, + _webSocketHandler: WebSocketHandler); + connect(): Promise; + // (undocumented) + generationConfig: LiveGenerationConfig; + // (undocumented) + systemInstruction?: Content; + // (undocumented) + toolConfig?: ToolConfig; + // (undocumented) + tools?: Tool[]; + } + +// @beta +export interface LiveModelParams { + // (undocumented) + generationConfig?: LiveGenerationConfig; + // (undocumented) + model: string; + // (undocumented) + systemInstruction?: string | Part | Content; + // (undocumented) + toolConfig?: ToolConfig; + // (undocumented) + tools?: Tool[]; +} + +// @beta +export const LiveResponseType: { + SERVER_CONTENT: string; + TOOL_CALL: string; + TOOL_CALL_CANCELLATION: string; +}; + +// @beta +export type LiveResponseType = (typeof LiveResponseType)[keyof typeof LiveResponseType]; + +// @beta +export interface LiveServerContent { + interrupted?: boolean; + modelTurn?: Content; + turnComplete?: boolean; + // (undocumented) + type: 'serverContent'; +} + +// @beta +export interface LiveServerToolCall { + functionCalls: FunctionCall[]; + // (undocumented) + type: 'toolCall'; +} + +// @beta +export interface LiveServerToolCallCancellation { + functionIds: string[]; + // (undocumented) + type: 'toolCallCancellation'; +} + +// @beta +export class LiveSession { + // @internal + constructor(webSocketHandler: WebSocketHandler, serverMessages: AsyncGenerator); + close(): Promise; + inConversation: boolean; + isClosed: boolean; + receive(): AsyncGenerator; + send(request: string | Array, turnComplete?: boolean): Promise; + sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise; + sendMediaStream(mediaChunkStream: ReadableStream): Promise; + } + // @public export const Modality: { readonly MODALITY_UNSPECIFIED: "MODALITY_UNSPECIFIED"; @@ -885,6 +986,11 @@ export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionRespon // @public export const POSSIBLE_ROLES: readonly ["user", "model", "function", "system"]; +// @beta +export interface PrebuiltVoiceConfig { + voiceName?: string; +} + // @public export interface PromptFeedback { // (undocumented) @@ -904,6 +1010,7 @@ export interface RequestOptions { export const ResponseModality: { readonly TEXT: "TEXT"; readonly IMAGE: "IMAGE"; + readonly AUDIO: "AUDIO"; }; // @beta @@ -1048,6 +1155,19 @@ export interface Segment { text: string; } +// @beta +export interface SpeechConfig { + voiceConfig?: VoiceConfig; +} + +// @beta +export function startAudioConversation(liveSession: LiveSession, options?: StartAudioConversationOptions): Promise; + +// @beta +export interface StartAudioConversationOptions { + functionCallingHandler?: (functionCalls: LiveServerToolCall['functionCalls']) => Promise; +} + // @public export interface StartChatParams extends BaseParams { // (undocumented) @@ -1130,6 +1250,11 @@ export interface VideoMetadata { startOffset: string; } +// @beta +export interface VoiceConfig { + prebuiltVoiceConfig?: PrebuiltVoiceConfig; +} + // @public (undocumented) export interface WebAttribution { // (undocumented) diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index da7c2500894..9161f501aa3 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -16,6 +16,8 @@ toc: path: /docs/reference/js/ai.anyofschema.md - title: ArraySchema path: /docs/reference/js/ai.arrayschema.md + - title: AudioConversationController + path: /docs/reference/js/ai.audioconversationcontroller.md - title: Backend path: /docs/reference/js/ai.backend.md - title: BaseParams @@ -124,6 +126,20 @@ toc: path: /docs/reference/js/ai.languagemodelmessagecontent.md - title: LanguageModelPromptOptions path: /docs/reference/js/ai.languagemodelpromptoptions.md + - title: LiveGenerationConfig + path: /docs/reference/js/ai.livegenerationconfig.md + - title: LiveGenerativeModel + path: /docs/reference/js/ai.livegenerativemodel.md + - title: LiveModelParams + path: /docs/reference/js/ai.livemodelparams.md + - title: LiveServerContent + path: /docs/reference/js/ai.liveservercontent.md + - title: LiveServerToolCall + path: /docs/reference/js/ai.liveservertoolcall.md + - title: LiveServerToolCallCancellation + path: /docs/reference/js/ai.liveservertoolcallcancellation.md + - title: LiveSession + path: /docs/reference/js/ai.livesession.md - title: ModalityTokenCount path: /docs/reference/js/ai.modalitytokencount.md - title: ModelParams @@ -136,6 +152,8 @@ toc: path: /docs/reference/js/ai.objectschemarequest.md - title: OnDeviceParams path: /docs/reference/js/ai.ondeviceparams.md + - title: PrebuiltVoiceConfig + path: /docs/reference/js/ai.prebuiltvoiceconfig.md - title: PromptFeedback path: /docs/reference/js/ai.promptfeedback.md - title: RequestOptions @@ -160,6 +178,10 @@ toc: path: /docs/reference/js/ai.searchentrypoint.md - title: Segment path: /docs/reference/js/ai.segment.md + - title: SpeechConfig + path: /docs/reference/js/ai.speechconfig.md + - title: StartAudioConversationOptions + path: /docs/reference/js/ai.startaudioconversationoptions.md - title: StartChatParams path: /docs/reference/js/ai.startchatparams.md - title: StringSchema @@ -176,6 +198,8 @@ toc: path: /docs/reference/js/ai.vertexaibackend.md - title: VideoMetadata path: /docs/reference/js/ai.videometadata.md + - title: VoiceConfig + path: /docs/reference/js/ai.voiceconfig.md - title: WebAttribution path: /docs/reference/js/ai.webattribution.md - title: WebGroundingChunk diff --git a/docs-devsite/ai.audioconversationcontroller.md b/docs-devsite/ai.audioconversationcontroller.md new file mode 100644 index 00000000000..18820a2fe55 --- /dev/null +++ b/docs-devsite/ai.audioconversationcontroller.md @@ -0,0 +1,41 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# AudioConversationController interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +A controller for managing an active audio conversation. + +Signature: + +```typescript +export interface AudioConversationController +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [stop](./ai.audioconversationcontroller.md#audioconversationcontrollerstop) | () => Promise<void> | (Public Preview) Stops the audio conversation, closes the microphone connection, and cleans up resources. Returns a promise that resolves when cleanup is complete. | + +## AudioConversationController.stop + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Stops the audio conversation, closes the microphone connection, and cleans up resources. Returns a promise that resolves when cleanup is complete. + +Signature: + +```typescript +stop: () => Promise; +``` diff --git a/docs-devsite/ai.functioncall.md b/docs-devsite/ai.functioncall.md index 1c789784fe1..b0bb2424a10 100644 --- a/docs-devsite/ai.functioncall.md +++ b/docs-devsite/ai.functioncall.md @@ -23,6 +23,7 @@ export interface FunctionCall | Property | Type | Description | | --- | --- | --- | | [args](./ai.functioncall.md#functioncallargs) | object | | +| [id](./ai.functioncall.md#functioncallid) | string | The id of the function call. This must be sent back in the associated [FunctionResponse](./ai.functionresponse.md#functionresponse_interface). | | [name](./ai.functioncall.md#functioncallname) | string | | ## FunctionCall.args @@ -33,6 +34,18 @@ export interface FunctionCall args: object; ``` +## FunctionCall.id + +The id of the function call. This must be sent back in the associated [FunctionResponse](./ai.functionresponse.md#functionresponse_interface). + +This property is only supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this property will be `undefined`. + +Signature: + +```typescript +id?: string; +``` + ## FunctionCall.name Signature: diff --git a/docs-devsite/ai.functionresponse.md b/docs-devsite/ai.functionresponse.md index e0838cf515a..980d964f703 100644 --- a/docs-devsite/ai.functionresponse.md +++ b/docs-devsite/ai.functionresponse.md @@ -22,9 +22,22 @@ export interface FunctionResponse | Property | Type | Description | | --- | --- | --- | +| [id](./ai.functionresponse.md#functionresponseid) | string | The id of the [FunctionCall](./ai.functioncall.md#functioncall_interface). | | [name](./ai.functionresponse.md#functionresponsename) | string | | | [response](./ai.functionresponse.md#functionresponseresponse) | object | | +## FunctionResponse.id + +The id of the [FunctionCall](./ai.functioncall.md#functioncall_interface). + +This property is only supported in the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)). When using the Gemini Developer API ([GoogleAIBackend](./ai.googleaibackend.md#googleaibackend_class)), this property will be `undefined`. + +Signature: + +```typescript +id?: string; +``` + ## FunctionResponse.name Signature: diff --git a/docs-devsite/ai.livegenerationconfig.md b/docs-devsite/ai.livegenerationconfig.md new file mode 100644 index 00000000000..1a920afa1e7 --- /dev/null +++ b/docs-devsite/ai.livegenerationconfig.md @@ -0,0 +1,139 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveGenerationConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration parameters used by [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) to control live content generation. + +Signature: + +```typescript +export interface LiveGenerationConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [frequencyPenalty](./ai.livegenerationconfig.md#livegenerationconfigfrequencypenalty) | number | (Public Preview) Frequency penalties. | +| [maxOutputTokens](./ai.livegenerationconfig.md#livegenerationconfigmaxoutputtokens) | number | (Public Preview) Specifies the maximum number of tokens that can be generated in the response. The number of tokens per word varies depending on the language outputted. Is unbounded by default. | +| [presencePenalty](./ai.livegenerationconfig.md#livegenerationconfigpresencepenalty) | number | (Public Preview) Positive penalties. | +| [responseModalities](./ai.livegenerationconfig.md#livegenerationconfigresponsemodalities) | [ResponseModality](./ai.md#responsemodality)\[\] | (Public Preview) The modalities of the response. | +| [speechConfig](./ai.livegenerationconfig.md#livegenerationconfigspeechconfig) | [SpeechConfig](./ai.speechconfig.md#speechconfig_interface) | (Public Preview) Configuration for speech synthesis. | +| [temperature](./ai.livegenerationconfig.md#livegenerationconfigtemperature) | number | (Public Preview) Controls the degree of randomness in token selection. A temperature value of 0 means that the highest probability tokens are always selected. In this case, responses for a given prompt are mostly deterministic, but a small amount of variation is still possible. | +| [topK](./ai.livegenerationconfig.md#livegenerationconfigtopk) | number | (Public Preview) Changes how the model selects token for output. A topK value of 1 means the select token is the most probable among all tokens in the model's vocabulary, while a topK value 3 means that the next token is selected from among the 3 most probably using probabilities sampled. Tokens are then further filtered with the highest selected temperature sampling. Defaults to 40 if unspecified. | +| [topP](./ai.livegenerationconfig.md#livegenerationconfigtopp) | number | (Public Preview) Changes how the model selects tokens for output. Tokens are selected from the most to least probable until the sum of their probabilities equals the topP value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively and the topP value is 0.5, then the model will select either A or B as the next token by using the temperature and exclude C as a candidate. Defaults to 0.95 if unset. | + +## LiveGenerationConfig.frequencyPenalty + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Frequency penalties. + +Signature: + +```typescript +frequencyPenalty?: number; +``` + +## LiveGenerationConfig.maxOutputTokens + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Specifies the maximum number of tokens that can be generated in the response. The number of tokens per word varies depending on the language outputted. Is unbounded by default. + +Signature: + +```typescript +maxOutputTokens?: number; +``` + +## LiveGenerationConfig.presencePenalty + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Positive penalties. + +Signature: + +```typescript +presencePenalty?: number; +``` + +## LiveGenerationConfig.responseModalities + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The modalities of the response. + +Signature: + +```typescript +responseModalities?: ResponseModality[]; +``` + +## LiveGenerationConfig.speechConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration for speech synthesis. + +Signature: + +```typescript +speechConfig?: SpeechConfig; +``` + +## LiveGenerationConfig.temperature + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Controls the degree of randomness in token selection. A `temperature` value of 0 means that the highest probability tokens are always selected. In this case, responses for a given prompt are mostly deterministic, but a small amount of variation is still possible. + +Signature: + +```typescript +temperature?: number; +``` + +## LiveGenerationConfig.topK + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Changes how the model selects token for output. A `topK` value of 1 means the select token is the most probable among all tokens in the model's vocabulary, while a `topK` value 3 means that the next token is selected from among the 3 most probably using probabilities sampled. Tokens are then further filtered with the highest selected `temperature` sampling. Defaults to 40 if unspecified. + +Signature: + +```typescript +topK?: number; +``` + +## LiveGenerationConfig.topP + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Changes how the model selects tokens for output. Tokens are selected from the most to least probable until the sum of their probabilities equals the `topP` value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively and the `topP` value is 0.5, then the model will select either A or B as the next token by using the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + +Signature: + +```typescript +topP?: number; +``` diff --git a/docs-devsite/ai.livegenerativemodel.md b/docs-devsite/ai.livegenerativemodel.md new file mode 100644 index 00000000000..7c52cad1a33 --- /dev/null +++ b/docs-devsite/ai.livegenerativemodel.md @@ -0,0 +1,109 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveGenerativeModel class +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal interactions with Gemini. + +This class should only be instantiated with [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `LiveGenerativeModel` class. + +Signature: + +```typescript +export declare class LiveGenerativeModel extends AIModel +``` +Extends: [AIModel](./ai.aimodel.md#aimodel_class) + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [generationConfig](./ai.livegenerativemodel.md#livegenerativemodelgenerationconfig) | | [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface) | (Public Preview) | +| [systemInstruction](./ai.livegenerativemodel.md#livegenerativemodelsysteminstruction) | | [Content](./ai.content.md#content_interface) | (Public Preview) | +| [toolConfig](./ai.livegenerativemodel.md#livegenerativemodeltoolconfig) | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | (Public Preview) | +| [tools](./ai.livegenerativemodel.md#livegenerativemodeltools) | | [Tool](./ai.md#tool)\[\] | (Public Preview) | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [connect()](./ai.livegenerativemodel.md#livegenerativemodelconnect) | | (Public Preview) Starts a [LiveSession](./ai.livesession.md#livesession_class). | + +## LiveGenerativeModel.generationConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +generationConfig: LiveGenerationConfig; +``` + +## LiveGenerativeModel.systemInstruction + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +systemInstruction?: Content; +``` + +## LiveGenerativeModel.toolConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +toolConfig?: ToolConfig; +``` + +## LiveGenerativeModel.tools + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +tools?: Tool[]; +``` + +## LiveGenerativeModel.connect() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Starts a [LiveSession](./ai.livesession.md#livesession_class). + +Signature: + +```typescript +connect(): Promise; +``` +Returns: + +Promise<[LiveSession](./ai.livesession.md#livesession_class)> + +A [LiveSession](./ai.livesession.md#livesession_class). + +#### Exceptions + +If the connection failed to be established with the server. + diff --git a/docs-devsite/ai.livemodelparams.md b/docs-devsite/ai.livemodelparams.md new file mode 100644 index 00000000000..fddca4f0e14 --- /dev/null +++ b/docs-devsite/ai.livemodelparams.md @@ -0,0 +1,87 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveModelParams interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Params passed to [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). + +Signature: + +```typescript +export interface LiveModelParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [generationConfig](./ai.livemodelparams.md#livemodelparamsgenerationconfig) | [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface) | (Public Preview) | +| [model](./ai.livemodelparams.md#livemodelparamsmodel) | string | (Public Preview) | +| [systemInstruction](./ai.livemodelparams.md#livemodelparamssysteminstruction) | string \| [Part](./ai.md#part) \| [Content](./ai.content.md#content_interface) | (Public Preview) | +| [toolConfig](./ai.livemodelparams.md#livemodelparamstoolconfig) | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | (Public Preview) | +| [tools](./ai.livemodelparams.md#livemodelparamstools) | [Tool](./ai.md#tool)\[\] | (Public Preview) | + +## LiveModelParams.generationConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +generationConfig?: LiveGenerationConfig; +``` + +## LiveModelParams.model + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +model: string; +``` + +## LiveModelParams.systemInstruction + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +systemInstruction?: string | Part | Content; +``` + +## LiveModelParams.toolConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +toolConfig?: ToolConfig; +``` + +## LiveModelParams.tools + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +tools?: Tool[]; +``` diff --git a/docs-devsite/ai.liveservercontent.md b/docs-devsite/ai.liveservercontent.md new file mode 100644 index 00000000000..f9c3ca1de79 --- /dev/null +++ b/docs-devsite/ai.liveservercontent.md @@ -0,0 +1,81 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveServerContent interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An incremental content update from the model. + +Signature: + +```typescript +export interface LiveServerContent +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [interrupted](./ai.liveservercontent.md#liveservercontentinterrupted) | boolean | (Public Preview) Indicates whether the model was interrupted by the client. An interruption occurs when the client sends a message before the model finishes it's turn. This is undefined if the model was not interrupted. | +| [modelTurn](./ai.liveservercontent.md#liveservercontentmodelturn) | [Content](./ai.content.md#content_interface) | (Public Preview) The content that the model has generated as part of the current conversation with the user. | +| [turnComplete](./ai.liveservercontent.md#liveservercontentturncomplete) | boolean | (Public Preview) Indicates whether the turn is complete. This is undefined if the turn is not complete. | +| [type](./ai.liveservercontent.md#liveservercontenttype) | 'serverContent' | (Public Preview) | + +## LiveServerContent.interrupted + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether the model was interrupted by the client. An interruption occurs when the client sends a message before the model finishes it's turn. This is `undefined` if the model was not interrupted. + +Signature: + +```typescript +interrupted?: boolean; +``` + +## LiveServerContent.modelTurn + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The content that the model has generated as part of the current conversation with the user. + +Signature: + +```typescript +modelTurn?: Content; +``` + +## LiveServerContent.turnComplete + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether the turn is complete. This is `undefined` if the turn is not complete. + +Signature: + +```typescript +turnComplete?: boolean; +``` + +## LiveServerContent.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: 'serverContent'; +``` diff --git a/docs-devsite/ai.liveservertoolcall.md b/docs-devsite/ai.liveservertoolcall.md new file mode 100644 index 00000000000..51ef6bb5d4b --- /dev/null +++ b/docs-devsite/ai.liveservertoolcall.md @@ -0,0 +1,53 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveServerToolCall interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +A request from the model for the client to execute one or more functions. + +Signature: + +```typescript +export interface LiveServerToolCall +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [functionCalls](./ai.liveservertoolcall.md#liveservertoolcallfunctioncalls) | [FunctionCall](./ai.functioncall.md#functioncall_interface)\[\] | (Public Preview) An array of function calls to run. | +| [type](./ai.liveservertoolcall.md#liveservertoolcalltype) | 'toolCall' | (Public Preview) | + +## LiveServerToolCall.functionCalls + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An array of function calls to run. + +Signature: + +```typescript +functionCalls: FunctionCall[]; +``` + +## LiveServerToolCall.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: 'toolCall'; +``` diff --git a/docs-devsite/ai.liveservertoolcallcancellation.md b/docs-devsite/ai.liveservertoolcallcancellation.md new file mode 100644 index 00000000000..2e9a63a81e7 --- /dev/null +++ b/docs-devsite/ai.liveservertoolcallcancellation.md @@ -0,0 +1,53 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveServerToolCallCancellation interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Notification to cancel a previous function call triggered by [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface). + +Signature: + +```typescript +export interface LiveServerToolCallCancellation +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [functionIds](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellationfunctionids) | string\[\] | (Public Preview) IDs of function calls that were cancelled. These refer to the id property of a [FunctionCall](./ai.functioncall.md#functioncall_interface). | +| [type](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellationtype) | 'toolCallCancellation' | (Public Preview) | + +## LiveServerToolCallCancellation.functionIds + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +IDs of function calls that were cancelled. These refer to the `id` property of a [FunctionCall](./ai.functioncall.md#functioncall_interface). + +Signature: + +```typescript +functionIds: string[]; +``` + +## LiveServerToolCallCancellation.type + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Signature: + +```typescript +type: 'toolCallCancellation'; +``` diff --git a/docs-devsite/ai.livesession.md b/docs-devsite/ai.livesession.md new file mode 100644 index 00000000000..6ae2cde711c --- /dev/null +++ b/docs-devsite/ai.livesession.md @@ -0,0 +1,190 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# LiveSession class +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Represents an active, real-time, bidirectional conversation with the model. + +This class should only be instantiated by calling [LiveGenerativeModel.connect()](./ai.livegenerativemodel.md#livegenerativemodelconnect). + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `LiveSession` class. + +Signature: + +```typescript +export declare class LiveSession +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [inConversation](./ai.livesession.md#livesessioninconversation) | | boolean | (Public Preview) Indicates whether this Live session is being controlled by an AudioConversationController. | +| [isClosed](./ai.livesession.md#livesessionisclosed) | | boolean | (Public Preview) Indicates whether this Live session is closed. | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [close()](./ai.livesession.md#livesessionclose) | | (Public Preview) Closes this session. All methods on this session will throw an error once this resolves. | +| [receive()](./ai.livesession.md#livesessionreceive) | | (Public Preview) Yields messages received from the server. This can only be used by one consumer at a time. | +| [send(request, turnComplete)](./ai.livesession.md#livesessionsend) | | (Public Preview) Sends content to the server. | +| [sendMediaChunks(mediaChunks)](./ai.livesession.md#livesessionsendmediachunks) | | (Public Preview) Sends realtime input to the server. | +| [sendMediaStream(mediaChunkStream)](./ai.livesession.md#livesessionsendmediastream) | | (Public Preview) Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). | + +## LiveSession.inConversation + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether this Live session is being controlled by an `AudioConversationController`. + +Signature: + +```typescript +inConversation: boolean; +``` + +## LiveSession.isClosed + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Indicates whether this Live session is closed. + +Signature: + +```typescript +isClosed: boolean; +``` + +## LiveSession.close() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Closes this session. All methods on this session will throw an error once this resolves. + +Signature: + +```typescript +close(): Promise; +``` +Returns: + +Promise<void> + +## LiveSession.receive() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Yields messages received from the server. This can only be used by one consumer at a time. + +Signature: + +```typescript +receive(): AsyncGenerator; +``` +Returns: + +AsyncGenerator<[LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) \| [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface) \| [LiveServerToolCallCancellation](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellation_interface)> + +An `AsyncGenerator` that yields server messages as they arrive. + +#### Exceptions + +If the session is already closed, or if we receive a response that we don't support. + +## LiveSession.send() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends content to the server. + +Signature: + +```typescript +send(request: string | Array, turnComplete?: boolean): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| request | string \| Array<string \| [Part](./ai.md#part)> | The message to send to the model. | +| turnComplete | boolean | Indicates if the turn is complete. Defaults to false. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +## LiveSession.sendMediaChunks() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends realtime input to the server. + +Signature: + +```typescript +sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mediaChunks | [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface)\[\] | The media chunks to send. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + +## LiveSession.sendMediaStream() + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Sends a stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface). + +Signature: + +```typescript +sendMediaStream(mediaChunkStream: ReadableStream): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mediaChunkStream | ReadableStream<[GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface)> | The stream of [GenerativeContentBlob](./ai.generativecontentblob.md#generativecontentblob_interface) to send. | + +Returns: + +Promise<void> + +#### Exceptions + +If this session has been closed. + diff --git a/docs-devsite/ai.md b/docs-devsite/ai.md index 3c7669204a1..c40d37d895e 100644 --- a/docs-devsite/ai.md +++ b/docs-devsite/ai.md @@ -21,8 +21,11 @@ The Firebase AI Web SDK. | function(ai, ...) | | [getGenerativeModel(ai, modelParams, requestOptions)](./ai.md#getgenerativemodel_c63f46a) | Returns a [GenerativeModel](./ai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | | [getImagenModel(ai, modelParams, requestOptions)](./ai.md#getimagenmodel_e1f6645) | (Public Preview) Returns an [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | +| [getLiveGenerativeModel(ai, modelParams)](./ai.md#getlivegenerativemodel_f2099ac) | (Public Preview) Returns a [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) class for real-time, bidirectional communication.The Live API is only supported in modern browser windows and Node >= 22. | | function(container, ...) | | [factory(container, { instanceIdentifier })](./ai.md#factory_6581aeb) | | +| function(liveSession, ...) | +| [startAudioConversation(liveSession, options)](./ai.md#startaudioconversation_01c8e7f) | (Public Preview) Starts a real-time, bidirectional audio conversation with the model. This helper function manages the complexities of microphone access, audio recording, playback, and interruptions. | ## Classes @@ -40,6 +43,8 @@ The Firebase AI Web SDK. | [ImagenImageFormat](./ai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) Defines the image format for images generated by Imagen.Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./ai.imagenmodelparams.md#imagenmodelparams_interface). | | [ImagenModel](./ai.imagenmodel.md#imagenmodel_class) | (Public Preview) Class for Imagen model APIs.This class provides methods for generating images using the Imagen model. | | [IntegerSchema](./ai.integerschema.md#integerschema_class) | Schema class for "integer" types. | +| [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) | (Public Preview) Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal interactions with Gemini.This class should only be instantiated with [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). | +| [LiveSession](./ai.livesession.md#livesession_class) | (Public Preview) Represents an active, real-time, bidirectional conversation with the model.This class should only be instantiated by calling [LiveGenerativeModel.connect()](./ai.livegenerativemodel.md#livegenerativemodelconnect). | | [NumberSchema](./ai.numberschema.md#numberschema_class) | Schema class for "number" types. | | [ObjectSchema](./ai.objectschema.md#objectschema_class) | Schema class for "object" types. The properties param must be a map of Schema objects. | | [Schema](./ai.schema.md#schema_class) | Parent class encompassing all Schema types, with static methods that allow building specific Schema types. This class can be converted with JSON.stringify() into a JSON string accepted by Vertex AI REST endpoints. (This string conversion is automatically done when calling SDK methods.) | @@ -52,6 +57,7 @@ The Firebase AI Web SDK. | --- | --- | | [AI](./ai.ai.md#ai_interface) | An instance of the Firebase AI SDK.Do not create this instance directly. Instead, use [getAI()](./ai.md#getai_a94a413). | | [AIOptions](./ai.aioptions.md#aioptions_interface) | Options for initializing the AI service using [getAI()](./ai.md#getai_a94a413). This allows specifying which backend to use (Vertex AI Gemini API or Gemini Developer API) and configuring its specific options (like location for Vertex AI). | +| [AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface) | (Public Preview) A controller for managing an active audio conversation. | | [BaseParams](./ai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | | [ChromeAdapter](./ai.chromeadapter.md#chromeadapter_interface) | (EXPERIMENTAL) Defines an inference "backend" that uses Chrome's on-device model, and encapsulates logic for detecting when on-device inference is possible.These methods should not be called directly by the user. | | [Citation](./ai.citation.md#citation_interface) | A single citation. | @@ -98,10 +104,16 @@ The Firebase AI Web SDK. | [LanguageModelMessage](./ai.languagemodelmessage.md#languagemodelmessage_interface) | (EXPERIMENTAL) An on-device language model message. | | [LanguageModelMessageContent](./ai.languagemodelmessagecontent.md#languagemodelmessagecontent_interface) | (EXPERIMENTAL) An on-device language model content object. | | [LanguageModelPromptOptions](./ai.languagemodelpromptoptions.md#languagemodelpromptoptions_interface) | (EXPERIMENTAL) Options for an on-device language model prompt. | +| [LiveGenerationConfig](./ai.livegenerationconfig.md#livegenerationconfig_interface) | (Public Preview) Configuration parameters used by [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) to control live content generation. | +| [LiveModelParams](./ai.livemodelparams.md#livemodelparams_interface) | (Public Preview) Params passed to [getLiveGenerativeModel()](./ai.md#getlivegenerativemodel_f2099ac). | +| [LiveServerContent](./ai.liveservercontent.md#liveservercontent_interface) | (Public Preview) An incremental content update from the model. | +| [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface) | (Public Preview) A request from the model for the client to execute one or more functions. | +| [LiveServerToolCallCancellation](./ai.liveservertoolcallcancellation.md#liveservertoolcallcancellation_interface) | (Public Preview) Notification to cancel a previous function call triggered by [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface). | | [ModalityTokenCount](./ai.modalitytokencount.md#modalitytokencount_interface) | Represents token counting info for a single modality. | | [ModelParams](./ai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_c63f46a). | | [ObjectSchemaRequest](./ai.objectschemarequest.md#objectschemarequest_interface) | Interface for JSON parameters in a schema of [SchemaType](./ai.md#schematype) "object" when not using the Schema.object() helper. | | [OnDeviceParams](./ai.ondeviceparams.md#ondeviceparams_interface) | (EXPERIMENTAL) Encapsulates configuration for on-device inference. | +| [PrebuiltVoiceConfig](./ai.prebuiltvoiceconfig.md#prebuiltvoiceconfig_interface) | (Public Preview) Configuration for a pre-built voice. | | [PromptFeedback](./ai.promptfeedback.md#promptfeedback_interface) | If the prompt was blocked, this will be populated with blockReason and the relevant safetyRatings. | | [RequestOptions](./ai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./ai.md#getgenerativemodel_c63f46a). | | [RetrievedContextAttribution](./ai.retrievedcontextattribution.md#retrievedcontextattribution_interface) | | @@ -113,12 +125,15 @@ The Firebase AI Web SDK. | [SchemaShared](./ai.schemashared.md#schemashared_interface) | Basic [Schema](./ai.schema.md#schema_class) properties shared across several Schema-related types. | | [SearchEntrypoint](./ai.searchentrypoint.md#searchentrypoint_interface) | Google search entry point. | | [Segment](./ai.segment.md#segment_interface) | Represents a specific segment within a [Content](./ai.content.md#content_interface) object, often used to pinpoint the exact location of text or data that grounding information refers to. | +| [SpeechConfig](./ai.speechconfig.md#speechconfig_interface) | (Public Preview) Configures speech synthesis. | +| [StartAudioConversationOptions](./ai.startaudioconversationoptions.md#startaudioconversationoptions_interface) | (Public Preview) Options for [startAudioConversation()](./ai.md#startaudioconversation_01c8e7f). | | [StartChatParams](./ai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./ai.generativemodel.md#generativemodelstartchat). | | [TextPart](./ai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | | [ThinkingConfig](./ai.thinkingconfig.md#thinkingconfig_interface) | Configuration for "thinking" behavior of compatible Gemini models.Certain models utilize a thinking process before generating a response. This allows them to reason through complex problems and plan a more coherent and accurate answer. | | [ToolConfig](./ai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | | [UsageMetadata](./ai.usagemetadata.md#usagemetadata_interface) | Usage metadata about a [GenerateContentResponse](./ai.generatecontentresponse.md#generatecontentresponse_interface). | | [VideoMetadata](./ai.videometadata.md#videometadata_interface) | Describes the input video content. | +| [VoiceConfig](./ai.voiceconfig.md#voiceconfig_interface) | (Public Preview) Configuration for the voice to used in speech synthesis. | | [WebAttribution](./ai.webattribution.md#webattribution_interface) | | | [WebGroundingChunk](./ai.webgroundingchunk.md#webgroundingchunk_interface) | A grounding chunk from the web.Important: If using Grounding with Google Search, you are required to comply with the [Service Specific Terms](https://cloud.google.com/terms/service-terms) for "Grounding with Google Search". | @@ -140,6 +155,7 @@ The Firebase AI Web SDK. | [ImagenPersonFilterLevel](./ai.md#imagenpersonfilterlevel) | (Public Preview) A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | | [ImagenSafetyFilterLevel](./ai.md#imagensafetyfilterlevel) | (Public Preview) A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | | [InferenceMode](./ai.md#inferencemode) | (EXPERIMENTAL) Determines whether inference happens on-device or in-cloud. | +| [LiveResponseType](./ai.md#liveresponsetype) | (Public Preview) The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). | | [Modality](./ai.md#modality) | Content part modality. | | [POSSIBLE\_ROLES](./ai.md#possible_roles) | Possible roles. | | [ResponseModality](./ai.md#responsemodality) | (Public Preview) Generation modalities to be returned in generation responses. | @@ -166,6 +182,7 @@ The Firebase AI Web SDK. | [LanguageModelMessageContentValue](./ai.md#languagemodelmessagecontentvalue) | (EXPERIMENTAL) Content formats that can be provided as on-device message content. | | [LanguageModelMessageRole](./ai.md#languagemodelmessagerole) | (EXPERIMENTAL) Allowable roles for on-device language model usage. | | [LanguageModelMessageType](./ai.md#languagemodelmessagetype) | (EXPERIMENTAL) Allowable types for on-device language model messages. | +| [LiveResponseType](./ai.md#liveresponsetype) | (Public Preview) The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). This is a property on all messages that can be used for type narrowing. This property is not returned by the server, it is assigned to a server message object once it's parsed. | | [Modality](./ai.md#modality) | Content part modality. | | [Part](./ai.md#part) | Content part - includes text, image/video, or function call/response part types. | | [ResponseModality](./ai.md#responsemodality) | (Public Preview) Generation modalities to be returned in generation responses. | @@ -280,6 +297,36 @@ export declare function getImagenModel(ai: AI, modelParams: ImagenModelParams, r If the `apiKey` or `projectId` fields are missing in your Firebase config. +### getLiveGenerativeModel(ai, modelParams) {:#getlivegenerativemodel_f2099ac} + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Returns a [LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) class for real-time, bidirectional communication. + +The Live API is only supported in modern browser windows and Node >= 22. + +Signature: + +```typescript +export declare function getLiveGenerativeModel(ai: AI, modelParams: LiveModelParams): LiveGenerativeModel; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ai | [AI](./ai.ai.md#ai_interface) | An [AI](./ai.ai.md#ai_interface) instance. | +| modelParams | [LiveModelParams](./ai.livemodelparams.md#livemodelparams_interface) | Parameters to use when setting up a [LiveSession](./ai.livesession.md#livesession_class). | + +Returns: + +[LiveGenerativeModel](./ai.livegenerativemodel.md#livegenerativemodel_class) + +#### Exceptions + +If the `apiKey` or `projectId` fields are missing in your Firebase config. + ## function(container, ...) ### factory(container, { instanceIdentifier }) {:#factory_6581aeb} @@ -301,6 +348,76 @@ export declare function factory(container: ComponentContainer, { instanceIdentif AIService +## function(liveSession, ...) + +### startAudioConversation(liveSession, options) {:#startaudioconversation_01c8e7f} + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Starts a real-time, bidirectional audio conversation with the model. This helper function manages the complexities of microphone access, audio recording, playback, and interruptions. + +Important: This function must be called in response to a user gesture (for example, a button click) to comply with [browser autoplay policies](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy). + +Signature: + +```typescript +export declare function startAudioConversation(liveSession: LiveSession, options?: StartAudioConversationOptions): Promise; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| liveSession | [LiveSession](./ai.livesession.md#livesession_class) | An active [LiveSession](./ai.livesession.md#livesession_class) instance. | +| options | [StartAudioConversationOptions](./ai.startaudioconversationoptions.md#startaudioconversationoptions_interface) | Configuration options for the audio conversation. | + +Returns: + +Promise<[AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface)> + +A `Promise` that resolves with an [AudioConversationController](./ai.audioconversationcontroller.md#audioconversationcontroller_interface). + +#### Exceptions + +`AIError` if the environment does not support required Web APIs (`UNSUPPORTED`), if a conversation is already active (`REQUEST_ERROR`), the session is closed (`SESSION_CLOSED`), or if an unexpected initialization error occurs (`ERROR`). + +`DOMException` Thrown by `navigator.mediaDevices.getUserMedia()` if issues occur with microphone access, such as permissions being denied (`NotAllowedError`) or no compatible hardware being found (`NotFoundError`). See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions) for a full list of exceptions. + +### Example + + +```javascript +const liveSession = await model.connect(); +let conversationController; + +// This function must be called from within a click handler. +async function startConversation() { + try { + conversationController = await startAudioConversation(liveSession); + } catch (e) { + // Handle AI-specific errors + if (e instanceof AIError) { + console.error("AI Error:", e.message); + } + // Handle microphone permission and hardware errors + else if (e instanceof DOMException) { + console.error("Microphone Error:", e.message); + } + // Handle other unexpected errors + else { + console.error("An unexpected error occurred:", e); + } + } +} + +// Later, to stop the conversation: +// if (conversationController) { +// await conversationController.stop(); +// } + +``` + ## AIErrorCode Standardized error codes that [AIError](./ai.aierror.md#aierror_class) can have. @@ -313,6 +430,7 @@ AIErrorCode: { readonly REQUEST_ERROR: "request-error"; readonly RESPONSE_ERROR: "response-error"; readonly FETCH_ERROR: "fetch-error"; + readonly SESSION_CLOSED: "session-closed"; readonly INVALID_CONTENT: "invalid-content"; readonly API_NOT_ENABLED: "api-not-enabled"; readonly INVALID_SCHEMA: "invalid-schema"; @@ -539,6 +657,23 @@ InferenceMode: { } ``` +## LiveResponseType + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). + +Signature: + +```typescript +LiveResponseType: { + SERVER_CONTENT: string; + TOOL_CALL: string; + TOOL_CALL_CANCELLATION: string; +} +``` + ## Modality Content part modality. @@ -579,6 +714,7 @@ Generation modalities to be returned in generation responses. ResponseModality: { readonly TEXT: "TEXT"; readonly IMAGE: "IMAGE"; + readonly AUDIO: "AUDIO"; } ``` @@ -785,6 +921,19 @@ export type LanguageModelMessageRole = 'system' | 'user' | 'assistant'; export type LanguageModelMessageType = 'text' | 'image' | 'audio'; ``` +## LiveResponseType + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The types of responses that can be returned by [LiveSession.receive()](./ai.livesession.md#livesessionreceive). This is a property on all messages that can be used for type narrowing. This property is not returned by the server, it is assigned to a server message object once it's parsed. + +Signature: + +```typescript +export type LiveResponseType = (typeof LiveResponseType)[keyof typeof LiveResponseType]; +``` + ## Modality Content part modality. diff --git a/docs-devsite/ai.prebuiltvoiceconfig.md b/docs-devsite/ai.prebuiltvoiceconfig.md new file mode 100644 index 00000000000..8627ae184b3 --- /dev/null +++ b/docs-devsite/ai.prebuiltvoiceconfig.md @@ -0,0 +1,43 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# PrebuiltVoiceConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration for a pre-built voice. + +Signature: + +```typescript +export interface PrebuiltVoiceConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [voiceName](./ai.prebuiltvoiceconfig.md#prebuiltvoiceconfigvoicename) | string | (Public Preview) The voice name to use for speech synthesis.For a full list of names and demos of what each voice sounds like, see [Chirp 3: HD Voices](https://cloud.google.com/text-to-speech/docs/chirp3-hd). | + +## PrebuiltVoiceConfig.voiceName + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +The voice name to use for speech synthesis. + +For a full list of names and demos of what each voice sounds like, see [Chirp 3: HD Voices](https://cloud.google.com/text-to-speech/docs/chirp3-hd). + +Signature: + +```typescript +voiceName?: string; +``` diff --git a/docs-devsite/ai.speechconfig.md b/docs-devsite/ai.speechconfig.md new file mode 100644 index 00000000000..95c63964974 --- /dev/null +++ b/docs-devsite/ai.speechconfig.md @@ -0,0 +1,41 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# SpeechConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures speech synthesis. + +Signature: + +```typescript +export interface SpeechConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [voiceConfig](./ai.speechconfig.md#speechconfigvoiceconfig) | [VoiceConfig](./ai.voiceconfig.md#voiceconfig_interface) | (Public Preview) Configures the voice to be used in speech synthesis. | + +## SpeechConfig.voiceConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures the voice to be used in speech synthesis. + +Signature: + +```typescript +voiceConfig?: VoiceConfig; +``` diff --git a/docs-devsite/ai.startaudioconversationoptions.md b/docs-devsite/ai.startaudioconversationoptions.md new file mode 100644 index 00000000000..08e91d2c7b5 --- /dev/null +++ b/docs-devsite/ai.startaudioconversationoptions.md @@ -0,0 +1,41 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# StartAudioConversationOptions interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Options for [startAudioConversation()](./ai.md#startaudioconversation_01c8e7f). + +Signature: + +```typescript +export interface StartAudioConversationOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [functionCallingHandler](./ai.startaudioconversationoptions.md#startaudioconversationoptionsfunctioncallinghandler) | (functionCalls: [LiveServerToolCall](./ai.liveservertoolcall.md#liveservertoolcall_interface)\['functionCalls'\]) => Promise<[Part](./ai.md#part)> | (Public Preview) An async handler that is called when the model requests a function to be executed. The handler should perform the function call and return the result as a Part, which will then be sent back to the model. | + +## StartAudioConversationOptions.functionCallingHandler + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +An async handler that is called when the model requests a function to be executed. The handler should perform the function call and return the result as a `Part`, which will then be sent back to the model. + +Signature: + +```typescript +functionCallingHandler?: (functionCalls: LiveServerToolCall['functionCalls']) => Promise; +``` diff --git a/docs-devsite/ai.voiceconfig.md b/docs-devsite/ai.voiceconfig.md new file mode 100644 index 00000000000..b22ac7e104c --- /dev/null +++ b/docs-devsite/ai.voiceconfig.md @@ -0,0 +1,41 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# VoiceConfig interface +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configuration for the voice to used in speech synthesis. + +Signature: + +```typescript +export interface VoiceConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [prebuiltVoiceConfig](./ai.voiceconfig.md#voiceconfigprebuiltvoiceconfig) | [PrebuiltVoiceConfig](./ai.prebuiltvoiceconfig.md#prebuiltvoiceconfig_interface) | (Public Preview) Configures the voice using a pre-built voice configuration. | + +## VoiceConfig.prebuiltVoiceConfig + +> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. +> + +Configures the voice using a pre-built voice configuration. + +Signature: + +```typescript +prebuiltVoiceConfig?: PrebuiltVoiceConfig; +``` diff --git a/packages/ai/integration/constants.ts b/packages/ai/integration/constants.ts index 1adfa4f47a0..f4a74e75039 100644 --- a/packages/ai/integration/constants.ts +++ b/packages/ai/integration/constants.ts @@ -54,6 +54,12 @@ const backendNames: Map = new Map([ const modelNames: readonly string[] = ['gemini-2.0-flash', 'gemini-2.5-flash']; +// The Live API requires a different set of models, and they're different for each backend. +const liveModelNames: Map = new Map([ + [BackendType.GOOGLE_AI, ['gemini-live-2.5-flash-preview']], + [BackendType.VERTEX_AI, ['gemini-2.0-flash-exp']] +]); + /** * Array of test configurations that is iterated over to get full coverage * of backends and models. Contains all combinations of backends and models. @@ -69,6 +75,25 @@ export const testConfigs: readonly TestConfig[] = backends.flatMap(backend => { }); }); +/** + * Test configurations used for the Live API integration tests. + */ +export const liveTestConfigs: readonly TestConfig[] = backends.flatMap( + backend => { + const testConfigs: TestConfig[] = []; + liveModelNames.get(backend.backendType)!.forEach(modelName => { + const ai = getAI(app, { backend }); + testConfigs.push({ + ai, + model: modelName, + toString: () => formatConfigAsString({ ai, model: modelName }) + }); + }); + + return testConfigs; + } +); + export const TINY_IMG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII='; export const IMAGE_MIME_TYPE = 'image/png'; diff --git a/packages/ai/integration/live.test.ts b/packages/ai/integration/live.test.ts new file mode 100644 index 00000000000..caa18970ab7 --- /dev/null +++ b/packages/ai/integration/live.test.ts @@ -0,0 +1,327 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { + BackendType, + getLiveGenerativeModel, + LiveGenerationConfig, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation, + ResponseModality +} from '../src'; +import { liveTestConfigs } from './constants'; +import { HELLO_AUDIO_PCM_BASE64 } from './sample-data/hello-audio'; + +// A helper function to consume the generator and collect text parts from one turn. +async function nextTurnText( + stream: AsyncGenerator< + LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation + > +): Promise { + let text = ''; + // We don't use `for await...of` on the generator, because that would automatically close the generator. + // We want to keep the generator open so that we can pass it to this function again to get the + // next turn's text. + let result = await stream.next(); + while (!result.done) { + const chunk = result.value as + | LiveServerContent + | LiveServerToolCall + | LiveServerToolCallCancellation; + switch (chunk.type) { + case 'serverContent': + if (chunk.turnComplete) { + return text; + } + + const parts = chunk.modelTurn?.parts; + if (parts) { + parts.forEach(part => { + if (part.text) { + text += part.text; + } else { + throw Error(`Expected TextPart but got ${JSON.stringify(part)}`); + } + }); + } + break; + default: + throw new Error(`Unexpected chunk type '${(chunk as any).type}'`); + } + + result = await stream.next(); + } + + return text; +} + +describe('Live', function () { + this.timeout(20000); + + const textLiveGenerationConfig: LiveGenerationConfig = { + responseModalities: [ResponseModality.TEXT], + temperature: 0, + topP: 0 + }; + + liveTestConfigs.forEach(testConfig => { + if (testConfig.ai.backend.backendType === BackendType.VERTEX_AI) { + return; + } + describe(`${testConfig.toString()}`, () => { + describe('Live', () => { + it('should connect, send a message, receive a response, and close', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + await session.send( + 'Where is Google headquarters located? Answer with the city name only.' + ); + const responseText = await responsePromise; + expect(responseText).to.exist; + expect(responseText).to.include('Mountain View'); + await session.close(); + }); + it('should handle multiple messages in a session', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const generator = session.receive(); + + await session.send( + 'Where is Google headquarters located? Answer with the city name only.' + ); + + const responsePromise1 = nextTurnText(generator); + const responseText1 = await responsePromise1; // Wait for the turn to complete + expect(responseText1).to.include('Mountain View'); + + await session.send( + 'What state is that in? Answer with the state name only.' + ); + + const responsePromise2 = nextTurnText(generator); + const responseText2 = await responsePromise2; // Wait for the second turn to complete + expect(responseText2).to.include('California'); + + await session.close(); + }); + + it('close() should be idempotent and terminate the stream', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const session = await model.connect(); + const generator = session.receive(); + + // Start consuming but don't wait for it to finish yet + const consumptionPromise = (async () => { + // This loop should terminate cleanly when close() is called + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of generator) { + } + })(); + + await session.close(); + + // Calling it again should not throw an error + await session.close(); + + // Should resolve without timing out + await consumptionPromise; + }); + }); + + describe('sendMediaChunks()', () => { + it('should send a single audio chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendMediaChunks([ + { + data: HELLO_AUDIO_PCM_BASE64, // "Hey, can you hear me?" + mimeType: 'audio/pcm' + } + ]); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + + it('should send multiple audio chunks in a single batch call', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + // TODO (dlarocque): Pass two PCM files with different audio, and validate that the model + // heard both. + await session.sendMediaChunks([ + { data: HELLO_AUDIO_PCM_BASE64, mimeType: 'audio/pcm' }, + { data: HELLO_AUDIO_PCM_BASE64, mimeType: 'audio/pcm' } + ]); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('sendMediaStream()', () => { + it('should consume a stream with multiple chunks and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + // TODO (dlarocque): Pass two PCM files with different audio, and validate that the model + // heard both. + const testStream = new ReadableStream({ + start(controller) { + controller.enqueue({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + controller.enqueue({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + controller.close(); + } + }); + + await session.sendMediaStream(testStream); + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + /** + * These tests are currently very unreliable. Their behavior seems to change frequently. + * Skipping them for now. + */ + /* + describe('function calling', () => { + // When this tests runs against the Google AI backend, the first message we get back + // has an `executableCode` part, and then + it('should trigger a function call', async () => { + const tool: FunctionDeclarationsTool = { + functionDeclarations: [ + { + name: 'fetchWeather', + description: + 'Get the weather conditions for a specific city on a specific date.', + parameters: Schema.object({ + properties: { + location: Schema.string({ + description: 'The city of the location' + }), + date: Schema.string({ + description: 'The date to fetch weather for.' + }) + } + }) + } + ] + }; + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + tools: [tool], + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const generator = session.receive(); + + const streamPromise = new Promise(async resolve => { + let text = ''; + let turnNum = 0; + for await (const chunk of generator) { + console.log('chunk', JSON.stringify(chunk)) + switch (chunk.type) { + case 'serverContent': + if (chunk.turnComplete) { + // Vertex AI only: + // For some unknown reason, the model's first turn will not be a toolCall, but + // will instead be an executableCode part in Google AI, and a groundingMetadata in Vertex AI. + // Let's skip this unexpected first message, waiting until the second turn to resolve with the text. This will definitely break if/when + // that bug is fixed. + if (turnNum === 0) { + turnNum = 1; + } else { + return resolve(text); + } + } else { + const parts = chunk.modelTurn?.parts; + if (parts) { + text += parts.flatMap(part => part.text).join(''); + } + } + break; + case 'toolCall': + // Send a fake function response + const functionResponse: FunctionResponsePart = { + functionResponse: { + id: chunk.functionCalls[0].id, // Only defined in Google AI + name: chunk.functionCalls[0].name, + response: { degrees: '22' } + } + }; + console.log('sending', JSON.stringify(functionResponse)) + await session.send([functionResponse]); + break; + case 'toolCallCancellation': + throw Error('Unexpected tool call cancellation'); + default: + throw Error('Unexpected chunk type'); + } + } + }); + + // Send a message that should trigger a function call to fetchWeather + await session.send('Whats the weather on June 15, 2025 in Toronto?'); + + const finalResponseText = await streamPromise; + expect(finalResponseText).to.include('22'); // Should include the result of our function call + + await session.close(); + }); + }); + */ + }); + }); +}); diff --git a/packages/ai/integration/sample-data/hello-audio.ts b/packages/ai/integration/sample-data/hello-audio.ts new file mode 100644 index 00000000000..7c3b8f2f693 --- /dev/null +++ b/packages/ai/integration/sample-data/hello-audio.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const HELLO_AUDIO_PCM_BASE64 = + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AAAAAAAAAQABAAEAAQAAAAAAAAD///7//v8AAAIAAgAAAP7/AgAKAAcA7P+1/3r/Xf9x/67/8v8dACgAIAAZABgAGwAZABIADQAKAAkACAAGAAYACAALAAsACQAJAAsADwATABUAFQATABIAEAARABIAFAAVABMAEAANAAsACwALAAoACgAKAAoACgAKAAoACQAJAAkACAAIAAgACAAIAAgACAAHAAcABwAHAAYABgAGAAYABQAFAAUABQAFAAUABAAEAAQAAwADAAMAAwADAAMAAgACAAIAAgABAAEAAQAAAAAAAAAAAAAAAAAAAAAA/////////////////v/+//7//v/+//7//v/+//7//v/+//7//f/9//3//f/9//3//f/9//3//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z/+//7//v//P/8//v/+//7//v/+//7//v/+//7//v/+//8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//3//f/9//3//f/9//z//P/8//z//P/8//3//f/9//3//f/9//3//f/9//3//f/9//3//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v////////////////////7//v//////////////////////AAAAAAAA///9//3/AAADAAQAAQD9//r//P8BAAUABAAAAP3//P///wEAAQAAAP//AAACAAIAAgABAAEAAQABAAEAAQAAAAAAAQABAAEAAQABAAEAAQABAAEAAgACAAIAAgACAAIAAgABAAEAAQACAAIAAgACAAIAAgACAAEAAQABAAEAAQABAAEAAgACAAIAAgACAAIAAgACAAIAAgACAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgABAAEAAQABAAEAAQABAAEAAQACAAIAAgACAAIAAgABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP///////////////////////////////wAAAAD///////////////////////////////////////////////////////////////////////////////////////8AAAAAAAAAAAAAAAAAAAAA//////////////////8AAAAAAAD///////////////////////////////////////////7//v//////////////////////////////////////AAAAAAAAAQABAAEAAQABAAEAAQABAAIAAgACAAIAAgABAAEAAQABAAAAAAAAAAAAAAD//wAA//////7//v/+//7//f/9//z//f/9//3//f/9//z//P/7//z//P/8//z//P/8//z/+//6//r/+v/6//r/+v/6//n/+f/5//n/+f/5//n/+f/5//n/+P/3//f/9//3//j/+P/3//f/9v/2//f/+f/5//n/+v/7//v//f/9////AQABAAIABAADAAMABQAHAAgACQALAAwADAAMAAsACgAKAAkACwAMAAsACgAJAAkABwAFAAUABgAHAAUABQAFAAQABAACAAQABQACAAEAAQACAAIAAAABAAMAAgACAAMABgAEAAIAAwAEAAIAAQADAAYABQAEAAUABgAIAAYAAwAJAAwACgAJAA4AEAARABAADwAUABgAFQAUABwAHAAVABQAFwAWABEADwASABEACwAHAAYABQAGAAcACgAPABAADgANAA4ADgARABIAFgAaABsAGQAVABEAEwAXABgAFAATABcAEQAIAAoADwAMAAYAAgAAAAAA/v/7//v/+P/4//7/AAD3//D/8f/0//D/7v/1//r/9//z//X/+P/z/+v/7f/v/+z/6f/q/+3/7v/u/+r/5f/l/+f/6v/w//D/6P/p/+r/5f/m/+z/7f/p/+f/5P/f/+b/7f/w//L/7P/m/+7/9f/3//7/BAABAPr/+P/8/wEAAgADAAYABAD9//v///8GAAkAAwAAAAAA+P/x//b/9//0//T/+/8AAPr/9//+/wIA/f/2//r/AQD+//3/CwAYABMADwANAAoAEQAZABAACQALAA4AEQANAAEA/v8FAAMAAQAFAAAA9//u/+b/5//p/+L/4v/n/9v/zv/O/8//zf/O/9X/4f/m/9v/1//b/9//5P/m/+b/1//R/9T/3v/m/+P/2//O/8j/xf/B/9H/4//h/+v/8f/y//T/+/8MAAoABgAFAP7/EQAWAA8AJQAzACoANAA+AC0AHQATABsAKQAjABYAHQAWAAsAFwAkADUAPQAzACsAHwAMAAkAIAArACYAGgAIAAkADQAPABsALwA0ACsAKgArABwAGQAdABgAFAAHAO7/7f/v/+v/6f/3/+r/yf/I/8//yP/G/77/uP/H/8b/0f/Z/9T/1f/L/8D/zf/Q/83/2P/b/9j/2//i/9n/4v/u/97/4P/k/9j/9f/7/9v/6v/6//H/CQAnACoAEQAHABgAJAApACkAKAA4AEUAOgA1ACgAFwAQABoAAQDl/93/0f/C/8P/uf+1/6n/lf+V/7D/s/+2/+L/6f/U//P/+P/n//j/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UwBdAFQATAA/ADwALwAcAAkACgAAAOz/8v/6//X/7//v//b/5//T/83/yf+//7r/t/+o/7r/v/+2/7f/xf/A/7T/sv+9/8n/yP/C/8r/uv/B/8b/wv+6/7//wf+x/7P/uf+5/8D/vv+4/8H/u//Q/7r/vv/E/8b/y//X/+7/DQAmACoAVgBoAGsAeAByAHUAdQBoAHUAhgCEAIcAjQCRAIgAkACEAIYAfwB1AGUAVgBJAEkARAA9ACwAMAAuAC8AMwBDADsAQgBCADkAOgA5ADcANgBCAEcAQwBCAD4ARAAzACUAJAA2ACsAQAAyACYAJAAJAP3/DAAJAAkACQD6/+r/9P/1/+z/7v/o/+r/+v/2/wQAAQAQABoAIwAqACsAKwAxACoAJQAgABEADgAHAPz//P8IAA8AEQD7//b/6f/f/+P/1//a/8X/yf+3/8f/wf+9/7f/sf+5/7D/vP+u/7b/q/+r/7f/sf+m/53/rP+d/53/of+v/67/qv/A/8P/0//r////DgAMAB0AKAA3ADAAOgA1ADsAOwA1AB4AJwAmAB8AGwAJAAAA///o/+//8f8FAA4ADgATAPr/7//6//X/9P/m/+f/5//f/+f/1v/Y/9//4//l/+//9P/4//f/6//6//b/9//0//f/7f/l/9j/2P/e/8P/vf+3/7j/tP+8/7T/xv+x/67/o/+k/6n/sP+v/7X/rP+2/73/xP/B/77/yv/N/9P/2P/3//r//f8BAP//CwAVABcAIQApACwANgA0AD8ARQBAAEIARwBBADUAOgBBADsAPABKAFAASQBKAFQAVABUAGIAZgBeAFwAXABLAE8ARwBTAEwAOAAsADIALQAoADgAKQAuADEAKgA2ACsAMAArACMAKgApACcAMwAkAB4ADgASAAYA+/8BAAAAGQAHAAsACwADAPz/CwD9//f/7//Y/9X/1v/T/8j/0P/W/8v/5v/h//T/AAAIABcAEwAMABMAFwD8/woA/v/9//z/AAD9/+P/AADy/+7/+f////7/+v8MAAUABwANABQAGwAcADEAKQA5AEwATgBPAE0AXABNAFwAaABcAGAAVgBhAF0AUwBIADwAQQAvACsAKgAoACEAIgAhABgAHQATACUAHwAdAB4AGAAbABAACwAKAAIABQD0//7/+//+//b/+P/x/9j/3//U/93/zP/N/8z/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iv+N/5H/nP+j/6z/sf+4/77/xP/O/9n/6v/7/wgAEAAWACEAKgAuADIANwA2ADoAOwA4ACsAGwAWABEADwAKAAQAAQD3/+r/5f/g/+T/4//d/9z/1P/Y/9b/0//Q/83/x//F/8n/yP/H/8H/wf/F/8n/1P/C/9P/6//9/w0ACQATABMAGwAmACQAJAAmACwALwAqAC0AMAAzADEAJAAgABoAFwAVAA4ACwABAPz/9v/y//P/6f/o/+n/7P/z//3/CAAHAAQAAwD9//j/+v/4/+z/2v/S/9r/5//q/+H/1P/U/+H/7/8AAAAA/v/9/wYAIQA6AEcAPQAyADoAVQB2AIsAiQB+AIYAlgClAKUAnQCVAIkAewBwAGYAXgBWAEwATwBQAFAAUwBXAFIASgBIAEkASABFADQAIgAkAC8ALAAhABAACQAPABIADAAAAPv/+v/3//T/8P/u/+7/8v/8/wcAEwAeACMAIQAhACMAJwAiABwADQD///z/+//6//v/9v/5/wIADAARABIAFQAYAB8AKgAxADYAOgA9AD4AQABEAE0AWQBdAFcAVQBbAGYAawBlAGMAZABoAGsAYwBbAF0AXgBhAGIAWQBHAD0AMwApACEAHQAeACEAKwAyADMAKgAYAA4ABQD5//L/7P/h/83/xv/Y/+//8//Z/83/2v/l/+b/2f/I/8X/zf/F/6//ov+a/5j/k/+J/4L/hP+J/5L/nP+j/67/tv+1/7n/vP+3/7L/r/+1/7j/s/+v/6n/pP+p/7P/tv+5/8T/x//D/8L/yf/S/9X/1P/R/9n/4P/h/+T/5//w//b/8//q/+n/7P/t/+3/6//w//H/8v/0//P/7//o/+v/7//x//L/9P///wwAFwAaABkAGgAcABwAFAAGAPz//P////n/8P/w//f/+//+////AAABAAEAAgAIAA0ADgAPABEADgAQABIAHAAkACgAKwAnABwAEwAPAAkA/v/x/+n/6v/u//L/9P/1//L/8f/t/+b/4//g/+H/6P/u/+7/8P/4//7/AgAIAA8AGQAjACwAKgAlACEAHwAeABgAEgATABMAEgAPAA4ADQAKAAoACwANAA8AEwATABMAGQAaABgAGgAgACMAIAAnAC4ALwAyADEANgA7ADkAMAAlACAAIgAoACgAJgAfABUADAADAAAAAgACAAIA/P/6//j/8f/q/+T/4//Y/8z/xv++/7b/qf+g/5r/kf+J/4P/gf+E/4f/i/+R/57/pP+n/6z/sf+3/7//x//K/9D/0v/U/9f/2f/X/9P/z//K/8n/zP/V/9r/3//h/9v/2P/X/9X/1//d/+P/4//i/+P/4v/g/9z/1P/T/9L/1v/c/+P/7f/4//z//f8DAAIAAwAGAAMABwAMAAoADAAMAAwADAABAPn/+//+/wAA/v/5//f/+/8AAAEA/////wIABgAQABoAIQArADAAMwA3ADwARABMAEkAPwA5ADYAOwBCAEIAQABDAEUASgBMAEwAUABUAFUAVQBWAFkAYABfAFUATwBXAGQAaQBfAFQAWwBnAF8ARwA3AD8AUQBRADgAIAAcACMAGwAGAPv/+f/2/+7/4v/Z/9P/z//L/8r/zf/V/9f/0v/K/8b/zP/W/9T/zP/M/8f/vv+6/7v/u/+z/6z/qv+z/7z/vf+8/8X/1P/o//L/8P/w//b///8EABMAJAArACcAIwApADMAOAAzADEANwA4ADIAKgAkACQAIAAbAB4AHQAdABwAFQAOAA8ADAAMAAoAAAD2//T/9//5//f/9//9/wIACAAQABYAGgAfACwAOQA/AEYATQBNAFAAVgBdAGQAZwBpAGoAagBrAHEAcQBlAFYASQBAADkAMAAtACcAKAApACIAHgAgACkAKgAnACMAHwAZABEADAACAPv/8//m/9v/0f/J/8b/wP+1/6z/qP+g/5X/kP+Q/5P/j/+L/43/lP+c/6L/qP+x/77/wv/G/8z/0v/W/9L/zP/M/8v/zP/K/8b/wf/A/8L/vf+5/7j/uf+6/7z/v/++/77/u/+3/7b/tf+1/7n/vf/C/8P/yf/S/9b/0v/G/8D/wv/C/8L/x//M/9P/2f/Z/9v/5P/z/wQAFAAnADoATQBiAGYAYwBhAFgAVABVAFQAUQBPAE4ATgBIAEoATABNAE0ARwBGAEoASwBJAD8ANQA5AD0ANgAuACgAJAAgABwAGQAaAB0AIAAfABgAFgATABUAHQAjAC8ANgAzAC0AKgAbABYADwAJAA0ACQAHAAUACwAQAA0AEgARAA8ACgACAAQACAALABAADwACAPT/3//S/8f/wf/F/8n/zf/H/73/t/+v/6T/l/+T/5f/pf+v/67/sP+t/6z/rv+0/7r/wv/H/9D/3f/n//D/9v/2//P/8P/3//7/BgASACEALQAsACsAJgAmAC0AMwA+AFAAXgBrAHUAcgB0AHEAYQBaAE8ARgBGAEcAQwA5ADAAJgAdABgAEgAJAP7//f/4/+v/6v/l/9//5f/m/9r/4P/g/9X/1v/R/8b/xP+7/7H/rP+u/7f/w//L/83/0P/Q/8//z//V/+T/6//x//f/AgALAAsABwAHAAoADQASABgAFgAWABoAGwAaABAACwAMAAkACQAYACUAKgArAC8ALAAoACsALgAxADAAKgAmACAAHQAXAA8ADQAIAA0AHAAdABwAFgAQAAgA/v/1/+P/4f/c/9b/2P/V/9X/2P/W/8f/xf/M/8f/wv+4/7X/sv+t/63/pf+a/5f/nv+s/7v/xf/O/9f/2P/a/9//4f/f/+D/7P/1//j/8P/i/9j/0P/I/8H/wf/F/87/1P/V/9v/5f/t//L/8//2//3/AwALAA4AEQAZABsAHgAiACEAJgArAC8AMAAyADYANgA6AEAAQQA8ADkANwA3ADkAMwAtACsAGwAIAPX/6P/k/9r/1f/P/8P/vf+0/63/rv+z/7T/tf+z/63/qf+r/7D/sP+x/7j/wf/G/8f/zf/U/93/6//7/wYADQAWACAALAA2ADgAQgBOAE8AVQBZAF4AZQBjAFgASQBFAEgASwBLAEgARgBIAEUAQQBAADsAOAA+AD4ANAAwACkAJAAjABoAEwANAAQA/v/6//L/6//u/+3/5P/c/9H/zf/L/8v/0P/N/9H/0P/O/9X/zv/O/9P/1f/W/9D/zv/R/9j/3f/k/+r/6//t/+3/6//u//L/9f/2//n//f8EAA8AFwAeACAAJQAlACYALwAwACoAIgAhACQAIwAhAB8AJwAmACcAJwAgACcALwAyADMANgA6ADgAMQAkACEAJgAmACcALQA0ADcANQAvACUAKQAuAC4AKgAlACMAIAAeABoADgAJAAgACwAQABAAFgAWABYAGAAWABYAEgATABUAGQAZABgAGgAkACoAHwAeABsAHQAmACYAJgApADEAMgAwACoAJQAnACgAJAAoACkAIgAmACsAKwA1ADkAQABKAE4AUQBUAFYAVQBZAF4AYgBfAFwAUQBFADoAMQAmAB4AHAAWABEADQALAAkACAAKABEAEAANAAoA///0/+//5f/g/+P/4f/i/+H/3f/e/+X/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vv+9/73/t/+w/6b/of+n/7D/t/++/8b/zv/U/9r/3v/e/+D/3v/d/93/2//d/+L/4v/j/+L/3//Z/9X/0v/U/9j/1f/U/9P/0v/V/9b/1//X/9z/4//p/+n/6P/q/+//9f/7/wIABwANABQAHwAjAB8AFQAQABQAGwAgACMAJwAuAC4ALAAoACcAJwAsADMANwA3ADAALAApACMAHgAcABYAEAAQABUAHQAfACEAJgAoACgAJwAmACQAKAApACgAJgAkAB8AGwAbABoAGQAQAAwADQANAA8AEQARABIAFAASABEAFwAfACUAIwAjACUAJQAmACQAJwApACoAKwAtADUAOgA+AD8ARgBNAFAASwBFAEMAPgA4ADEAMAAtACcAHAAXABYAFQATABAADwAMAAoACgANAA0ADwAHAAIA/f/7//v/9v/z/+//7//u/+z/6f/x//r/AAD///j/9f/5//3//f8AAAYACAAIAAYABQAEAP///v/+//7/+f/w/+n/5v/k/93/2f/V/9L/0v/R/9P/0P/O/83/y//N/8v/y//L/9H/1//Z/9n/2P/a/9v/3v/b/9v/3v/g/+f/9P8AAAsAGAAjACoALQAqACgAJQAiAB8AIwAjACEAKAAxADYAPwBCAEQARgBLAE8AVQBeAGYAaABmAF4AVABIAD0AMQAnACQAHQAQAAEA9f/v/+b/1f/E/7//wf/F/8X/wv/D/8f/y//Q/9D/1P/b/93/3f/Z/9n/1f/R/9H/0f/N/8r/vv+4/7H/rP+k/53/nv+d/6L/pf+p/6z/q/+u/7H/sv+4/7//w//I/83/3P/p//D/+v/+/wIABQAEAAAA+f/2//X/8f/y//X/8f/u/+7/8P/u//H/9//4//n/9v/3//b/+P///wMA/v/3/+3/4//f/93/3//i/+P/4v/l/+f/6P/p/+f/5v/k/+H/3f/e/+D/6P/u//P/+v///wIAAwAHAAkACwAHAAUABgABAPz/+f/2//n/+//8/wMACgAUAB4AIwAkACYAKQApACYAIAAgAB4AHwAdABgADgAHAP///P/8//f/8f/w//D/8v/2/wIAEgAZAB0AIQAsADMAOAA0ADIAKgAfABMABAD+//b/7//k/9r/0f/I/8T/xP/H/83/zv/O/9H/2f/f/+L/6P/s//L/9P/z//T/8//t/+f/4//f/+D/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KgArACIAGgAZABUAEQAMAAoABAD5/+//7f/u/+X/2v/O/8H/s/+p/6f/p/+n/6n/sf+t/6D/nP+Y/5r/p/+r/6//v//I/87/zf/N/9X/3//f/9b/0//P/8v/z//T/9b/2f/Y/9n/3P/Z/93/4P/c/9v/3f/c/+L/5P/k/+P/3//c/+D/4f/g/+b/8v/2//n/+f/5//X/7P/n/+b/3f/Z/9z/2f/a/97/3//c/9X/zf/C/7T/p/+Y/5n/nf+Z/4v/f/99/3n/bf9o/2X/Zv9o/2z/bv9u/27/cP90/3T/eP91/3f/gP+G/4n/jP+O/47/lP+b/6T/pv+h/53/nv+e/6X/q/+s/7P/u//C/83/1f/U/9f/4//p//H/+P8BAAgABwAFAAgACQAKAAsACQAIAAEA8v/s/+f/4v/b/9X/0f/Q/9L/1P/U/9f/3P/b/9T/z//L/9H/2//b/9z/2//d/9z/4//l/+n/8P/t/+f/4f/W/9T/zv/G/8X/w/+8/7n/uP+3/7D/rf+t/6z/qP+i/6T/p/+r/7v/yv/N/8//0//R/9j/3v/k/+3/9f/7//z/AgAPAB0AJQAsADAALAAwADAALQApACkAKgAvADIAMQA0AD8ASABUAGQAbQB2AH0AggCQAJ4AogCxAMAAxgDMANEA2gDkAOoA8gD4APoA+QD3APMA9ADyAOoA6ADnAOIA4ADgAN0A3ADXANAAzQDLAMQAwQDCAL8AuACsAJ8AkwCKAIkAhgCDAIEAegBxAG4AagBjAFgAUQBDADUALgAmACMAHwAbABcAEgANAPr/5//f/+D/3v/X/9H/zv/K/8r/0f/R/8v/v//A/7j/sv+4/73/zf/V/9v/4f/m//L/+v/+/wkAGAAhACMALAA2ADQALgAsACwALgAtAC0ALwA1ADoAOwA5ADoAPQA+ADwAPAA9AD8APQA2ADUAMwA6AEEARgBOAFEAVQBgAGIAXwBgAGMAZgBgAFcATQBDAEEANgApACAAGwAWABAAAQDv/+3/7f/l/93/1P/J/77/sv+j/5r/kv+I/4T/f/+C/4f/iP+N/47/jf+H/3//gP+C/33/e/96/3//hP+K/47/i/+I/4X/hP+D/3v/cP9v/2j/Yf9h/17/Vv9S/1H/T/9W/13/Yv9m/2z/d/+D/5P/oP+r/7D/s/+2/7r/wf/H/83/1P/X/9j/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7//x//D/9v/3//L/8f8GABkAGQAaABoAIQAsADEAMgAvADYAOQA3AC0AKgAnACgAKgAdABcADwAGAPv/+P/3//r/8//u//b/+f8EAAMABwAAAPH/5//e/9T/0//a/9H/yP/E/7n/uf/B/77/xP/L/8X/wv/D/83/1P/V/9n/2v/g/+r/7P/0//j//v8GAAIA//8BAAQAAAD+/wEABwARABIAEgAaABwAHgAgACQALwAwACwAMQA3ADAAKwAnACcAIgAcABkAEwAUABAAEgAXAB0AIwAoADAANQA+AEAAOwA2ADMANAA1ADYALwAoACwAJAANAP7/9P/r/9b/xP++/6//qP+e/53/pf+k/6P/pP+q/6//qf+o/7T/uv+3/7H/s/+6/8X/yP/J/9P/3f/k/+n/7//5/wUADgAaACsANwBJAFEAUgBeAF4AYgBhAFUAVQBZAF4AXgBfAGUAZQBmAF4AWABZAFkATwBFAD4AOwA8AC8AJwAbABAAAwD3//T/8f/y/+z/5v/g/93/1P/Y/9//4v/i/93/2f/X/9T/2P/g/+P/8f/v/+n/9P/x/+b/5P/n/+3/7P/j/+P/6v/j/9//2v/a/9f/1v/e/+P/8f8EAAUACgAKAA0AEgAJAPn/7f/p/+T/3f/e/+T/3//T/8//z//K/8T/w//B/8T/yf/N/8//1P/Z/9j/1v/S/9X/2//e/9v/4P/e/9f/1v/T/9r/4//t//L//f8JABQAGwAgABkAEAAZABwAHwAhACYAJwAgABUACAAGAAkADAAKAA4AFQAaABQAGgAVABQADwAJAAwABwAJABAAEQAaAB8AIAAiACoAMgAtACoALwAmABgAGQAfAB0AFgAUABEADgAOAAwACAAGAAcABwAAAAsAGQAiAC0AKgAoACIAJwAuAC8AMgA4ADoAMQAqACMAJAAiAB8AFwABAPD/5f/b/9n/1P/T/9X/1P/V/+L/7f/7/wMAAgAOABgAFwAVABYAHAAaABYAGwAiAB8AGwARAAQABQALAAoABwD9//r//f8DAAMACAAfACgAJAAgACoALAAlACAAHAAdABsAFwASABIAFQATAA8AFQAaABAABwAAAPn/8//4//z/9v/8/wMAAwD9//z///8AAPL/7P/v//f//f/v/+b/6f/x/+//7P/q//X/9v/z//L/9/8CAAwAFgAQAAwADQAOAAsABgD/////+//u/+v/3P/Q/9D/w//A/8P/uv+z/6z/p/+r/6b/p/+v/6//r/+9/8H/xv/Z/9P/2f/p/+z/8v8AAAwAIAAkACoAOAA+AEUARgBJAFQAXABZAFUATwBLAEoASgBCADUALwArACcAIQAoADUAMAAuACEAGQAeACUAIgAsADYALgAvAC4ALQAvADMAMAAxACwAJAAhACIAJQAaABkAGQAaABcAEAAPAAwABAD+//n/8v/o/+j/6//n/+f/3//m/+j/2v/Q/9D/1P/W/8z/w//I/8n/wP+8/7b/t/+0/6j/o/+n/7X/v//G/8v/1f/U/8r/yP/I/7//vP+5/7n/tf+7/7//vP/G/8X/wv/C/8X/3f/q/+3/8v/7//z//f/y//L/AQAHAAgABQAHABEAMAApADAAMgAOAOX/6P/s/8j/zf/p/+D/2/8BABIAFgAjABoACwD//wcADgALABEAEgABAPL/7//q/+//8f/3//X/6v/k/9j/0v+7/6z/pf+c/5L/lP+m/7X/vv+9/8X/yv/B/7//x//R/9r/5P/x/wMAHgAiACsAOgBDAE0ATgBfAGsAbwBvAG4AewB6AHEAYwBbAFoAUABMAFcAXgBaAFQASABFAFQATgBCAEAAPwA2ACcAHgAeACIAEQAMAAcACAADAOf/1v/W/9r/zP/L/9D/zf/E/7r/rv+r/6j/m/+b/6L/p/+o/7T/vv++/8b/1v/g/+L/4f/c/9f/3P/p/+3/9//w/+r/+P/7/wUACAARABYAEQAUAB4AKQAxADIALgArADEASgBMAFwAYQBtAGoAeQCBAIUAkgCOAJoAmQCfAKUAowCdAJgAkwCPAJAAhwB+AHMAeQCAAH0AdwByAHMAcQB1AGMAVQBWAEwARQA1ADUAPAA7ACcAIAAUABUADAACAP3//P/5/+r/5f/d/9P/yv/E/7H/sf+p/6f/pf+Y/5L/if+B/3T/cf94/33/c/9u/2b/aP9q/27/c/94/3//d/93/3T/e/9+/3b/cv90/3P/b/9x/3b/fv+H/5P/m/+p/7X/wf/D/87/1//Y/93/7f/4//z/9f/v//7/AgD9//n/AQABAPb/8P/y//P/9P/y//P/AAANAAwACwAOABIAFQAQABcAEwATAAYADQATABEAEAAOAAkACQALAAMACgD7////EAATABgAFQAMAAwABwABAAkAAAACAP3//P/2//P/7P/v/+//3v/h/+3//f///wEA9/8BAAkAAgD8//f/+P/1//b/+f/7//X/8v/0//b/AAAJAAsACwALABAADwAJAAAAAAACAPz/8f/p/+j/7P/j/9T/zP/G/8j/uv++/7z/wv/C/8n/z//Q/9X/3P/d/9r/2P/V/9//4//t/+//AAAHAAgABwAIAA4AEQAZACQALwA0ADMALwAlACUAIwApACwAIQAmACgAKAApACgAKgAcABsAJAApADkAPgBAAEQASwBLAE0AVwBWAFsAWABaAFwAXABZAE0AUgBLAEUAOAAvACsALAAzAC8AMAAsAC4AJAAiAC0ANAA5ADkAPQA2ADEAKwAxADEAMQAzADIAKQApAC0AMQAyADEALwAqACoAJgAbACQAHQAbABsABwAFAAMA/f/x/+X/2//c/9z/1//U/9P/0f/N/8X/wP+6/73/zP/W/+L/6P/y//b//f8EAA0AJQA2ADoAMgA0ADUAMwA1AD8ASgBZAGAAYABuAGgAbgBoAGsAZwBqAGYAYwByAG0AbgBlAFwAWABOAEIAOAAzACgAIwAXABMABQAKAAMA8//g/9T/0v/N/87/zP/R/8f/xv+8/7n/tv+m/5z/mf+X/5L/lf+Z/6r/rP+k/6H/oP+k/6j/tP/A/8//0f/T/8f/w//A/7z/wf/C/8//yv/S/9b/3v/e/9j/2//Y/9f/3//k/+X/6//u/+X/3//i/+T/5f/k/+H/3v/e/9//2//X/9//3//g/9P/1f/S/8//y//E/8n/xv/E/8X/w//B/7//wf/K/9T/1//V/9j/2v/d/+D/5P/o//D/9P/u/+r/5//l/+H/4//e/93/2//i/+n/6v/u/+X/4P/i/+H/3v/T/83/zP/O/8n/v//E/8L/uv+p/6D/qP+w/7z/uv/I/9n/zP/H/8v/zP/R/9T/0v/S/8//zf/G/8b/zP/Y/8//0P/b/+L/7P/o/+z/+P8EAAkAFAAfACYANwA2ADoAPQBHAEoATgBhAGwAbwBsAG8AbwB1AG0AZABoAF4AYABbAFsAagB0AHcAeQBzAGYAXABRAD8AMwAfABUACgD6/+T/zv/P/8b/w/+5/7P/wP/A/8D/vv/D/9H/2v/Y/9r/7v/5/wEAAgADAA0AEQADAPn/+f/4//n/9P/8/woAEwAOABYAHgAcACUAJQAgACIAFQATABcAEAALAPz/8//v//P/8f/u/+L/3P/Z/8//zP/P/9L/1f/P/77/vf/H/83/x//I/8b/wf+2/7j/yv/Z/97/2f/e/+P/9v/7//7/CgAOABoAJQA/AFAAYABqAH0AiACLAJkAowCtALUAugDEAMEAwADFAMAAwwC+AMkAywDEALwAsgCyAKkAqgCgAJUAjACDAHQAXgBPAEoARQA7ADUAMQAtACwAHwAUABQAFQAaABsAGwAZABIADwAPAAsACAAAAAYABAACAAsADwAXABUAFQAOAA4ACwATABkAEgASABMAGQAcACIAHgAfABgADQAUABMAEQAaABAACAD//+//9v/0/+3/7P/e/9P/0f/P/87/zf+//7H/qv+g/5X/lP+I/4H/ff94/3b/ef9+/4X/j/+J/5X/nP+b/63/vP+//8v/0P/K/9r/3P/V/9r/4//r//b/8f/y//7/AgALAB0ALAA/AE0ASQBCADcANwA/AEUAPwA0ACsALgAtACoAIwAhABkACQD///f/+/8CAAIA8f/l/9r/xP+//7T/qP+a/4f/gP+D/4j/gf+B/3b/ff99/33/gP95/3v/c/9t/2j/a/9n/2n/c/9z/2v/dP92/27/cf9x/3j/f/+E/4P/k/+m/6j/uf/C/8r/0f/Y/97/5f/x//f/+v8AAAkABgAIAAcAAwALAAoAEQAdACgALAAmADEAPgA5AD0AQgBCAE8AUwBbAF8AYABWAEoASQBNAEMANQAtADUAPwBHAD8ALQAqACUAGgAOAPv/9f/s/+P/5f/h/9//2P/W/8//zv/V/9f/3v/V/9n/2//r//D/+P/7//b//P/8/wkACwAaACYAIgAeABUAFwAaABYAGAAcACMAHQAaABsAHAAmADAAKwAnACMAKgAvADEALwAlACkAHAAZACUANAA+AEAAQgA3ADcANgBDAEkATABSAFQARwBMAFUAVQBZAEsAQgA2ADwANwAwACkAIwAoACEAHgAXABUAGAAWABMA///9//L/4//m/9P/2f/f/+X/5v/Y/9H/xP/C/8n/1f/V/9T/1v/R/8v/yv/L/9H/4v/e/+L/5v/w//n/DAAVAB8AHAAaAB8AHgAnABoAGAAZABcAEwASABQADgAHAPr/9P/w//T/7//h/9z/1//j/9v/0v/R/9X/0//V/8//2//y//z/BQALABUAFAAfABsAGgAiAB8AKQA4AD8ATABTAFYAZQBiAGkAbABxAH8AgwCHAIMAfAB8AH8AfwCCAHsAeACCAHoAcQBnAFkAWABMAEMAQgAyACMAEwAFAP3/9P/n/+P/5v/m/+n/4v/S/87/w/+5/6z/p/+j/6T/ov+b/6b/uf/J/9D/yv/H/8r/zv/b/+f/6v/v//b/AgAOABUAJgAoADgASABWAFgAXQBuAHgAegBxAHEAaQBeAFsAVwBTAE0AQwAyAB4AHgAYAA0A+v/p/+f/7f/v/+r/7P/u/+f/2f/f/+v/7v/q/+D/3P/a/9n/1v/W/9f/0//E/7v/u/++/7n/s/+r/7L/tv+v/6r/r/+7/7v/s/+x/7n/wv/G/77/uf+1/7f/tf+4/8L/uv+y/6b/qP+q/7H/qP+m/6H/lv+S/5b/nv+i/5r/k/+c/6X/of+j/63/t/+8/7//x//K/8b/xf/H/8j/0v/X/9v/3f/V/8z/zP/N/8P/yv/S/9X/3f/d/9r/3f/W/9X/4f/f/+T/2f/W/9b/0v/N/8n/xP+9/7T/qP+k/6L/o/+Z/5D/gP97/3T/cP9r/23/c/9x/4D/hf+N/4//k/+V/5r/nP+a/5z/o/+x/7f/wv/I/9D/1v/d/+3/9P/y//j/BgAPAB8ALQAwADYASQBRAGAAbwB9AHwAdgB5AIUAhQCLAJQAmACVAIkAiwCGAJAAlQCUAJIAgwBzAHAAbgB0AHgAdQBwAGIAWQBYAFUAVgBOAE8AVABLAEQANwA2ADcAMQAnACIAIgAmACoALQA3ADwAOAA2AD0AQABHAEwATgBVAEwAPgBBAEwAVABaAFIAVwBcAGIAZQBmAGcAbAByAHAAcwB4AHcAeACAAIAAfQBwAGoAbgBvAHgAagBqAGgAZgBxAGwAdgBtAHMAcwBwAHcAdAB5AHkAdwB5AHQAeQB2AHUAgAB+AIUAiACIAIIAfgB4AHAAdgBpAFsAWABQAE8ARgBAAD0AOwA3ADMALgA4ADwAMQAmABcACAAGAP3//f/y/+b/4f/W/8v/wv+2/7P/tP+y/7D/s/+9/8j/0v/U/9P/1v/W/9r/3//o/+//8//6//r////7//b/5P/Z/9j/2P/i/9//1v/W/9L/y//F/77/wf/B/77/u//B/7z/s/+s/6P/p/+p/5z/j/+R/5L/k/+P/4v/hf95/3P/b/90/3H/cf9v/2//bv9o/2T/YP9m/2X/Xf9b/1//a/9u/3T/gv+H/5P/lP+T/5n/qv+z/7j/u/+9/73/uf+w/6n/qf+j/57/l/+H/3//df9w/23/Y/9W/0n/Rf9F/0b/Sf9O/0b/Rv9F/0X/RP88/zn/Nf85/zb/Nf85/z7/Qv9J/03/Xf9m/3P/d/98/4T/h/+Y/6T/sv+5/7b/vf/C/8X/xf/P/9v/6P/y//T/7//r/+//7v/u//L/+f///wwADAAOABIAGgAhACMAIgAjACQAIwAuADIAOQBAADcANQA1ADQAMAAvADAAOgBIAEsATABJAEkARgA9ADYANgAvACkAMAAwADUANAAwADgAPAA8ADYALQAzAC8AKgAtADUAQABDAEsAWQBoAHEAfACJAJEAlACQAJIAkgCNAIIAhQCGAIQAiACCAIUAhwCCAIEAfgB+AIMAgQB7AHwAdgBvAGYAWABPAEgARQBIAEcASQBJAEUARwBJAEkASgBMAEkAQQA6ADwAOwA5ADwAQgBJAFAAUQBVAF4AZgBsAHIAcAB1AIAAhQCMAIoAiACLAJEAngCdAJQAlACSAI4AjwCOAI4AiQCEAIgAiACEAH4AdgByAGMAUwBSAFoAZgBZAEgASwBNAEoANQAjAB0AHgAXAAUA/v/4//n//v/z//L/+f/4/+r/3P/W/8z/wv+4/7j/xv/O/8X/v//H/87/0P/S/9r/4//q//D/9P8BAAQAAgAGAAMA/v/5//f/+f8AAAEABwAQABIAFwAaAB4AIQApADEAOwBFAEwAVQBcAFwAXgBiAGIAXgBYAE4APAAqAB0AFQASAAsA+v/n/+L/4//i/9//4f/h/9n/0P/M/8z/v/+r/6P/m/+N/4L/ef9y/23/aP9g/1b/Uv9R/0r/Rv9D/z//N/85/zv/Mv8w/zT/Of86/z3/RP9J/0//W/9o/3D/f/+S/5z/oP+m/63/sv+2/7v/wf/O/97/5P/j/+T/6//x//f/+//8//3/9//u//H//P8CAAUACAAQABoAHgAcAB0AHwAlACMAHgAeABsAGQAWABYAEQAHAAQA9v/p/+P/3v/W/8z/xv/A/7r/uP+5/77/vv+6/7f/tv+3/7v/u/+7/7n/t/+5/7X/tf+8/8v/1P/V/9//5//v//f/9//6//v/+//9//n/8v/p/97/1f/T/9X/1v/T/8//zP/S/8//xf/C/8L/v/+9/7j/sf+w/67/r/+y/7b/vP/C/8b/xP/G/87/0//T/9b/3v/o/+//+f///wEAAgADAAgADwAbACUAKQApACgAKgAqACsALQAzADsANAArACcAJQAkACgALgAwADcAQQBGAEcASQBLAE4AVABhAGkAaQBmAGgAZgBiAGMAYgBhAGYAbQBxAHUAdQBuAGUAXgBYAFIATABHAD4ANQAoABwAFAAMAAUA///9/wIABgAGAAUABAACAP3/+P/2//T/8f/p/+L/5P/p/+7/8v/4/wEABwAKAA4ADwAOAA4AEQAVABUAFQAZABkAFQAPAAgA///3//L/6//o/+v/8v/3/wAACAAPABIAEwAUABQAFQAXABkAHgApADAALQArADAAOAA8AD8AQwBHAEcARAA/AD4AQgBHAEcAQgA+AD0ANgAtACkAKAAoACUAIgAjACAAHAAcAB4AIQAiACAAGwAVABIAEgAPAA0AEAAbACkALwAsACYAJQAgABgAEwALAAUA/v/2/+3/4f/V/8v/xv+7/6z/ov+b/5T/jf+H/4b/g/99/3j/dv93/3f/cf9u/27/bf9t/3X/hv+d/7T/y//i//X/CAAcACwAQABbAHEAeAB8AIUAigCHAIQAhwCIAIkAjACIAHwAdgBzAGoAXwBaAFMARwA9ADwAPAAyACUAGgAZABwAGgAZABgAGwAdAB0AGQAYAB4AJQAqAC0AMAArACMAIAAeAB8AIAAgACUAKQAoACMAGAAOAAgAAgD5/+//5//g/97/3f/W/87/zf/I/8D/uf+7/77/wv/H/8f/x//H/8X/w//G/8X/v/++/8f/z//Q/9P/2f/j/+f/5v/s//j/BAAMABAAFQAYAB0AJAAsADMANwA8ADoAOAA1ADUANQAvACcAIAAaAA0A/f/z/+3/6P/j/93/2P/Q/8T/uf+y/7D/tP+3/77/yv/T/9b/1f/R/9D/1P/Y/9b/z//M/87/yv/A/7z/wf/G/8X/wv/B/8L/xf/H/8X/wf/B/8D/vP+2/7X/s/+w/6f/nf+V/5D/jf+K/4P/gf+B/3//fP96/3j/c/9t/2j/Zv9l/2j/aP9q/2//cP9u/27/b/9v/2z/bP9w/3r/hv+P/57/sf+//8v/1//g/+z/+f8CAAcADQAVACAALAA4AEMASwBPAE8ASgBLAFMAVwBVAFAASgBEAD4AOAAvACQAGgASAAoABQAFAAQAAAAAAAAA/P/w/+b/5P/q/+z/5v/d/9X/0v/S/9L/1P/b/+H/5P/p//L/+f/9/wYADwAZACUAMAA1ADgAPAA/AD4AOQA0ADAALgApACQAJQAlACAAHwAfAB0AFwARAAoABgAEAP//+f/4/wQAFAAaABUAEAAbACwAOAA7AEEASgBVAFsAWABUAFEAVwBfAGUAaQBuAHoAggCJAI8AmgCqALoAwgC/AL4AwAC/AMAAxADHAMEAwwDKAM4AzgDKAMUAwwDGAMoAzQDPAM8AygC+ALQArwCsAKcAnwCWAJIAjgCGAH4AcgBrAGQAWQBPAEQAOgAzADMANgA3ADYANAAyACwAKAApACkAIgAdAB4AIAAhACAAHQAYABUAEwAOAAUA+//0/+v/4P/Y/9v/2//V/8r/vv+z/63/qf+k/53/lv+X/5v/ov+n/6j/qf+p/6f/oP+d/6H/o/+j/6j/tf/F/9T/4f/r//H/+P/9////AAACAAQACQANAA0ACwAJAAgABgADAP///f////n/8f/u/+7/7P/n/+D/0//H/7//tf+s/6j/p/+o/6b/pv+o/6z/tP+3/7D/qv+o/6T/mf+V/5v/pv+z/8D/yf/N/8//0v/U/9b/2v/c/9f/1v/a/93/3//i/+T/3f/L/7r/rP+d/47/fv9s/1z/Tv9D/z//Qf89/zf/Mf8o/yH/Hf8d/x//If8k/yr/MP8z/zr/Sf9Y/2P/cv+C/4r/kP+Y/5//pv+o/6j/s//A/8j/y//S/9r/3v/j/+n/7f/v//L/8//0//b/9v/0//j/AgAJAAwACwAIAAAA+//6//v//f/+//3//f8FAA8AEgARABMAGwAgACQAJwAmACsAMwA1ADUAOAA/AEcASwBRAFUAWABdAGMAbgB7AH0AegBzAGwAaQBmAGYAaABkAFsAVQBRAFEAUABKAEYARgBDAD4APQA6ADkAOwA+AD4APQA6ADkANgAxACwAJwAhABkADQADAP7//f/9//r/8//v/+//7v/u/+z/6P/o//H/+v8CAAoAEAAQAAsADgAUABYAGQAfACcALQA0ADIALQAqACwALwAyADAAKwAoACYAJgAiABoAFgAbACMAJgAkACQAIgAjACcAKwAoACQAIQAbABYAEwATABUAGgAdAB8AHAAXABYAGgAjACsALgAsACYAHQAWABEADQAHAAMAAQD9//n/+P/x/+n/4v/b/9T/y//J/8r/yf/I/8f/xf/G/8j/x//H/8X/w//C/8P/xv/J/9P/4f/s/+//8P/z//b/+f/6/wMABwAGAAcABwADAP////8CAAcADQARABMAGgAjACoAMAA2ADwAQAA+ADsAOAA1ADYAOgA7ADsAQgBMAFEAUQBPAE8AVQBgAGUAZABiAGUAZABeAFoAWQBWAFYAVgBTAFIAUABJAEEAOwA0ACsAKwAsACMAFQAQAAwACwAIAAUAAwAAAAAAAQAAAP7//f/3/+7/6P/i/97/2v/Y/9X/zP/G/8n/0P/P/8v/xv/B/7j/rf+o/6f/qv+2/8H/xP/C/8z/2v/h/+X/6v/t/+//9P/8/wIAAwD+//j/8//1//z/AwAEAAYADAASABcAGAAVAAwAAAD2/+X/1f/N/8j/wf/A/8T/wv/A/7//v/+8/7v/v//B/7//uP+1/7j/u//A/8j/0//e/+X/5//p/+7/9v/8/wEABgAJAA8AGwApADEAOAA/AEcASABCADgAMwA0ADQALgApACkAJAAdABkAGAATAA4ABgD8//b/8//u/+f/4P/Z/9L/zP/I/8X/w//D/8b/zf/N/8X/w//F/8X/w//B/7z/t/+z/7D/rf+n/6H/nf+f/6D/nf+a/5n/mP+V/5P/j/+I/37/d/93/37/g/+F/4f/if+M/4//k/+Y/6H/rf+8/8j/1//q//r/CgAbACcALgA2AEEASABSAGAAbgB5AH8AgQB9AHsAfAB6AHYAdABxAGgAYwBhAF8AYQBnAGcAZABjAGIAXQBUAEkAQQA5AC0AHwARAAcA/v/w/+L/2P/W/9v/3//i/+H/3//e/97/3f/a/9r/4P/n/+v/8f/3//j/+/8FABAAFgAeACkAMgA3ADcALgAnACQAIAAZABIADgAPAA4ACAACAP7/+f/2//H/7P/k/+H/3//X/8v/xf/E/8b/yf/L/8j/xv/L/8//0f/U/9j/3P/f/+T/6//u//L/+f/7//7/AwAJAAwACAAAAAAABQAKAA0AEQAWAB4AJQAoACYAJAAnADMAOQA3ADQANQA4ADQAKgAcABAABwADAAAA/v8AAAEA/v/3/+3/4v/c/9v/3P/g/+H/3f/V/9H/0v/Z/+D/5v/x//r//v8BAAYACgAKAAUAAgAEAAcACQATACMAMAA0ADkAPgBBAEIARQBIAEcASwBPAE0AQgA6ADoAOwA7AD0AQAA8ADYAMAApACUAJAAnACoAKgAoACkALQAvAC8AMgA2ADgAOQA8AD8AQAA9ADYALQAnACMAHwAfACIAIAAdABsAGwAfACcALQAuAC8ANAA0ADAALwAvACwAIgAXABAACwADAPr/9P/v/+r/6f/m/97/0f/H/77/s/+q/6v/sP+z/7X/uf++/8P/xP/H/8//2f/e/93/3f/c/9f/1v/b/+H/5v/s//D/8//5/wMADgAUABYAFgAVAA8ADAARABgAGwAYABQAEQASABYAFgAVABAACgAEAP///P/4//j/9v/y/+//7f/n/93/1P/K/7//sf+k/5r/k/+L/4H/d/9y/3P/df94/37/gf+C/4L/g/+F/4f/iP+J/43/kf+N/4f/hv+I/4n/iv+N/5P/n/+q/67/rv+s/6n/qP+r/6//s/+6/7z/v//E/8f/yP/G/8j/zP/R/9j/4v/v//j///8HABAAHQApAC8AMwA0ADIAKgAlACEAHAAXABgAHAAcABkAFQAMAAMA/P/1/+z/3//S/8r/xP+//7z/uf+8/8H/wf+8/7j/t/+6/8T/z//W/9v/4P/l//D//P8GAA8AGgAjACkAKwAsAC8ANgBBAEgATgBTAFwAZgBvAHgAhACSAJ0AoACfAKEAqgCuAK4ArQCnAJwAjwCEAHwAdQBtAGQAXQBYAFcAVwBXAFMASgBBAD0ANQAoAB4AGwAZABYAFAAUABUAFwAWABEADAAIAAcACAAOABMAGAAeACMAJgAoAC4ALgArACsALQAxADcAOwA8ADsAOQAzAC4AJgAhAB8AHwAcABgAFAAQABIAEwATABAACQAHAAoADgALAAUAAgACAAAA+v/1//b//v8HAAoACgALAAsADAAPABQAHwAvADsARABQAF8AcACAAIwAlACdAKcAtAC5ALgAuwDDAMUAvwC8AL0AvgC+ALsAugC5ALcAswCtAKUAmwCOAH4AbgBkAFoAUABDADgAMAAsACUAGgAQAAsACgAGAP7/8P/f/9H/w/+1/6T/mP+P/4P/dv9r/2H/Vf9P/0j/Qf83/y3/J/8q/zD/NP85/0H/Rf9J/1H/Vv9V/1P/Vf9a/2P/av9u/3H/d/94/3X/dP9y/3H/cP9w/3H/cP9y/3j/e/9+/4X/kf+h/6//vP/D/8n/zv/Q/9P/1f/Q/8r/xf/F/8T/wv/I/9L/3P/g/+T/6v/w//j/AwAQACIAMwA+AEUARQBFAEcAQwA8ADUAMwAvACMAFwAOAAkAAQD1/+f/2P/N/8L/uf+0/7H/rP+q/6r/r/+z/7f/uv+7/73/xP/K/87/y//D/73/wv/I/83/0f/V/9r/3v/j/+f/5v/f/9b/0P/R/9P/0P/M/8n/yP/I/8f/wf+8/7P/qv+h/5r/j/+G/4D/ff98/33/gP+D/4r/lf+b/6b/t//G/8//0//X/9j/3v/o//L/+P/9/wAAAgAKABQAIQAuADkAQABBAEEAQABBAEMAQwBBAEUATABTAFYAWQBbAFgAVwBUAFMAVwBcAGIAZQBkAGAAXgBeAGEAZQBtAHMAeACBAIYAjQCSAJMAkACOAIsAhAB5AHAAbABqAGgAYQBZAFMATwBQAFIAUwBVAFQAVABZAFwAXgBdAF4AYgBhAGAAXQBbAFoAVABMAEQAPAAzAC0AKQAmACUAJgArADEAMwAwACsAJgAlACkALAArACkALgA7AEsAXQBtAHgAfAB/AIEAhQCEAIIAgAB6AHcAfQCCAH8AegB2AHMAbgBlAF8AXwBjAGMAXgBaAFMASwBLAE4ASgBDADoAMAAoACEAGgAWABcAFgAUABIAEAAOABAADgADAPP/5P/a/87/xP++/7r/uf/A/8n/y//H/8D/u/+5/7T/rf+q/6j/pv+d/5H/gv9u/1z/UP9K/0f/SP9L/03/UP9V/1f/V/9W/1b/Wv9c/1//Y/9q/3b/hP+Q/6D/rv+4/8H/yP/Q/93/6P/v//H/8//2//f/9//2//L/8v/2//3/BAANABcAGgAeACYALwA5AEEASQBTAFsAXgBdAFsAVwBTAE8ATQBHAD8AOAAyACwAJwAfABMABQD9//X/6f/f/9n/1f/N/8T/vv+6/7P/pv+a/5P/kP+N/4f/fv93/3P/c/9y/27/Z/9h/17/Xv9g/2X/Zf9i/2P/a/98/4n/j/+X/6P/rf+0/7z/yP/V/9r/2//d/+D/4f/e/9j/1f/V/9T/0P/L/8v/z//U/9z/5v/x//j/+f/9/wUADQANAAsACAAKAAsADgAQABEAGAAdACMAKgAwADUANQAxAC4ALgAwADYAOwBAAEAAPwA/AEAAQgBFAEgASABHAEcATABTAFkAXQBfAF8AXQBaAFgAWABcAGEAYgBeAFkAVQBSAFAATwBSAFMAUABNAEoASwBRAFQAUgBOAEkAPwA0ACkAHwAYABAADAANABMAHgAkACcAKgApACkAJgAkACEAHgAXAA0ABQABAPv/7//m/+T/6P/r/+//8f/u/+r/6//v//j/+//5//r//f8HABEAFgAaABoAHgAmADAAOgBDAEgATQBRAFUAVgBWAE8AQgA5ADgAPgBCAEIARABHAEQAPgA8ADcAMAAsACUAGQAIAPf/6P/a/8z/wP+3/63/oP+X/5P/j/+N/4z/kP+W/5z/ov+n/6z/sf+6/8P/yf/P/9T/1v/Z/97/5//x//b/+/8CAAsAEwAWABkAGwAcABwAHQAgACAAGgAWABkAHQAcABcAFgAeACUAIwAWAAwACgAKAAQA+P/p/9//1//K/8D/u/+7/7T/q/+i/6D/p/+w/7f/u/++/7//vP+1/7L/uv/F/8v/0P/X/9//5P/q//T/+////wIADAAVABcAFgAZABkAFwASAAsABQABAAAA///7//H/6v/p/+v/5v/h/+D/4f/m/+b/5f/k/+T/4//h/97/3P/c/9n/1P/T/9T/2v/e/+X/6v/t//L/9f/5//v/+v/+/wMABwAKABAAFwAbACAAKAAvADIANQA1ADUANgA3ADYANQA2ADgAOwA7ADkANQAuACwAKwAsACwALQAoACAAGgAYABcAEgAJAAQABwAJAAQA///6//n/+P/3//r///8DAAQABAADAAEA/v8AAAQADAASABUAFgAcACIAJwAnACQAJgArADEANwA+AEUATQBQAFMAVgBbAF4AWwBZAFoAWABSAEsASQBLAEcAPQAzACkAHAASAAgA/f/1//T/8//u/+b/4f/h/+P/5f/l/9//1v/T/9T/0//R/9D/zf/K/8f/xP/A/7j/sf+r/6j/pP+k/6j/r/+z/7b/uP+4/7b/t/+7/8H/xP/B/8H/w//G/8f/yf/P/9T/2f/h/+b/6v/t//P/9//2//f/AAALAA8ADgAPABYAIQApADAANgA9AEIAQwBFAEcARQBAADoAOgA9AD8AOAAuACkAJwAmAB4AEQACAPn/8//x/+3/5P/b/9L/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9v/8//3/AAAJABcAJwA0ADwAQgBEAEQARQBEAD8AOgA4ADsAQwBMAFAAVABYAFsAWwBZAFcAVgBUAFIAUABNAFAAVgBdAFoAVABPAEsARgBBAD8APgBAAEMARQBIAE0AUQBPAEsARwBFAEUAQwA/ADoAMwAqAB4AFgATABIAFgAcAB8AIgAjACAAHgAcABgAEwARAA8ADwAKAAYABgACAPr/8//t/+r/5f/i/+D/4v/i/+P/4v/d/9T/x/+8/7L/q/+n/6T/ov+d/5j/kf+Q/5L/lP+U/5X/mf+e/6L/pv+u/7f/v//F/8//1v/b/+D/6P/v//H/9P/5//3/AwAOABQAFwAaAB4AIgAhAB0AHQAhAB4AFwASABIAEwAVABkAHAAiACkALQAwADMANQAzADMAOQA9AD4APAA3ADMANQA9AEEAQwBBAD8APgA/AD0AOQA4ADoAPgBHAE4AUwBQAEoARwBGAEIAOwAyACgAHgASAAgA/v/1//D/7v/o/97/1//R/8r/v/+z/6f/n/+c/5b/j/+I/4P/fv95/3b/df96/4D/iv+V/57/pP+n/6n/rP+u/6z/p/+o/6//u//E/8r/0P/V/9//6//y//f/+v/+/wIABAAFAAcACgAOABQAFwAYABcAFQAWABkAGAAWABQAFgAZABsAGAAQAAkACAALABAAEAANAAkABwAFAAAA+P/0//f//v8EAAsAFgAdACEAJQAsADEAMwA1ADUAMgAvACsAKQAkAB8AGwASAAoABwAIAAoACAAGAAYABQAAAP7//f/7//j/9P/u/+j/5v/q//P//f8JABQAHQAeABQABwD///n/+f/5//b/8f/r/+j/5f/k/+X/5f/i/9//3f/g/+b/7P/w//P/7//q/+T/4P/f/+H/5P/n/+j/4//f/+P/6//z//v/BAAMABIAEwATABQAFQAQAAgAAgABAAUABwAHAAcABQD+//b/8v/x//D/7f/s/+3/6//r/+r/5v/e/9b/zf/E/77/vv++/8H/xf/M/9f/4v/t//X//P8GABUAIwAuADgARABOAFMAUgBSAFAATgBPAFIAVABVAFUAUABGADwANgA1ADIALQAqACcAJwAoACkAKwAsAC4ALwAuAC4ALgAsACYAIQAiACAAGAAQAAwADAAQABEADgALAAsAEQAUABEADgAOAAsABAD7//f/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6P/3/wUAEgAWABYAFwAWABUAFAAcACMAJgAeABQACwAHAAQAAgD+//3//v/6//D/5//l/+P/3v/Z/9P/0P/V/97/5P/m/+f/5P/f/93/3P/a/9P/zf/K/8r/yP/C/7z/t/+0/7b/vv/I/87/z//P/9H/1//c/+L/6v/v//T/9P/y/+3/6v/o/+n/5//j/93/3P/e/+D/4f/i/+b/6P/r//D/9P/9/wUAEAAcACEAJgAuADgAPwBCAEQARgBGAEUARABFAEkARgBAADoAMgAuACwAMgA1ADUAOABAAEUARgBDAEUASwBPAFIAUgBXAFwAXQBeAFsAWABbAF4AYwBrAHcAhACMAJEAkQCOAIsAhQCAAHwAdQBuAGgAYQBZAFEATgBOAEwARwBAADYALAAdAAwA///5//j/9//z/+3/5//g/97/4P/l/+j/6v/w//L/9f/4//n/+f/6//v//f/7//r/9//1//X/8v/x//H/8v/2//r//P/8//z/+P/x/+n/4//f/9z/2P/V/9L/z//N/8//0//U/9X/2f/e/+D/4v/n/+j/6f/r/+//9v/9/wMABwAKABIAHAAlACsALgAzADwARABEAEIAQgBDAEQARQBGAEkASQBDADsALwAfAA8ABAD7//H/6v/l/+X/5f/l/+b/5P/i/+T/5P/h/+H/5v/r/+v/6f/l/+D/2//U/8//z//R/8//zf/H/8P/wP+7/7b/sv+v/6n/ov+b/5f/kv+M/4j/hv+F/4f/if+J/4X/gv9+/37/gP+B/4L/gP+B/4j/jP+Q/5b/of+y/8P/zf/P/9H/1//b/9//4f/i/+f/7P/t//D/7v/r/+f/4f/f/97/3//i/+f/7f/x/+//6P/l/+j/8f/9/woAEgAdACoAMgA4AD4ASABQAFQAVgBcAGMAaABoAGMAXQBbAFoAVwBSAE4ATgBNAEsASABFADwALgAiABsAFAAJAP//9//x/+r/3//U/8//zv/M/8b/v/+8/7v/uv+6/7r/uP+4/7f/uP+5/7v/wP/G/8r/zv/S/9f/3P/e/+H/5P/m/+f/6f/y//z/AwAIAAsADgASABQAFQAUABQAFwAeACIAIAAaABcAFgATAAcA/v/+/wMABAADAAcACwAOABAAFAAZABoAGQAaABwAHAAYABYAGQAeACUALQA3AEYAVgBjAG4AeQCBAIkA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kf+b/7r/uP+n/63/wv/k//j/6f8gAD0AJwD6//H/BwD6//n/4v/V/9j/zP8DAAQADQAcAA0ADwBQAFMAZwApAPf/BgD3//r//P8DAP//AgAFAAkACQAFAAEA/v/7//n/+v/5//3/AAABAAIABgAHAAcABgAHAAkACAAHAAsACwALAA4ADgAOAA0ACwAJAAYAAwABAAAA//8AAAIAAgAEAAYABwAHAAgACAAHAAUAAwAAAP7//P/6//n/+f/4//j/+P/5//r//P/+/wAAAQACAAMAAwACAAEA///+//7//f/9////AQAGAAoADQASABUAFgAXABYAFAARAA4ACwAIAAUABAADAAMAAwADAAQABAAEAAMAAgAAAP7//P/6//j/9//2//T/9P/1//X/9f/1//b/9//5//v//f///wEAAgADAAQABAADAAMAAwACAAIAAwAEAAYACQALAA4AEAARABIAEQAPAA0ACQAHAAQAAQD///7//v//////AAABAAIAAgABAAEAAAD///7//f/8//z/+//7//z//P/9//3//v///wAAAQACAAQABAAEAAMAAwADAAIAAQABAAAAAAAAAAEAAQACAAQABAAFAAUABgAGAAUABQAFAAMAAwAEAAIAAgACAAIAAgABAAAAAAD///3//v/+//7//v///wEAAwAFAAQAAQD9//3//P///wEABAD6//3/BwACAAUA/P///wEA/f/9/wIA/v8FAAMAAAD///3/AQD+////AwACAP3/+f8BAAQAAgD///7/AQADAAEAAQAAAP//AwADAP///v8CAAIA///+/wAAAQAAAAAAAQAAAAAAAQAAAAAAAAA='; diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts index 55d2eaa4ad3..65ecbbdcba8 100644 --- a/packages/ai/src/api.test.ts +++ b/packages/ai/src/api.test.ts @@ -16,7 +16,14 @@ */ import { ImagenModelParams, ModelParams, AIErrorCode } from './types'; import { AIError } from './errors'; -import { getAI, ImagenModel, getGenerativeModel, getImagenModel } from './api'; +import { + getAI, + ImagenModel, + LiveGenerativeModel, + getGenerativeModel, + getImagenModel, + getLiveGenerativeModel +} from './api'; import { expect } from 'chai'; import { AI } from './public-types'; import { GenerativeModel } from './models/generative-model'; @@ -216,4 +223,62 @@ describe('Top level API', () => { expect(genModel).to.be.an.instanceOf(ImagenModel); expect(genModel.model).to.equal('publishers/google/models/my-model'); }); + + it('getLiveGenerativeModel throws if no apiKey is provided', () => { + const fakeVertexNoApiKey = { + ...fakeAI, + app: { options: { projectId: 'my-project', appId: 'my-appid' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_API_KEY); + expect((e as AIError).message).equals( + `AI: The "apiKey" field is empty in the local ` + + `Firebase config. Firebase AI requires this field to` + + ` contain a valid API key. (${AI_TYPE}/${AIErrorCode.NO_API_KEY})` + ); + } + }); + it('getLiveGenerativeModel throws if no projectId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_PROJECT_ID); + expect((e as AIError).message).equals( + `AI: The "projectId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid project ID. (${AI_TYPE}/${AIErrorCode.NO_PROJECT_ID})` + ); + } + }); + it('getLiveGenerativeModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', projectId: 'my-project' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_APP_ID); + expect((e as AIError).message).equals( + `AI: The "appId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid app ID. (${AI_TYPE}/${AIErrorCode.NO_APP_ID})` + ); + } + }); + it('getLiveGenerativeModel gets a LiveGenerativeModel', () => { + const liveGenerativeModel = getLiveGenerativeModel(fakeAI, { + model: 'my-model' + }); + expect(liveGenerativeModel).to.be.an.instanceOf(LiveGenerativeModel); + expect(liveGenerativeModel.model).to.equal( + 'publishers/google/models/my-model' + ); + }); }); diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts index 2cb8e670277..fc789f303a4 100644 --- a/packages/ai/src/api.ts +++ b/packages/ai/src/api.ts @@ -26,18 +26,31 @@ import { HybridParams, ModelParams, RequestOptions, - AIErrorCode + AIErrorCode, + LiveModelParams } from './types'; import { AIError } from './errors'; -import { AIModel, GenerativeModel, ImagenModel } from './models'; +import { + AIModel, + GenerativeModel, + LiveGenerativeModel, + ImagenModel +} from './models'; import { encodeInstanceIdentifier } from './helpers'; import { GoogleAIBackend } from './backend'; +import { WebSocketHandlerImpl } from './websocket'; export { ChatSession } from './methods/chat-session'; +export { LiveSession } from './methods/live-session'; export * from './requests/schema-builder'; export { ImagenImageFormat } from './requests/imagen-image-format'; -export { AIModel, GenerativeModel, ImagenModel, AIError }; +export { AIModel, GenerativeModel, LiveGenerativeModel, ImagenModel, AIError }; export { Backend, VertexAIBackend, GoogleAIBackend } from './backend'; +export { + startAudioConversation, + AudioConversationController, + StartAudioConversationOptions +} from './methods/live-session-helpers'; declare module '@firebase/component' { interface NameServiceMapping { @@ -163,3 +176,29 @@ export function getImagenModel( } return new ImagenModel(ai, modelParams, requestOptions); } + +/** + * Returns a {@link LiveGenerativeModel} class for real-time, bidirectional communication. + * + * The Live API is only supported in modern browser windows and Node >= 22. + * + * @param ai - An {@link AI} instance. + * @param modelParams - Parameters to use when setting up a {@link LiveSession}. + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @beta + */ +export function getLiveGenerativeModel( + ai: AI, + modelParams: LiveModelParams +): LiveGenerativeModel { + if (!modelParams.model) { + throw new AIError( + AIErrorCode.NO_MODEL, + `Must provide a model name for getLiveGenerativeModel. Example: getLiveGenerativeModel(ai, { model: 'my-model-name' })` + ); + } + const webSocketHandler = new WebSocketHandlerImpl(); + return new LiveGenerativeModel(ai, modelParams, webSocketHandler); +} diff --git a/packages/ai/src/constants.ts b/packages/ai/src/constants.ts index b6bd8e220ad..82482527f3b 100644 --- a/packages/ai/src/constants.ts +++ b/packages/ai/src/constants.ts @@ -21,7 +21,7 @@ export const AI_TYPE = 'AI'; export const DEFAULT_LOCATION = 'us-central1'; -export const DEFAULT_BASE_URL = 'https://firebasevertexai.googleapis.com'; +export const DEFAULT_DOMAIN = 'firebasevertexai.googleapis.com'; export const DEFAULT_API_VERSION = 'v1beta'; diff --git a/packages/ai/src/methods/live-session-helpers.test.ts b/packages/ai/src/methods/live-session-helpers.test.ts new file mode 100644 index 00000000000..d7d1e2aabbf --- /dev/null +++ b/packages/ai/src/methods/live-session-helpers.test.ts @@ -0,0 +1,356 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { SinonFakeTimers, SinonStub, SinonStubbedInstance } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { AIError } from '../errors'; +import { startAudioConversation } from './live-session-helpers'; +import { LiveServerContent, LiveServerToolCall, Part } from '../types'; +import { logger } from '../logger'; +import { isNode } from '@firebase/util'; + +use(sinonChai); +use(chaiAsPromised); + +// A mock message generator to simulate receiving messages from the server. +class MockMessageGenerator { + private resolvers: Array<(result: IteratorResult) => void> = []; + isDone = false; + + next(): Promise> { + return new Promise(resolve => this.resolvers.push(resolve)); + } + + simulateMessage(message: any): void { + const resolver = this.resolvers.shift(); + if (resolver) { + resolver({ value: message, done: false }); + } + } + + endStream(): void { + if (this.isDone) { + return; + } + this.isDone = true; + this.resolvers.forEach(resolve => + resolve({ value: undefined, done: true }) + ); + this.resolvers = []; + } +} + +// A mock LiveSession to intercept calls to the server. +class MockLiveSession { + isClosed = false; + inConversation = false; + send = sinon.stub(); + sendMediaChunks = sinon.stub(); + messageGenerator = new MockMessageGenerator(); + receive = (): MockMessageGenerator => this.messageGenerator; +} + +// Stubs and mocks for Web APIs used by the helpers. +let mockAudioContext: SinonStubbedInstance; +let mockMediaStream: SinonStubbedInstance; +let getUserMediaStub: SinonStub; +let mockWorkletNode: SinonStubbedInstance; +let mockSourceNode: SinonStubbedInstance; +let mockAudioBufferSource: any; + +function setupGlobalMocks(): void { + // Mock AudioWorkletNode + mockWorkletNode = { + port: { + postMessage: sinon.stub(), + onmessage: null + }, + connect: sinon.stub(), + disconnect: sinon.stub() + } as any; + sinon.stub(global, 'AudioWorkletNode').returns(mockWorkletNode); + + // Mock AudioContext + mockAudioBufferSource = { + connect: sinon.stub(), + start: sinon.stub(), + stop: sinon.stub(), + onended: null, + buffer: { duration: 0.5 } // Mock duration for scheduling + }; + mockSourceNode = { + connect: sinon.stub(), + disconnect: sinon.stub() + } as any; + mockAudioContext = { + resume: sinon.stub().resolves(), + close: sinon.stub().resolves(), + createBuffer: sinon.stub().returns({ + getChannelData: sinon.stub().returns(new Float32Array(1)) + } as any), + createBufferSource: sinon.stub().returns(mockAudioBufferSource), + createMediaStreamSource: sinon.stub().returns(mockSourceNode), + audioWorklet: { + addModule: sinon.stub().resolves() + }, + state: 'suspended' as AudioContextState, + currentTime: 0 + } as any; + sinon.stub(global, 'AudioContext').returns(mockAudioContext); + + // Mock other globals + sinon.stub(global, 'Blob').returns({} as Blob); + sinon.stub(URL, 'createObjectURL').returns('blob:http://localhost/fake-url'); + + // Mock getUserMedia + mockMediaStream = { + getTracks: sinon.stub().returns([{ stop: sinon.stub() } as any]) + } as any; + getUserMediaStub = sinon.stub().resolves(mockMediaStream); + if (typeof navigator === 'undefined') { + (global as any).navigator = { + mediaDevices: { getUserMedia: getUserMediaStub } + }; + } else { + if (!navigator.mediaDevices) { + (navigator as any).mediaDevices = {}; + } + sinon + .stub(navigator.mediaDevices, 'getUserMedia') + .callsFake(getUserMediaStub); + } +} + +describe('Audio Conversation Helpers', () => { + let clock: SinonFakeTimers; + + if (isNode()) { + return; + } + + beforeEach(() => { + clock = sinon.useFakeTimers(); + setupGlobalMocks(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('startAudioConversation', () => { + let liveSession: MockLiveSession; + beforeEach(() => { + liveSession = new MockLiveSession(); + }); + + it('should throw if the session is closed.', async () => { + liveSession.isClosed = true; + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /on a closed LiveSession/); + }); + + it('should throw if a conversation is in progress.', async () => { + liveSession.inConversation = true; + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /is already in progress/); + }); + + it('should throw if APIs are not supported.', async () => { + (global as any).AudioWorkletNode = undefined; // Simulate lack of support + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /not supported in this environment/); + }); + + it('should throw if microphone permissions are denied.', async () => { + getUserMediaStub.rejects( + new DOMException('Permission denied', 'NotAllowedError') + ); + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(DOMException, /Permission denied/); + }); + + it('should return a controller with a stop method on success.', async () => { + const controller = await startAudioConversation(liveSession as any); + expect(controller).to.have.property('stop').that.is.a('function'); + // Ensure it doesn't throw during cleanup + await expect(controller.stop()).to.be.fulfilled; + }); + }); + + describe('AudioConversationRunner', () => { + let liveSession: MockLiveSession; + let warnStub: SinonStub; + + beforeEach(() => { + liveSession = new MockLiveSession(); + warnStub = sinon.stub(logger, 'warn'); + }); + + afterEach(() => { + warnStub.restore(); + }); + + it('should send processed audio chunks received from the worklet.', async () => { + const controller = await startAudioConversation(liveSession as any); + expect(mockWorkletNode.port.onmessage).to.be.a('function'); + + // Simulate the worklet sending a message + const fakeAudioData = new Int16Array(128); + mockWorkletNode.port.onmessage!({ data: fakeAudioData } as MessageEvent); + + await clock.tickAsync(1); + + expect(liveSession.sendMediaChunks).to.have.been.calledOnce; + const [sentChunk] = liveSession.sendMediaChunks.getCall(0).args[0]; + expect(sentChunk.mimeType).to.equal('audio/pcm'); + expect(sentChunk.data).to.be.a('string'); + await controller.stop(); + }); + + it('should queue and play audio from a serverContent message.', async () => { + const controller = await startAudioConversation(liveSession as any); + const serverMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + role: 'model', + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ] // base64 for dummy data + } + }; + + liveSession.messageGenerator.simulateMessage(serverMessage); + await clock.tickAsync(1); // allow message processing + + expect(mockAudioContext.createBuffer).to.have.been.calledOnce; + expect(mockAudioBufferSource.start).to.have.been.calledOnce; + await controller.stop(); + }); + + it('should call function handler and send result on toolCall message.', async () => { + const handlerStub = sinon.stub().resolves({ + functionResponse: { name: 'get_weather', response: { temp: '72F' } } + } as Part); + const controller = await startAudioConversation(liveSession as any, { + functionCallingHandler: handlerStub + }); + + const toolCallMessage: LiveServerToolCall = { + type: 'toolCall', + functionCalls: [{ name: 'get_weather', args: { location: 'LA' } }] + }; + + liveSession.messageGenerator.simulateMessage(toolCallMessage); + await clock.tickAsync(1); + + expect(handlerStub).to.have.been.calledOnceWith( + toolCallMessage.functionCalls + ); + expect(liveSession.send).to.have.been.calledOnceWith([ + { functionResponse: { name: 'get_weather', response: { temp: '72F' } } } + ]); + await controller.stop(); + }); + + it('should clear queue and stop sources on an interruption message.', async () => { + const controller = await startAudioConversation(liveSession as any); + + // 1. Enqueue some audio that is "playing" + const playingMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ], + role: 'model' + } + }; + liveSession.messageGenerator.simulateMessage(playingMessage); + await clock.tickAsync(1); + expect(mockAudioBufferSource.start).to.have.been.calledOnce; + + // 2. Enqueue another chunk that is now scheduled + liveSession.messageGenerator.simulateMessage(playingMessage); + await clock.tickAsync(1); + expect(mockAudioBufferSource.start).to.have.been.calledTwice; + + // 3. Send interruption message + const interruptionMessage: LiveServerContent = { + type: 'serverContent', + interrupted: true + }; + liveSession.messageGenerator.simulateMessage(interruptionMessage); + await clock.tickAsync(1); + + // Assert that all scheduled sources were stopped. + expect(mockAudioBufferSource.stop).to.have.been.calledTwice; + + // 4. Send new audio post-interruption + const newMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ], + role: 'model' + } + }; + liveSession.messageGenerator.simulateMessage(newMessage); + await clock.tickAsync(1); + + // Assert a new source was created and started (total of 3 starts) + expect(mockAudioBufferSource.start).to.have.been.calledThrice; + + await controller.stop(); + }); + + it('should warn if no function handler is provided for a toolCall message.', async () => { + const controller = await startAudioConversation(liveSession as any); + liveSession.messageGenerator.simulateMessage({ + type: 'toolCall', + functionCalls: [{ name: 'test' }] + }); + await clock.tickAsync(1); + + expect(warnStub).to.have.been.calledWithMatch( + /functionCallingHandler is undefined/ + ); + await controller.stop(); + }); + + it('stop() should call cleanup and release all resources.', async () => { + const controller = await startAudioConversation(liveSession as any); + + // Need to spy on the internal runner's cleanup method. This is a bit tricky. + // We can't do it directly. Instead, we'll just check the mock results. + await controller.stop(); + + expect(mockWorkletNode.disconnect).to.have.been.calledOnce; + expect(mockSourceNode.disconnect).to.have.been.calledOnce; + expect(mockMediaStream.getTracks()[0].stop).to.have.been.calledOnce; + expect(mockAudioContext.close).to.have.been.calledOnce; + expect(liveSession.inConversation).to.be.false; + }); + }); +}); diff --git a/packages/ai/src/methods/live-session-helpers.ts b/packages/ai/src/methods/live-session-helpers.ts new file mode 100644 index 00000000000..e52715de36e --- /dev/null +++ b/packages/ai/src/methods/live-session-helpers.ts @@ -0,0 +1,497 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { logger } from '../logger'; +import { + AIErrorCode, + GenerativeContentBlob, + LiveServerContent, + LiveServerToolCall, + Part +} from '../types'; +import { LiveSession } from './live-session'; +import { Deferred } from '@firebase/util'; + +const SERVER_INPUT_SAMPLE_RATE = 16_000; +const SERVER_OUTPUT_SAMPLE_RATE = 24_000; + +const AUDIO_PROCESSOR_NAME = 'audio-processor'; + +/** + * The JS for an `AudioWorkletProcessor`. + * This processor is responsible for taking raw audio from the microphone, + * converting it to the required 16-bit 16kHz PCM, and posting it back to the main thread. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor + * + * It is defined as a string here so that it can be converted into a `Blob` + * and loaded at runtime. + */ +const audioProcessorWorkletString = ` + class AudioProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + this.targetSampleRate = options.processorOptions.targetSampleRate; + // 'sampleRate' is a global variable available inside the AudioWorkletGlobalScope, + // representing the native sample rate of the AudioContext. + this.inputSampleRate = sampleRate; + } + + /** + * This method is called by the browser's audio engine for each block of audio data. + * Input is a single input, with a single channel (input[0][0]). + */ + process(inputs) { + const input = inputs[0]; + if (input && input.length > 0 && input[0].length > 0) { + const pcmData = input[0]; // Float32Array of raw audio samples. + + // Simple linear interpolation for resampling. + const resampled = new Float32Array(Math.round(pcmData.length * this.targetSampleRate / this.inputSampleRate)); + const ratio = pcmData.length / resampled.length; + for (let i = 0; i < resampled.length; i++) { + resampled[i] = pcmData[Math.floor(i * ratio)]; + } + + // Convert Float32 (-1, 1) samples to Int16 (-32768, 32767) + const resampledInt16 = new Int16Array(resampled.length); + for (let i = 0; i < resampled.length; i++) { + const sample = Math.max(-1, Math.min(1, resampled[i])); + if (sample < 0) { + resampledInt16[i] = sample * 32768; + } else { + resampledInt16[i] = sample * 32767; + } + } + + this.port.postMessage(resampledInt16); + } + // Return true to keep the processor alive and processing the next audio block. + return true; + } + } + + // Register the processor with a name that can be used to instantiate it from the main thread. + registerProcessor('${AUDIO_PROCESSOR_NAME}', AudioProcessor); +`; + +/** + * A controller for managing an active audio conversation. + * + * @beta + */ +export interface AudioConversationController { + /** + * Stops the audio conversation, closes the microphone connection, and + * cleans up resources. Returns a promise that resolves when cleanup is complete. + */ + stop: () => Promise; +} + +/** + * Options for {@link startAudioConversation}. + * + * @beta + */ +export interface StartAudioConversationOptions { + /** + * An async handler that is called when the model requests a function to be executed. + * The handler should perform the function call and return the result as a `Part`, + * which will then be sent back to the model. + */ + functionCallingHandler?: ( + functionCalls: LiveServerToolCall['functionCalls'] + ) => Promise; +} + +/** + * Dependencies needed by the {@link AudioConversationRunner}. + * + * @internal + */ +interface RunnerDependencies { + audioContext: AudioContext; + mediaStream: MediaStream; + sourceNode: MediaStreamAudioSourceNode; + workletNode: AudioWorkletNode; +} + +/** + * Encapsulates the core logic of an audio conversation. + * + * @internal + */ +export class AudioConversationRunner { + /** A flag to indicate if the conversation has been stopped. */ + private isStopped = false; + /** A deferred that contains a promise that is resolved when stop() is called, to unblock the receive loop. */ + private readonly stopDeferred = new Deferred(); + /** A promise that tracks the lifecycle of the main `runReceiveLoop`. */ + private readonly receiveLoopPromise: Promise; + + /** A FIFO queue of 24kHz, 16-bit PCM audio chunks received from the server. */ + private readonly playbackQueue: ArrayBuffer[] = []; + /** Tracks scheduled audio sources. Used to cancel scheduled audio when the model is interrupted. */ + private scheduledSources: AudioBufferSourceNode[] = []; + /** A high-precision timeline pointer for scheduling gapless audio playback. */ + private nextStartTime = 0; + /** A mutex to prevent the playback processing loop from running multiple times concurrently. */ + private isPlaybackLoopRunning = false; + + constructor( + private readonly liveSession: LiveSession, + private readonly options: StartAudioConversationOptions, + private readonly deps: RunnerDependencies + ) { + this.liveSession.inConversation = true; + + // Start listening for messages from the server. + this.receiveLoopPromise = this.runReceiveLoop().finally(() => + this.cleanup() + ); + + // Set up the handler for receiving processed audio data from the worklet. + // Message data has been resampled to 16kHz 16-bit PCM. + this.deps.workletNode.port.onmessage = event => { + if (this.isStopped) { + return; + } + + const pcm16 = event.data as Int16Array; + const base64 = btoa( + String.fromCharCode.apply( + null, + Array.from(new Uint8Array(pcm16.buffer)) + ) + ); + + const chunk: GenerativeContentBlob = { + mimeType: 'audio/pcm', + data: base64 + }; + void this.liveSession.sendMediaChunks([chunk]); + }; + } + + /** + * Stops the conversation and unblocks the main receive loop. + */ + async stop(): Promise { + if (this.isStopped) { + return; + } + this.isStopped = true; + this.stopDeferred.resolve(); // Unblock the receive loop + await this.receiveLoopPromise; // Wait for the loop and cleanup to finish + } + + /** + * Cleans up all audio resources (nodes, stream tracks, context) and marks the + * session as no longer in a conversation. + */ + private cleanup(): void { + this.interruptPlayback(); // Ensure all audio is stopped on final cleanup. + this.deps.workletNode.port.onmessage = null; + this.deps.workletNode.disconnect(); + this.deps.sourceNode.disconnect(); + this.deps.mediaStream.getTracks().forEach(track => track.stop()); + if (this.deps.audioContext.state !== 'closed') { + void this.deps.audioContext.close(); + } + this.liveSession.inConversation = false; + } + + /** + * Adds audio data to the queue and ensures the playback loop is running. + */ + private enqueueAndPlay(audioData: ArrayBuffer): void { + this.playbackQueue.push(audioData); + // Will no-op if it's already running. + void this.processPlaybackQueue(); + } + + /** + * Stops all current and pending audio playback and clears the queue. This is + * called when the server indicates the model's speech was interrupted with + * `LiveServerContent.modelTurn.interrupted`. + */ + private interruptPlayback(): void { + // Stop all sources that have been scheduled. The onended event will fire for each, + // which will clean up the scheduledSources array. + [...this.scheduledSources].forEach(source => source.stop(0)); + + // Clear the internal buffer of unprocessed audio chunks. + this.playbackQueue.length = 0; + + // Reset the playback clock to start fresh. + this.nextStartTime = this.deps.audioContext.currentTime; + } + + /** + * Processes the playback queue in a loop, scheduling each chunk in a gapless sequence. + */ + private async processPlaybackQueue(): Promise { + if (this.isPlaybackLoopRunning) { + return; + } + this.isPlaybackLoopRunning = true; + + while (this.playbackQueue.length > 0 && !this.isStopped) { + const pcmRawBuffer = this.playbackQueue.shift()!; + try { + const pcm16 = new Int16Array(pcmRawBuffer); + const frameCount = pcm16.length; + + const audioBuffer = this.deps.audioContext.createBuffer( + 1, + frameCount, + SERVER_OUTPUT_SAMPLE_RATE + ); + + // Convert 16-bit PCM to 32-bit PCM, required by the Web Audio API. + const channelData = audioBuffer.getChannelData(0); + for (let i = 0; i < frameCount; i++) { + channelData[i] = pcm16[i] / 32768; // Normalize to Float32 range [-1.0, 1.0] + } + + const source = this.deps.audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(this.deps.audioContext.destination); + + // Track the source and set up a handler to remove it from tracking when it finishes. + this.scheduledSources.push(source); + source.onended = () => { + this.scheduledSources = this.scheduledSources.filter( + s => s !== source + ); + }; + + // To prevent gaps, schedule the next chunk to start either now (if we're catching up) + // or exactly when the previous chunk is scheduled to end. + this.nextStartTime = Math.max( + this.deps.audioContext.currentTime, + this.nextStartTime + ); + source.start(this.nextStartTime); + + // Update the schedule for the *next* chunk. + this.nextStartTime += audioBuffer.duration; + } catch (e) { + logger.error('Error playing audio:', e); + } + } + + this.isPlaybackLoopRunning = false; + } + + /** + * The main loop that listens for and processes messages from the server. + */ + private async runReceiveLoop(): Promise { + const messageGenerator = this.liveSession.receive(); + while (!this.isStopped) { + const result = await Promise.race([ + messageGenerator.next(), + this.stopDeferred.promise + ]); + + if (this.isStopped || !result || result.done) { + break; + } + + const message = result.value; + if (message.type === 'serverContent') { + const serverContent = message as LiveServerContent; + if (serverContent.interrupted) { + this.interruptPlayback(); + } + + const audioPart = serverContent.modelTurn?.parts.find(part => + part.inlineData?.mimeType.startsWith('audio/') + ); + if (audioPart?.inlineData) { + const audioData = Uint8Array.from( + atob(audioPart.inlineData.data), + c => c.charCodeAt(0) + ).buffer; + this.enqueueAndPlay(audioData); + } + } else if (message.type === 'toolCall') { + if (!this.options.functionCallingHandler) { + logger.warn( + 'Received tool call message, but StartAudioConversationOptions.functionCallingHandler is undefined. Ignoring tool call.' + ); + } else { + try { + const resultPart = await this.options.functionCallingHandler( + message.functionCalls + ); + if (!this.isStopped) { + void this.liveSession.send([resultPart]); + } + } catch (e) { + throw new AIError( + AIErrorCode.ERROR, + `Function calling handler failed: ${(e as Error).message}` + ); + } + } + } + } + } +} + +/** + * Starts a real-time, bidirectional audio conversation with the model. This helper function manages + * the complexities of microphone access, audio recording, playback, and interruptions. + * + * @remarks Important: This function must be called in response to a user gesture + * (for example, a button click) to comply with {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy | browser autoplay policies}. + * + * @example + * ```javascript + * const liveSession = await model.connect(); + * let conversationController; + * + * // This function must be called from within a click handler. + * async function startConversation() { + * try { + * conversationController = await startAudioConversation(liveSession); + * } catch (e) { + * // Handle AI-specific errors + * if (e instanceof AIError) { + * console.error("AI Error:", e.message); + * } + * // Handle microphone permission and hardware errors + * else if (e instanceof DOMException) { + * console.error("Microphone Error:", e.message); + * } + * // Handle other unexpected errors + * else { + * console.error("An unexpected error occurred:", e); + * } + * } + * } + * + * // Later, to stop the conversation: + * // if (conversationController) { + * // await conversationController.stop(); + * // } + * ``` + * + * @param liveSession - An active {@link LiveSession} instance. + * @param options - Configuration options for the audio conversation. + * @returns A `Promise` that resolves with an {@link AudioConversationController}. + * @throws `AIError` if the environment does not support required Web APIs (`UNSUPPORTED`), if a conversation is already active (`REQUEST_ERROR`), the session is closed (`SESSION_CLOSED`), or if an unexpected initialization error occurs (`ERROR`). + * @throws `DOMException` Thrown by `navigator.mediaDevices.getUserMedia()` if issues occur with microphone access, such as permissions being denied (`NotAllowedError`) or no compatible hardware being found (`NotFoundError`). See the {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions | MDN documentation} for a full list of exceptions. + * + * @beta + */ +export async function startAudioConversation( + liveSession: LiveSession, + options: StartAudioConversationOptions = {} +): Promise { + if (liveSession.isClosed) { + throw new AIError( + AIErrorCode.SESSION_CLOSED, + 'Cannot start audio conversation on a closed LiveSession.' + ); + } + + if (liveSession.inConversation) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'An audio conversation is already in progress for this session.' + ); + } + + // Check for necessary Web API support. + if ( + typeof AudioWorkletNode === 'undefined' || + typeof AudioContext === 'undefined' || + typeof navigator === 'undefined' || + !navigator.mediaDevices + ) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Audio conversation is not supported in this environment. It requires the Web Audio API and AudioWorklet support.' + ); + } + + let audioContext: AudioContext | undefined; + try { + // 1. Set up the audio context. This must be in response to a user gesture. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy + audioContext = new AudioContext(); + if (audioContext.state === 'suspended') { + await audioContext.resume(); + } + + // 2. Prompt for microphone access and get the media stream. + // This can throw a variety of permission or hardware-related errors. + const mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: true + }); + + // 3. Load the AudioWorklet processor. + // See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet + const workletBlob = new Blob([audioProcessorWorkletString], { + type: 'application/javascript' + }); + const workletURL = URL.createObjectURL(workletBlob); + await audioContext.audioWorklet.addModule(workletURL); + + // 4. Create the audio graph: Microphone -> Source Node -> Worklet Node + const sourceNode = audioContext.createMediaStreamSource(mediaStream); + const workletNode = new AudioWorkletNode( + audioContext, + AUDIO_PROCESSOR_NAME, + { + processorOptions: { targetSampleRate: SERVER_INPUT_SAMPLE_RATE } + } + ); + sourceNode.connect(workletNode); + + // 5. Instantiate and return the runner which manages the conversation. + const runner = new AudioConversationRunner(liveSession, options, { + audioContext, + mediaStream, + sourceNode, + workletNode + }); + + return { stop: () => runner.stop() }; + } catch (e) { + // Ensure the audio context is closed on any setup error. + if (audioContext && audioContext.state !== 'closed') { + void audioContext.close(); + } + + // Re-throw specific, known error types directly. The user may want to handle `DOMException` + // errors differently (for example, if permission to access audio device was denied). + if (e instanceof AIError || e instanceof DOMException) { + throw e; + } + + // Wrap any other unexpected errors in a standard AIError. + throw new AIError( + AIErrorCode.ERROR, + `Failed to initialize audio recording: ${(e as Error).message}` + ); + } +} diff --git a/packages/ai/src/methods/live-session.test.ts b/packages/ai/src/methods/live-session.test.ts new file mode 100644 index 00000000000..7db9daaebe6 --- /dev/null +++ b/packages/ai/src/methods/live-session.test.ts @@ -0,0 +1,294 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { + LiveResponseType, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation +} from '../types'; +import { LiveSession } from './live-session'; +import { WebSocketHandler } from '../websocket'; +import { AIError } from '../errors'; +import { logger } from '../logger'; + +use(sinonChai); +use(chaiAsPromised); + +class MockWebSocketHandler implements WebSocketHandler { + connect = stub().resolves(); + send = spy(); + close = stub().resolves(); + + private messageQueue: unknown[] = []; + private streamClosed = false; + private listenerPromiseResolver: (() => void) | null = null; + + async *listen(): AsyncGenerator { + while (!this.streamClosed) { + if (this.messageQueue.length > 0) { + yield this.messageQueue.shift(); + } else { + // Wait until a new message is pushed or the stream is ended. + await new Promise(resolve => { + this.listenerPromiseResolver = resolve; + }); + } + } + } + + simulateServerMessage(message: object): void { + this.messageQueue.push(message); + if (this.listenerPromiseResolver) { + // listener is waiting for our message + this.listenerPromiseResolver(); + this.listenerPromiseResolver = null; + } + } + + endStream(): void { + this.streamClosed = true; + if (this.listenerPromiseResolver) { + this.listenerPromiseResolver(); + this.listenerPromiseResolver = null; + } + } +} + +describe('LiveSession', () => { + let mockHandler: MockWebSocketHandler; + let session: LiveSession; + let serverMessagesGenerator: AsyncGenerator; + + beforeEach(() => { + mockHandler = new MockWebSocketHandler(); + serverMessagesGenerator = mockHandler.listen(); + session = new LiveSession(mockHandler, serverMessagesGenerator); + }); + + describe('send()', () => { + it('should format and send a valid text message', async () => { + await session.send('Hello there'); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + clientContent: { + turns: [{ role: 'user', parts: [{ text: 'Hello there' }] }], + turnComplete: true + } + }); + }); + + it('should format and send a message with an array of Parts', async () => { + const parts = [ + { text: 'Part 1' }, + { inlineData: { mimeType: 'image/png', data: 'base64==' } } + ]; + await session.send(parts); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData.clientContent.turns[0].parts).to.deep.equal(parts); + }); + }); + + describe('sendMediaChunks()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const chunks = [{ data: 'base64', mimeType: 'audio/webm' }]; + await session.sendMediaChunks(chunks); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { mediaChunks: chunks } + }); + }); + }); + + describe('sendMediaStream()', () => { + it('should send multiple chunks from a stream', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue({ data: 'chunk1', mimeType: 'audio/webm' }); + controller.enqueue({ data: 'chunk2', mimeType: 'audio/webm' }); + controller.close(); + } + }); + + await session.sendMediaStream(stream); + + expect(mockHandler.send).to.have.been.calledTwice; + const firstCall = JSON.parse(mockHandler.send.getCall(0).args[0]); + const secondCall = JSON.parse(mockHandler.send.getCall(1).args[0]); + expect(firstCall.realtimeInput.mediaChunks[0].data).to.equal('chunk1'); + expect(secondCall.realtimeInput.mediaChunks[0].data).to.equal('chunk2'); + }); + + it('should re-throw an AIError if the stream reader throws', async () => { + const errorStream = new ReadableStream({ + pull(controller) { + controller.error(new Error('Stream failed!')); + } + }); + await expect(session.sendMediaStream(errorStream)).to.be.rejectedWith( + AIError, + /Stream failed!/ + ); + }); + }); + + describe('receive()', () => { + it('should correctly parse and transform all server message types', async () => { + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage({ + serverContent: { modelTurn: { parts: [{ text: 'response 1' }] } } + }); + mockHandler.simulateServerMessage({ + toolCall: { functionCalls: [{ name: 'test_func' }] } + }); + mockHandler.simulateServerMessage({ + toolCallCancellation: { functionIds: ['123'] } + }); + mockHandler.simulateServerMessage({ + serverContent: { turnComplete: true } + }); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.have.lengthOf(4); + expect(responses[0]).to.deep.equal({ + type: LiveResponseType.SERVER_CONTENT, + modelTurn: { parts: [{ text: 'response 1' }] } + } as LiveServerContent); + expect(responses[1]).to.deep.equal({ + type: LiveResponseType.TOOL_CALL, + functionCalls: [{ name: 'test_func' }] + } as LiveServerToolCall); + expect(responses[2]).to.deep.equal({ + type: LiveResponseType.TOOL_CALL_CANCELLATION, + functionIds: ['123'] + } as LiveServerToolCallCancellation); + }); + + it('should log a warning and skip messages that are not objects', async () => { + const loggerStub = stub(logger, 'warn'); + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage(null as any); + mockHandler.simulateServerMessage('not an object' as any); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.be.empty; + expect(loggerStub).to.have.been.calledTwice; + expect(loggerStub).to.have.been.calledWithMatch( + /Received an invalid message/ + ); + + loggerStub.restore(); + }); + + it('should log a warning and skip objects of unknown type', async () => { + const loggerStub = stub(logger, 'warn'); + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage({ unknownType: { data: 'test' } }); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.be.empty; + expect(loggerStub).to.have.been.calledOnce; + expect(loggerStub).to.have.been.calledWithMatch( + /Received an unknown message type/ + ); + + loggerStub.restore(); + }); + }); + + describe('close()', () => { + it('should call the handler, set the isClosed flag, and be idempotent', async () => { + expect(session.isClosed).to.be.false; + await session.close(); + expect(mockHandler.close).to.have.been.calledOnce; + expect(session.isClosed).to.be.true; + + // Call again to test idempotency + await session.close(); + expect(mockHandler.close).to.have.been.calledOnce; // Should not be called again + }); + + it('should terminate an active receive() loop', async () => { + const received: unknown[] = []; + const receivePromise = (async () => { + for await (const msg of session.receive()) { + received.push(msg); + } + })(); + + mockHandler.simulateServerMessage({ + serverContent: { modelTurn: { parts: [{ text: 'one' }] } } + }); + // Allow the first message to be processed + await new Promise(r => setTimeout(r, 10)); + expect(received).to.have.lengthOf(1); + + await session.close(); + mockHandler.endStream(); // End the mock stream + + await receivePromise; // This should now resolve + + // No more messages should have been processed + expect(received).to.have.lengthOf(1); + }); + + it('methods should throw after session is closed', async () => { + await session.close(); + await expect(session.send('test')).to.be.rejectedWith(AIError, /closed/); + await expect(session.sendMediaChunks([])).to.be.rejectedWith( + AIError, + /closed/ + ); + const generator = session.receive(); + await expect(generator.next()).to.be.rejectedWith(AIError, /closed/); + }); + }); +}); diff --git a/packages/ai/src/methods/live-session.ts b/packages/ai/src/methods/live-session.ts new file mode 100644 index 00000000000..11e5346adc0 --- /dev/null +++ b/packages/ai/src/methods/live-session.ts @@ -0,0 +1,234 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AIErrorCode, + GenerativeContentBlob, + LiveResponseType, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation, + Part +} from '../public-types'; +import { formatNewContent } from '../requests/request-helpers'; +import { AIError } from '../errors'; +import { WebSocketHandler } from '../websocket'; +import { logger } from '../logger'; +import { + _LiveClientContent, + _LiveClientRealtimeInput +} from '../types/live-responses'; + +/** + * Represents an active, real-time, bidirectional conversation with the model. + * + * This class should only be instantiated by calling {@link LiveGenerativeModel.connect}. + * + * @beta + */ +export class LiveSession { + /** + * Indicates whether this Live session is closed. + * + * @beta + */ + isClosed = false; + /** + * Indicates whether this Live session is being controlled by an `AudioConversationController`. + * + * @beta + */ + inConversation = false; + + /** + * @internal + */ + constructor( + private webSocketHandler: WebSocketHandler, + private serverMessages: AsyncGenerator + ) {} + + /** + * Sends content to the server. + * + * @param request - The message to send to the model. + * @param turnComplete - Indicates if the turn is complete. Defaults to false. + * @throws If this session has been closed. + * + * @beta + */ + async send( + request: string | Array, + turnComplete = true + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const newContent = formatNewContent(request); + + const message: _LiveClientContent = { + clientContent: { + turns: [newContent], + turnComplete + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends realtime input to the server. + * + * @param mediaChunks - The media chunks to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + // The backend does not support sending more than one mediaChunk in one message. + // Work around this limitation by sending mediaChunks in separate messages. + mediaChunks.forEach(mediaChunk => { + const message: _LiveClientRealtimeInput = { + realtimeInput: { mediaChunks: [mediaChunk] } + }; + this.webSocketHandler.send(JSON.stringify(message)); + }); + } + + /** + * Sends a stream of {@link GenerativeContentBlob}. + * + * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaStream( + mediaChunkStream: ReadableStream + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const reader = mediaChunkStream.getReader(); + while (true) { + try { + const { done, value } = await reader.read(); + + if (done) { + break; + } else if (!value) { + throw new Error('Missing chunk in reader, but reader is not done.'); + } + + await this.sendMediaChunks([value]); + } catch (e) { + // Re-throw any errors that occur during stream consumption or sending. + const message = + e instanceof Error ? e.message : 'Error processing media stream.'; + throw new AIError(AIErrorCode.REQUEST_ERROR, message); + } + } + } + + /** + * Yields messages received from the server. + * This can only be used by one consumer at a time. + * + * @returns An `AsyncGenerator` that yields server messages as they arrive. + * @throws If the session is already closed, or if we receive a response that we don't support. + * + * @beta + */ + async *receive(): AsyncGenerator< + LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation + > { + if (this.isClosed) { + throw new AIError( + AIErrorCode.SESSION_CLOSED, + 'Cannot read from a Live session that is closed. Try starting a new Live session.' + ); + } + for await (const message of this.serverMessages) { + if (message && typeof message === 'object') { + if (LiveResponseType.SERVER_CONTENT in message) { + yield { + type: 'serverContent', + ...(message as { serverContent: Omit }) + .serverContent + } as LiveServerContent; + } else if (LiveResponseType.TOOL_CALL in message) { + yield { + type: 'toolCall', + ...(message as { toolCall: Omit }) + .toolCall + } as LiveServerToolCall; + } else if (LiveResponseType.TOOL_CALL_CANCELLATION in message) { + yield { + type: 'toolCallCancellation', + ...( + message as { + toolCallCancellation: Omit< + LiveServerToolCallCancellation, + 'type' + >; + } + ).toolCallCancellation + } as LiveServerToolCallCancellation; + } else { + logger.warn( + `Received an unknown message type from the server: ${JSON.stringify( + message + )}` + ); + } + } else { + logger.warn( + `Received an invalid message from the server: ${JSON.stringify( + message + )}` + ); + } + } + } + + /** + * Closes this session. + * All methods on this session will throw an error once this resolves. + * + * @beta + */ + async close(): Promise { + if (!this.isClosed) { + this.isClosed = true; + await this.webSocketHandler.close(1000, 'Client closed session.'); + } + } +} diff --git a/packages/ai/src/models/index.ts b/packages/ai/src/models/index.ts index cb694a5360b..5d2492d6784 100644 --- a/packages/ai/src/models/index.ts +++ b/packages/ai/src/models/index.ts @@ -17,4 +17,5 @@ export * from './ai-model'; export * from './generative-model'; +export * from './live-generative-model'; export * from './imagen-model'; diff --git a/packages/ai/src/models/live-generative-model.test.ts b/packages/ai/src/models/live-generative-model.test.ts new file mode 100644 index 00000000000..495f340b846 --- /dev/null +++ b/packages/ai/src/models/live-generative-model.test.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { use, expect } from 'chai'; +import sinon, { SinonFakeTimers, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { AI } from '../public-types'; +import { LiveSession } from '../methods/live-session'; +import { WebSocketHandler } from '../websocket'; +import { GoogleAIBackend } from '../backend'; +import { LiveGenerativeModel } from './live-generative-model'; +import { AIError } from '../errors'; + +use(sinonChai); +use(chaiAsPromised); + +// A controllable mock for the WebSocketHandler interface +class MockWebSocketHandler implements WebSocketHandler { + connect = stub().resolves(); + send = stub(); + close = stub().resolves(); + + private serverMessages: unknown[] = []; + private generatorController: { + resolve: () => void; + promise: Promise; + } | null = null; + + async *listen(): AsyncGenerator { + while (true) { + if (this.serverMessages.length > 0) { + yield this.serverMessages.shift(); + } else { + const promise = new Promise(resolve => { + this.generatorController = { resolve, promise: null! }; + }); + await promise; + } + } + } + + // Test method to simulate a message from the server + simulateServerMessage(message: object): void { + this.serverMessages.push(message); + if (this.generatorController) { + this.generatorController.resolve(); + this.generatorController = null; + } + } +} + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new GoogleAIBackend(), + location: 'us-central1' +}; + +describe('LiveGenerativeModel', () => { + let mockHandler: MockWebSocketHandler; + let clock: SinonFakeTimers; + + beforeEach(() => { + mockHandler = new MockWebSocketHandler(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + it('connect() should call handler.connect and send setup message', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + const connectPromise = model.connect(); + + // Ensure connect was called before simulating server response + expect(mockHandler.connect).to.have.been.calledOnce; + + // Wait for the setup message to be sent + await clock.runAllAsync(); + + expect(mockHandler.send).to.have.been.calledOnce; + const setupMessage = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(setupMessage.setup.model).to.include('my-model'); + + // Simulate successful handshake and resolve the promise + mockHandler.simulateServerMessage({ setupComplete: true }); + const session = await connectPromise; + expect(session).to.be.an.instanceOf(LiveSession); + await session.close(); + }); + + it('connect() should throw if handshake fails', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + // Simulate a failed handshake + mockHandler.simulateServerMessage({ error: 'handshake failed' }); + await expect(connectPromise).to.be.rejectedWith( + AIError, + /Server connection handshake failed/ + ); + }); + + it('connect() should pass through connection errors', async () => { + mockHandler.connect.rejects(new Error('Connection refused')); + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + await expect(model.connect()).to.be.rejectedWith('Connection refused'); + }); + + it('connect() should pass through setup parameters correctly', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { + model: 'gemini-pro', + generationConfig: { temperature: 0.8 }, + systemInstruction: { role: 'system', parts: [{ text: 'Be a pirate' }] } + }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData.setup.generationConfig).to.deep.equal({ temperature: 0.8 }); + expect(sentData.setup.systemInstruction.parts[0].text).to.equal( + 'Be a pirate' + ); + mockHandler.simulateServerMessage({ setupComplete: true }); + await connectPromise; + }); +}); diff --git a/packages/ai/src/models/live-generative-model.ts b/packages/ai/src/models/live-generative-model.ts new file mode 100644 index 00000000000..251df095202 --- /dev/null +++ b/packages/ai/src/models/live-generative-model.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIModel } from './ai-model'; +import { LiveSession } from '../methods/live-session'; +import { AIError } from '../errors'; +import { + AI, + AIErrorCode, + BackendType, + Content, + LiveGenerationConfig, + LiveModelParams, + Tool, + ToolConfig +} from '../public-types'; +import { WebSocketHandler } from '../websocket'; +import { WebSocketUrl } from '../requests/request'; +import { formatSystemInstruction } from '../requests/request-helpers'; +import { _LiveClientSetup } from '../types/live-responses'; + +/** + * Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal + * interactions with Gemini. + * + * This class should only be instantiated with {@link getLiveGenerativeModel}. + * + * @beta + */ +export class LiveGenerativeModel extends AIModel { + generationConfig: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: Content; + + /** + * @internal + */ + constructor( + ai: AI, + modelParams: LiveModelParams, + /** + * @internal + */ + private _webSocketHandler: WebSocketHandler + ) { + super(ai, modelParams.model); + this.generationConfig = modelParams.generationConfig || {}; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction( + modelParams.systemInstruction + ); + } + + /** + * Starts a {@link LiveSession}. + * + * @returns A {@link LiveSession}. + * @throws If the connection failed to be established with the server. + * + * @beta + */ + async connect(): Promise { + const url = new WebSocketUrl(this._apiSettings); + await this._webSocketHandler.connect(url.toString()); + + let fullModelPath: string; + if (this._apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + fullModelPath = `projects/${this._apiSettings.project}/${this.model}`; + } else { + fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`; + } + + const setupMessage: _LiveClientSetup = { + setup: { + model: fullModelPath, + generationConfig: this.generationConfig, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction + } + }; + + try { + // Begin listening for server messages, and begin the handshake by sending the 'setupMessage' + const serverMessages = this._webSocketHandler.listen(); + this._webSocketHandler.send(JSON.stringify(setupMessage)); + + // Verify we received the handshake response 'setupComplete' + const firstMessage = (await serverMessages.next()).value; + if ( + !firstMessage || + !(typeof firstMessage === 'object') || + !('setupComplete' in firstMessage) + ) { + await this._webSocketHandler.close(1011, 'Handshake failure'); + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + 'Server connection handshake failed. The server did not respond with a setupComplete message.' + ); + } + + return new LiveSession(this._webSocketHandler, serverMessages); + } catch (e) { + // Ensure connection is closed on any setup error + await this._webSocketHandler.close(); + throw e; + } + } +} diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts index 3fa0b33012f..90195b4b788 100644 --- a/packages/ai/src/requests/request.ts +++ b/packages/ai/src/requests/request.ts @@ -20,13 +20,14 @@ import { AIError } from '../errors'; import { ApiSettings } from '../types/internal'; import { DEFAULT_API_VERSION, - DEFAULT_BASE_URL, + DEFAULT_DOMAIN, DEFAULT_FETCH_TIMEOUT_MS, LANGUAGE_TAG, PACKAGE_VERSION } from '../constants'; import { logger } from '../logger'; import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { BackendType } from '../public-types'; export enum Task { GENERATE_CONTENT = 'generateContent', @@ -51,7 +52,7 @@ export class RequestUrl { } private get baseUrl(): string { - return this.requestOptions?.baseUrl || DEFAULT_BASE_URL; + return this.requestOptions?.baseUrl || `https://${DEFAULT_DOMAIN}`; } private get apiVersion(): string { @@ -81,6 +82,28 @@ export class RequestUrl { } } +export class WebSocketUrl { + constructor(public apiSettings: ApiSettings) {} + toString(): string { + const url = new URL(`wss://${DEFAULT_DOMAIN}`); + url.pathname = this.pathname; + + const queryParams = new URLSearchParams(); + queryParams.set('key', this.apiSettings.apiKey); + url.search = queryParams.toString(); + + return url.toString(); + } + + private get pathname(): string { + if (this.apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + return 'ws/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent'; + } else { + return `ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/${this.apiSettings.location}`; + } + } +} + /** * Log language and "fire/version" to x-goog-api-client */ diff --git a/packages/ai/src/types/content.ts b/packages/ai/src/types/content.ts index a08af95086c..bd8ccf1ef10 100644 --- a/packages/ai/src/types/content.ts +++ b/packages/ai/src/types/content.ts @@ -147,6 +147,15 @@ export interface FileDataPart { * @public */ export interface FunctionCall { + /** + * The id of the function call. This must be sent back in the associated {@link FunctionResponse}. + * + * + * @remarks This property is only supported in the Gemini Developer API ({@link GoogleAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + id?: string; name: string; args: object; } @@ -161,6 +170,14 @@ export interface FunctionCall { * @public */ export interface FunctionResponse { + /** + * The id of the {@link FunctionCall}. + * + * @remarks This property is only supported in the Gemini Developer API ({@link GoogleAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + id?: string; name: string; response: object; } diff --git a/packages/ai/src/types/enums.ts b/packages/ai/src/types/enums.ts index 956be64ba75..701cd4a695d 100644 --- a/packages/ai/src/types/enums.ts +++ b/packages/ai/src/types/enums.ts @@ -328,7 +328,12 @@ export const ResponseModality = { * Image. * @beta */ - IMAGE: 'IMAGE' + IMAGE: 'IMAGE', + /** + * Audio. + * @beta + */ + AUDIO: 'AUDIO' } as const; /** diff --git a/packages/ai/src/types/error.ts b/packages/ai/src/types/error.ts index d6ed22e047c..a230f683f37 100644 --- a/packages/ai/src/types/error.ts +++ b/packages/ai/src/types/error.ts @@ -75,6 +75,9 @@ export const AIErrorCode = { /** An error occurred while performing a fetch. */ FETCH_ERROR: 'fetch-error', + /** An error occurred because an operation was attempted on a closed session. */ + SESSION_CLOSED: 'session-closed', + /** An error associated with a Content object. */ INVALID_CONTENT: 'invalid-content', diff --git a/packages/ai/src/types/live-responses.ts b/packages/ai/src/types/live-responses.ts new file mode 100644 index 00000000000..66170f1a5ab --- /dev/null +++ b/packages/ai/src/types/live-responses.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Content, GenerativeContentBlob, Part } from './content'; +import { LiveGenerationConfig, Tool, ToolConfig } from './requests'; + +/** + * User input that is sent to the model. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientContent { + clientContent: { + turns: [Content]; + turnComplete: boolean; + }; +} + +/** + * User input that is sent to the model in real time. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientRealtimeInput { + realtimeInput: { + mediaChunks: GenerativeContentBlob[]; + }; +} +/** + * The first message in a Live session, used to configure generation options. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientSetup { + setup: { + model: string; + generationConfig?: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; + }; +} diff --git a/packages/ai/src/types/requests.ts b/packages/ai/src/types/requests.ts index 93921c6f14e..21ccce8bd11 100644 --- a/packages/ai/src/types/requests.ts +++ b/packages/ai/src/types/requests.ts @@ -51,6 +51,17 @@ export interface ModelParams extends BaseParams { systemInstruction?: string | Part | Content; } +/** + * Params passed to {@link getLiveGenerativeModel}. + * @beta + */ +export interface LiveModelParams { + model: string; + generationConfig?: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} /** * Request sent through {@link GenerativeModel.generateContent} * @public @@ -124,6 +135,57 @@ export interface GenerationConfig { thinkingConfig?: ThinkingConfig; } +/** + * Configuration parameters used by {@link LiveGenerativeModel} to control live content generation. + * + * @beta + */ +export interface LiveGenerationConfig { + /** + * Configuration for speech synthesis. + */ + speechConfig?: SpeechConfig; + /** + * Specifies the maximum number of tokens that can be generated in the response. The number of + * tokens per word varies depending on the language outputted. Is unbounded by default. + */ + maxOutputTokens?: number; + /** + * Controls the degree of randomness in token selection. A `temperature` value of 0 means that the highest + * probability tokens are always selected. In this case, responses for a given prompt are mostly + * deterministic, but a small amount of variation is still possible. + */ + temperature?: number; + /** + * Changes how the model selects tokens for output. Tokens are + * selected from the most to least probable until the sum of their probabilities equals the `topP` + * value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively + * and the `topP` value is 0.5, then the model will select either A or B as the next token by using + * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + */ + topP?: number; + /** + * Changes how the model selects token for output. A `topK` value of 1 means the select token is + * the most probable among all tokens in the model's vocabulary, while a `topK` value 3 means that + * the next token is selected from among the 3 most probably using probabilities sampled. Tokens + * are then further filtered with the highest selected `temperature` sampling. Defaults to 40 + * if unspecified. + */ + topK?: number; + /** + * Positive penalties. + */ + presencePenalty?: number; + /** + * Frequency penalties. + */ + frequencyPenalty?: number; + /** + * The modalities of the response. + */ + responseModalities?: ResponseModality[]; +} + /** * Params for {@link GenerativeModel.startChat}. * @public @@ -344,3 +406,41 @@ export interface ThinkingConfig { */ includeThoughts?: boolean; } + +/** + * Configuration for a pre-built voice. + * + * @beta + */ +export interface PrebuiltVoiceConfig { + /** + * The voice name to use for speech synthesis. + * + * For a full list of names and demos of what each voice sounds like, see {@link https://cloud.google.com/text-to-speech/docs/chirp3-hd | Chirp 3: HD Voices}. + */ + voiceName?: string; +} + +/** + * Configuration for the voice to used in speech synthesis. + * + * @beta + */ +export interface VoiceConfig { + /** + * Configures the voice using a pre-built voice configuration. + */ + prebuiltVoiceConfig?: PrebuiltVoiceConfig; +} + +/** + * Configures speech synthesis. + * + * @beta + */ +export interface SpeechConfig { + /** + * Configures the voice to be used in speech synthesis. + */ + voiceConfig?: VoiceConfig; +} diff --git a/packages/ai/src/types/responses.ts b/packages/ai/src/types/responses.ts index d9b76155a3a..4a01e79a77c 100644 --- a/packages/ai/src/types/responses.ts +++ b/packages/ai/src/types/responses.ts @@ -427,3 +427,73 @@ export interface CountTokensResponse { */ promptTokensDetails?: ModalityTokenCount[]; } + +/** + * An incremental content update from the model. + * + * @beta + */ +export interface LiveServerContent { + type: 'serverContent'; + /** + * The content that the model has generated as part of the current conversation with the user. + */ + modelTurn?: Content; + /** + * Indicates whether the turn is complete. This is `undefined` if the turn is not complete. + */ + turnComplete?: boolean; + /** + * Indicates whether the model was interrupted by the client. An interruption occurs when + * the client sends a message before the model finishes it's turn. This is `undefined` if the + * model was not interrupted. + */ + interrupted?: boolean; +} + +/** + * A request from the model for the client to execute one or more functions. + * + * @beta + */ +export interface LiveServerToolCall { + type: 'toolCall'; + /** + * An array of function calls to run. + */ + functionCalls: FunctionCall[]; +} + +/** + * Notification to cancel a previous function call triggered by {@link LiveServerToolCall}. + * + * @beta + */ +export interface LiveServerToolCallCancellation { + type: 'toolCallCancellation'; + /** + * IDs of function calls that were cancelled. These refer to the `id` property of a {@link FunctionCall}. + */ + functionIds: string[]; +} + +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * + * @beta + */ +export const LiveResponseType = { + SERVER_CONTENT: 'serverContent', + TOOL_CALL: 'toolCall', + TOOL_CALL_CANCELLATION: 'toolCallCancellation' +}; + +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * This is a property on all messages that can be used for type narrowing. This property is not + * returned by the server, it is assigned to a server message object once it's parsed. + * + * @beta + */ +export type LiveResponseType = + (typeof LiveResponseType)[keyof typeof LiveResponseType]; diff --git a/packages/ai/src/websocket.test.ts b/packages/ai/src/websocket.test.ts new file mode 100644 index 00000000000..6d8f08282e7 --- /dev/null +++ b/packages/ai/src/websocket.test.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { SinonFakeTimers, SinonStub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { WebSocketHandlerImpl } from './websocket'; +import { AIError } from './errors'; + +use(sinonChai); +use(chaiAsPromised); + +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + readyState: number = MockWebSocket.CONNECTING; + sentMessages: Array = []; + url: string; + private listeners: Map> = new Map(); + + constructor(url: string) { + this.url = url; + } + + send(data: string | ArrayBuffer): void { + if (this.readyState !== MockWebSocket.OPEN) { + throw new Error('WebSocket is not in OPEN state'); + } + this.sentMessages.push(data); + } + + close(): void { + if ( + this.readyState === MockWebSocket.CLOSED || + this.readyState === MockWebSocket.CLOSING + ) { + return; + } + this.readyState = MockWebSocket.CLOSING; + setTimeout(() => { + this.readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new Event('close')); + }, 10); + } + + addEventListener(type: string, listener: EventListener): void { + if (!this.listeners.has(type)) { + this.listeners.set(type, new Set()); + } + this.listeners.get(type)!.add(listener); + } + + removeEventListener(type: string, listener: EventListener): void { + this.listeners.get(type)?.delete(listener); + } + + dispatchEvent(event: Event): void { + this.listeners.get(event.type)?.forEach(listener => listener(event)); + } + + triggerOpen(): void { + this.readyState = MockWebSocket.OPEN; + this.dispatchEvent(new Event('open')); + } + + triggerMessage(data: any): void { + this.dispatchEvent(new MessageEvent('message', { data })); + } + + triggerError(): void { + this.dispatchEvent(new Event('error')); + } +} + +describe('WebSocketHandlerImpl', () => { + let handler: WebSocketHandlerImpl; + let mockWebSocket: MockWebSocket; + let clock: SinonFakeTimers; + let webSocketStub: SinonStub; + + beforeEach(() => { + webSocketStub = sinon + .stub(globalThis, 'WebSocket') + .callsFake((url: string) => { + mockWebSocket = new MockWebSocket(url); + return mockWebSocket as any; + }); + clock = sinon.useFakeTimers(); + handler = new WebSocketHandlerImpl(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('connect()', () => { + it('should resolve on open event', async () => { + const connectPromise = handler.connect('ws://test-url'); + expect(webSocketStub).to.have.been.calledWith('ws://test-url'); + + await clock.tickAsync(1); + mockWebSocket.triggerOpen(); + + await expect(connectPromise).to.be.fulfilled; + }); + + it('should reject on error event', async () => { + const connectPromise = handler.connect('ws://test-url'); + await clock.tickAsync(1); + mockWebSocket.triggerError(); + + await expect(connectPromise).to.be.rejectedWith( + AIError, + /Error event raised on WebSocket/ + ); + }); + }); + + describe('listen()', () => { + beforeEach(async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + }); + + it('should yield multiple messages as they arrive', async () => { + const generator = handler.listen(); + + const received: unknown[] = []; + const listenPromise = (async () => { + for await (const msg of generator) { + received.push(msg); + } + })(); + + // Use tickAsync to allow the consumer to start listening + await clock.tickAsync(1); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 1 })])); + + await clock.tickAsync(10); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); + + await clock.tickAsync(5); + mockWebSocket.close(); + await clock.runAllAsync(); // Let timers finish + + await listenPromise; // Wait for the consumer to finish + + expect(received).to.deep.equal([ + { + foo: 1 + }, + { + foo: 2 + } + ]); + }); + + it('should buffer messages that arrive before the consumer calls .next()', async () => { + const generator = handler.listen(); + + // Create a promise that will consume the generator in a separate async context + const received: unknown[] = []; + const consumptionPromise = (async () => { + for await (const message of generator) { + received.push(message); + } + })(); + + await clock.tickAsync(1); + + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 1 })])); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); + + await clock.tickAsync(1); + mockWebSocket.close(); + await clock.runAllAsync(); + + await consumptionPromise; + + expect(received).to.deep.equal([ + { + foo: 1 + }, + { + foo: 2 + } + ]); + }); + }); + + describe('close()', () => { + it('should be idempotent and not throw if called multiple times', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + const closePromise1 = handler.close(); + await clock.runAllAsync(); + await closePromise1; + + await expect(handler.close()).to.be.fulfilled; + }); + + it('should wait for the onclose event before resolving', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + let closed = false; + const closePromise = handler.close().then(() => { + closed = true; + }); + + // The promise should not have resolved yet + await clock.tickAsync(5); + expect(closed).to.be.false; + + // Now, let the mock's setTimeout for closing run, which triggers onclose + await clock.tickAsync(10); + + await expect(closePromise).to.be.fulfilled; + expect(closed).to.be.true; + }); + }); + + describe('Interaction between listen() and close()', () => { + it('should allow close() to take precedence and resolve correctly, while also terminating the listener', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + const generator = handler.listen(); + const listenPromise = (async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of generator) { + } + })(); + + const closePromise = handler.close(); + + await clock.runAllAsync(); + + await expect(closePromise).to.be.fulfilled; + await expect(listenPromise).to.be.fulfilled; + + expect(mockWebSocket.readyState).to.equal(MockWebSocket.CLOSED); + }); + }); +}); diff --git a/packages/ai/src/websocket.ts b/packages/ai/src/websocket.ts new file mode 100644 index 00000000000..fa34f2d48c3 --- /dev/null +++ b/packages/ai/src/websocket.ts @@ -0,0 +1,241 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from './errors'; +import { logger } from './logger'; +import { AIErrorCode } from './types'; + +/** + * A standardized interface for interacting with a WebSocket connection. + * This abstraction allows the SDK to use the appropriate WebSocket implementation + * for the current JS environment (Browser vs. Node) without + * changing the core logic of the `LiveSession`. + * @internal + */ + +export interface WebSocketHandler { + /** + * Establishes a connection to the given URL. + * + * @param url The WebSocket URL (e.g., wss://...). + * @returns A promise that resolves on successful connection or rejects on failure. + */ + connect(url: string): Promise; + + /** + * Sends data over the WebSocket. + * + * @param data The string or binary data to send. + */ + send(data: string | ArrayBuffer): void; + + /** + * Returns an async generator that yields parsed JSON objects from the server. + * The yielded type is `unknown` because the handler cannot guarantee the shape of the data. + * The consumer is responsible for type validation. + * The generator terminates when the connection is closed. + * + * @returns A generator that allows consumers to pull messages using a `for await...of` loop. + */ + listen(): AsyncGenerator; + + /** + * Closes the WebSocket connection. + * + * @param code - A numeric status code explaining why the connection is closing. + * @param reason - A human-readable string explaining why the connection is closing. + */ + close(code?: number, reason?: string): Promise; +} + +/** + * A wrapper for the native `WebSocket` available in both Browsers and Node >= 22. + * + * @internal + */ +export class WebSocketHandlerImpl implements WebSocketHandler { + private ws?: WebSocket; + + constructor() { + if (typeof WebSocket === 'undefined') { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'The WebSocket API is not available in this environment. ' + + 'The "Live" feature is not supported here. It is supported in ' + + 'modern browser windows, Web Workers with WebSocket support, and Node >= 22.' + ); + } + } + + connect(url: string): Promise { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(url); + this.ws.binaryType = 'blob'; // Only important to set in Node + this.ws.addEventListener('open', () => resolve(), { once: true }); + this.ws.addEventListener( + 'error', + () => + reject( + new AIError( + AIErrorCode.FETCH_ERROR, + `Error event raised on WebSocket` + ) + ), + { once: true } + ); + this.ws!.addEventListener('close', (closeEvent: CloseEvent) => { + if (closeEvent.reason) { + logger.warn( + `WebSocket connection closed by server. Reason: '${closeEvent.reason}'` + ); + } + }); + }); + } + + send(data: string | ArrayBuffer): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not open.'); + } + this.ws.send(data); + } + + async *listen(): AsyncGenerator { + if (!this.ws) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'WebSocket is not connected.' + ); + } + + const messageQueue: unknown[] = []; + const errorQueue: Error[] = []; + let resolvePromise: (() => void) | null = null; + let isClosed = false; + + const messageListener = async (event: MessageEvent): Promise => { + let data: string; + if (event.data instanceof Blob) { + data = await event.data.text(); + } else if (typeof event.data === 'string') { + data = event.data; + } else { + errorQueue.push( + new AIError( + AIErrorCode.PARSE_FAILED, + `Failed to parse WebSocket response. Expected data to be a Blob or string, but was ${typeof event.data}.` + ) + ); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + return; + } + + try { + const obj = JSON.parse(data) as unknown; + messageQueue.push(obj); + } catch (e) { + const err = e as Error; + errorQueue.push( + new AIError( + AIErrorCode.PARSE_FAILED, + `Error parsing WebSocket message to JSON: ${err.message}` + ) + ); + } + + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + + const errorListener = (): void => { + errorQueue.push( + new AIError(AIErrorCode.FETCH_ERROR, 'WebSocket connection error.') + ); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + + const closeListener = (event: CloseEvent): void => { + if (event.reason) { + logger.warn( + `WebSocket connection closed by the server with reason: ${event.reason}` + ); + } + isClosed = true; + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + // Clean up listeners to prevent memory leaks + this.ws?.removeEventListener('message', messageListener); + this.ws?.removeEventListener('close', closeListener); + this.ws?.removeEventListener('error', errorListener); + }; + + this.ws.addEventListener('message', messageListener); + this.ws.addEventListener('close', closeListener); + this.ws.addEventListener('error', errorListener); + + while (!isClosed) { + if (errorQueue.length > 0) { + const error = errorQueue.shift()!; + throw error; + } + if (messageQueue.length > 0) { + yield messageQueue.shift()!; + } else { + await new Promise(resolve => { + resolvePromise = resolve; + }); + } + } + + // If the loop terminated because isClosed is true, check for any final errors + if (errorQueue.length > 0) { + const error = errorQueue.shift()!; + throw error; + } + } + + close(code?: number, reason?: string): Promise { + return new Promise(resolve => { + if (!this.ws) { + return resolve(); + } + + this.ws.addEventListener('close', () => resolve(), { once: true }); + // Calling 'close' during these states results in an error. + if ( + this.ws.readyState === WebSocket.CLOSED || + this.ws.readyState === WebSocket.CONNECTING + ) { + return resolve(); + } + + if (this.ws.readyState !== WebSocket.CLOSING) { + this.ws.close(code, reason); + } + }); + } +} diff --git a/packages/ai/test-utils/types.d.ts b/packages/ai/test-utils/types.ts similarity index 100% rename from packages/ai/test-utils/types.d.ts rename to packages/ai/test-utils/types.ts