Fishjam.io 如何利用Gemini Live构建了一个多扬声器AI游戏

想象一下热闹的晚宴:杯子碰撞声,话语未说完,三个人同时笑着。对人类来说,处理这些问题是本能的。对人工智能来说,这是一场噩梦。开发者已经有效地掌握了一对一聊天的可预测流程。但应对群体对话,尤其是人们打断彼此说话时,就难得多。

Bernard Gawor和Software Mansion的Fishjam团队着手展示他们的选择性转发单元解决方案,开发一款独特的演示应用来解决这个问题。这就是《深海故事》游戏诞生的过程。

故事设定很简单:一群侦探进入会议室,试图解开一个谜团。反转?“谜语大师”,即知道秘密解答并回答问题的实体,实际上是一个双子声人工智能代理。这要求代理实时倾听、理解并响应一组用户。

声音代理人的解剖学

首先,让我们看看AI语音代理通常如何处理数据。它通常通过模块化流水线运行,包含以下步骤:

  • 语音转文字(S2T):该系统通过谷歌语音转文字、OpenAI Whisper 或 ElevenLabs 的转录服务等模型,将用户的语音输入转换为文本。
  • 大型语言模型(LLM):转录后的文本会由大型语言模型(如 Gemini、GPT-4、Claude)处理,以理解上下文并生成合适的文本回复。
  • 文本转语音(TTS):文本回复通过Google Cloud TTS、ElevenLabs或Azure TTS等服务被转换回自然语音。
  • 实时音频流媒体:音频以极低的延迟反馈给用户。
标准语音AI流水线

另一种日益流行的架构是语音转语音(Speech-to-Speech)。与传统的将语音转换为文本再转换回来的管道不同,该架构将原始音频直接输入模型,并在一步内生成音频输出。这种统一的方法不仅降低了延迟,还保留了非语言特征,使模型能够高精度识别并复制细微的人类情感、语调和节奏。

一对一与群体的情境

大多数标准SDK让设置一对一对话相对简单。例如,使用 Gemini Live API SDK 时:

const { GoogleGenAI } = require("@google/genai");

// 1. Setup
const ai = new GoogleGenAI({ apiKey: "YOUR_API_KEY" });

async function startAgent() {
  // 2. Connect
  const session = await ai.live.connect({
    model: "gemini-2.5-flash-native-audio-preview-12-2025",
    config: { responseModalities: ["AUDIO"] },
  });

  console.log("Agent Connected!");

  // 3. Listen for the Agent's Voice
  session.receive(async (msg) => {
    // This loop runs every time the AI sends an audio chunk
    if (msg.serverContent?.modelTurn?.parts) {
      const audioData = msg.serverContent.modelTurn.parts[0].inlineData.data;
      console.log(`Received Audio Chunk (${audioData.length} bytes)`);
      // In a real app, you would send 'audioData' to your audio output device
    }
  });

  // 4. Send Your Voice (Simulated)
  // Real apps pipe microphone data here continuously
  console.log("Sending audio...");
  await session.sendRealtimeInput([
    {
      mimeType: "audio/pcm;rate=16000",
      data: "BASE64_ENCODED_PCM_AUDIO_STRING_GOES_HERE",
    },
  ]);
}

startAgent();

然而,这些SDK假设只有一个音频输入流。在会议室中,音频流是独立的、异步且重叠的。他们必须确定如何在不丢失上下文或引入不可接受延迟的情况下,汇总这些输入给谜语大师。

他们评估了三种针对多讲者环境的具体架构策略:

  1. 服务器端聚合:这种方法涉及将所有播放器音频流混合到一个通道中,然后再发送给AI代理。虽然实现简单,但音频混合使语音转文字(S2T)模型极难准确转录,尤其是在用户相互重复说话时。这会导致“幻觉”或漏答。
  2. 每位客户的代理人:这种方法会为房间里的每个玩家分配一个独立的语音AI代理。这造成了混乱的用户体验(所有代理同时说话),并防止共享游戏状态。而且成本高昂,因为每个用户流都消耗不同的处理令牌。
  3. 使用VAD进行服务器端过滤:在这种方法中,他们通过语音活动检测(VAD)实现了一个集中式守门人。他们等待玩家开口,锁定“输入槽”,只将该玩家的音频转发给AI代理。一旦他们停止说话,锁就会被释放,允许另一位玩家提问。这就是他们最终采用的解决方案。

超越一对一:一款“深海故事”游戏网页应用

关键技术

  • 鱼塞:一个实时通信平台,通过WebRTC(SFU)处理点对点音频流传输。(不熟悉WebRTC/SFU吗?看看他们的指南)
  • Gemini GenAI 语音代理:提供一个简单的 SDK,使创建语音代理和初始化音频对话变得简单。

架构概述

