Angular中使用Firebase AI Logic和Gemini构建客户端实时TTS
原文:https://dev.to/gde/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini-37hc(作者 @railsstudent)
在之前的Dev.to文章中,我展示了如何使用Gemini和Firebase Cloud Functions构建富有表现力的文本转语音。虽然该设置运行良好,但它需要构建和部署自定义后端端点。Firebase AI Logic使得在客户端运行Gemini TTS成为可能,同时保持生产安全性,从而消除了自定义后端代码,并通过Git集成自动化部署。
1. 技术栈
项目技术栈:
- Angular 22: 截至2026年8月的最新版本。
- Node.js LTS: 截至2026年5月的LTS版本。
- Firebase Remote Config: 管理动态参数,例如TTS模型名称,并有条件地配置有限使用令牌。
- Firebase AI Logic: 与Gemini生成模型接口。
- Firebase App Check: 防止滥用并验证令牌真实性。
- Firebase App Hosting: 简化部署到Firebase App Hosting环境。通过一次代码推送,最新更改部署到生产环境。
公开的Google Gemini开发者API在我的地区(香港)受限。然而,Agent Platform Gemini API(Google Cloud)提供企业级访问,在这里运行可靠,因此我为这个Firebase AI Logic演示选择了Agent Platform Gemini API。
1.1 安装全局依赖
npm install -g firebase-tools
使用npm全局安装或更新firebase-tools。
firebase logout
firebase login
登出并重新通过Firebase进行身份验证。
1.2 安装依赖
npm i --save-exact firebase npm i --save-exact --save-dev firebase-tools serve
安装依赖以调用Firebase AI Logic API、使用Firebase CLI生成文件并服务生产构建。
firebase init
执行firebase init并按照提示设置Firebase AI Logic、Emulators、App Hosting和Remote Config。
如果您有一个现有项目或多个项目,可以在命令行上指定项目ID。
firebase init --project <PROJECT_ID>
完成设置步骤后,Firebase工具会生成配置文件,例如.firebaserc和firebase.json。您可以在GitHub仓库中查看.firebaserc和firebase.json。
1.3. 生成Firebase配置文件
我们让antigravity-cli(Google发布的一个终端优先的AI编码代理)和Gemini Flash模型创建了两个Node.js脚本来完成以下操作:
- 以JSON格式生成默认的Remote Config值并保存到
public/remote-config-defaults.json。您可以阅读脚本的完整清单。 - 填充Firebase配置对象并将其写入
public/firebase.config.json。如果我们硬编码公共密钥并将其推送到GitHub,GitHub会触发误报。您可以阅读脚本的完整清单和环境变量模板。
# 生成的firebase配置 firebase.config.json
将firebase.config.json添加到.gitignore以防止意外提交。
在构建时,Angular会将这两个JSON文件打包到dist目录中,以提供初始配置值。
2. 架构

用户向Firebase AI Logic提交文本以合成语音。Firebase生成完整的L16音频负载并将其返回给客户端。由于HTML音频元素不支持L16格式,应用程序将音频转换为WAV Blob,然后将Blob URL绑定到元素源。

第二个流程流式传输音频,并将L16块发送到Angular应用程序。音频播放器创建一个AudioBufferSourceNode来播放块数据,并清理资源以防止内存泄漏。
3. 源代码
虽然完整的代码库可在ng-firebase-tts仓库中找到,但该应用程序依赖于Firebase Remote Config来管理配置、App Check来防止滥用以及App Hosting来部署Angular应用程序。
以下部分说明了如何初始化Firebase应用和App Check,以及如何激活Remote Config值。
4. Firebase 集成
public/firebase.config.json 文件包含了公开的 Firebase API 密钥、敏感的 reCAPTCHA Enterprise 密钥,以及用于在本地开发中绕过设备认证的 App Check 调试令牌。这些值在 Firebase 应用和 App Check 初始化中至关重要。
4.1. Firebase 基础设施
@Service()
export class ConfigService {
#app: FirebaseApp | undefined = undefined;
#remoteConfig: RemoteConfig | undefined = undefined;
/*... getter 方法已省略... */
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,
});
}
}
initialize 方法初始化 Firebase App,配置 App Check,设置 Firebase AI,并将远程配置的值赋给 appConfig。
appConfig 对象持有位置、Gemini TTS 模型名称以及有限使用 App Check 令牌标志。
4.2. 远程配置与构建代理平台后端
在 Firebase 远程配置中设置 TTS 模型名称和有限使用 App Check 令牌参数。这两个参数都有条件值。当 Firebase Web 应用名为 firebase-ai-logic-tts 时,TTS 模型为 gemini-3.1-flash-tts-preview,且有限使用 App Check 令牌标志被设为 true。



