南北阁 Nanbeige 4.1-3B 实战教程:添加WebRTC音视频通话接口实现多模态交互

今天我们来玩点不一样的。你已经体验过南北阁 Nanbeige 4.1-3B 这个轻量级模型的文本对话能力了,它流式输出很丝滑,思考过程也能看得一清二楚。但你想过没有,如果能让这个模型不仅能“听”文字,还能“看”画面、“听”声音,甚至和你实时视频通话,那会是什么体验?

这就是我们今天要做的:给这个纯文本对话工具,加上一双“眼睛”和一对“耳朵”。我们将通过 WebRTC 技术,为它集成音视频通话接口,让它从一个单纯的聊天机器人,升级成一个能看、能听、能说的多模态交互助手。想象一下,你可以打开摄像头,让它实时分析你手头的物品、帮你读一段文档、甚至根据你的手势给出反馈。

听起来很酷?别担心,整个过程并不复杂。我们不需要重写整个项目,而是在现有流畅的 Streamlit 界面上,巧妙地“嫁接”上音视频能力。我会手把手带你,从环境准备、代码集成,到最终实现一个能跑起来的原型。你会发现,让 AI 模型“睁开眼”,其实比你想象的要简单。

1. 项目回顾与升级思路

在开始动手之前,我们先快速回顾一下手头的“地基”——南北阁 Nanbeige 4.1-3B 对话工具,并理清我们要在上面盖的“新楼”是什么样子。

1.1 现有工具的核心能力

你手上的这个工具,核心优势在于“轻快准”:

  • 轻:30亿参数的模型,对硬件极其友好,普通显卡甚至纯CPU都能跑起来,部署门槛低。
  • 快:采用了 TextIteratorStreamer,实现了逐字输出的“打字机”效果,交互感很强,没有卡顿感。
  • 准:严格遵循了官方推荐的加载和推理参数(比如那个关键的 eos_token_id=166101),保证了模型输出的效果和稳定性。
  • 直观:最大的亮点是把模型的“思考过程”(CoT)用折叠面板展示了出来。你可以选择看它一步步的推理,也可以直接看最终答案,设计得很人性化。

它的架构很清晰:一个用 Streamlit 构建的现代化网页界面,后端是加载好的 Nanbeige 模型。用户输入文字,模型思考并流式输出文字。这是一个高效、专注的文本对话管道。

1.2 引入WebRTC的目标

我们现在要做的,就是给这条文本管道,并联上音视频管道。WebRTC 正是实现这个目标的绝佳技术。

WebRTC 是什么? 你可以把它理解成浏览器内置的“实时通信引擎”。它允许网页应用在用户之间(Peer-to-Peer)直接传输音频、视频流和数据,延迟极低,无需安装插件。我们正是要利用它的两个核心能力:

  1. 获取媒体流:调用用户的摄像头和麦克风。
  2. 处理与传输:将获取到的音视频流,实时地交给我们的 Python 后端进行处理。

我们的升级目标:

  1. 增加音视频输入:在现有聊天界面旁,增加一个视频预览窗口和麦克风开关。
  2. 实现多模态输入:用户可以选择用文字提问,也可以直接“说话”或者“展示图片/视频帧”给模型。
  3. 扩展模型交互:后端需要能接收、处理这些音视频数据(例如,将音频转为文字,将视频帧转为图像描述),再交给 Nanbeige 模型进行理解和回复。
  4. 保持原有体验:不破坏已有的流式文本输出、CoT折叠展示等优秀特性。

简单说,就是从 “文本输入 -> 文本输出” 的单车道,升级为 “(文本/音频/视频)输入 -> 智能处理 -> 文本输出” 的多车道立交桥。

2. 环境准备与依赖安装

工欲善其事,必先利其器。我们需要在原有环境的基础上,新增几个处理音视频和 Web 实时通信的库。

2.1 检查与安装Python库

假设你的项目已经能正常运行原有的 Nanbeige 对话工具。现在,打开你的终端,激活项目所用的 Python 环境,安装以下新依赖:

# 核心WebRTC支持库,用于在Streamlit中集成音视频组件
pip install streamlit-webrtc

# 音频处理库,用于录制、播放和转换音频格式
pip install pyaudio