游戏逻辑由后端处理,后端负责会议室和节点连接。

  • 玩家连接:当玩家通过前端客户端加入游戏时,他们会通过 Fishjam Web SDK 连接音频/视频。(参见:Fishjam React 快速入门
  • 桥梁:游戏开始时,后端会创建一个 Fishjam 代理。该代理在音视频室中表现为“幽灵对等”;它的唯一目的是捕捉玩家的音频并转发给AI,反之亦然。
  • 大脑:后端通过 WebSocket 与 Gemini 代理连接,并将播放器的音频流转发到 Gemini,反之亦然。
架构图

实现细节

1. 初始化客户端和游戏室

import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
import GeminiIntegration from '@fishjam-cloud/js-server-sdk/gemini';

const fishjamClient = new FishjamClient({
  fishjamId: process.env.FISHJAM_ID!,
  managementToken: process.env.FISHJAM_TOKEN!,
});

const genAi = GeminiIntegration.createClient({
  apiKey: process.env.GOOGLE_API_KEY!,
});

const gameRoom = await fishjamClient.createRoom();

2. 创建鱼塞特工

当第一位玩家加入游戏室时,他们会创建Fishjam代理,在后台捕捉玩家的音频。

import GeminiIntegration from "@fishjam-cloud/js-server-sdk/gemini";

const { agent } = await fishjamClient.createAgent(gameRoom.id, {
  subscribeMode: "auto",
  // Use their preset to match the required audio format (16kHz)
  output: GeminiIntegration.geminiInputAudioSettings,
});
// agentTrack enables to send audio back to players
const agentTrack = agent.createTrack(
  GeminiIntegration.geminiOutputAudioSettings,
);

3. 配置和初始化AI谜语大师

当用户选择故事场景时,他们会根据特定上下文(谜题解答和“游戏主持人”身份)配置双子座代理。

const session = await genAi.live.connect({
  model: GEMINI_MODEL,
  config: {
    responseModalities: [Modality.AUDIO],
    systemInstruction:
      "here's the story: ..., and its solution: ... you should answer only yes or no questions about this story",
  },
  callbacks: {
    // Gemini -> Fishjam
    onmessage: (msg) => {
      if (msg.data) {
        // send Riddle Master's audio responses back to players
        const pcmData = Buffer.from(msg.data, "base64");
        agent.sendData(agentTrack.id, pcmData);
      }

      if (msg.serverContent?.interrupted) {
        console.log("Agent was interrupted by user.");
        // Clears the buffer on the Fishjam media server
        agent.interruptTrack(agentTrack.id);
      }
    },
  },
});

4. 音频桥接(胶水)

最后一块拼图是SFU与AI之间的桥梁。他们会从Fishjam代理那里捕捉音频流(玩家说的话),然后通过自定义的VAD(语音活动检测)过滤器。该滤波器实现了“互斥”锁定机制:识别第一个活跃扬声器,锁定其 ID,并仅将其音频转发给 Gemini。其他同时进行的音频在当前发言者完成回合前均被忽略。

VAD逻辑图

以下是该逻辑的简化代码:

// State to track who currently "holds the floor"
let activeSpeakerId: string | null = null;

// They capture audio chunks from ALL players in the room
agent.on("audioTrack", (userId, pcmChunk) => {
  vadService.process(userId, pcmChunk);
});

// VAD Processor Logic
vadService.on("activity", (userId, isSpeaking, audioData) => {
  if (activeSpeakerId === null && isSpeaking) {
    activeSpeakerId = userId; // Lock the floor
  }

  // They only forward audio if it comes from the person holding the lock
  if (userId === activeSpeakerId) {
    voiceAgentSession.sendAudio(audioData);

    // If the active speaker stops speaking (silence detected), release the lock
    if (!isSpeaking) {
      // (Optional: Add a debounce delay here to prevent cutting off pauses)
      activeSpeakerId = null;
    }
  }
});

群体人工智能面临的挑战

构建多用户语音界面相比一对一聊天带来了独特的挑战:

  • 地板控制:标准语音转文本模型在多名玩家同时说话时可能会遇到困难。判断AI应响应哪个玩家,还是仅仅听从,需要谨慎处理。
  • 延迟:实时响应对沉浸感至关重要。整个流程(音频→文本→LLM→音频)必须在毫秒内完成。
  • 音质:通过跨网络转码和流媒体保持音频清晰至关重要。

幸运的是,Fishjam 的 WebRTC 实现在很大程度上解决了延迟和音频质量问题。地板控制的挑战需要在后台进行精心结构化的实施,但其实并不难!

自己试试这款游戏吧!

他们在现场演示中实现了上述功能。召集朋友,尝试用他们的AI谜语大师一起解开谜团!

如果有人正在开发基于AI的实时视频或音频功能并需要帮助,可以通过Discord联系团队。

原创文章,作者:ROCKYCOO,如若转载,请注明出处:https://aiyixun.com/fishjam-io-%e5%a6%82%e4%bd%95%e5%88%a9%e7%94%a8gemini-live%e6%9e%84%e5%bb%ba%e4%ba%86%e4%b8%80%e4%b8%aa%e5%a4%9a%e6%89%ac%e5%a3%b0%e5%99%a8ai%e6%b8%b8%e6%88%8f/

喜欢 (0)
ROCKYCOO的头像ROCKYCOO
上一篇 7小时前
下一篇 7小时前

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注