Files
ylhan 35e107c290 助手本体:会话引擎、LLM 路由、记忆、身份、语音适配
从 Obsidian 笔记库剥离为独立仓库。规划文档与 P0 报告留在笔记库,
代码(助手本体、TTS 服务、P0 基准脚本、ESP32 固件)在此。

架构:服务器是助手,本地是客户端
- 助手本体跑在 cadadmin-server,N5095 只做 USB 外设桥与网络桥接
- 起因:实测 N5095 无 AVX2,跑不动可用的语音模型

当前技术栈
- LLM:DeepSeek deepseek-flash(云端,必须关思考)
- TTS:Qwen3-TTS 1.7B(服务器 3090,独立 HTTP 服务)
- STT:Zipformer 中英双语 int8(CPU,中文识别率未测)

实测:端到端 16/16 通过,首音延迟 1545ms
(对比:本地 LLM + vits-zh-ll 是 179ms,但音色不满意——音质与延迟
在此硬件上此消彼长)

设计要点
- MemoryStore 接口按"可能远程"设计(全 async、可超时、可失败)
- user_id / session_id / track_id 三者分开,人脸只作候选不自动切身份
- 记忆查询强制带 owner_user_id,无按 id 单独取的口子
- 自动抽取的记忆是"候选",默认不参与检索
- 异步写记忆绑定创建时的身份,不读"当前用户"
- 引擎与传输解耦,只用文件音频即可端到端测试

模型与音频产物不进版本管理(1.1GB),由 download_models.sh 恢复
2026-09-15 11:05:09 +08:00

74 lines
2.2 KiB
Python

#!/usr/bin/env python3
"""线程数扫描:查清 N5095 上 num_threads 的真实影响。
bench_stt.py 里 2 线程 RTF=0.22、4 线程 RTF=0.87,性能反而掉了 4 倍。
本脚本对 1/2/3/4 线程各重复多轮,排除预热与顺序干扰,确认是否为真实现象。
"""
import os
import statistics
import time
import wave
import numpy as np
import sherpa_onnx
HERE = os.path.dirname(os.path.abspath(__file__))
STT = os.path.join(HERE, "models", "stt")
WAVS = os.path.join(HERE, "wavs")
SR = 16000
CHUNK = int(SR * 0.1)
REPEATS = int(os.environ.get("REPEATS", "5"))
def read_wav(p):
with wave.open(p, "rb") as w:
return np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
def build(nt):
return sherpa_onnx.OnlineRecognizer.from_transducer(
tokens=os.path.join(STT, "tokens.txt"),
encoder=os.path.join(STT, "encoder-epoch-99-avg-1.int8.onnx"),
decoder=os.path.join(STT, "decoder-epoch-99-avg-1.int8.onnx"),
joiner=os.path.join(STT, "joiner-epoch-99-avg-1.int8.onnx"),
num_threads=nt, sample_rate=SR, feature_dim=80,
decoding_method="greedy_search", enable_endpoint_detection=False,
)
def decode_once(rec, samples):
s = rec.create_stream()
t0 = time.perf_counter()
pos = 0
while pos < len(samples):
s.accept_waveform(SR, samples[pos:pos + CHUNK])
pos += CHUNK
while rec.is_ready(s):
rec.decode_stream(s)
return time.perf_counter() - t0
def main():
wav = read_wav(os.path.join(WAVS, "stt_0.wav"))
dur = len(wav) / SR
print(f"音频 {dur:.2f}s,每档重复 {REPEATS} 轮(含 1 轮预热,不计入)\n")
print(f"{'线程':>4} {'RTF中位':>9} {'RTF最差':>9} {'各轮RTF':>34}")
print("-" * 62)
for nt in (1, 2, 3, 4):
rec = build(nt)
decode_once(rec, wav[: SR * 2]) # 预热
rtfs = []
for _ in range(REPEATS):
rtfs.append(decode_once(rec, wav) / dur)
shown = " ".join(f"{r:.2f}" for r in rtfs)
print(f"{nt:>4} {statistics.median(rtfs):>9.2f} {max(rtfs):>9.2f} {shown:>34}")
del rec
print("\n=== 参照:系统负载 ===")
print(open("/proc/loadavg").read().strip())
if __name__ == "__main__":
main()