从 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 恢复
70 lines
2.1 KiB
Python
70 lines
2.1 KiB
Python
#!/usr/bin/env python3
|
|
"""按目录递归下载 HF 仓库文件到本地(走 hf-mirror)。
|
|
|
|
Piper 中文 medium 档没有 lexicon.txt,改用 espeak-ng-data 做音素化,
|
|
而那是一堆小文件,用 shell 逐个 curl 太啰嗦,所以写这个。
|
|
|
|
用法:
|
|
python3 hf_fetch.py <repo> <本地目录> [远端前缀]
|
|
python3 hf_fetch.py csukuangfj/vits-piper-zh_CN-huayan-medium \
|
|
models/tts_piper_medium espeak-ng-data
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
MIRROR = os.environ.get("HF_MIRROR", "https://hf-mirror.com")
|
|
UA = {"User-Agent": "curl/8.5.0"} # hf-mirror 对 urllib 默认 UA 返回 403
|
|
|
|
|
|
def _open(url, timeout):
|
|
return urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=timeout)
|
|
|
|
|
|
def api_tree(repo):
|
|
url = f"{MIRROR}/api/models/{repo}/tree/main?recursive=true"
|
|
with _open(url, 60) as r:
|
|
return json.load(r)
|
|
|
|
|
|
def fetch(url, dest):
|
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
tmp = dest + ".part"
|
|
with _open(url, 300) as r, open(tmp, "wb") as f:
|
|
while True:
|
|
b = r.read(1 << 16)
|
|
if not b:
|
|
break
|
|
f.write(b)
|
|
os.replace(tmp, dest)
|
|
|
|
|
|
def main():
|
|
repo, outdir = sys.argv[1], sys.argv[2]
|
|
prefix = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
files = [f for f in api_tree(repo) if f["type"] == "file"]
|
|
if prefix:
|
|
files = [f for f in files if f["path"].startswith(prefix)]
|
|
print(f"{repo}: 取 {len(files)} 个文件 → {outdir}")
|
|
for f in files:
|
|
rel = f["path"]
|
|
dest = os.path.join(outdir, rel)
|
|
if os.path.exists(dest) and os.path.getsize(dest) == f.get("size", -1):
|
|
continue
|
|
# espeak 的 voices/!v/ 下有带空格的文件名(如 "Mr serious"),必须转义
|
|
url = f"{MIRROR}/{repo}/resolve/main/{urllib.parse.quote(rel)}"
|
|
try:
|
|
fetch(url, dest)
|
|
except Exception as e:
|
|
print(f" 跳过 {rel}: {type(e).__name__}: {e}")
|
|
continue
|
|
print(f" {rel} ({f.get('size',0)/1024:.0f} KB)")
|
|
print("完成")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|