Speech-to-text with GeminiSTTService (Gemini Live transcription models) and GoogleSTTService (Google Cloud Speech-to-Text V2).
Overview
pipedog ports both of Pipecat's Google speech-to-text services. They use different APIs and different credentials:
GeminiSTTService: streams audio to a Gemini Live transcription model (gemini-3.5-transcribe-liveby default) over the Gemini Live API. Authenticates with a Gemini API key, or with Google Cloud credentials on Vertex AI. Supports automatic language detection, language hints, adaptation phrases and verbatim or smart transcription.GoogleSTTService: Google Cloud Speech-to-Text V2 streaming recognition. Authenticates with a Google Cloud service account or Application Default Credentials (ADC). Supports multiple languages, phrase-set adaptation, denoising and automatic stream renewal.
Upstream page
Pipecat's documentation for the same services.
Source
packages/google/src/gemini_live/stt.ts and stt.ts.
Cloud Speech-to-Text docs
Models, languages and adaptation for Speech-to-Text V2.
Installation
bun add @pipedog/googleGoogleSTTService also needs the Cloud Speech Node SDK, an optional peer dependency:
bun add @google-cloud/speechimport { GeminiSTTService } from "@pipedog/google/gemini_live/stt";
import { GoogleSTTService } from "@pipedog/google/stt";Prerequisites
GeminiSTTService: a Gemini API key from Google AI Studio, passed asapiKey. The examples read it fromGOOGLE_API_KEY. On Vertex AI (vertexai: true), a Google Cloud project with the Vertex AI API enabled, and a service account key or ADC instead.GoogleSTTService: a Google Cloud project with the Speech-to-Text API enabled, and a service account key (as a file or a JSON string) or ADC. The examples readGOOGLE_APPLICATION_CREDENTIALS(a key file path) orGOOGLE_CREDENTIALS_JSON(the key itself). The project ID comes from the credentials.
GeminiSTTService
Configuration
Prop
Type
Settings
Runtime-updatable settings, passed as settings: new GeminiSTTService.Settings({...}).
| Setting | Type | Default | Description |
|---|---|---|---|
model | string | "gemini-3.5-transcribe-live" | Gemini Live transcription model. |
language | Language | string | null | null | A single language hint. |
languages | (Language | string)[] | [] | Language hints. Take precedence over language. |
languageAuto | boolean | null | null | Automatic detection. null detects unless hints are given; false turns it off. |
adaptationPhrases | string[] | null | null | Phrases to bias recognition toward. |
mode | "VERBATIM" | "SMART" | null | null (model: "VERBATIM") | "SMART" removes disfluencies and applies light cleanup. |
Usage
import { Language } from "@pipedog/core/transcriptions/language";
import { GeminiSTTService } from "@pipedog/google/gemini_live/stt";
const stt = new GeminiSTTService({
apiKey: process.env.GOOGLE_API_KEY ?? "",
});
stt.addEventHandler("on_connected", async () => {
console.log("connected to the Gemini Live API");
});With language hints, adaptation phrases and smart transcription:
const hinted = new GeminiSTTService({
apiKey: process.env.GOOGLE_API_KEY ?? "",
settings: new GeminiSTTService.Settings({
languages: [Language.ES_ES, Language.EN_US],
adaptationPhrases: ["pipedog", "oatmilk"],
mode: "SMART",
}),
});Vertex AI
vertexai: true (pipedog only) connects through Vertex AI instead of the Gemini Developer API.
It takes location (required, e.g. "eu" for data residency), projectId (defaults to
GOOGLE_CLOUD_PROJECT), and credentials (a service account JSON string) or credentialsPath,
falling back to Application Default Credentials, as the Vertex LLM services do. apiKey is
rejected in this mode.
const vertex = new GeminiSTTService({
vertexai: true,
location: "eu",
projectId: process.env.GOOGLE_CLOUD_PROJECT ?? "",
// credentials: "<service account JSON>", or credentialsPath; otherwise ADC.
reportDetectedLanguage: false,
settings: new GeminiSTTService.Settings({ languages: [Language.ES_ES] }),
});Vertex's Live API rejects the languageAuto and languageHints fields, so in this mode the
language codes are sent as inputAudioTranscription.languageCodes, and no codes means automatic
detection. languageAuto: false has no Vertex equivalent and is ignored.
Reported language
Each transcription frame carries the language the model reports. That value can be wrong,
especially on short utterances. With reportDetectedLanguage: false (pipedog only), frames carry
the configured language when exactly one is configured, and null otherwise.
Changing languages during a session
setLanguageCodes(codes) (pipedog only) sets the language codes to transcribe, for example when
the bot switches language. The codes are sent as-is and take precedence over the language and
languages settings until one of those is updated. An empty list means automatic detection. Pass
a function (() => string[]) to have the codes read each time they are needed.
// For example from a function the LLM calls when the conversation changes language.
// The session reconnects with the new codes when the bot next starts speaking.
await vertex.setLanguageCodes(["en-US"]);The session does not reconnect at once. When the codes differ from the ones it was opened with,
the service reconnects on the next BotStartedSpeakingFrame. If the user is mid-turn then, it
waits until the turn ends, so no utterance is cut off. Audio that arrives while it reconnects is
buffered and sent to the new session. GoogleSTTService has the same method.
Event handlers
| Event | Description |
|---|---|
on_connected | Connected to the Gemini Live API. |
on_disconnected | Disconnected from the Gemini Live API. |
on_connection_error | The first connection failed, or three connection attempts failed in a row; the service stops retrying. |
Notes
- Emits
InterimTranscriptionFrames as results arrive and aTranscriptionFramefor each finished utterance. - On a
VADUserStoppedSpeakingFrame, the service sends an audio-stream-end signal so the model finalizes the utterance at once. Without an upstream VAD, the model finalizes on its own schedule. - Language hints and automatic detection are mutually exclusive. If both are set, hints win.
- Any settings change reconnects the session.
- The Live session is opened in the background when the
StartFramearrives, so theStartFrame(and the bot's first words) don't wait for it. Audio is buffered until the session is live. Upstream holds theStartFrameuntil the session is open;awaitConnectionOnStart: true(pipedog only) does the same. - Audio is sent at the pipeline's input sample rate. The model works best with 16 kHz mono PCM.
GoogleSTTService
Configuration
Prop
Type
Settings
Runtime-updatable settings, passed as settings: new GoogleSTTService.Settings({...}).
| Setting | Type | Default | Description |
|---|---|---|---|
model | string | "latest_long" | Speech V2 model. |
languages | Language[] | [Language.EN_US] | Recognition languages. |
languageCodes | string[] | null | null | Google language codes sent as-is, instead of languages, e.g. ["auto"] for Chirp's language detection (pipedog only). |
language | Language | null | null | Setting it replaces languages (and languageCodes) with that one language. |
useSeparateRecognitionPerChannel | boolean | false | Recognize each channel separately. |
enableAutomaticPunctuation | boolean | true | Add punctuation. |
enableSpokenPunctuation | boolean | false | Transcribe spoken punctuation. |
enableSpokenEmojis | boolean | false | Transcribe spoken emojis. |
profanityFilter | boolean | false | Filter profanity. |
enableWordTimeOffsets | boolean | false | Word timings. |
enableWordConfidence | boolean | false | Word confidence scores. |
enableInterimResults | boolean | true | Emit interim results. |
enableVoiceActivityEvents | boolean | false | Voice activity events. |
adaptation | SpeechAdaptation | null | null | Phrase sets that bias recognition. |
denoiserConfig | DenoiserConfig | null | null | Background-noise removal (Chirp 3 models only), e.g. { denoise_audio: true }. |
adaptation and denoiserConfig are plain objects. Keys may be snake_case, as Google documents
them, or camelCase. A phrase_sets entry is a phrase set resource name or an inline phrase set.
Referenced phrase sets must live in the same location as the service.
Usage
import { STTUpdateSettingsFrame } from "@pipedog/core/frames/frames";
import { Language } from "@pipedog/core/transcriptions/language";
import { GoogleSTTService } from "@pipedog/google/stt";
const stt = new GoogleSTTService({
credentialsPath: process.env.GOOGLE_APPLICATION_CREDENTIALS ?? "",
location: "global",
settings: new GoogleSTTService.Settings({
model: "latest_long",
languages: [Language.EN_US],
enableInterimResults: true,
}),
});Speech adaptation:
// Bias recognition with a stored phrase set and an inline one.
const biased = new GoogleSTTService({
credentials: process.env.GOOGLE_CREDENTIALS_JSON ?? "",
settings: new GoogleSTTService.Settings({
adaptation: {
phrase_sets: [
"projects/my-project/locations/global/phraseSets/catalog",
{ phrases: [{ value: "pipedog", boost: 15.0 }] },
],
},
}),
});Change settings mid-call with an STTUpdateSettingsFrame:
// Changing settings reconnects the stream.
const update = new STTUpdateSettingsFrame({
delta: new GoogleSTTService.Settings({ languages: [Language.ES_ES, Language.EN_US] }),
});Event handlers
| Event | Description |
|---|---|
on_connected | The service connected. |
on_disconnected | The service disconnected. |
Notes
- Emits
InterimTranscriptionFrames (withenableInterimResults) andTranscriptionFrames. - Google limits a streaming call to a few minutes. The service renews the stream every 4
minutes (
GoogleSTTService.STREAMING_LIMIT, in ms) and carries the unfinished audio over. - A settings change reconnects the stream.
- The
telephonymodel rejects adaptation; the service logs a warning and transcribes without it. - With inline
credentials, a missingproject_idthrows in the constructor. - With
languageCodes: ["auto"], transcripts carry the language Google detected. - A gRPC CANCELLED error (the call was cancelled on our side) ends that stream quietly instead of being reported as an error.
Finalizing on VAD stop
Google sends a final result only once its own endpointing decides the utterance is over, about
1.5 s after speech ends. With finalizeOnVadStop: true (pipedog only), the service half-closes
the request stream when a VADUserStoppedSpeakingFrame arrives, so Google returns the final for
the audio it has right away (about 0.6 s), and opens a new stream for the next audio, which waits
in the queue meanwhile. The finals of the closed stream are pushed as one finalized
TranscriptionFrame, so the turn can end without waiting for the STT timeout. Put a VAD before
the service.
const stt = new GoogleSTTService({
location: "eu",
finalizeOnVadStop: true,
settings: new GoogleSTTService.Settings({ model: "chirp_3", languageCodes: ["auto"] }),
});Switching languages mid-call
setLanguageCodes(codes) (pipedog only, the same API as GeminiSTTService) changes the
languageCodes without cutting off the user: when the codes differ from the current ones, the
stream reconnects on the next BotStartedSpeakingFrame, or once the user's turn ends if they are
speaking over the bot. Pass a function to have the codes read again each time the bot starts
speaking; it stays in effect until the language, languages or languageCodes setting is
updated.
await stt.setLanguageCodes(["es-ES", "en-US"]);
// or follow the bot's language:
await stt.setLanguageCodes(() => [currentLanguageCode()]);Sharing a client and prewarming
With sharedClient: true (pipedog only), services with the same location, credentials,
clientOptions and speechModule use one Speech client per process instead of one each, so a
server running one pipeline per call opens one gRPC channel and fetches tokens once.
prewarm() connects it ahead of time (it loads the SDK, resolves the project ID, opens the
channel and fetches a token) and returns false with a warning instead of throwing if that
fails; the first stream then connects as usual.
const sttOptions = { location: "eu", sharedClient: true };
await new GoogleSTTService(sttOptions).prewarm(); // at server start-up
// per call:
const stt = new GoogleSTTService({ ...sttOptions, settings: new GoogleSTTService.Settings({ model: "chirp_3" }) });It is off by default, as upstream builds a client per service, because a shared client puts every concurrent stream of the process on one gRPC channel. Services never close their client, so one call ending doesn't affect the others.
Runtime notes
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | Nothing. |
| Node.js | ✅ yes | Nothing. |
| Deno | ✅ yes | Nothing. |
| Cloudflare Workers | ✅ yes | Nothing. |
Generated from pipecat.runtimes and pipecat.compat in packages/google/package.json: ✅ means the compat smoke test passes on that runtime.
GeminiSTTService uses GoogleGenAI from @google/genai, which opens the Live API over the
global WebSocket and runs on Bun, Node, Deno and Cloudflare Workers. In Vertex AI mode it
authenticates through google-auth-library, which only the SDK's Node build supports, so that mode
runs on Bun, Node and Deno, not on Workers. The client is created on first connect, so a subclass
that overrides _createClient() can read its own fields there.
Cloud Speech-to-Text V2 streaming is gRPC only, so GoogleSTTService loads the Node SDK
@google-cloud/speech lazily on first connect. That needs a Node-compatible runtime, not
Workers. To use another transport, pass a client that implements GoogleSTTClient
(streamingRecognize() and, optionally, getProjectId()). clientOptions (pipedog only) is
merged into the SDK's SpeechClient options.
Upstream resolves credentials and the project ID in the constructor. Here, with
credentialsPath or ADC, they are resolved on first connect (in setup()), so "No valid
credentials provided." and "Project ID not found in credentials" are raised there. Inline
credentials are still checked in the constructor.
The deprecated upstream InputParams class, the params option, setLanguages() and
updateOptions() are not ported. GoogleSTTSettings.languageCodes, deprecated upstream, is kept
for codes Language cannot express, such as "auto".
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).