fix: ensure_default_models 每次启动重新检查模型文件存在性

This commit is contained in:
2026-07-04 16:02:42 +08:00
parent 6810d1ca13
commit 8fc82813d3
2 changed files with 173 additions and 73 deletions

95
.test_e2e.sh Normal file
View File

@@ -0,0 +1,95 @@
#!/bin/bash
# M2.1 端到端测试 — 文字对话管线 (LLM → TTS)
set -e
cd ~/Showen/ShowenV2
echo "=== M2.1 端到端测试: 文字对话 ==="
# 停生产服务
systemctl --user stop showen_v2.service
sleep 1
# 启动新编译的 release 版 (后台)
source ~/.cargo/env
./target/release/showen_v2 --config configs/dog_state_machine.json > /tmp/m21_e2e.log 2>&1 &
APP_PID=$!
echo "启动 PID=$APP_PID, 等待服务就绪..."
# 等待 HTTP 服务就绪 (最多 15s)
for i in $(seq 1 15); do
if curl -s http://127.0.0.1:5000/api/status > /dev/null 2>&1; then
echo "服务就绪 (${i}s)"
break
fi
sleep 1
done
# 测试 1: 模型管理 API
echo ""
echo "=== 测试 1: GET /api/models ==="
MODELS_RESP=$(curl -s http://127.0.0.1:5000/api/models)
echo "$MODELS_RESP" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if 'models' in data:
print(f'模型数: {len(data[\"models\"])}')
for m in data['models'][:4]:
status = '已下载' if m.get('downloaded') else '未下载'
print(f' - {m[\"name\"]} ({m[\"kind\"]}) [{status}]')
print(f'配额: {data.get(\"quota\", 0) // (1024*1024)}MB, 已用: {data.get(\"used_space\", 0) // (1024*1024)}MB')
else:
print('错误:', data)
" 2>&1 || echo "模型 API 响应解析失败: $MODELS_RESP"
# 测试 2: 文字对话 (LLM + TTS 管线, 跳过 ASR)
echo ""
echo "=== 测试 2: POST /api/chat/text ==="
echo "输入: '你好,你叫什么名字?'"
START=$(date +%s%N)
CHAT_RESP=$(curl -s -X POST http://127.0.0.1:5000/api/chat/text \
-H "Content-Type: application/json" \
-d '{"text":"你好,你叫什么名字?","session_id":"e2e_test"}' \
--max-time 60)
END=$(date +%s%N)
MS=$(( (END - START) / 1000000 ))
echo "响应耗时: ${MS}ms"
echo "$CHAT_RESP" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
if data.get('error'):
print(f'错误: {data[\"error\"]}')
else:
print(f'回复文本: {data.get(\"reply_text\", \"(空)\")}')
audio = data.get('reply_audio_path')
if audio:
print(f'回复音频: {audio}')
else:
print('回复音频: (无)')
print(f'会话: {data.get(\"session_id\")}')
except Exception as e:
print(f'解析失败: {e}')
print(f'原始响应: {sys.stdin.read()[:200]}')
" 2>&1
# 检查音频文件是否真的生成
echo ""
echo "=== 检查 TTS 音频文件 ==="
ls -lh /tmp/showen_ai/tts_*.wav 2>/dev/null | tail -3 || echo "无 TTS 音频文件"
# 清理
echo ""
echo "=== 清理 ==="
kill $APP_PID 2>/dev/null
wait $APP_PID 2>/dev/null
sleep 1
# 恢复生产服务
systemctl --user start showen_v2.service
sleep 2
systemctl --user is-active showen_v2.service
echo "生产服务已恢复"
echo ""
echo "=== 测试完成 ==="

View File

@@ -148,82 +148,87 @@ impl ModelManager {
} }
/// 初始化默认模型清单(首次启动或清单缺失时) /// 初始化默认模型清单(首次启动或清单缺失时)
/// 每次启动都重新检查文件存在性,更新 downloaded 状态(符号链接可能后建)
pub fn ensure_default_models(&mut self) -> Result<()> { pub fn ensure_default_models(&mut self) -> Result<()> {
if !self.registry.models.is_empty() { // 首次:注册表为空,填充默认清单
return Ok(()); if self.registry.models.is_empty() {
let defaults = vec![
ModelInfo {
id: "qwen2.5-0.5b-q4_k_m".to_string(),
kind: ModelKind::Llm,
name: "Qwen2.5 0.5B Q4 (推荐默认)".to_string(),
version: "0.5b".to_string(),
size: 469 * 1024 * 1024,
memory_required: 1024 * 1024 * 1024,
recommended_tier: "4G 内存档可用".to_string(),
url: "https://hf-mirror.com/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf".to_string(),
sha256: String::new(),
filename: "qwen2.5-0.5b-q4_k_m.gguf".to_string(),
},
ModelInfo {
id: "gemma3-1b-q4_k_m".to_string(),
kind: ModelKind::Llm,
name: "Gemma3 1B Q4 (可选档)".to_string(),
version: "1b".to_string(),
size: 800 * 1024 * 1024,
memory_required: 2 * 1024 * 1024 * 1024,
recommended_tier: "建议 8G 以上".to_string(),
url: "https://hf-mirror.com/google/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-q4_k_m.gguf".to_string(),
sha256: String::new(),
filename: "gemma3-1b-q4_k_m.gguf".to_string(),
},
ModelInfo {
id: "whisper-tiny".to_string(),
kind: ModelKind::Asr,
name: "Whisper Tiny (中文)".to_string(),
version: "tiny".to_string(),
size: 75 * 1024 * 1024,
memory_required: 200 * 1024 * 1024,
recommended_tier: "4G 内存档可用".to_string(),
url: "https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin".to_string(),
sha256: String::new(),
filename: "ggml-tiny.bin".to_string(),
},
ModelInfo {
id: "piper-zh-huayan".to_string(),
kind: ModelKind::Tts,
name: "Piper zh_CN huayan (中文女声)".to_string(),
version: "medium".to_string(),
size: 63 * 1024 * 1024,
memory_required: 150 * 1024 * 1024,
recommended_tier: "4G 内存档可用".to_string(),
url: "https://hf-mirror.com/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx".to_string(),
sha256: String::new(),
filename: "zh_CN-huayan-medium.onnx".to_string(),
},
];
for info in defaults {
self.registry.models.push(LocalModelEntry {
info,
downloaded: false,
download_progress: 0,
});
}
// 设置默认 active 模型
if !self.registry.active.contains_key(&ModelKind::Llm) {
self.registry.active.insert(ModelKind::Llm, "qwen2.5-0.5b-q4_k_m".to_string());
}
if !self.registry.active.contains_key(&ModelKind::Asr) {
self.registry.active.insert(ModelKind::Asr, "whisper-tiny".to_string());
}
if !self.registry.active.contains_key(&ModelKind::Tts) {
self.registry.active.insert(ModelKind::Tts, "piper-zh-huayan".to_string());
}
} }
// Spike 推荐的默认模型清单 // 每次启动都重新检查文件存在性(符号链接可能后建,或文件被手动删除)
let defaults = vec![ for entry in &mut self.registry.models {
ModelInfo { let path = self.store_dir.join(entry.info.kind.dir_name()).join(&entry.info.filename);
id: "qwen2.5-0.5b-q4_k_m".to_string(), let exists = path.exists();
kind: ModelKind::Llm, entry.downloaded = exists;
name: "Qwen2.5 0.5B Q4 (推荐默认)".to_string(), entry.download_progress = if exists { 100 } else { 0 };
version: "0.5b".to_string(),
size: 469 * 1024 * 1024,
memory_required: 1024 * 1024 * 1024,
recommended_tier: "4G 内存档可用".to_string(),
url: "https://hf-mirror.com/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf".to_string(),
sha256: String::new(), // 实际部署时填充
filename: "qwen2.5-0.5b-q4_k_m.gguf".to_string(),
},
ModelInfo {
id: "gemma3-1b-q4_k_m".to_string(),
kind: ModelKind::Llm,
name: "Gemma3 1B Q4 (可选档)".to_string(),
version: "1b".to_string(),
size: 800 * 1024 * 1024,
memory_required: 2 * 1024 * 1024 * 1024,
recommended_tier: "建议 8G 以上".to_string(),
url: "https://hf-mirror.com/google/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-q4_k_m.gguf".to_string(),
sha256: String::new(),
filename: "gemma3-1b-q4_k_m.gguf".to_string(),
},
ModelInfo {
id: "whisper-tiny".to_string(),
kind: ModelKind::Asr,
name: "Whisper Tiny (中文)".to_string(),
version: "tiny".to_string(),
size: 75 * 1024 * 1024,
memory_required: 200 * 1024 * 1024,
recommended_tier: "4G 内存档可用".to_string(),
url: "https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin".to_string(),
sha256: String::new(),
filename: "ggml-tiny.bin".to_string(),
},
ModelInfo {
id: "piper-zh-huayan".to_string(),
kind: ModelKind::Tts,
name: "Piper zh_CN huayan (中文女声)".to_string(),
version: "medium".to_string(),
size: 63 * 1024 * 1024,
memory_required: 150 * 1024 * 1024,
recommended_tier: "4G 内存档可用".to_string(),
url: "https://hf-mirror.com/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx".to_string(),
sha256: String::new(),
filename: "zh_CN-huayan-medium.onnx".to_string(),
},
];
for info in defaults {
let filename = info.filename.clone();
let downloaded = self.store_dir.join(info.kind.dir_name()).join(&filename).exists();
self.registry.models.push(LocalModelEntry {
info,
downloaded,
download_progress: if downloaded { 100 } else { 0 },
});
}
// 设置默认 active 模型
if !self.registry.active.contains_key(&ModelKind::Llm) {
self.registry.active.insert(ModelKind::Llm, "qwen2.5-0.5b-q4_k_m".to_string());
}
if !self.registry.active.contains_key(&ModelKind::Asr) {
self.registry.active.insert(ModelKind::Asr, "whisper-tiny".to_string());
}
if !self.registry.active.contains_key(&ModelKind::Tts) {
self.registry.active.insert(ModelKind::Tts, "piper-zh-huayan".to_string());
} }
self.save_registry()?; self.save_registry()?;