# 这是pyaudio在Windows上可能需要的底层依赖,如果在其他系统安装pyaudio失败,可以尝试先安装这个
# pip install pipwin
# pipwin install pyaudio

# 强大的音视频处理库,我们将用它来解码、处理视频帧
pip install opencv-python-headless # 使用headless版本,无需GUI支持,更适合服务器环境

# 语音识别库,用于将用户的语音输入转换成文字(这是实现语音交互的关键)
pip install SpeechRecognition

# 语音转文字引擎的后端,我们选择离线的Vosk,它轻量且无需网络API
pip install vosk

# 可选但推荐:用于播放提示音或合成语音回复(本期先聚焦输入,输出可后续扩展)
# pip install playsound
# pip install gTTS

安装注意事项:

  • pyaudio 在某些系统上可能安装稍麻烦,如果遇到问题,请搜索“pip install pyaudio [你的操作系统]”寻找解决方案,通常需要一些系统级的音频开发包。
  • opencv-python-headless 比完整的 opencv-python 更轻量,因为我们不需要在服务器端弹出任何视频窗口。
  • vosk 需要下载对应的语音识别模型文件(稍后会讲到)。

2.2 下载语音识别模型

我们使用 Vosk 进行离线语音识别。它需要一个小型的模型文件。这里我们下载一个适用于中文、体积较小的模型。

  1. 访问 Vosk 的模型仓库:https://alphacephei.com/vosk/models
  2. 找到一个适合的模型,例如 vosk-model-small-cn-0.22(约40MB),这个模型对于中文语音识别有不错的效果。
  3. 下载并解压。在你的项目目录下,创建一个名为 models 的文件夹,将解压后的模型文件放进去。
    你的项目目录/
    ├── app.py                 # 你的Streamlit主程序
    ├── models/
    │   └── vosk-model-small-cn-0.22/  # 解压后的模型文件
    └── ...
    

环境准备好后,我们就可以开始动手改造代码了。

3. 代码集成与实现步骤

我们将对原有的 Streamlit 应用进行模块化改造。核心思路是:在侧边栏增加音视频控制面板,在主界面复用原有的聊天区域,并创建新的后台处理逻辑。

3.1 改造应用界面与状态管理

首先,我们修改 app.py 的头部,导入新的库并初始化一些全局状态。为了清晰,我们将较大的功能块(如视频处理、语音识别)拆分成函数。

# app.py
import streamlit as st
from streamlit_webrtc import webrtc_streamer, WebRtcMode, ClientSettings
import av
import cv2
import numpy as np
import queue
import threading
import json
import time
from pathlib import Path
# 原有的模型加载和推理相关导入
# from transformers import ...
# import torch

# 语音识别相关
import speech_recognition as sr
from vosk import Model, KaldiRecognizer
import pyaudio

# --- 状态初始化 ---
# 初始化session state,用于在页面重载间保持数据
if 'messages' not in st.session_state:
    st.session_state.messages = []  # 保存对话历史
if 'audio_queue' not in st.session_state:
    st.session_state.audio_queue = queue.Queue()  # 用于传递音频数据
if 'last_video_frame' not in st.session_state:
    st.session_state.last_video_frame = None  # 保存最后一帧视频图像
if 'recognized_text' not in st.session_state:
    st.session_state.recognized_text = ""  # 保存语音识别结果
if 'webrtc_ctx' not in st.session_state:
    st.session_state.webrtc_ctx = None  # 保存WebRTC上下文,便于控制

# --- 语音识别初始化(Vosk)---
@st.cache_resource
def load_vosk_model():
    model_path = Path("models/vosk-model-small-cn-0.22")
    if not model_path.exists():
        st.error(f"未找到Vosk模型,请下载并放置在 {model_path} 目录下")
        return None
    return Model(str(model_path))

vosk_model = load_vosk_model()
recognizer = None
if vosk_model:
    recognizer = KaldiRecognizer(vosk_model, 16000)  # 采样率16kHz

3.2 创建音视频处理回调函数

这是 WebRTC 的核心。我们需要定义当收到音频帧和视频帧时,应该做什么。

