Kotaemon语音输入:WebRTC集成部署教程

1. 为什么需要语音输入?

想象一下这个场景:你正在研究一份几十页的技术文档,想快速找到某个API的用法。传统的方式是,你得在搜索框里一个字一个字地敲关键词,或者手动滚动浏览。但如果能直接对着麦克风问一句:“这个项目的数据库连接配置在哪里?”然后系统立刻就能定位并回答你,是不是方便多了?

这就是为Kotaemon这样的RAG(检索增强生成)系统集成语音输入的价值所在。它把“搜索”这个动作,从“打字”变成了“说话”,大大降低了交互门槛,提升了信息获取的效率。对于文档问答(DocQA)场景来说,这不仅仅是增加了一个功能,更是优化了整个用户体验。

今天,我们就来手把手教你,如何为Kotaemon这个开源的RAG UI页面,集成WebRTC技术,实现稳定、低延迟的语音输入功能。整个过程清晰明了,即使你对WebRTC不熟悉,也能跟着一步步完成。

2. 准备工作与环境概览

在开始敲代码之前,我们先来理清思路,看看需要准备些什么。

2.1 你需要了解的核心技术:WebRTC

WebRTC(Web实时通信)是一套允许网页浏览器进行实时音视频通信的API。我们用它来实现语音输入,主要是看中它的两个优点:

  1. 低延迟:音频数据直接在浏览器和服务器之间传输,无需经过中间服务器转发,延迟极低。
  2. 高质量:支持多种音频编解码器,能提供清晰的语音质量。

简单来说,我们会在Kotaemon的前端页面里,用WebRTC打开用户的麦克风,采集语音流,然后通过一个信令服务器建立连接,最后将音频流发送到后端进行语音识别(ASR)。

2.2 项目架构与组件

整个集成涉及三个部分,关系如下图所示(概念图):

用户浏览器 (Kotaemon前端) <--WebRTC流--> 信令/媒体服务器 <--音频数据--> 语音识别服务(ASR)
        |                                          |
        |                                          |
        |___________识别文本___________> Kotaemon后端 (RAG处理)
  1. Kotaemon前端:我们需要修改其UI,增加一个“语音输入”按钮,并集成WebRTC客户端代码。
  2. 信令服务器:这是一个轻量级的服务,用于帮助前端和后端建立WebRTC连接。我们将使用Node.js快速搭建一个。
  3. 语音识别服务:接收音频流并转换为文字。我们可以选择集成开源的模型(如Whisper)或使用成熟的云服务API。

2.3 工具与依赖准备

确保你的开发环境已准备好:

  • Kotaemon项目:本地可运行的Kotaemon实例。你可以从它的GitHub仓库克隆。
  • Node.js (v14以上):用于运行信令服务器。
  • Python 3.8+:如果你的语音识别服务打算用Whisper等Python库。
  • 一个现代浏览器:Chrome、Firefox或Edge,它们对WebRTC支持良好。

好了,概念清楚了,工具也齐了,我们开始动手。

3. 第一步:搭建信令服务器

信令服务器就像电话接线员,它不传输实际的语音数据,只负责让前后端“交换联系方式”(即网络地址等信息),以便它们能直接建立连接。

我们创建一个简单的Node.js服务器。在你的工作目录下,新建一个文件夹 kotaemon-voice-signaling。

3.1 初始化项目并安装依赖

cd kotaemon-voice-signaling
npm init -y
npm install express socket.io

这里我们用了 express 作为web框架,socket.io 来实现前后端之间的实时双向通信,用于交换WebRTC信令。

3.2 编写信令服务器代码

创建一个文件 server.js,写入以下内容:

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "http://localhost:3000", // 改成你的Kotaemon前端地址
    methods: ["GET", "POST"]
  }
});

// 存储房间与用户的映射(简单示例,生产环境需更健壮)
const rooms = {};

