Files
Owner/README.md
T

297 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Owner 语音桌宠
这是一个 Python 语音桌宠运行程序。当前第一版先提供无 GUI 的真实实时语音循环:
`小杰小杰` 本地唤醒 -> 本地应答“我在” -> 本地降噪 -> 主说话人端点采集问题 -> 本地 STT/实时字幕 -> 携带本次进程内临时历史调用云端 LLM -> 本地 TTS -> 本机扬声器播放 -> 回到待机继续监听。
## 当前能力
- `owner_voice_pet run-live`:真实常驻语音循环。
- `owner_voice_pet run-live --once`:只跑一轮,便于验收。
- `owner_voice_pet run-agent-live`:完整全双工 Agent 主入口,启动后直接 listening,播放中可被有效用户语音打断。
- `owner_voice_pet run-agent-live --check-config`:只检查全双工 Agent 配置和 APM 就绪状态,不打开麦克风。
- `owner_voice_pet agent-self-test --profile full-duplex --turns 3`:无人值守自测 STT、LLM、TTS、打断、记忆和工具路由。
- `owner_voice_pet audio-self-test --duration 10 --check-echo`:检查设备、WebRTC APM、回声抑制和打断延迟。
- `.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_BARGE_IN_DEBUG=0
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=1
OWNER_MEMORY_PROVIDER=faiss_sqlite
OWNER_MEMORY_TOP_K=5
OWNER_MEMORY_AUTO_SAVE_SENSITIVE=0
OWNER_TOOL_ROUTER_ENABLED=1
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` 是回声保护上限,真实运行时只在每段回复的第一个播报片段使用一次,并压到 120 ms 内,避免每个短句都重新进入 500 ms 免打断窗口。`OWNER_BARGE_IN_MIN_SPEECH_MS=250` 是最短人声配置上限,实际运行会按 `OWNER_INTERRUPT_TARGET_LATENCY_MS=200` 和 chunk 粒度收紧,避免参数本身超过目标打断延迟。播放中如果 VAD 检测到有效用户语音,且不像当前助手回放 reference,就先停止剩余播报;STT 只用于后续识别打断内容,不再作为停播前置条件。`OWNER_BARGE_IN_SPEAKER_GATE_ENABLED=1` 会同时建立本次会话用户临时音色画像和当前助手回放音色画像;用户音色匹配优先于助手回放拒绝,避免“用户说话 + 扬声器回声”混合时被先当成 AI 自己声音丢掉。设置 `OWNER_BARGE_IN_DEBUG=1` 后,终端会输出打断监听启动、VAD 累计、回声门控拒绝原因和触发时长,便于现场定位。音色画像只在进程内使用,不写文件、不发送给 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 入口
为避免破坏当前已经可用的旧语音循环,全双工 Agent 使用独立入口:
```bash
.venv/bin/python -m owner_voice_pet run-agent-live
```
`run-agent-live` 不需要先说唤醒词,启动后会直接进入 listening。完整模式默认要求 `OWNER_AUDIO_APM_PROVIDER=webrtc``OWNER_AUDIO_APM_REQUIRED=1`,麦克风 capture 必须经过 WebRTC APM 的 AEC/NS/AGC 后再进入 VAD、STT 和打断检测;播放 PCM 会同步写入 render reference。若本机没有真实 WebRTC APM provider,入口会以 `AUDIO_APM_UNAVAILABLE` 明确失败,不再回退到旧 `run-live` 或伪全双工。
只检查全双工配置、不打开麦克风:
```bash
.venv/bin/python -m owner_voice_pet run-agent-live --check-config
```
旧 wake-word turn-based 入口仍保留:
```bash
.venv/bin/python -m owner_voice_pet run-live
```
全双工 Agent 当前架构使用单一 `AudioHub` 拥有麦克风输入,VAD、STT、InterruptController 和诊断订阅各自独立的 processed capture cursor,不再抢读同一个 Transport 队列。打断不等待 STT partial:在 `thinking/speaking/tool_running` 中,只要 processed capture 上的有效用户语音达到阈值,就取消当前 LLM/TTS/playback/tool 子图,并把已确认的用户音频缓存给下一轮输入。`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=1``OWNER_TOOL_ROUTER_ENABLED=1` 默认开启,`OWNER_OPENINTERPRETER_ENABLED=0``OWNER_BROWSER_PLAYWRIGHT_ENABLED=0``OWNER_COMPUTER_CONTROL_ENABLED=0` 默认关闭,避免未确认的外部控制自动执行。
无人值守 Agent 自测:
```bash
.venv/bin/python -m owner_voice_pet agent-self-test --profile full-duplex --turns 3
```
音频/APM 诊断:
```bash
.venv/bin/python -m owner_voice_pet audio-self-test --duration 10 --check-echo
```
如果当前机器没有真实 WebRTC APM binding`audio-self-test` 会返回 `success=false``AUDIO_APM_UNAVAILABLE`;这表示完整全双工音频底座尚未满足,不应把 fake APM 结果当成人工验收通过。需要做确定性开发自测时,可以临时使用 `OWNER_AUDIO_APM_PROVIDER=fake`
当前已落地的全双工基础模块:
- `full_duplex_audio`AudioHub、多消费者 capture/render ring buffer、fake WebRTC APM、APM 探针和 required startup 决策。
- `full_duplex_control`:全双工状态机、事件诊断字段、取消 token graph 和恢复协调器。
- `full_duplex_speech`VAD provider contract、Silero VAD 边界、Streaming STT worker、fake STT、InterruptionDetector 和 InterruptController。
- `full_duplex_response`LLM streaming contract、句子切分、TTS 文本净化、Streaming TTS wrapper、fake Streaming TTS 和可中断播放队列。
- `full_duplex_runtime`:新 `run-agent-live` runtime 边界,串联 AudioHub、取消图、流式回复、长期记忆和 ToolRouter。
- `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
```
全双工 Agent 单轮验收:
```bash
.venv/bin/python -m owner_voice_pet run-agent-live --once
```
常驻重复对话:
```bash
.venv/bin/python -m owner_voice_pet run-live
```
常驻全双工 Agent
```bash
.venv/bin/python -m owner_voice_pet run-agent-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-...` 形式密钥。