{"slug": "build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini", "title": "Build Real-Time Client-Side TTS in Angular Using Firebase AI Logic and Gemini", "summary": "A new tutorial by a developer on Dev.to demonstrates building real-time client-side text-to-speech in Angular 22 using Firebase AI Logic and the Gemini model, eliminating custom backend code and automating deployment via Git integration. The project uses Firebase Remote Config, App Check, and App Hosting, and converts L16 audio to WAV for playback, with the full codebase available in the ng-firebase-tts repository.", "body_md": "# Build Real-Time Client-Side TTS in Angular Using Firebase AI Logic and Gemini\n\nIn my previous article on Dev.to, I showed how to build expressive text-to-speech using Gemini and Firebase Cloud Functions. While that setup worked beautifully, it required building and deploying custom backend endpoints. Firebase AI Logic enables running Gemini TTS client-side with production secu\n\nIn my previous article on Dev.to, I showed how to build expressive text-to-speech using Gemini and Firebase Cloud Functions. While that setup worked beautifully, it required building and deploying custom backend endpoints. Firebase AI Logic enables running Gemini TTS client-side with production security, eliminating custom backend code and automating deployment via Git integration. Project technical stack: Angular 22: The latest version as of August 2026. Node.js LTS: The LTS version as of May 2026. Firebase Remote Config: Manages dynamic parameters, such as TTS model names, and conditionally configures limited-use tokens. Firebase AI Logic: Interfaces with Gemini generative models. Firebase App Check: Prevents abuse and verifies token authenticity. Firebase App Hosting: Streamlined deployment to the Firebase App Hosting environment. With a single code push, the latest changes deploy to the production environment. The public Google Gemini Developer API is restricted in my region (Hong Kong). However, the Agent Platform Gemini API (Google Cloud) offers enterprise access that works reliably here, so I chose the Agent Platform Gemini API for this Firebase AI Logic demo. npm install -g firebase-tools Install or update firebase-tools globally using npm. firebase logout firebase login Log out and re-authenticate with Firebase. npm i --save-exact firebase npm i --save-exact --save-dev firebase-tools serve Install the dependencies to call the Firebase AI Logic API, use the Firebase CLI to generate files, and serve the production build. firebase init Execute firebase init and follow the prompts to set up the Firebase AI Logic, Emulators, App Hosting, and Remote Config. If you have an existing project or multiple projects, you can specify the project ID on the command line. firebase init --project <PROJECT_ID> After completing the setup steps, the Firebase tools generate the configuration files such as .firebaserc and firebase.json. You can view the .firebaserc and firebase.json in the GitHub repo. We asked antigravity-cli (a terminal-first AI coding agent released by Google) and the Gemini Flash model to create two Node.js scripts to do the following: Generate default Remote Config values in JSON format and save them to public/remote-config-defaults.json. You can read the complete listing of the script. Populate the Firebase configuration object and write it to public/firebase.config.json. If we hardcode the public keys and push them to GitHub, GitHub triggers a false alarm. You can read the complete listing of the script and the environment variable template. # generated firebase configuration firebase.config.json Add firebase.config.json to .gitignore to prevent accidental commits. During build time, Angular bundles both JSON files into the dist directory to provide initial configuration values. Users submit text to Firebase AI Logic to synthesize speech. Firebase generates the complete L16 audio payload and returns it to the client. Because the HTML audio element does not support the L16 format, the application converts the audio to a WAV Blob before binding the Blob URL to the element source. The second flow streams the audio and sends the L16 chunks to the Angular application. The audio player creates an AudioBufferSourceNode to play the chunk data and cleans up resources to prevent memory leaks. While the full codebase is available in the ng-firebase-tts repository, the application relies on Firebase Remote Config to manage configuration, App Check to prevent abuse, and App Hosting to deploy the Angular application. The following sections illustrate how to initialize the Firebase app and App Check, and how to activate Remote Config values. The public/firebase.config.json file contains the public Firebase API key, the sensitive reCAPTCHA Enterprise key, and the App Check debug token to bypass device attestation in local development. These values are critical in Firebase App and App Check initialization. @Service() export class ConfigService { #app: FirebaseApp | undefined = undefined; #remoteConfig: RemoteConfig | undefined = undefined; /*... getter methods are omitted... */ get appConfig(): AppRemoteConfig { return this.#appConfig; } get aiBackend(): AI { return this.#aiBackend; } async initialize(): Promise<void> { this.#app = initializeApp(firebaseConfig.app); (globalThis as any).FIREBASE_APPCHECK_DEBUG_TOKEN = firebaseConfig.appCheckDebugToken || true; initializeAppCheck(this.#app, { provider: new ReCaptchaEnterpriseProvider(firebaseConfig.recaptchaEnterpriseKey), isTokenAutoRefreshEnabled: true, }); this.#remoteConfig = getRemoteConfig(this.#app); this.#remoteConfig.defaultConfig = remoteConfigDefaults; await fetchAndActivate(this.#remoteConfig); this.#appConfig = { vertexAILocation: getValue(this.#remoteConfig, 'vertexAILocation').asString(), useLimitedUseAppCheckTokens: getValue( this.#remoteConfig, 'useLimitedUseAppCheckTokens', ).asBoolean(), geminiTTSModelName: getValue(this.#remoteConfig, 'geminiTTSModelName').asString(), }; this.#aiBackend = getAI(this.#app, { backend: new AgentPlatformBackend(this.#appConfig.vertexAILocation), useLimitedUseAppCheckTokens: this.#appConfig.useLimitedUseAppCheckTokens, }); } } The initialize method initializes Firebase App, configures App Check, sets up Firebase AI, and assigns Remote Config values to appConfig. The appConfig object holds the location, the Gemini TTS model name, and the limited-use App Check token flag. Configure the TTS model name and the limited-use App Check token parameters in Firebase Remote Config. Both parameters have conditional values. When the Firebase web application is firebase-ai-logic-tts, the TTS model is gemini-3.1-flash-tts-preview, and the limited-use App Check token flag is set to true. export const AI_BACKEND = new InjectionToken<AI>('AI_BACKEND'); export function provideFirebase() { return makeEnvironmentProviders([ { provide: AI_BACKEND, useFactory: () => inject(ConfigService).aiBackend, }, ]); } The AI_BACKEND injection token provides a factory function to return the Firebase AI from ConfigService. export const appConfig: ApplicationConfig = { providers: [ ... other providers ... provideAppInitializer(async () => await inject(ConfigService).initialize()), provideFirebase(), ], }; provideAppInitializer and provideFirebase initialize Firebase and configure Firebase AI during application startup. The private createModel method calls getGenerativeModel to create a generative model configured with speechConfig. The subsequent workflows use this model to convert L16 audio to WAV or stream L16 playback directly. @Service() export class TextToSpeechService { readonly #configService = inject(ConfigService); readonly #modelName = this.#configService.appConfig.geminiTTSModelName; private createModel(voiceName: string) { return getGenerativeModel(this.#aiBackend, { model: this.#modelName, generationConfig: { responseModalities: [ResponseModality.AUDIO], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName }, }, languageCode: 'en-US', }, }, }); } } async synthesize(text: string, voiceName: string): Promise<Blob> { const model = this.createModel(voiceName); const result = await model.generateContent([text]); const chunk = this.extractValidChunkData(result.response); const { data, mimeType } = chunk; return convertToWav(decodeBase64(data), mimeType); } The extractValidChunkData method extracts binary audio data and the MIME type from the response payload. The synthesize method retrieves the entire audio payload in L16 format. However, the HTML audio element does not support L16, so the application must convert the data to WAV format before assigning the Blob URL to the element source. See the conversion to WAV code for the full implementation. This implementation remains straightforward because it avoids managing response streams and incremental audio chunks. However, users suffer from latency when the text is long and it produces a long audio stream. To eliminate playback latency, the next section explores streaming chunks directly via the Web Audio API AudioContext. async *synthesizeStream(text: string, voiceName: string): AsyncGenerator<RawAudioBinary | undefined> { const model = this.createModel(voiceName); let firstMimeType = ''; let sampleRate = DEFAULT_SAMPLE_RATE; const responseStream = await model.generateContentStream([text]); for await (const chunk of responseStream.stream) { const chunkData = this.extractValidChunkData(chunk); if (chunkData) { const { data, mimeType } = chunkData; const decodedData = decodeBase64(data); if (!firstMimeType && mimeType) { firstMimeType = mimeType; sampleRate = parseMimeType(firstMimeType).sampleRate; } yield { decodedData, sampleRate }; } } yield undefined; } The synthesizeStream method returns an asynchronous generator that yields the raw binary data and the sample rate. The MIME type is audio/l16; rate=24000; channels=1, so parseMimeType extracts the sample rate for the audio player's AudioContext. While AudioPlayerService is beyond the scope of this article, the source code demonstrates how to play back audio without using an HTML audio element. Next, we will build a reactive user interface in Angular that renders an HTML audio element to play audio. The TextToSpeechComponent delegates audio management to the view service to maintain separation of concerns. The TextToSpeechComponent displays three buttons to generate speech from text across three different scenarios: Scenario 1: Synthesizes the entire audio payload and binds the resulting Blob URL to an HTML audio element. Scenario 2: Streams audio chunks incrementally and plays the speech immediately via the Web Audio API. This mode hides the HTML audio element because audio plays directly through the Web Audio API. @Component({ selector: 'app-text-to-speech', templateUrl: './text-to-speech.component.html', styleUrl: './text-to-speech.component.css', imports: [SpinnerIconComponent, NgTemplateOutlet], providers: [TextToSpeechViewService], }) export class TextToSpeechComponent { private readonly speechService = inject(TextToSpeechViewService); interestingFact = input<string | undefined>(undefined); audioPrompt = input.required<string>(); voice = input.required<string>(); async generateSpeech(mode: GenerateSpeechMode) { const fact = this.interestingFact(); await this.speechService.generateSpeech(mode, { prompt: this.audioPrompt(), voice: this.voice(), fact: this.interestingFact(), }); } } The TextToSpeechViewService encapsulates TextToSpeechService and AudioPlayerService to coordinate speech synthesis and audio playback. @Injectable() export class TextToSpeechViewService { private readonly speechService = inject(TextToSpeechService); private readonly audioPlayerService = inject(AudioPlayerService); #audioUrl = signal<string | undefined>(undefined); audioUrl = this.#audioUrl.asReadonly(); private processStreamChunk(isInitialized: boolean, playbackRate: number, chunk: RawAudioBinary) { if (!isInitialized) { this.audioPlayerService.initialize(chunk.sampleRate, playbackRate); isInitialized = true; } this.audioPlayerService.processChunk(chunk.decodedData); return isInitialized; } private async handleSync(promptArgs: FactConfig) { const blob = await this.speechService.synthesize(promptArgs.prompt, promptArgs.voice); this.setAudioUrl(blob); } private async handleStream(promptArgs: FactConfig) { let isInitialized = false; const { prompt, voice } = promptArgs; for await (const chunk of this.speechService.synthesizeStream(prompt, voice)) { isInitialized = this.processStreamChunk(isInitialized, 1, chunk); } } private setAudioUrl(finalBlob: Blob | undefined) { if (finalBlob) { const createdUrl = URL.createObjectURL(finalBlob); this.#audioUrl.set(createdUrl); return createdUrl; } return undefined; } async generateSpeech(mode: GenerateSpeechMode, promptArgs: FactConfig) { revokeBlobURL(this.#audioUrl()); this.#audioUrl.set(undefined); switch (mode) { case 'sync': await this.handleSync(promptArgs); break; case 'web_audio_api': await this.handleStream(promptArgs); break; } } } handleSync invokes the Firebase SDK to synthesize audio using the Gemini TTS model, generates a Blob URL, updates #audioUrl, and renders the HTML audio element. handleStream calls the Firebase SDK to use the Gemini TTS model to stream the audio instead. The audio context receives the chunk data, plays it immediately, and avoids rendering the HTML audio element. The integration of text-to-speech with Firebase AI Logic empowers Angular applications for real-time audio generation. The Angular application handles text-to-speech entirely on the client. Pushing changes to Git triggers an automatic deployment to Firebase App Hosting. Try cloning the GitHub repository, uploading an image to generate an obscure fact, and using the Gemini 3.1 Flash TTS preview model to speak it with the specified scene, emotion, and pace. Firebase AI Logic TTS GitHub Repo Firebase AI Logic TTS Demo Firebase AI Logic - Get Started Firebase AI Logic - Generate Speech Firebase AI Logic - App Check Dynamically update your Firebase AI Logic app with Firebase Remote Config\n\n## Key Takeaways\n\n- •In my previous article on Dev.to, I showed how to build expressive text-to-speech using Gemini and Firebase Cloud Functions\n- •This story was reported by\n**Dev.to**, covering developments in the** dev**space. - •AI advancements continue to reshape industries — read the full article on Dev.to for complete coverage.\n\n📖 Continue reading the full article:\n\n[Read Full Article on Dev.to →](https://dev.to/gde/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini-37hc)", "url": "https://wpnews.pro/news/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini", "canonical_source": "https://ainexusdaily.vercel.app/article/2026-09-01-build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini", "published_at": "2026-09-01 10:52:37+00:00", "updated_at": "2026-09-01 11:52:54.376657+00:00", "lang": "en", "topics": ["generative-ai", "ai-tools", "ai-infrastructure"], "entities": ["Angular", "Firebase AI Logic", "Gemini", "Firebase Remote Config", "Firebase App Check", "Firebase App Hosting", "Google Gemini Developer API", "Agent Platform Gemini API"], "alternates": {"html": "https://wpnews.pro/news/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini", "markdown": "https://wpnews.pro/news/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini.md", "text": "https://wpnews.pro/news/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini.txt", "jsonld": "https://wpnews.pro/news/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini.jsonld"}}