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工具会生成配置文件,例如.firebasercfirebase.json。您可以在GitHub仓库中查看.firebasercfirebase.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. 架构

Angular,Firebase AI Logic,Gemini,TTS,实时语音合成,前端开发

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

Angular,Firebase AI Logic,Gemini,TTS,实时语音合成,前端开发

第二个流程流式传输音频,并将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。

Angular,Firebase AI Logic,Gemini,TTS,实时语音合成,前端开发

Angular,Firebase AI Logic,Gemini,TTS,实时语音合成,前端开发

Angular,Firebase AI Logic,Gemini,TTS,实时语音合成,前端开发

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(),
  ],
};


provideAppInitializerprovideFirebase 在应用启动期间初始化 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 封装了 TextToSpeechServiceAudioPlayerService,以协调语音合成与音频播放。

@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 预览模型,以指定的场景、情感和语速将其朗读出来。

资源

原文:https://dev.to/gde/build-real-time-client-side-tts-in-angular-using-firebase-ai-logic-and-gemini-37hc(作者 @railsstudent)

发布评论
全部评论(0)