前言

随着 Qwen3 系列模型和 Realtime API 协议的开源,本地搭建一个低延迟、全离线的实时语音 AI 伴侣已经不再是难事。本文介绍一个名为 cybergirl-bundle(赛博女友整合包) 的开源项目,它把 LLM 推理、语音识别、语音合成、网页前端全部打包成一个双击即用的整合包,目标用户是希望在自己电脑上跑一个"会说话的 AI 伴侣"的开发者。

项目核心特点:

  • 全离线:部署完成后断网可用,所有模型在本地推理
  • 双击启动:一个 .bat 拉起三个服务并自动打开浏览器
  • OpenAI Realtime API 协议:浏览器通过 WebSocket 直连语音服务
  • GPU 加速:llama.cpp + CUDA 12.4 + PyTorch cu124
  • 可分发:整合包内置 uv、Python、ffmpeg,接收方无需预装任何开发环境

在这里插入图片描述

一、系统架构

整个系统由三个独立服务组成,通过 HTTP 和 WebSocket 串联:

┌──────────────┐   HTTP /v1    ┌──────────────────┐  WebSocket  ┌──────────────┐
│  llama-server│ ◄──────────►  │ speech-to-speech │ ◄─────────► │   Browser    │
│  (LLM 大脑)  │  OpenAI API   │   (语音中枢)     │  Realtime   │ (呼吸球 UI)  │
│  端口 8080   │               │   端口 8765      │   API       │  端口 7860   │
└──────────────┘               └──────────────────┘             └──────────────┘
   Qwen3-4B GGUF                Whisper + Qwen3-TTS              Web Audio API

在这里插入图片描述

1.1 服务①:LLM 推理(llama.cpp)

使用 llama.cpp 的 llama-server.exe 加载 Qwen3-4B-Instruct-2507 的 Q4_K_M 量化版本,对外暴露 OpenAI 兼容的 /v1 接口。关键启动参数:

llama-server.exe -m %MODEL_PATH% -c 8192 -fa on --port 8080 -ngl 99
参数 含义
-c 8192 上下文窗口,显存吃紧可降到 4096
-fa on Flash Attention 加速
-ngl 99 全部层 offload 到 GPU

显存充裕(12GB+)时可切换到 Qwen3-30B-A3B-Instruct,MoE 架构在消费级显卡上也有可接受的速度。

1.2 服务②:语音中枢(speech-to-speech)

这是整个系统的核心,基于 speech-to-speech 项目,在一个进程内串联了三件事:

  • STTopenai/whisper-large-v3,中文识别
  • LLM 转发:通过 responses-api 后端把请求转发给本地 llama-server
  • TTSQwen3-TTS-1.7B,支持多音色和参考音频克隆

对外暴露一个 WebSocket 端点 ws://localhost:8765/v1/realtime,实现了 OpenAI Realtime API 协议,前端可以像调用 OpenAI 官方 Realtime 接口一样使用它。关键启动参数:

speech-to-speech.exe ^
  --mode realtime ^
  --stt whisper --stt_model_name openai/whisper-large-v3 ^
  --language zh ^
  --llm_backend responses-api ^
  --model_name qwen3-4b ^
  --responses_api_base_url http://127.0.0.1:8080/v1 ^
  --responses_api_stream ^
  --tts qwen3 --qwen3_tts_backend torch ^
  --qwen3_tts_speaker vivian ^
  --qwen3_tts_language zh ^
  --enable_live_transcription

⚠️ Windows 上必须加 --qwen3_tts_backend torch,因为默认的 ggml 后端依赖 C++ 包,在 Windows 上经常缺失。

1.3 服务③:Web 网页(FastAPI + 静态前端)