io.on('connection', (socket) => {
  console.log('用户已连接:', socket.id);

  // 加入房间
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    rooms[socket.id] = roomId;
    console.log(`用户 ${socket.id} 加入了房间 ${roomId}`);

    // 通知房间内其他用户,有新用户加入(用于点对点通信,本例中为简化,主要服务于前端与ASR服务端)
    socket.to(roomId).emit('user-connected', socket.id);
  });

  // 转发WebRTC信令(offer, answer, candidate)
  socket.on('signal', ({ to, from, signal }) => {
    io.to(to).emit('signal', { from, signal });
  });

  // 处理断开连接
  socket.on('disconnect', () => {
    const roomId = rooms[socket.id];
    if (roomId) {
      socket.to(roomId).emit('user-disconnected', socket.id);
      delete rooms[socket.id];
    }
    console.log('用户断开连接:', socket.id);
  });
});

const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
  console.log(`信令服务器运行在 http://localhost:${PORT}`);
});

这个服务器做了三件事:

  1. 允许客户端(Kotaemon前端)通过 join-room 事件加入一个“房间”。
  2. 在客户端之间转发WebRTC协商信号(signal 事件)。
  3. 管理用户连接和断开。

运行它:node server.js。保持这个终端窗口打开。

4. 第二步:修改Kotaemon前端集成WebRTC

现在,我们要在Kotaemon的聊天界面添加一个麦克风按钮,并编写语音采集的逻辑。

4.1 添加语音输入按钮

找到Kotaemon前端项目中渲染聊天输入框的组件文件(例如,可能叫 ChatInput.vue, ChatInput.jsx 或类似的)。在输入框旁边添加一个按钮。

这里以假设的React组件为例:

// 在ChatInput组件中
import React, { useState, useRef } from 'react';
import { Mic, MicOff } from 'lucide-react'; // 假设使用lucide图标库

const ChatInput = ({ onSendMessage }) => {
  const [inputText, setInputText] = useState('');
  const [isRecording, setIsRecording] = useState(false);
  // 我们将在这里定义 mediaRecorder 和 socket 的 ref
  const mediaRecorderRef = useRef(null);
  const socketRef = useRef(null);
  const audioChunksRef = useRef([]);

  // 初始化语音录制和WebRTC的逻辑将放在这里
  const handleVoiceInputToggle = async () => {
    if (!isRecording) {
      await startRecording();
    } else {
      stopRecording();
    }
    setIsRecording(!isRecording);
  };

  const startRecording = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mediaRecorder = new MediaRecorder(stream);
      mediaRecorderRef.current = mediaRecorder;
      audioChunksRef.current = [];

      mediaRecorder.ondataavailable = (event) => {
        if (event.data.size > 0) {
          audioChunksRef.current.push(event.data);
          // 这里可以实时发送音频块到后端,或者等停止后一次性发送
          sendAudioChunk(event.data);
        }
      };

      mediaRecorder.start(250); // 每250ms收集一次数据
      console.log('开始录音...');
    } catch (err) {
      console.error('无法访问麦克风:', err);
      alert('无法访问麦克风,请检查权限。');
    }
  };

  const stopRecording = () => {
    if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
      mediaRecorderRef.current.stop();
      mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
      console.log('停止录音。');
      // 可选:将所有chunks合并并发送
      // finalizeAndSendAudio();
    }
  };

  const sendAudioChunk = (chunk) => {
    // 这里需要实现将音频数据发送到你的语音识别后端
    // 例如,通过WebSocket或HTTP POST
    console.log('发送音频块,大小:', chunk.size);
    // 伪代码:websocket.send(chunk);
  };

  return (
    <div className="chat-input-container">
      <input
        type="text"
        value={inputText}
        onChange={(e) => setInputText(e.target.value)}
        placeholder="输入问题,或点击麦克风说话..."
        onKeyPress={(e) => e.key === 'Enter' && onSendMessage(inputText)}
      />
      <button onClick={() => onSendMessage(inputText)}>发送</button>
      {/* 语音输入按钮 */}
      <button
        onClick={handleVoiceInputToggle}
        className={`voice-btn ${isRecording ? 'recording' : ''}`}
        title={isRecording ? '停止录音' : '开始语音输入'}
      >
        {isRecording ? <MicOff size={20} /> : <Mic size={20} />}
      </button>
    </div>
  );
};