# --- 音视频处理回调函数 ---
class VideoProcessor:
    def __init__(self):
        self.frames_received = 0

    def recv(self, frame):
        """处理接收到的视频帧"""
        img = frame.to_ndarray(format="bgr24")  # 将帧转换为OpenCV格式
        self.frames_received += 1

        # 每隔一定帧数处理一次,避免过于频繁(例如每秒处理5次)
        if self.frames_received % 6 == 0:  # 假设30fps,每6帧处理一次即5fps处理频率
            # 将当前帧存入session state,供其他部分(如截图按钮)使用
            st.session_state.last_video_frame = img.copy()

            # 这里可以添加图像分析逻辑,例如:
            # 1. 使用图像描述模型(如BLIP)生成描述
            # 2. 进行物体检测
            # 3. 简单显示在界面上
            # 示例:在侧边栏显示一个缩略图(需要转换为RGB)
            # st.sidebar.image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), caption=f"最新画面", use_column_width=True)

        # 必须返回一个视频帧
        return av.VideoFrame.from_ndarray(img, format="bgr24")

class AudioProcessor:
    def __init__(self):
        self.audio_buffer = []
        self.sample_rate = 16000
        self.channels = 1

    def recv(self, frame):
        """处理接收到的音频帧"""
        # 将音频帧转换为numpy数组
        audio_data = frame.to_ndarray()
        # 如果是多声道,取平均值转为单声道
        if len(audio_data.shape) > 1:
            audio_data = audio_data.mean(axis=1)
        # 存入队列,供独立的语音识别线程消费
        st.session_state.audio_queue.put(audio_data.tobytes())
        # 必须返回一个音频帧(通常原样返回)
        return frame

# --- 独立的语音识别线程函数 ---
def speech_recognition_thread():
    """从音频队列中取出数据,进行持续语音识别"""
    p = pyaudio.PyAudio()
    # 注意:这里的recognizer是之前初始化的Vosk识别器
    global recognizer
    if not recognizer:
        return

    while True:
        try:
            # 从队列获取音频数据块
            audio_chunk = st.session_state.audio_queue.get(timeout=1.0)
            if recognizer.AcceptWaveform(audio_chunk):
                # 识别出一句完整的话
                result = json.loads(recognizer.Result())
                text = result.get("text", "")
                if text:
                    st.session_state.recognized_text = text
                    # 这里可以触发一个事件,例如自动将识别文本填入输入框
                    # st.session_state.user_input = text
            else:
                # 部分识别结果
                partial = json.loads(recognizer.PartialResult())
                partial_text = partial.get("partial", "")
                # 可以实时更新一个“正在听...”的显示区域
                # st.sidebar.caption(f"正在识别: {partial_text}")
        except queue.Empty:
            continue
        except Exception as e:
            print(f"语音识别线程错误: {e}")
            break

# 启动语音识别线程(确保只启动一次)
if 'recognition_thread_started' not in st.session_state:
    st.session_state.recognition_thread_started = True
    threading.Thread(target=speech_recognition_thread, daemon=True).start()

3.3 构建增强版Streamlit界面

现在,我们来重新布局页面,将音视频控制、原有聊天界面和新的状态显示结合起来。

# --- 页面布局 ---
st.set_page_config(page_title="Nanbeige 4.1-3B 多模态助手", layout="wide")

st.title("🎤👁️ Nanbeige 4.1-3B 多模态交互助手")

