fix: 解析 llama-cli 输出,只提取生成回复 (去除 banner/进度动画/统计)
This commit is contained in:
@@ -194,9 +194,18 @@ impl AiBackend for LocalCliBackend {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// llama-cli 输出到 stdout,取生成部分
|
// llama-cli --single-turn 输出格式:
|
||||||
let reply = String::from_utf8_lossy(&output.stdout);
|
// [banner/logo]
|
||||||
Ok(reply.trim().to_string())
|
// > <user_input> ← 用户输入回显
|
||||||
|
// [进度动画 |/-\]
|
||||||
|
// <生成的回复> ← 我们要提取的部分
|
||||||
|
// [空行]
|
||||||
|
// [ Prompt: ... | Generation: ... ] ← 性能统计
|
||||||
|
//
|
||||||
|
// 解析策略:找到 "> " 开头的行,跳过进度动画行,取实际回复行
|
||||||
|
let raw = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let reply = parse_llama_output(&raw);
|
||||||
|
Ok(reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()> {
|
fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()> {
|
||||||
@@ -275,3 +284,86 @@ impl AiBackend for CloudBackend {
|
|||||||
"cloud"
|
"cloud"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 解析 llama-cli --single-turn 的 stdout 输出,提取生成的回复文本
|
||||||
|
///
|
||||||
|
/// 输出格式:
|
||||||
|
/// ```text
|
||||||
|
/// [banner/logo ASCII art]
|
||||||
|
/// build : ...
|
||||||
|
/// model : ...
|
||||||
|
/// ...
|
||||||
|
/// > <user_input>
|
||||||
|
///
|
||||||
|
/// [进度动画 |/-\]
|
||||||
|
/// <生成的回复>
|
||||||
|
///
|
||||||
|
/// [ Prompt: ... | Generation: ... ]
|
||||||
|
///
|
||||||
|
/// Exiting...
|
||||||
|
/// ```
|
||||||
|
fn parse_llama_output(raw: &str) -> String {
|
||||||
|
let lines: Vec<&str> = raw.lines().collect();
|
||||||
|
let mut reply_lines = Vec::new();
|
||||||
|
let mut after_user_input = false;
|
||||||
|
|
||||||
|
for line in &lines {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
|
||||||
|
// 跳过空行(但在回复区内保留)
|
||||||
|
if trimmed.is_empty() && !after_user_input {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到用户输入回显行 "> xxx"
|
||||||
|
if trimmed.starts_with('>') {
|
||||||
|
after_user_input = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !after_user_input {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过进度动画行(只含 |/-\ 和空格)
|
||||||
|
let cleaned: String = trimmed.chars().filter(|c| !"|\\-/ ".contains(*c)).collect();
|
||||||
|
if cleaned.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遇到性能统计行 [ Prompt: ... ] 停止
|
||||||
|
if trimmed.starts_with('[') && trimmed.contains("Generation") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 遇到 "Exiting..." 停止
|
||||||
|
if trimmed == "Exiting..." {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_lines.push(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_lines.join("\n").trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_llama_output() {
|
||||||
|
let raw = " █ █ build : b1-fdb1db8\n\
|
||||||
|
model : model_store/llm/qwen2.5-0.5b-q4_k_m.gguf\n\
|
||||||
|
\n\
|
||||||
|
> hello\n\
|
||||||
|
\n\
|
||||||
|
|/-\\|/-\\ 汪!\n\
|
||||||
|
\n\
|
||||||
|
[ Prompt: 31.1 t/s | Generation: 26.3 t/s ]\n\
|
||||||
|
\n\
|
||||||
|
Exiting...\n";
|
||||||
|
|
||||||
|
let reply = parse_llama_output(raw);
|
||||||
|
assert_eq!(reply, "汪!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user