
解锁实时多模态魔法:使用 Next.js 构建一个快速的 Gemini 2.0 应用程序!
- Rifx.Online
- Generative AI , Large Language Models , AI Applications
- 05 Mar, 2025
使用 Gemini 2.0 API 和 Next.js 构建多模态实时应用:音频、视频和转录
Gemini终极开发教程
嘿,Gemini开发教程又来了。这次,我将介绍一个新的实践网页项目,演示如何使用Next.js框架构建一个基于Gemini 2.0多模态实时API的无服务器应用程序,实现一个生产就绪的聊天应用,支持实时音频和视频互动,使用Typescript编写。
请观看演示视频:
这个项目在架构和用户界面上相较于我之前的多模态聊天应用演示有了显著的改进,而非功能上的改进。摆脱了单独的客户端-服务器结构,我使用Next.js构建了一个紧凑的解决方案,作为统一的框架。
Next.js同时管理Node.js的服务器端操作和React支持的客户端UI更新,利用这些,我构建了一个高效的开发管道,便于维护和部署,最重要的是,该框架可以轻松扩展,以支持更多自定义功能,作为商业实时项目的启动器。
技术简要概述
该应用程序接受来自客户端摄像头和麦克风的视频和音频输入,通过自定义音频处理例程在本地处理这些流,并通过WebSocket将媒体数据发送到Gemini API端点。响应——包括Gemini生成的音频输出和转录——被处理并集成回由Shadcn UI组件支持的互动聊天界面中。
系统架构
该应用的架构专注于实时性能和最小延迟。以下是一个框图,展示了组件在框架栈内的交互以及过程工作流:
系统由以下主要功能模块组成:
1. 媒体捕获与处理:
- 浏览器中的“CameraPreview 组件”使用标准 web API 捕获用户的视频和音频流。
- 音频数据被发送到“音频处理模块”,在这里 AudioWorklet(在 audio-processor.js 中实现)将原始 PCM 音频处理为二进制块。
2. 发送到 Gemini API:
- 处理后的 PCM 音频块(以及编码为 base64 图像的视频帧)被交给“GeminiWebSocket 服务”。
- 此服务与 Gemini 多模态实时 API 端点保持可靠的 WebSocket 连接。
3. 音频输出处理:
- 在接收到来自 Gemini API 的音频响应后,服务现在将传入的 PCM 数据路由到“音频输出管理器”。
- 在这里,数据被推入队列,以确保音频块按顺序播放。
- 此模块还计算音频播放级别以更新用户界面指示器。
4. 音频播放调度:
- 排队的音频片段随后被发送到“音频播放模块”。
- 使用 Web Audio API,该模块基于处理后的 PCM 数据创建一个
AudioBuffer
并进行播放。 - 在一个片段播放完成后,服务检查队列以继续播放任何剩余音频,确保连续输出。
5. 转录服务:
- 一旦通过 Gemini WebSocket 服务检测到完整的轮次,累积的音频(通过工具函数转换为 WAV 格式)将被发送到“转录工作流”。
- 然后转录文本被返回并集成到聊天界面中。
- 我们必须将 PCM 转换为 WAV 的原因是,尽管实验性的多模态实时 API 支持 PCM 格式,但 PCM 格式不在 Gemini 的基本音频理解支持列表中。
6. 聊天 UI 更新:
- 最后,任何文本响应消息都会传递到“聊天 UI 组件”。该模块使用特定的 ShadcnUI 小部件,例如用于显示可滚动对话窗口的
ScrollArea
组件和Avatar
组件。
代码讲解:关键功能模块
以下是项目中关键组件和功能模块的详细讲解。每个部分都包含简要说明和注释的代码片段。
0. 可自定义的参数
- 环境变量:Gemini API 密钥存储在 .env.local 文件中。
- 模型名称和端点:该项目使用特定的模型标识符。转录工作流使用“
gemini-2.0-flash-lite-preview-02–05
”或甚至“gemini-1.5-flash-8b
”模型来完成这个简单的任务,而实时流媒体则引用“models/gemini-2.0-flash-exp
”。 - 音频采样率:配置了两种采样率:16000 Hz 采样率用于从 CameraPreview 组件捕获音频,24000 Hz 采样率用于播放音频响应。
1. Gemini WebSocket 服务
app/services/geminiWebSocket.ts
文件中的 GeminiWebSocket 类负责管理与 Gemini API 的 WebSocket 连接。
它处理初始设置、媒体数据块传输、音频播放和持续的媒体处理。
连接建立和设置
当调用 connect 方法时,它使用提供的 API 密钥构建的 URL 打开 WebSocket 连接。一旦连接成功,服务立即发送初始设置消息以配置响应方式(例如,AUDIO):
// GeminiWebSocket.connect method snippet
this.ws = new WebSocket(WS_URL);
this.ws.onopen = () => {
this.isConnected = true;
this.sendInitialSetup(); // Send configuration to Gemini API
};
发送媒体数据
当捕获音频(PCM)或图像(JPEG)数据时,它会被编码为 base64 并传递给 sendMediaChunk
函数。该函数相应地构建消息负载并通过 WebSocket 发送:
// sendMediaChunk function snippet
const message = {
realtime_input: {
media_chunks: [{
mime_type: mimeType === "audio/pcm" ? "audio/pcm" : mimeType,
data: b64Data
}]
}
};
this.ws?.send(JSON.stringify(message));
这种抽象确保每个媒体数据块都以一致的格式处理。
响应处理和音频播放
handleMessage
方法接收来自 Gemini API 的消息。它通过检查 MIME 类型来判断音频数据是否返回,并相应地进行处理:
// Key snippet from handleMessage (audio processing)
if (part.inlineData?.mimeType === "audio/pcm;rate=24000") {
this.accumulatedPcmData.push(part.inlineData.data);
this.playAudioResponse(part.inlineData.data);
}
在收集 PCM 数据后,该方法将其转换并排队进行播放。当系统确定模型的一个回合已完成时,它使用一个工具函数将累积的 PCM 数据转换为 WAV 格式(见下文),然后将其发送到转录服务。
2. 转录服务
app/services/transcriptionService.ts
中的 TranscriptionService 类封装了用于转录音频响应的逻辑。当 Gemini 回合完成时,累积的 PCM 数据被转换为 WAV,然后将该 WAV 数据传递给转录服务。
转录音频方法
transcribeAudio
方法接受 base64 编码的 WAV 数据,并将其发送到 Gemini 生成模型进行转录。然后解析响应并以纯文本形式返回:
// TranscriptionService.transcribeAudio method snippet
async transcribeAudio(audioBase64: string, mimeType: string = "audio/wav"): Promise<string> {
// Calls Gemini’s generative model to generate transcription text
const result = await this.model.generateContent([
{
inlineData: { mimeType: mimeType, data: audioBase64 }
},
{ text: "Please transcribe the spoken language in this audio accurately." },
]);
return result.response.text();
}
再一次,一旦 Genimi API 允许我们通过支持并发响应模式 [“AUDIO”, “TEXT”] 来响应组合音频和文本流,我们就可以取消这一转录部分。
3. 摄像头预览和音频捕获
CameraPreview.tsx
组件是客户端媒体捕获功能的核心。它管理视频显示、麦克风捕获和定期图像捕获以发送到 Gemini。
切换摄像头
toggleCamera
函数负责开启和关闭媒体流。它请求用户设备的视频和音频流,设置媒体源,并将其分配给相关元素:
// toggleCamera function snippet in CameraPreview.tsx
const toggleCamera = async () => {
if (isStreaming && stream) {
// Stop all tracks and clear the stream
stream.getTracks().forEach(track => track.stop());
videoRef.current.srcObject = null;
setStream(null);
setIsStreaming(false);
} else {
// Request media from the browser
const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: { sampleRate: 16000, channelCount: 1, echoCancellation: true }
});
// Combine the streams and set them for display and processing
videoRef.current.srcObject = videoStream;
setStream(new MediaStream([...videoStream.getTracks(), ...audioStream.getTracks()]));
setIsStreaming(true);
}
};
定期图像捕获
除了音频外,还定期从视频流中捕获图像以发送到 Gemini。captureAndSendImage
函数将当前视频帧绘制到一个隐藏的画布上,将其转换为 base64 编码的 JPEG,然后通过 WebSocket 连接发送:
// captureAndSendImage snippet from CameraPreview.tsx
const captureAndSendImage = () => {
// Draw video frame onto canvas
const canvas = videoCanvasRef.current;
const context = canvas.getContext('2d');
context.drawImage(videoRef.current, 0, 0);
// Convert the canvas image to JPEG base64 data
const imageData = canvas.toDataURL('image/jpeg', 0.8);
const b64Data = imageData.split(',')[1];
geminiWsRef.current.sendMediaChunk(b64Data, "image/jpeg");
};
这种定期轮询确保 Gemini 除了实时音频外,还能接收到稳定的视觉数据流。
4. 音频处理和 PCM 到 WAV 转换
为了确保从麦克风捕获的音频被正确处理和转录,我们创建了两个关键组件:一个用于实时 PCM 处理的 AudioWorklet
和一个将 PCM 数据转换为 WAV 格式的工具函数。
AudioWorklet 处理器
自定义的 AudioWorklet
(在 public/worklets/audio-processor.js
中)实时累积和处理原始音频样本。当缓冲区满时,它将浮点样本转换为 16 位整数数组,计算音频水平,并将处理后的 PCM 数据发送回主线程:
// AudioProcessor class in audio-processor.js
class AudioProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
// Accumulate and process PCM samples
// When buffer is full, convert samples to 16-bit PCM
this.port.postMessage({
pcmData: buffer, // Buffer containing PCM samples
level: Math.min(computedLevel * 5, 100)
}, [buffer]);
return true;
}
}
registerProcessor('audio-processor', AudioProcessor);
这将重负载音频处理到专用线程,同时保持主 UI 的响应性。
PCM 到 WAV 转换工具
在将音频数据发送进行转录之前,必须将 PCM 数据转换为 WAV 格式。app/utils/audioUtils.ts
中的 pcmToWav 函数通过构造 WAV 头并附加原始音频样本来完成此操作:
// pcmToWav function snippet from audioUtils.ts
export function pcmToWav(pcmData: string, sampleRate: number = 24000): Promise<string> {
// Decode base64 PCM string, create WAV header, combine with PCM samples
// Return a promise that resolves to a base64-encoded WAV string
}
5. 聊天 UI 组件
聊天界面在 app/page.tsx
中渲染。该文件利用 ShacdnUI 组件实现干净且响应式的布局。关键组件包括:
ScrollArea
:提供一个可滚动的容器用于聊天消息。Avatar
,AvatarImage
,AvatarFallback
:用于显示 Gemini 的个人资料图片,以确保视觉上的清晰区分。
聊天消息的渲染通过以下方式完成:
{messages.map((message, index) => (
message.type === 'human' ? (
<HumanMessage key={`msg-${index}`} text={message.text} />
) : (
<GeminiMessage key={`msg-${index}`} text={message.text} />
)
))}
运行应用程序
现在,掌握了这个项目的基本知识,我们可以运行应用程序。
克隆仓库:
首先从 GitHub 克隆完整的源代码:
git clone https://github.com/yeyu2/gemini-nextjs.git
安装依赖:
导航到项目根目录,并使用 npm(或 yarn)安装依赖:
cd gemini-nextjs
npm install
环境配置:
在根目录下创建一个 .env.local
文件,并添加您的 Gemini API 密钥:
NEXT_PUBLIC_GEMINI_API_KEY=YOUR_API_KEY_HERE
启动开发服务器:
使用 Next.js 开发服务器启动无服务器应用程序:
npm run dev
打开您的浏览器并访问 http://localhost:3000 以查看应用程序的运行情况。
结论
本教程概述了如何使用 Gemini 2.0 构建无服务器实时流应用程序。通过使用 Next.js 框架,该应用程序实现了更简单、更有效的架构,适用于演示和后续升级。
该应用程序的所有源代码都可以在我的 GitHub 仓库 中找到。欢迎下载、尝试,并在评论中分享您的想法。
感谢您的阅读。