export default ChatInput;

上面的代码使用了浏览器更通用的 MediaRecorder API来采集音频,它比直接使用WebRTC的 RTCPeerConnection 发送流更简单,适合“录制-发送”的模式。对于需要真正实时流式识别的场景,才需要完整的WebRTC RTCPeerConnection 将媒体流直接推送到服务端。

4.2 连接信令服务器并建立WebRTC连接(进阶)

如果你需要极低延迟的流式识别,就需要建立真正的WebRTC对等连接,将音频流直接推送到一个支持WebRTC的语音识别服务。这更复杂,需要后端也支持WebRTC。

这里给出一个简化的概念性代码,展示如何初始化一个 RTCPeerConnection 并连接到我们的信令服务器:

// 在ChatInput组件内添加
import { io } from 'socket.io-client';

// 初始化阶段
useEffect(() => {
  const socket = io('http://localhost:3001'); // 信令服务器地址
  socketRef.current = socket;

  socket.on('connect', () => {
    console.log('已连接到信令服务器');
    socket.emit('join-room', 'voice-room-1'); // 加入一个房间
  });

  // 监听来自“对方”(ASR服务端)的信令
  socket.on('signal', async ({ from, signal }) => {
    if (signal.type === 'offer') {
      // 创建Answer并设置远程描述
      // ... WebRTC复杂协商逻辑
    }
    // ... 处理candidate等
  });

  return () => {
    socket.disconnect();
  };
}, []);

// 在startRecording中,创建PeerConnection并添加音频流
const startRecording = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const peerConnection = new RTCPeerConnection(configuration); // 需要STUN/TURN服务器配置

  // 添加音频轨道
  stream.getAudioTracks().forEach(track => peerConnection.addTrack(track, stream));

  // 创建Offer并发送给信令服务器
  const offer = await peerConnection.createOffer();
  await peerConnection.setLocalDescription(offer);
  socketRef.current.emit('signal', {
    to: 'asr-server-id', // 需要知道ASR服务端的Socket ID
    from: socketRef.current.id,
    signal: offer
  });

  // ... 处理onicecandidate等
};

由于完整的WebRTC点对点集成代码量较大,且需要对应的后端支持,本篇教程以集成 MediaRecorder 实现“按块发送”为主要实践路径。这种方式足以满足大部分“语音提问”场景的延迟要求。

5. 第三步:集成语音识别服务(后端)

前端采集到音频数据(无论是Blob块还是WebRTC流),都需要发送到后端进行识别。我们以使用开源Whisper模型为例,搭建一个简单的识别端点。

5.1 创建Flask语音识别服务

在你的Python环境中,安装必要库:

pip install flask flask-cors torch transformers soundfile librosa
# 或者使用 faster-whisper (推荐,效率更高)
# pip install faster-whisper

创建一个文件 asr_server.py:

from flask import Flask, request, jsonify
from flask_cors import CORS
import whisper
import tempfile
import os

app = Flask(__name__)
CORS(app)  # 允许跨域请求

# 加载模型(首次运行会下载,需要一定时间)
print("正在加载Whisper模型...")
model = whisper.load_model("base") # 可选:tiny, base, small, medium, large
print("模型加载完毕。")

@app.route('/transcribe', methods=['POST'])
def transcribe_audio():
    """接收音频文件并转写成文字"""
    if 'audio' not in request.files:
        return jsonify({'error': '未提供音频文件'}), 400

    audio_file = request.files['audio']
    
    # 保存上传的临时文件
    with tempfile.NamedTemporaryFile(delete=False, suffix='.webm') as tmp_file:
        audio_file.save(tmp_file.name)
        temp_path = tmp_file.name

    try:
        # 使用Whisper进行转录
        result = model.transcribe(temp_path, fp16=False) # fp16=False 兼容更多环境
        transcription = result['text'].strip()
        print(f"识别结果: {transcription}")
    except Exception as e:
        return jsonify({'error': f'识别失败: {str(e)}'}), 500
    finally:
        # 清理临时文件
        os.unlink(temp_path)

    return jsonify({'text': transcription})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