[server.py](file:///h:/赛博女友/cybergirl-bundle/web/server.py) 只是一个极简的 FastAPI 静态文件载体,真正的逻辑全在 [index.html](file:///h:/赛博女友/cybergirl-bundle/web/static/index.html) 里。浏览器加载页面后,直接通过 WebSocket 连到 s2s 服务,中间不经过任何代理:

app = FastAPI(title="赛博女友", docs_url=None, redoc_url=None)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")

@app.get("/", response_class=HTMLResponse)
async def index():
    return FileResponse(STATIC_DIR / "index.html", media_type="text/html; charset=utf-8")

这种"前端直连 Realtime API"的设计让音频流无需经过 Web 服务转发,延迟更低。


二、核心技术实现

2.1 OpenAI Realtime API 协议落地

前端连接 WebSocket 后,第一件事是发送 session.update 配置会话:

sendJson({
  type: "session.update",
  session: {
    type: "realtime",                    // 必填,否则被拒绝
    instructions: settings.instructions, // 系统提示词
    audio: {
      input: {
        transcription: { model: "whisper-large-v3" },
        turn_detection: {
          type: "server_vad",
          threshold: 0.2,                // VAD 灵敏度
          prefix_padding_ms: 1000,       // 语音前填充
          silence_duration_ms: 1000,     // 静音判定时长
        },
      },
      output: { voice: settings.voice }, // 实时切换音色
    },
  },
});

几个踩坑点值得注意:

  1. type: "realtime" 字段必填:缺少这个字段请求会被直接拒绝
  2. voice 字段支持运行时切换:通过再次发送 session.update 即可热切换音色,无需重连
  3. VAD 参数调优threshold=0.2 + silence_duration_ms=1000 是为了能捕获"拜拜"这种短语音,默认参数会截断到 480ms 导致 whisper 报 “too few tokens”

2.2 Web Audio API 流式播放

TTS 返回的是 PCM 音频块(audio.delta 事件,base64 编码),前端需要用 Web Audio API 实时播放。这里有几个容易踩的坑:

坑1:AudioBufferSourceNode 被 GC 回收

如果创建 AudioBufferSourceNode 后立即 start() 而不保留引用,浏览器可能在播放前就把它当垃圾回收了,表现为长回复播到一半就停。解决方案是维护一个 activeSources 数组,播放结束才释放:

const activeSources = [];
src.onended = () => {
  const i = activeSources.indexOf(src);
  if (i >= 0) activeSources.splice(i, 1);
};
activeSources.push(src);
src.start(nextPlayTime);

坑2:AudioContext 被浏览器挂起

当标签页切到后台,浏览器会自动 suspend AudioContext,导致 currentTime 停止增长,预调度循环空转。需要在 pump 循环里检测并恢复:

if (playCtx.state !== 'running') {
  dbg("WARN", "⚠ playCtx state=" + playCtx.state);
  playCtx.resume();
  return; // 下一轮再调度
}

坑3:预调度过早导致缓冲被丢弃

浏览器会丢弃调度时间过远的 buffer。需要限制 nextPlayTime - currentTime ≤ PRE_SCHEDULE_S(默认 1 秒)才创建 source,用 pendingChunks 队列 + setTimeout 延迟调度。

2.3 半双工防回环

全双工模式下,AI 说话的声音会被麦克风回采,触发 VAD 误判形成"自言自语"回环。项目的解决方案是半双工静音:

case "response.created":
  // AI 开始生成,立即静音麦克风上行
  micMutedByHalfDup = true;
  break;

case "response.done":
  // 不能立即解除,否则 TTS 尾音还会触发 VAD
  setTimeout(() => { micMutedByHalfDup = false; }, 1500);
  break;

2.4 短语音识别优化

中文里"拜拜"“嗯”"好"这种极短词很容易被 VAD 截断成 480ms 的片段,whisper 会报 token 不足。优化手段:

  1. VAD 参数放宽:silence_duration_ms=1000prefix_padding_ms=1000
  2. 短音频补静音到 2 秒(前后各补一半)
  3. whisper 强制 max_new_tokens=30min_new_tokens=4

三、整合包工程化

这个项目最值得借鉴的是它的整合包工程化思路——让一个依赖 CUDA、PyTorch、多个大模型的应用可以像绿色软件一样分发。

3.1 uv 包管理器 + 内嵌 Python

整个项目用 uv 管理 Python 环境,并把 Python 解释器缓存到项目目录内:

set UV_PYTHON_INSTALL_DIR=%~dp0python-cache

接收方电脑不需要预装 Python,首次运行 首次部署.bat 时 uv 会自动从缓存安装。

3.2 CUDA 版 PyTorch 强制锁定

PyPI 默认的 torch 是 CPU 版,直接装会导致推理慢死。通过 pyproject.toml 强制指定 CUDA 12.4 源:

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch-cu124" }
torchaudio = { index = "pytorch-cu124" }

3.3 离线模式

启动脚本里设置两个环境变量,强制使用本地缓存模型,避免运行时尝试联网:

set HF_HUB_OFFLINE=1
set TRANSFORMERS_OFFLINE=1

3.4 端口就绪检测

三个服务有启动顺序依赖,用 PowerShell 脚本轮询端口而不是死等:

# wait-port.ps1
while ($elapsed -lt $Timeout) {
    try {
        Invoke-WebRequest "http://127.0.0.1:$Port/health" -TimeoutSec 2 -ErrorAction Stop
        Write-Host "$Name ready"; exit 0
    } catch { Start-Sleep -Seconds 2 }
}

3.5 模型分发策略

国内从 HuggingFace 下载极慢,项目改用魔搭社区(ModelScope):

模型 用途 体积
Qwen3-4B-Instruct-2507-GGUF LLM 大脑 ~2.5GB
Qwen3-30B-A3B-Instruct-2507-GGUF LLM 大脑(进阶) ~18GB
whisper-large-v3 语音识别 ~3GB
Qwen3-TTS-1.7B 语音合成 ~3.4GB

whisper 和 TTS 模型预置在 s2s\.hf-cache\ 随包分发,LLM 模型因体积大由用户手动下载。


四、TTS 音色与声音克隆

Qwen3-TTS-1.7B 支持 CustomVoice 模式,内置多个音色:

音色 性别 备注
vivian 默认
serena / ono_anna
aiden / ryan

进阶用法是参考音频克隆,通过 --qwen3_tts_ref_audio--qwen3_tts_ref_text 指定一段参考音频,TTS 会模仿其音色合成。这个参数与 --qwen3_tts_speaker 互斥。

项目还支持运行时通过 session.updateaudio.output.voice 字段实时切换音色,无需重启服务。


五、资源占用实测

在 RTX 3090 24GB + 32GB 内存的真实环境下的实测数据:

组件 显存 内存
llama-server(Qwen3-4B,-c 32768) ~7.4GB
speech-to-speech(whisper + TTS + VAD) ~7.8GB
系统 + 浏览器 ~1.3GB
合计 ~7.4GB ~16.5GB

LLM 的内存占用大头是 KV cache,-c 32768 会吃掉约 4.8GB。降到 -c 8192 可省 3GB。

8GB 显存的显卡可以跑 Qwen3-4B + 8192 上下文;12GB 显存可以尝试 30B-A3B。


六、部署流程速览

接收方(最终用户)只需三步:

  1. 解压整合包到任意目录(不要放 OneDrive 同步目录)
  2. 双击 首次部署.bat(下载 PyTorch 约 2.5GB,耗时 5-15 分钟)
  3. 双击 启动赛博女友.bat,浏览器自动打开

硬件要求:

项目 最低 推荐
显卡 NVIDIA 8GB 显存 RTX 30/40 系 12GB+
内存 16GB 32GB
硬盘 25GB SSD 30GB+

仅支持 NVIDIA 显卡,AMD/Intel 显卡无法运行。


七、踩过的坑与解决方案

整理几个实际开发中遇到的关键问题:

现象 原因 解决
torch 报 CUDA 不可用 装成 CPU 版 pyproject.toml 锁定 cu124 源
网页点球没反应 WS 地址填了 127.0.0.1 必须填 localhost:8765
长回复播到一半停 AudioBufferSourceNode 被 GC 维护 activeSources 数组
切后台后音频停 AudioContext 被 suspend pump 循环检测并 resume()
"拜拜"识别失败 VAD 截断太短 放宽 VAD 参数 + 短音频补静音
AI 自言自语回环 扬声器回采触发 VAD 半双工静音 + 延迟解除
.bat 中文乱码 编码问题 必须用 GBK 编码保存
Windows 拒绝运行 bat 无数字签名 “更多信息"→"仍要运行”

八、总结

这个项目展示了一个完整的本地实时语音 AI 工程化方案,几个值得学习的设计:

  1. 协议选型:直接复用 OpenAI Realtime API,前端可以无缝替换后端
  2. 整合包思路:uv + 内嵌 Python + 预置依赖,让 CUDA 应用也能像绿色软件分发
  3. Web Audio API 实战:流式 PCM 播放的 GC、suspend、预调度问题及解法
  4. 半双工防回环:消费级麦克风场景下的实用方案

适合想要学习端到端语音 AI 系统、或者想在自己电脑上跑一个离线 AI 伴侣的开发者参考。整个系统模块清晰,STT/LLM/TTS 三个组件都可以单独替换,扩展性强。


分享通用链接 https://my.feishu.cn/wiki/J8yCwIDSGifI4jkIg9Yca7Xynuc?from=from_copylink
提取码: rbdb2u
需要的自取

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