从 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 恢复
74 lines
2.2 KiB
Python
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()
|