export const AI_BACKEND = new InjectionToken<AI>('AI_BACKEND');
export function provideFirebase() {
return makeEnvironmentProviders([
{
provide: AI_BACKEND,
useFactory: () => inject(ConfigService).aiBackend,
},
]);
}
AI_BACKEND 注入令牌提供了一个工厂函数,用于从 ConfigService 返回 Firebase AI。
export const appConfig: ApplicationConfig = {
providers: [
... 其他提供者 ...
provideAppInitializer(async () => await inject(ConfigService).initialize()),
provideFirebase(),
],
};
provideAppInitializer 和 provideFirebase 在应用启动期间初始化 Firebase 并配置 Firebase AI。
5. Angular 中的 TextToSpeechService 定义
私有 createModel 方法调用 getGenerativeModel,并使用 speechConfig 进行配置来创建一个生成模型。后续工作流使用此模型将 L16 音频转换为 WAV 格式,或直接播放 L16 流。
@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',
},
},
});
}
}
5.1 流程一:L16 音频转换为 WAV
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);
}
extractValidChunkData 方法从响应负载中提取二进制音频数据和 MIME 类型。
synthesize 方法获取整个 L16 格式的音频负载。但是,HTML 音频元素不支持 L16 格式,因此应用程序必须在将 Blob URL 分配给元素源之前,先将数据转换为 WAV 格式。
完整的转换为 WAV 代码实现请参阅此链接。
此实现保持简单,因为它避免了管理响应流和增量音频块。然而,当文本较长并产生较长的音频流时,用户会感受到延迟。为了消除播放延迟,下一节将探讨如何通过 Web Audio API 的 AudioContext 直接流式传输音频块。
5.2 流程二:L16 音频即时播放
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;
}
synthesizeStream 方法返回一个异步生成器,该生成器产出原始二进制数据和采样率。其 MIME 类型为 audio/l16; rate=24000; channels=1,因此 parseMimeType 会提取出采样率,供音频播放器的 AudioContext 使用。
虽然 AudioPlayerService 的具体实现超出了本文范围,但其源代码展示了如何在不使用 HTML 音频元素的情况下播放音频。
接下来,我们将在 Angular 中构建一个响应式用户界面,该界面将渲染一个 HTML 音频元素来播放音频。
6. 在 Angular 中构建响应式 UI
TextToSpeechComponent 将音频管理职责委托给视图服务,以保持关注点分离。
6.1 文字转语音组件
TextToSpeechComponent 显示三个按钮,用于在三种不同场景下将文本转换为语音:
- 场景 1:合成完整的音频数据,并将生成的 Blob URL 绑定到 HTML 音频元素。
- 场景 2:增量式流式传输音频块,并立即通过 Web Audio API 播放语音。此模式隐藏 HTML 音频元素,因为音频直接通过 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(),
});
}
}
6.2 Angular 中的 TextToSpeechView 定义
TextToSpeechViewService 封装了 TextToSpeechService 和 AudioPlayerService,以协调语音合成与音频播放。
@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 调用 Firebase SDK,使用 Gemini TTS 模型合成音频,生成 Blob URL,更新 #audioUrl,并渲染 HTML 音频元素。
handleStream 则调用 Firebase SDK,使用 Gemini TTS 模型来流式传输音频。音频上下文接收块数据,立即播放,从而避免渲染 HTML 音频元素。
结论
将文字转语音与 Firebase AI Logic 集成,为 Angular 应用实现了实时音频生成能力。
该 Angular 应用在客户端完全处理文字转语音。将代码变更推送到 Git 会触发自动部署到 Firebase App Hosting。
你可以尝试克隆 GitHub 仓库,上传一张图片来生成一个冷知识,并使用 Gemini 3.1 Flash TTS 预览模型,以指定的场景、情感和语速将其朗读出来。
资源
- Firebase AI Logic TTS GitHub 仓库
- Firebase AI Logic TTS 演示
- Firebase AI Logic - 入门
- Firebase AI Logic - 生成语音
- Firebase AI Logic - App Check
- 使用 Firebase Remote Config 动态更新您的 Firebase AI Logic 应用
原文:https://dev.to/gde/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini-37hc(作者 @railsstudent)