Type something to search...
如何利用交互式画布构建实时双子座 2.0 学习助手

如何利用交互式画布构建实时双子座 2.0 学习助手

Gemini 开发教程 V5

在本教程中,我们将继续使用 Gemini 2.0 及其多模态 Live API 构建迷人的实时聊天应用程序。这次我们将基于绘图画布构建一个具有实时语音和文本交互的学习助手。

您可能已经看过 OpenAI 的旧视频 https://youtu.be/_nSmkyDNulk,演示了 got-4o 如何通过提出引导性问题和提供提示来帮助学生在 Khan Academy 的屏幕上学习数学,从而引导学生找到正确答案。

通过使用我们之前的屏幕共享演示项目,我们可以轻松地通过共享学习网站或文档的屏幕,操作一些编辑工具,让 Gemini 2.0 帮助用户学习内容,从而复制相同的体验。

然而,对于现实世界的应用,仅依赖屏幕共享有几个限制。充其量,这是一种黑客方式,在现实世界中,有许多原因需要避免这种做法,因为它将应用程序与特定环境耦合。例如,即使 web 应用程序是用纯 HTML 和 Javascript 编写的,几乎不可能重用它以转移到移动应用程序。它充满了多线程与屏幕共享、共同编辑和语音通话,这需要跨应用程序操作。因此,当您想要开发一个强大且商业化的产品时,集成和控制用户体验可能会相当困难。

这就是专门围绕画布元素构建的应用内文档编辑器变得至关重要的地方。通过构建我们自己的图像加载和编辑界面,我们可以获得更多控制权,并为未来的功能和用途提供更高的可扩展性和灵活性。

系统架构

现在,让我们来看一下我们项目的设计。

我们将继续使用与之前的屏幕共享演示相同的项目结构,但我们将在客户端与服务器和 Gemini 多模态实时 API 的交互方式上引入一些关键变化。最大的变化是我们用一个交互式画布元素替代了屏幕共享组件,用户可以在其中加载图像并使用绘图工具进行一些基本编辑。客户端还包括音频捕获、播放、文本转录以及与服务器通信的 WebSocket 连接。

在服务器端,代码与我们上一个演示保持一致;也就是说,在接收到消息后,服务器将这两个媒体组件的图像和音频转发给实时 API,图像是从画布元素中捕获的。它还负责接收来自 Gemini API 的流响应并将其转发回客户端。语音流也将被传送到 Gemini 1.5 进行转录生成。

现在,我们将逐步讲解代码。由于行数稍多,我将只解释代码中最重要的部分。如果您想查看完整代码,可以在我的 GitHub 仓库 中找到并下载,它不仅包含本教程中的项目,还包括 Gemini 2.0 系列中的之前项目,包括屏幕共享、转录输出和相机交互。

代码演示

现在,让我们开始代码演示的服务器端部分。

服务器设计

安装依赖项,包括两个 Gemini API,生产环境的 google-generativeai 用于语音转文本生成和实验性的多模态实时 API google-genai 用于 Gemini 2.0 实时音频和图像互动,以及用于音频处理的 websocketspydub

pip install --upgrade google-genai==0.3.0 google-generativeai==0.8.3 websockets pydub

同时,您还应该确保在您的计算机上安装了 FFmpeg 软件。对于 Ubuntu 系统,您可以使用 apt-get install ffmpeg 来安装。

接下来是代码:

import asyncio
import json
import os
import websockets
from google import genai
import base64
import io
from pydub import AudioSegment
import google.generativeai as generative
import wave

## Load API key from environment
os.environ['GOOGLE_API_KEY'] = ''
generative.configure(api_key=os.environ['GOOGLE_API_KEY'])
MODEL = "gemini-2.0-flash-exp"  # use your model ID
TRANSCRIPTION_MODEL = "gemini-1.5-flash-8b"

client = genai.Client(
  http_options={
    'api_version': 'v1alpha',
  }
)

第一部分通过环境变量配置 Google AI Studio 访问 API 的密钥,并指定我们将使用的模型。在这里,我们使用 gemini-2.0-flash-exp 模型进行实时图像和音频交互,并使用 gemini-1.5-flash-8b 模型进行语音转文本转录,因为它速度非常快且足够准确,适合这个简单的任务。

服务器端逻辑的核心是 gemini_session_handler 函数。

每当客户端建立新的 WebSocket 连接时,都会调用此函数。它管理与 Gemini 实时 API 的连接,并作为后台任务调用 send_to_gemini()receive_from_gemini() 函数,这些是数据处理逻辑的主要部分。