这个服务提供了一个 /transcribe 接口,接收一个音频文件(如前端 MediaRecorder 生成的webm格式),然后调用Whisper模型进行识别,并返回文本。

5.2 修改前端发送逻辑

回到Kotaemon前端,修改 sendAudioChunk 或 finalizeAndSendAudio 函数,将音频数据发送到这个识别服务。

// 在ChatInput组件中
const finalizeAndSendAudio = async () => {
  if (audioChunksRef.current.length === 0) return;

  const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm;codecs=opus' });
  const formData = new FormData();
  formData.append('audio', audioBlob, 'recording.webm');

  try {
    const response = await fetch('http://localhost:5000/transcribe', {
      method: 'POST',
      body: formData,
    });
    const data = await response.json();
    if (data.text) {
      // 将识别到的文本填入输入框,或直接发送
      setInputText(data.text);
      // 可选:自动触发发送 onSendMessage(data.text);
      console.log('识别结果:', data.text);
    } else {
      console.error('识别失败:', data.error);
    }
  } catch (error) {
    console.error('发送音频失败:', error);
  } finally {
    audioChunksRef.current = [];
  }
};

将 stopRecording 函数末尾调用 finalizeAndSendAudio()。

6. 第四步:串联测试与效果查看

现在,让我们把所有的部分串联起来,进行测试。

  1. 启动服务:

    • 终端1:运行信令服务器 node server.js (端口3001)。
    • 终端2:运行语音识别服务 python asr_server.py (端口5000)。
    • 终端3:启动你的Kotaemon前端项目(例如,通常在端口3000)。
  2. 操作Kotaemon:

    • 打开浏览器,访问Kotaemon(如 http://localhost:3000)。
    • 使用默认账号密码 admin/admin 登录。
    • 在聊天界面,你应该能看到新添加的麦克风按钮。
  3. 测试语音输入:

    • 点击麦克风按钮,授权浏览器使用麦克风。
    • 对着麦克风清晰地说一个问题,比如:“什么是RAG?”
    • 点击按钮停止录音。
    • 稍等片刻,识别出的文本“什么是RAG?”应该会自动出现在输入框中。
    • 点击发送,Kotaemon就会像处理普通文本输入一样,从你的文档中检索并生成答案。

效果预期:你成功地将一个离线、可自部署的语音输入功能集成到了Kotaemon中。用户现在可以通过说话来提问,系统将语音转为文字,再利用Kotaemon强大的RAG能力从文档中找出答案。

7. 总结与后续优化建议

通过以上步骤,我们完成了一个基础的Kotaemon语音输入集成。它包含了前端录音、后端识别和简单的信令通信。虽然这是一个入门级的实现,但已经具备了核心功能。

为了让这个功能更强大、更可靠,你可以考虑以下优化方向:

  • 流式识别:将目前的“录制-停止-发送-识别”模式改为真正的流式识别,用户一边说话,文字就一边实时显示出来,体验更佳。这需要将后端识别服务改造为支持WebSocket或gRPC流式接口,并与前端的 MediaRecorder 或 RTCPeerConnection 更紧密地结合。
  • UI/UX优化:添加录音动画、音量可视化、中间结果预览(如流式识别时的部分文本)、以及更友好的错误提示(如麦克风被禁用、网络错误等)。
  • 后端服务化:将语音识别服务容器化(Docker),并考虑使用性能更好的引擎,如 faster-whisper 或专门的流式ASR服务(如Vosk)。
  • 错误处理与降级:增强网络中断、识别失败等情况下的处理逻辑,例如提供“重试”按钮,或自动降级为提示用户手动输入。
  • 多模态扩展:Kotaemon本身支持图文对话。未来甚至可以探索“语音+图像”的多模态输入,例如用户描述一张图的内容并提问。

语音交互是提升应用易用性的重要手段。希望这篇教程能帮助你顺利地为Kotaemon装上“耳朵”,让你的文档问答系统变得更加智能和便捷。


获取更多AI镜像

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

Logo

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

更多推荐