254 lines
16 KiB
Markdown
254 lines
16 KiB
Markdown
# Owner 语音桌宠
|
||
|
||
这是一个 Python 语音桌宠运行程序。当前第一版先提供无 GUI 的真实实时语音循环:
|
||
|
||
`小杰小杰` 本地唤醒 -> 本地应答“我在” -> 本地降噪 -> 主说话人端点采集问题 -> 本地 STT/实时字幕 -> 携带本次进程内临时历史调用云端 LLM -> 本地 TTS -> 本机扬声器播放 -> 回到待机继续监听。
|
||
|
||
## 当前能力
|
||
|
||
- `owner_voice_pet run-live`:真实常驻语音循环。
|
||
- `owner_voice_pet run-live --once`:只跑一轮,便于验收。
|
||
- `owner_voice_pet run-agent-live --check-config`:全双工 Agent 新入口的配置检查。当前用于迁移期验收,真实运行仍走 `run-live`。
|
||
- `.env` 直接读取配置,不要求导出 shell 环境变量。
|
||
- 唤醒词检测使用本地 `sherpa-onnx` KWS 模型,不走云端 ASR。
|
||
- `OWNER_SPEECH_PROVIDER=local`:默认除 LLM 外全用本地语音链路;`cloud` 仅作为显式兼容选项。
|
||
- 本地语音默认:wake/VAD/STT/实时字幕/TTS/降噪全部在本机执行,LLM 继续走 `.env` 中的云端配置。
|
||
- 本地设备:`sounddevice` 读取麦克风,扬声器或 `afplay` 播放。
|
||
- 本地模型:`models/` 存放 `sherpa-onnx` wake/VAD/STT/denoiser 模型,目录不提交 Git。
|
||
- Pipeline:`run-live` 使用 stage 化 `VoiceAssistantPipeline`,通过事件总线输出终端状态。
|
||
- 主说话人端点:默认 `OWNER_ENDPOINT_MODE=primary_speaker`,本轮音色消失后结束录音,避免背景噪声拖慢 STT。
|
||
- 自动持续对话:助手回复后自动判断是否需要继续听用户回答,默认规则优先、LLM 小分类器兜底,不确定就恢复待机。
|
||
- 播报打断:助手播报超过回声保护期后,如果检测到有效用户语音和实时字幕,会停止剩余播报并处理新输入。
|
||
- 临时上下文:同一次 `run-live` 进程内携带最近 user/assistant 历史,退出即清空。
|
||
|
||
## 首次准备
|
||
|
||
```bash
|
||
python3.11 -m venv .venv
|
||
.venv/bin/python -m pip install --upgrade pip
|
||
.venv/bin/python -m pip install -e '.[speech]'
|
||
cp .env.example .env
|
||
```
|
||
|
||
然后编辑 `.env`,填写本地密钥和模型配置。当前默认示例:
|
||
|
||
```dotenv
|
||
OWNER_LLM_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
|
||
OWNER_LLM_API_KEY=
|
||
OWNER_LLM_MODEL=mimo-v2.5
|
||
OWNER_LLM_API_STYLE=chat_completions
|
||
OWNER_LLM_STREAMING_ENABLED=1
|
||
OWNER_ASSISTANT_MODE=turn_based_voice_pet
|
||
OWNER_REALTIME_TRANSCRIPT_ENABLED=1
|
||
OWNER_REALTIME_TRANSCRIPT_IDLE_TIMEOUT_MS=1500
|
||
OWNER_WAKE_PROVIDER=local_kws
|
||
OWNER_WAKE_KEYWORDS_FILE=
|
||
OWNER_WAKE_KWS_THRESHOLD=0.15
|
||
OWNER_WAKE_KWS_SCORE=1.0
|
||
OWNER_WAKE_ACK_TEXT=我在
|
||
OWNER_POST_PLAYBACK_DRAIN_MS=0
|
||
OWNER_PIPELINE_MODE=live_turn_based
|
||
OWNER_ENDPOINT_MODE=primary_speaker
|
||
OWNER_NOISE_FILTER_ENABLED=1
|
||
OWNER_NOISE_FILTER_PROVIDER=sherpa_onnx_gtcrn
|
||
OWNER_WAKE_DENOISE_ENABLED=0
|
||
OWNER_SPEAKER_PROFILE_MS=600
|
||
OWNER_SPEAKER_PROFILE_MIN_MS=120
|
||
OWNER_SPEAKER_ABSENT_MS=300
|
||
OWNER_SPEAKER_SIMILARITY_THRESHOLD=0.70
|
||
OWNER_SPEAKER_MIN_RMS=0.012
|
||
OWNER_VAD_PROVIDER=hybrid
|
||
OWNER_VAD_THRESHOLD=0.5
|
||
OWNER_VAD_MIN_DURATION_MS=250
|
||
OWNER_VAD_END_SILENCE_MS=350
|
||
OWNER_VAD_NO_SPEECH_TIMEOUT_MS=5000
|
||
OWNER_VAD_MAX_RECORDING_MS=12000
|
||
OWNER_SPEECH_PROVIDER=local
|
||
OWNER_ASR_MODEL=mimo-v2.5-asr
|
||
OWNER_TTS_MODEL=mimo-v2.5-tts
|
||
OWNER_TTS_VOICE=mimo_default
|
||
OWNER_CONTEXT_MODE=session_memory
|
||
OWNER_CONTINUOUS_DIALOG_ENABLED=1
|
||
OWNER_CONTINUATION_DECISION_PROVIDER=hybrid
|
||
OWNER_CONTINUATION_CONFIDENCE_THRESHOLD=0.65
|
||
OWNER_FOLLOWUP_LISTEN_TIMEOUT_MS=3000
|
||
OWNER_BARGE_IN_ENABLED=1
|
||
OWNER_BARGE_IN_MIN_SPEECH_MS=250
|
||
OWNER_BARGE_IN_ECHO_GUARD_MS=500
|
||
OWNER_BARGE_IN_SPEAKER_GATE_ENABLED=1
|
||
OWNER_BARGE_IN_USER_SIMILARITY_THRESHOLD=0.62
|
||
OWNER_BARGE_IN_ASSISTANT_REJECT_THRESHOLD=0.72
|
||
OWNER_BARGE_IN_LISTEN_INTERVAL_MS=20
|
||
OWNER_BARGE_IN_CHUNK_MS=30
|
||
OWNER_END_CHIME_ENABLED=1
|
||
OWNER_END_CHIME_FILE=assets/sounds/codex-notification.wav
|
||
OWNER_END_CHIME_FREQUENCY_HZ=880
|
||
OWNER_END_CHIME_DURATION_MS=140
|
||
OWNER_AUDIO_APM_PROVIDER=webrtc
|
||
OWNER_AUDIO_AEC_ENABLED=1
|
||
OWNER_AUDIO_NS_ENABLED=1
|
||
OWNER_AUDIO_AGC_ENABLED=1
|
||
OWNER_AUDIO_APM_REQUIRED=1
|
||
OWNER_AUDIO_FRAME_MS=20
|
||
OWNER_AUDIO_RING_BUFFER_MS=3000
|
||
OWNER_INTERRUPT_ENABLED=1
|
||
OWNER_INTERRUPT_TARGET_LATENCY_MS=200
|
||
OWNER_STREAMING_STT_PROVIDER=faster_whisper
|
||
OWNER_STREAMING_STT_PRODUCT_CANDIDATE=sensevoice
|
||
OWNER_STREAMING_TTS_PROVIDER=cosyvoice
|
||
OWNER_MEMORY_ENABLED=0
|
||
OWNER_MEMORY_PROVIDER=faiss_sqlite
|
||
OWNER_MEMORY_TOP_K=5
|
||
OWNER_MEMORY_AUTO_SAVE_SENSITIVE=0
|
||
OWNER_TOOL_ROUTER_ENABLED=0
|
||
OWNER_TOOL_MAX_CALLS_PER_TURN=5
|
||
OWNER_TOOL_TIMEOUT_MS=30000
|
||
OWNER_OPENINTERPRETER_ENABLED=0
|
||
OWNER_OPENINTERPRETER_COMMAND=openinterpreter
|
||
OWNER_BROWSER_PLAYWRIGHT_ENABLED=0
|
||
OWNER_COMPUTER_CONTROL_ENABLED=0
|
||
```
|
||
|
||
`OWNER_WAKE_PROVIDER=local_kws` 表示唤醒词“小杰小杰”由本地模型检测。唤醒命中后会先本地播报 `OWNER_WAKE_ACK_TEXT=我在`,再开始听取问题。`OWNER_SPEECH_PROVIDER=local` 表示正式问题 STT、实时字幕和 TTS 都走本地模型或 macOS 本地能力;云端只接收 final 文本和本次会话历史用于 LLM 回复。
|
||
|
||
`OWNER_NOISE_FILTER_ENABLED=1` 表示唤醒后的正式问题阶段默认启用本地 GTCRN 降噪。降噪后的同一份音频会进入 VAD、实时字幕和 final STT;wake 阶段默认保持原始音频,`OWNER_WAKE_DENOISE_ENABLED=0` 可以避免降噪影响“小杰小杰”的 KWS 特征。
|
||
|
||
`OWNER_REALTIME_TRANSCRIPT_ENABLED=1` 表示录音期间会使用本地 streaming STT 实时显示中间转写,终端会输出 `实时转写:...`。为了过滤噪音,实时字幕默认不会显示 `家`、`家确` 这类很短的瞬时误识别;最终发送给 LLM 的内容仍只以 `转写结果:...` 为准。`OWNER_REALTIME_TRANSCRIPT_IDLE_TIMEOUT_MS=1500` 表示已有实时字幕后,如果 1.5 秒内没有新的文字输出,就直接结束本轮录音进入 final STT;设置为 `0` 可以关闭这个停滞端点。`OWNER_REALTIME_TRANSCRIPT_ENABLED=0` 可以临时关闭实时显示。
|
||
|
||
`OWNER_CONTINUOUS_DIALOG_ENABLED=1` 表示每轮回复播放后会自动判断是否继续对话。若助手回复里明显在问用户、要求补充信息或让用户选择,终端会输出 `继续对话:3秒内可直接回答`,这 3 秒内可以不用再说“小杰小杰”。若助手只是完成回答、报错、拒绝或判断不确定,就直接恢复待机。`OWNER_CONTINUATION_DECISION_PROVIDER=hybrid` 表示先用本地规则判断,规则不确定时才调用云端 LLM 做小分类;低于 `OWNER_CONTINUATION_CONFIDENCE_THRESHOLD=0.65` 的结果按待机处理。
|
||
|
||
`OWNER_BARGE_IN_ENABLED=1` 表示播报中允许打断。播放回复时会启动后台麦克风监听,不再等每个播放 chunk 结束后才检查输入;`OWNER_BARGE_IN_LISTEN_INTERVAL_MS=20` 控制监听间隔,`OWNER_BARGE_IN_CHUNK_MS=30` 控制播放停止粒度。播放开始后的 `OWNER_BARGE_IN_ECHO_GUARD_MS=500` 毫秒内忽略麦克风输入,之后如果检测到至少 `OWNER_BARGE_IN_MIN_SPEECH_MS=250` 毫秒有效用户语音,并且 realtime STT 给出有效 partial,就停止剩余播报。`OWNER_BARGE_IN_SPEAKER_GATE_ENABLED=1` 会同时建立本次会话用户临时音色画像和当前助手回放音色画像,默认要求用户相似度达到 `OWNER_BARGE_IN_USER_SIMILARITY_THRESHOLD=0.62`,并拒绝相似度高于 `OWNER_BARGE_IN_ASSISTANT_REJECT_THRESHOLD=0.72` 的助手回放音色,避免 AI 自己的声音触发打断或实时字幕。音色画像只在进程内使用,不写文件、不发送给 LLM。上下文只记录已经完整播出的 assistant 句子,未播出的内容不会写入临时历史。
|
||
|
||
`OWNER_END_CHIME_ENABLED=1` 表示对话自然结束或追问超时恢复待机前会播放一声项目内置提示音,默认文件是 `assets/sounds/codex-notification.wav`。提示音不走 TTS,也不会写入上下文;如果 `OWNER_END_CHIME_FILE` 指向的文件缺失,会回退到本地合成短音,`OWNER_END_CHIME_FREQUENCY_HZ` 和 `OWNER_END_CHIME_DURATION_MS` 只影响这个回退音。设置 `OWNER_END_CHIME_ENABLED=0` 可以关闭。
|
||
|
||
## 全双工 Agent 迁移入口
|
||
|
||
`add-full-duplex-agent-voice-assistant` 的实现会分阶段推进。为避免破坏当前已经可用的真人语音循环,新架构使用独立入口:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet run-agent-live --check-config
|
||
```
|
||
|
||
这个命令会把有效运行模式固定为 `full_duplex_agent` 并输出全双工配置摘要,但当前不会启动未接线的全双工运行时。真实语音交互仍继续使用 `run-live`。后续实现会在这个入口下逐步接入 WebRTC APM、持续监听、Streaming STT/TTS、长期记忆和 Tool Router。
|
||
|
||
全双工 Agent 相关配置默认只做规划和安全关闭:`OWNER_AUDIO_APM_PROVIDER=webrtc` 代表目标音频底座,`OWNER_LLM_STREAMING_ENABLED=1` 控制 LLM 以流式响应供后续句子级 TTS 消费,旧变量 `OWNER_LLM_STREAM` 仍兼容;`OWNER_STREAMING_STT_PROVIDER=faster_whisper` 和 `OWNER_STREAMING_TTS_PROVIDER=cosyvoice` 是后续 provider 目标;`OWNER_MEMORY_ENABLED=0`、`OWNER_TOOL_ROUTER_ENABLED=0`、`OWNER_OPENINTERPRETER_ENABLED=0`、`OWNER_BROWSER_PLAYWRIGHT_ENABLED=0`、`OWNER_COMPUTER_CONTROL_ENABLED=0` 默认关闭,避免尚未完成安全边界前执行长期记忆或工具任务。
|
||
|
||
当前已落地的全双工基础模块:
|
||
|
||
- `full_duplex_audio`:音频帧 fixture、capture/render ring buffer、fake WebRTC APM、APM 探针和 fallback 决策。
|
||
- `full_duplex_control`:全双工状态机、事件诊断字段、取消 token graph 和恢复协调器。
|
||
- `full_duplex_speech`:VAD provider contract、Silero VAD 边界、Streaming STT contract、fake STT 和 interruption detector。
|
||
- `full_duplex_response`:LLM streaming contract、句子切分、TTS 文本净化、fake Streaming TTS 和可中断播放队列。
|
||
- `agent_memory`:SQLite memory schema、FAISS index manifest 校验、fake/disabled memory manager、敏感写入策略和 memory recall 注入。
|
||
- `tool_router`:结构化工具调用、风险分类、预算防循环、审计脱敏、`memory.search`、`memory.save` 和 `shell.readonly`。
|
||
- `external_adapters`:Open Interpreter、Playwright 和 Computer Control 的默认关闭边界。
|
||
- `full_duplex_testing`:fake 全双工 fixture、性能指标和诊断脱敏测试工具。
|
||
|
||
可选依赖按能力分组,不会被默认安装强制拉入:
|
||
|
||
```bash
|
||
.venv/bin/python -m pip install -e '.[full-duplex]'
|
||
.venv/bin/python -m pip install -e '.[streaming-stt]'
|
||
.venv/bin/python -m pip install -e '.[memory]'
|
||
.venv/bin/python -m pip install -e '.[browser]'
|
||
```
|
||
|
||
工具执行安全策略见 `docs/full_duplex_agent_security.md`。
|
||
|
||
## 本地模型
|
||
|
||
首次运行前必须准备本地语音模型;同一脚本会下载 wake、VAD、2025 中文 CTC STT 和 GTCRN denoiser 模型:
|
||
|
||
```bash
|
||
python3.11 scripts/download_speech_models.py --dir models
|
||
.venv/bin/python -m owner_voice_pet model-check --models-dir models
|
||
```
|
||
|
||
`models/` 已在 `.gitignore` 中,不会提交大模型文件。
|
||
|
||
默认本地 STT 使用 sherpa-onnx 官方 2025 中文 CTC int8 模型,manifest 里的关键文件是 `tokens.txt` 和 `model.int8.onnx`。旧 14M transducer 模型仍可通过旧 manifest 兼容,但不再作为默认实时字幕质量基线。
|
||
|
||
本地唤醒关键词文件位于 `models/wake/keywords.txt`。默认 `OWNER_WAKE_KWS_THRESHOLD=0.15` 已偏向灵敏;如果真人唤醒仍不灵敏,可以继续降到 `0.10`,若误唤醒变多再回调到 `0.20`。
|
||
|
||
默认 `OWNER_ENDPOINT_MODE=primary_speaker` 会在本轮问题开头建立临时音色画像;`OWNER_SPEAKER_PROFILE_MIN_MS=120` 表示最少 120 毫秒有效语音即可让画像参与端点判断。当这个主说话人音色连续消失 `OWNER_SPEAKER_ABSENT_MS=300` 毫秒后,直接结束录音进入转写。它不保存长期声纹、不做主人注册、不跨进程记忆。
|
||
|
||
默认 `OWNER_POST_PLAYBACK_DRAIN_MS=0` 表示“我在”播放结束后只清理已经积压的输入队列,不再额外读取并丢弃后续音频,避免切掉正式问题开头。默认 `OWNER_VAD_PROVIDER=hybrid` 会同时使用本地 `sherpa-onnx` VAD 和能量阈值兜底,帮助开始录音;结束录音优先由主说话人端点控制。如果你想临时回退旧行为,可以设置 `OWNER_ENDPOINT_MODE=vad`。如果唤醒后你已经停说但还长时间显示“录音中”,优先调小 `OWNER_SPEAKER_ABSENT_MS`,例如 `250`;如果句尾容易被切掉,再调高到 `400`。
|
||
|
||
## 设备检查
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet device-check
|
||
```
|
||
|
||
输出里 `ok=true` 表示已检测到可用麦克风和扬声器。
|
||
|
||
## 运行
|
||
|
||
模拟麦克风自动验收:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet simulate-live --turns 2
|
||
```
|
||
|
||
这个命令不打开真实麦克风,会把生成的模拟麦克风音频帧喂进当前 `VoiceAssistantPipeline`,默认跑两轮唤醒到播放闭环。输出 JSON 中 `success=true` 表示两轮 wake、录音、实时字幕、final STT、临时上下文、LLM、TTS、播放和恢复待机都通过。模拟帧里故意包含 `家`、`家确` 这类短噪声 partial 和背景说话帧,用来验证实时字幕过滤和主说话人端点。
|
||
|
||
需要复现同一组模拟输入时:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet simulate-live --turns 2 --write-fixture /tmp/owner-simulated-mic.jsonl
|
||
.venv/bin/python -m owner_voice_pet simulate-live --turns 2 --fixture /tmp/owner-simulated-mic.jsonl
|
||
```
|
||
|
||
真实 Provider 完整链路自测:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet real-live-check --turns 2
|
||
```
|
||
|
||
这个命令不用真人对着麦克风说话,会用 macOS `say/afconvert` 生成“小杰小杰”和两轮问题音频,再驱动真实 `VoiceAssistantPipeline`:本地 KWS、本地 VAD、本地降噪、本地实时字幕、本地 final STT、云端 LLM、本地 TTS、Transport 播放、恢复待机都会跑到。默认会真实播放 ACK 和回复;如果只想检查链路但不发声,可以加 `--no-playback`。输出 JSON 中 `success=true` 表示完整链路通过,并会检查第二轮 LLM 请求是否携带第一轮临时历史。
|
||
|
||
输出里的 `timing` 会给出命令 `started_at`、`finished_at`、总 `duration_ms`、各 phase 耗时,以及按 turn/stage 聚合的 `stage_timings`,例如 wake 等待、ACK、等待说话、录音、final STT、LLM 到首个 TTS、TTS 播放、整轮总耗时。`llm_request_timings` 会记录每次云端 LLM 请求真正发出去的 `sent_at`、首个回复文本耗时 `first_delta_ms` 和请求总耗时 `duration_ms`。
|
||
|
||
单轮验收:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet run-live --once
|
||
```
|
||
|
||
常驻重复对话:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet run-live
|
||
```
|
||
|
||
运行后终端状态来自 pipeline event bus,会显示待机、唤醒命中、应答中、请说出问题、录音中、检测到用户语音、实时转写、用户语音结束、转写中、转写结果、思考中、播放中、继续对话或恢复待机等状态。说“小杰小杰”,听到“我在”且看到“请说出问题”后再提问;如果助手回复后判断需要你继续回答,可以在 3 秒内直接说下一句,不需要再次唤醒。若助手已经完成回答,会自动恢复待机。本次进程内会携带临时历史,程序退出后不保存。背景噪声下如果实时字幕仍偶发短错字,先看最终 `转写结果`;最终文本才会进入 LLM。
|
||
|
||
## 验证
|
||
|
||
```bash
|
||
.venv/bin/python -m compileall src tests scripts
|
||
.venv/bin/python -m unittest discover -s tests
|
||
.venv/bin/python -m owner_voice_pet simulate-live --turns 3
|
||
.venv/bin/python -m owner_voice_pet real-live-check --turns 2 --no-playback
|
||
.venv/bin/python -m owner_voice_pet acceptance
|
||
.venv/bin/python -m owner_voice_pet validate-assets
|
||
.venv/bin/python -m owner_voice_pet security-check
|
||
.venv/bin/python -m owner_voice_pet model-check --models-dir models
|
||
.venv/bin/python -m owner_voice_pet device-check
|
||
openspec validate --all --strict
|
||
```
|
||
|
||
LLM smoke:
|
||
|
||
```bash
|
||
.venv/bin/python -m owner_voice_pet llm-smoke --no-stream --message '用一句中文回复:小杰在线。'
|
||
```
|
||
|
||
## 安全约束
|
||
|
||
- API key 只写入本地 `.env`,`.env` 不提交 Git。
|
||
- `models/`、`.venv/` 和临时音频文件不提交 Git。
|
||
- 默认不持久化麦克风原始音频。
|
||
- 默认不使用云端 ASR/TTS;云端 LLM 只接收最终用户文本和本次运行内临时历史。
|
||
- `security-check` 会扫描已跟踪文本文件中的 `sk-...` 和 `tp-...` 形式密钥。
|