async def gemini_session_handler(client_websocket: websockets.WebSocketServerProtocol):
    """处理 websocket 会话中与 Gemini API 的交互。"""
    try:
        config_message = await client_websocket.recv()
        config_data = json.loads(config_message)
        config = config_data.get("setup", {})
        
        config["system_instruction"] = """您是一个学习助手。
        检查学生的问题和答案。
        提出引导性问题并提供提示,引导学生找到正确答案。"""
        

        async with client.aio.live.connect(model=MODEL, config=config) as session:
            print("已连接到 Gemini API")

            async def send_to_gemini():
                """将来自客户端 websocket 的消息发送到 Gemini API。"""
                try:
                  async for message in client_websocket:
                      try:
                          data = json.loads(message)
                          if "realtime_input" in data:
                              for chunk in data["realtime_input"]["media_chunks"]:
                                  if chunk["mime_type"] == "audio/pcm":
                                      await session.send({"mime_type": "audio/pcm", "data": chunk["data"]})
                                      
                                  elif chunk["mime_type"] == "image/jpeg":
                                      await session.send({"mime_type": "image/jpeg", "data": chunk["data"]})
                                      
                      except Exception as e:
                          print(f"发送到 Gemini 时出错: {e}")
                  print("客户端连接关闭(发送)")
                except Exception as e:
                     print(f"发送到 Gemini 时出错: {e}")
                finally:
                   print("send_to_gemini 关闭")



            async def receive_from_gemini():
                """接收来自 Gemini API 的响应并将其转发给客户端,循环直到回合结束。"""
                try:
                    while True:
                        try:
                            print("接收来自 gemini 的数据")
                            async for response in session.receive():
                                if response.server_content is None:
                                    print(f'未处理的服务器消息! - {response}')
                                    continue

                                model_turn = response.server_content.model_turn
                                if model_turn:
                                    for part in model_turn.parts:
                                        if hasattr(part, 'text') and part.text is not None:
                                            await client_websocket.send(json.dumps({"text": part.text}))
                                        elif hasattr(part, 'inline_data') and part.inline_data is not None:
                                            print("音频 mime_type:", part.inline_data.mime_type)
                                            base64_audio = base64.b64encode(part.inline_data.data).decode('utf-8')
                                            
                                            await client_websocket.send(json.dumps({"audio": base64_audio}))
                                            
                                            # 在这里累积音频数据
                                            if not hasattr(session, 'audio_data'):
                                                session.audio_data = b''
                                            session.audio_data += part.inline_data.data
                                            
                                            print("音频已接收")

                                if response.server_content.turn_complete:
                                    print('\n<回合完成>')
                                    # 在这里转录累积的音频
                                    transcribed_text = transcribe_audio(session.audio_data)
                                    if transcribed_text:    
                                        await client_websocket.send(json.dumps({
                                            "text": transcribed_text
                                        }))
                                    # 清除累积的音频数据
                                    session.audio_data = b''
                        except websockets.exceptions.ConnectionClosedOK:
                            print("客户端连接正常关闭(接收)")
                            break  # 如果连接关闭,则退出循环
                        except Exception as e:
                            print(f"接收来自 Gemini 时出错: {e}")
                            break 

                except Exception as e:
                      print(f"接收来自 Gemini 时出错: {e}")
                finally:
                      print("Gemini 连接关闭(接收)")


            # 启动发送循环
            send_task = asyncio.create_task(send_to_gemini())
            # 将接收循环作为后台任务启动
            receive_task = asyncio.create_task(receive_from_gemini())
            await asyncio.gather(send_task, receive_task)


    except Exception as e:
        print(f"Gemini 会话中出错: {e}")
    finally:
        print("Gemini 会话关闭。")

首先,它接收来自客户端的配置数据,包括系统指令和来自客户端的响应方式。

然后,它与 Gemini API 建立连接并开始使用模型的 session

send_to_gemini() 函数管理从客户端到 Gemini API 的消息流。它接收客户端发送的媒体块,将音频和图像数据打包到 Gemini API 消息格式中并发送。所有这些都是异步完成的,确保流畅的传输。

receive_from_gemini() 函数负责监听 Gemini API 的响应并将数据转发给客户端。我们使用一个 while 循环,直到连接终止或触发错误为止。一旦生成模型响应,我们处理音频数据并将其发送回客户端播放。同时,音频数据将被累积,一旦设置了 turn_complete 标志,音频数据将被发送到 Gemini 1.5 flash 模型进行转录。