# 侧边栏 - 控制面板
with st.sidebar:
    st.header("⚙️ 控制面板")

    # 原有的模型参数设置可以保留或精简
    # ...

    st.markdown("---")
    st.header("🎥 音视频输入")

    # WebRTC 音视频流组件
    webrtc_ctx = webrtc_streamer(
        key="nanbeige-webrtc",
        mode=WebRtcMode.SENDRECV,  # 既发送也接收,我们主要用发送(获取用户媒体)
        client_settings=ClientSettings(
            rtc_configuration={"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]},
            media_stream_constraints={
                "video": True,
                "audio": True,
            },
        ),
        video_processor_factory=VideoProcessor,
        audio_processor_factory=AudioProcessor,
        async_processing=True,
    )
    st.session_state.webrtc_ctx = webrtc_ctx

    if webrtc_ctx.state.playing:
        st.success("摄像头和麦克风已激活")
        # 显示视频截图按钮
        if st.button("📸 截图并分析当前画面"):
            if st.session_state.last_video_frame is not None:
                # 保存或处理截图
                img_rgb = cv2.cvtColor(st.session_state.last_video_frame, cv2.COLOR_BGR2RGB)
                # 这里可以调用一个图像描述函数,将img_rgb传给模型
                # description = describe_image(img_rgb)
                # st.write(f"画面描述: {description}")
                # 暂时先显示图片
                st.image(img_rgb, caption="截取的画面", use_column_width=True)
                st.info("图像分析功能需接入视觉模型(如BLIP)后生效。")
            else:
                st.warning("尚未接收到视频帧,请稍候。")
    else:
        st.info("点击上方「START」按钮开启摄像头和麦克风")

    st.markdown("---")
    st.header("🗣️ 语音识别状态")
    if st.session_state.recognized_text:
        st.write(f"**最新识别结果:** {st.session_state.recognized_text}")
        if st.button("💬 将识别文本发送给模型"):
            # 将识别到的文本填入主输入框并触发发送(这里需要一些状态联动)
            # 一种简单实现:将文本存入一个特殊状态,在主界面检查并处理
            st.session_state.pending_audio_text = st.session_state.recognized_text
            st.session_state.recognized_text = ""  # 清空当前识别结果
            st.rerun()  # 触发重载以处理新文本
    else:
        st.caption("语音识别待命中...")

# 主区域 - 聊天界面 (基本保持原样,但增加对语音输入的处理)
st.header("💬 与 Nanbeige 对话")

# 显示历史消息
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# 检查是否有来自语音识别的待处理文本
user_input = ""
if 'pending_audio_text' in st.session_state and st.session_state.pending_audio_text:
    user_input = st.session_state.pending_audio_text
    del st.session_state.pending_audio_text

# 文本输入框
if prompt := st.chat_input("输入您的问题,或使用语音...", value=user_input):
    # 将用户输入添加到历史并显示
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    # 调用原有的模型生成函数(假设这个函数叫 `generate_response`)
    # 它应该接收prompt,并返回一个生成器用于流式输出
    with st.chat_message("assistant"):
        message_placeholder = st.empty()
        full_response = ""

        # 这里调用你的模型响应生成函数
        # 例如:response_generator = generate_response(prompt, st.session_state.messages)
        # 为了示例,我们模拟一个流式响应
        def mock_generator(text):
            for chunk in text.split():
                yield chunk + " "
                time.sleep(0.05)

        # 模拟调用
        for chunk in mock_generator(f"这是对「{prompt}」的模拟回复。实际应接入Nanbeige模型。"):
            full_response += chunk
            message_placeholder.markdown(full_response + "▌")
        message_placeholder.markdown(full_response)

        # 将助手回复加入历史
        st.session_state.messages.append({"role": "assistant", "content": full_response})

# 清空历史按钮(保留原有功能)
if st.sidebar.button("🗑️ 清空对话历史"):
    st.session_state.messages = []
    st.rerun()

4. 功能演示与效果展示

代码整合完毕后,让我们来看看这个升级版工具能做什么。

4.1 启动与界面概览

在终端运行 streamlit run app.py。浏览器打开后,你会看到一个分为两栏的界面:

  • 左侧侧边栏:这是我们的“控制中心”。上半部分保留了原有的模型设置(如果有),下半部分是全新的“音视频输入”和“语音识别状态”面板。
  • 右侧主区域:和以前一样的聊天窗口,显示对话历史。

4.2 核心功能操作流程

1. 激活音视频输入: 点击侧边栏上方的 “START” 按钮。浏览器会请求摄像头和麦克风权限,请点击“允许”。成功后,你会看到“摄像头和麦克风已激活”的提示。此时,你的视频画面应该已经能传输到后端(虽然界面上没有直接显示预览,但数据已在处理)。

2. 进行语音对话:

  • 保持麦克风开启,直接对着麦克风说话,比如:“你好,介绍一下你自己。”
  • 观察侧边栏的“语音识别状态”区域。稍等片刻,你说话的文本内容就会显示在“最新识别结果”后面。
  • 点击下方的 “将识别文本发送给模型” 按钮。神奇的事情发生了:你刚刚说的话,自动填充到了主聊天窗口的输入框,并像你手动输入一样发送给了 Nanbeige 模型。
  • 模型开始流式思考(显示“思考中...”),并最终给出文字回复。整个过程,你只用动嘴,不用动手打字。

3. 使用视觉输入:

  • 确保摄像头已开启。将你想让模型“看”的东西放在摄像头前,比如一本书的封面。
  • 点击侧边栏的 “截图并分析当前画面” 按钮。当前摄像头画面会被捕获并显示在侧边栏下方。
  • (进阶) 目前我们只是展示了截图功能。要真正让模型“理解”图片,你需要在此处集成一个视觉语言模型(如 BLIP、Qwen-VL)。集成后,点击按钮就能自动生成对画面的描述,并将描述文本作为问题发送给 Nanbeige。例如,自动生成问题:“图片里是一本蓝色的书,书名是《人工智能导论》,请根据封面设计风格谈谈它的受众。”

4.3 多模态交互的想象空间

通过这个基础框架,你已经搭建了一个多模态交互的管道。你可以在此基础上继续扩展:

  • 实时视频分析:不点击截图,而是让模型持续分析视频流,描述场景变化。
  • 音频直接驱动:去掉“点击发送”的步骤,实现语音指令的自动触发(例如,检测到“小阁小阁”唤醒词后,自动将后续语音转为文字并查询)。
  • 音视频输出:不仅输入是多模态,输出也可以。将模型的文字回复,通过 TTS 合成语音播放出来,实现真正的语音对话。
  • 结合具体场景:比如,做一个“实时翻译助手”,你说中文,它显示英文文本;或者做一个“产品讲解员”,你展示商品,它自动介绍特性。

5. 总结与展望

回顾一下我们完成的工作:我们成功地将 WebRTC 音视频流接入到了原本纯文本的 Nanbeige 对话工具中,构建了一个支持语音和视觉输入的多模态交互原型。

5.1 本教程核心要点

  1. 轻量级集成:我们没有替换 Streamlit,而是在其生态内,利用 streamlit-webrtc 组件无缝添加了音视频能力。原有优秀的文本对话体验得以完整保留。
  2. 模块化设计:将视频处理 (VideoProcessor)、音频处理 (AudioProcessor) 和语音识别(独立线程)分离,代码结构清晰,便于后续维护和扩展。
  3. 离线优先:语音识别选用了离线的 Vosk 模型,保证了所有交互的隐私性和低延迟,符合原工具“纯本地运行”的哲学。
  4. 渐进式增强:我们首先实现了“语音转文字”和“视频截图”这两个最实用、最易实现的功能,为后续集成更复杂的视觉模型打下了坚实基础。

5.2 可能遇到的问题与解决思路

  • WebRTC 连接失败:这通常是由于网络环境限制(如某些企业防火墙)或 STUN 服务器不可用。可以尝试更换 iceServers 中的 STUN 服务器地址,或者在内网环境下使用。
  • 语音识别不准:Vosk 小模型对发音、环境噪音有一定要求。可以尝试:
    • 使用更大的 Vosk 模型(如 vosk-model-cn-0.22,约1.4G)。
    • 在调用识别前,增加简单的音频降噪预处理(pydub 库)。
    • 如果对实时性要求不高,可以换用识别率更高的云端 API(如百度、阿里云的短语音识别),但会引入网络延迟。
  • 性能开销:同时运行大语言模型、语音识别和视频流处理,对 CPU 有一定压力。确保你的设备有足够的内存。可以考虑将视频帧处理频率降低(调整 VideoProcessor 中的帧采样逻辑)。

5.3 下一步的探索方向

你现在拥有的是一个功能强大的“脚手架”。接下来,可以沿着这些方向深化:

  1. 接入视觉模型:这是最直接的升级。将 BLIP2、Qwen-VL 等开源视觉语言模型集成进来,让工具真正能“看懂”图片和视频,实现图文对话。
  2. 优化用户体验:
    • 在界面上实时显示摄像头预览。
    • 增加语音输入的 VAD(语音活动检测),自动开始/结束录音。
    • 为模型的文字回复加上语音合成(TTS),实现全双工语音对话。
  3. 探索具体应用:基于这个多模态框架,可以开发出许多有趣的应用,如智能直播助手、远程协作指导、互动教育工具等。

让 AI 模型从“文本大脑”进化成“多感官智能体”,WebRTC 是你手中那把关键的钥匙。希望这篇教程能帮你打开这扇门,开始构建更自然、更强大的人机交互应用。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