def transcribe_audio(audio_data):
    """使用 Gemini 1.5 Flash 转录音频。"""
    try:
        # 确保我们有有效的音频数据
        if not audio_data:
            return "未接收到音频数据。"
            
        # 将 PCM 转换为 MP3
        mp3_audio_base64 = convert_pcm_to_mp3(audio_data)
        if not mp3_audio_base64:
            return "音频转换失败。"
            
        transcription_client = generative.GenerativeModel(model_name=TRANSCRIPTION_MODEL)
        
        prompt = """生成语音的转录文本。 
        请不要在回复中包含任何其他文本。 
        如果您听不到语音,请只说 '<无法识别>'。"""
        
        response = transcription_client.generate_content(
            [
                prompt,
                {
                    "mime_type": "audio/mp3", 
                    "data": base64.b64decode(mp3_audio_base64),
                }
            ]
        )
            
        return response.text

    except Exception as e:
        print(f"转录错误: {e}")
        return "转录失败。", None

def convert_pcm_to_mp3(pcm_data):
    """将 PCM 音频转换为 base64 编码的 MP3。"""
    try:
        # 首先在内存中创建 WAV
        wav_buffer = io.BytesIO()
        with wave.open(wav_buffer, 'wb') as wav_file:
            wav_file.setnchannels(1)  # 单声道
            wav_file.setsampwidth(2)  # 16 位
            wav_file.setframerate(24000)  # 24kHz
            wav_file.writeframes(pcm_data)
        
        wav_buffer.seek(0)
        audio_segment = AudioSegment.from_wav(wav_buffer)

        mp3_buffer = io.BytesIO()
        audio_segment.export(mp3_buffer, format="mp3", codec="libmp3lame")
        
        # 转换为 base64
        mp3_base64 = base64.b64encode(mp3_buffer.getvalue()).decode('utf-8')
        return mp3_base64
        
    except Exception as e:
        print(f"将 PCM 转换为 MP3 时出错: {e}")
        return None

这两个辅助函数 convert_pcm_to_mp3(),顾名思义,用于将模型响应音频转换为 MP3,因为 Gemini 1.5 音频输入不支持 PCM 格式。然后由 transcribe_audio() 函数调用,它将通过与传统的 google-generativeai API 交互生成语音的转录文本。

最后,主函数非常简单,它设置了WebSocket服务器以监听特定端口。对于每个新的连接,调用gemini_session_handler()来建立一个session,然后,由于正在等待asyncio.Future(),服务器将无限期运行。

客户端设计

现在,让我们转到用 HTML 和 JavaScript 编写的客户端代码。完整的代码可以从我的 GitHub 仓库 下载。

转到 HTML 部分,我们有一个简单的布局,包括图像上传、绘图控件和一个显示图像的画布。同时还有一个转录文本区域,用于显示转录结果。

async function renderFileOnCanvas(file) {
      if (file.type.startsWith("image/")) {
         const reader = new FileReader();
            reader.onload = (event) => {
                 const img = new Image();
                    img.onload = () => {
                      canvas.width = img.width; // Update Canvas width
                      canvas.height = img.height; // Update Canvas height
                      context.drawImage(img, 0, 0, canvas.width, canvas.height);
                   };
                img.src = event.target.result;
             };
            reader.readAsDataURL(file);
        }

    }

在 JavaScript 部分,renderFileOnCanvas() 函数负责将选定的图像加载到画布上,它处理所有必要的步骤,以确保图像正确显示在画布内,并动态更新画布大小,以避免图像变形。

imageLoader.addEventListener('change', async (e) => {
          const file = e.target.files[0];
          if (file) {

                  await renderFileOnCanvas(file);
          }
     });

这个监听器触发 renderFileOnCanvas 函数,将加载的图像绘制到画布上。它还检查文件参数,如果存在,它将调用 renderFileOnCanvas 函数将选定的图像绘制到画布上。

    function captureImage() {
        const imageData = canvas.toDataURL("image/jpeg").split(",")[1].trim();
        currentFrameB64 = imageData;
    }

    window.addEventListener("load", async () => {
             //await startWebcam();

             setInterval(captureImage, 3000);
             await initializeAudioContext();
            connect();

        });

captureImage() 函数负责将画布的内容转换为 base64,以便可以与音频输入一起发送到服务器,设置为每 3 秒进行一次。

最后,startAudioInput()stopAudioInput() 函数管理麦克风访问、捕获音频并通过 WebSocket 将其发送到后端。我们在之前的教程中已经展示过这些。receiveMessage() 函数管理所有传入消息,通过显示文本输出或播放音频输出来处理,如前面的教程所示。

function receiveMessage(event) {
            const messageData = JSON.parse(event.data);
            const response = new Response(messageData);

            if(response.text){
                displayMessage("GEMINI: " + response.text);
            }
            if(response.audioData){
              injestAudioChuckToPlay(response.audioData);
            }
        }

这些是代码的关键部分。

现在,让我们运行代码。

运行应用程序

启动网页应用程序的流程仍然是一样的。

通过运行 Python 文件来启动服务器。WebSocket 服务器将在代码中定义的 8093 端口上运行。

HTML/JS 源代码(index.htmlpcm-processor.js)可以从我的 GitHub 仓库 下载。

通过在项目文件夹下运行命令来启动客户端:

python -m http.server

现在,我们可以在 8000 端口访问本地服务器。在浏览器中输入 URL HTTP://localhost:8000 来尝试应用程序。

在网页上,您可以上传一个图像文件,然后点击对话按钮与 Gemini 2.0 聊天,同时在图像画布上进行实时绘图。这里是我为体验拍摄的视频。

感谢您的阅读。如果您觉得有帮助,请为这篇文章鼓掌 👏。您的鼓励和评论对我来说意义重大,无论是精神上还是经济上。🍔

在您离开之前:

✍️ 如果您有任何问题,请给我留言或在 XDiscord 上找到我,在那里您可以获得我在开发和部署方面的积极支持。

☕️ 如果您想要独家资源和技术服务,订阅我在 Ko-fi 上的服务将是一个不错的选择。

💯 我也欢迎任何创新和全栈开发工作的聘用。

Related Posts

使用 ChatGPT 搜索网络功能的 10 种创意方法

使用 ChatGPT 搜索网络功能的 10 种创意方法

例如,提示和输出 你知道可以使用 ChatGPT 的“搜索网络”功能来完成许多任务,而不仅仅是基本的网络搜索吗? 对于那些不知道的人,ChatGPT 新的“搜索网络”功能提供实时信息。 截至撰写此帖时,该功能仅对使用 ChatGPT 4o 和 4o-mini 的付费会员开放。 ![](https://images.weserv.nl/?url=https://cdn-im

阅读更多
在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和科技这样一个动态的行业中,保持领先意味着不断提升你的技能。无论你是希望深入了解人工智能模型性能、掌握数据分析,还是希望通过人工智能转变传统领域如法律,这些课程都是你成功的捷径。以下是一个精心策划的高价值课程列表,可以助力你的职业发展,并让你始终处于创新的前沿。 1. 生成性人工智能简介课程: [生成性人工智能简介](https://genai.works

阅读更多
10 个强大的 Perplexity AI 提示,让您的营销任务自动化

10 个强大的 Perplexity AI 提示,让您的营销任务自动化

在当今快速变化的数字世界中,营销人员总是在寻找更智能的方法来简化他们的工作。想象一下,有一个个人助理可以为您创建受众档案,建议营销策略,甚至为您撰写广告文案。这听起来像是一个梦想? 多亏了像 Perplexity 这样的 AI 工具,这个梦想现在成为现实。通过正确的提示,您可以将 AI 转变为您的 个人营销助理。在本文中,我将分享 10 个强大的提示,帮助您自动

阅读更多
10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

人工智能技术,如机器学习、自然语言处理和数据分析,正在重新定义传统设计方法。从自动化重复任务到实现个性化用户体验,人工智能使设计师能够更加专注于战略思维和创造力。随着这一趋势的不断增长,UI/UX 设计师越来越多地采用 AI 驱动的工具来促进他们的工作。利用人工智能不仅能提供基于数据的洞察,还为满足多样化用户需求的创新设计解决方案开辟了机会。 1. 用户角色开发 目的

阅读更多
在几分钟内完成数月工作的 100 种人工智能工具

在几分钟内完成数月工作的 100 种人工智能工具

人工智能(AI)的快速发展改变了企业的运作方式,使人们能够在短短几分钟内完成曾经需要几周或几个月的任务。从内容创作到网站设计,AI工具帮助专业人士节省时间,提高生产力,专注于创造力。以下是按功能分类的100个AI工具的全面列表,以及它们在现实世界中的使用实例。 1. 研究工具 研究可能耗时,但人工智能工具使查找、分析和组织数据变得更加容易。**ChatGPT, Cop

阅读更多
你从未知道的 17 个令人惊叹的 GitHub 仓库

你从未知道的 17 个令人惊叹的 GitHub 仓库

Github 隐藏的宝石!! 立即收藏的代码库 学习编程相对简单,但掌握编写更好代码的艺术要困难得多。GitHub 是开发者的宝藏,那里“金子”是其他人分享的精心编写的代码。通过探索 GitHub,您可以发现如何编写更清晰的代码,理解高质量代码的样子,并学习成为更熟练开发者的基本步骤。 1. notwaldorf/emoji-translate *谁需

阅读更多