fix aha run multiple inputs bug

This commit is contained in:
jhqxxx
2026-01-31 18:37:28 +08:00
parent fe299c3401
commit 16e3aefaa7
16 changed files with 308 additions and 123 deletions
+24 -15
View File
@@ -65,7 +65,7 @@ aha -m qwen3vl-2b
**语法:**
```bash
aha run [OPTIONS] --model <MODEL> --input <INPUT> --weight-path <WEIGHT_PATH>
aha run [OPTIONS] --model <MODEL> --input <INPUT> [--input <INPUT2>] --weight-path <WEIGHT_PATH>
```
**选项:**
@@ -73,30 +73,39 @@ aha run [OPTIONS] --model <MODEL> --input <INPUT> --weight-path <WEIGHT_PATH>
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `-m, --model <MODEL>` | 模型类型(必选) | - |
| `-in, --input <INPUT>` | 输入文本或文件路径(模型特定解释) | - |
| `-out, --output <OUTPUT>` | 输出文件路径(可选,未指定则自动生成) | - |
| `-i, --input <INPUT>` | 输入文本或文件路径(模型特定解释,支持1-2个参数, input1 提示文本, input2: 文件地址 | - |
| `-o, --output <OUTPUT>` | 输出文件路径(可选,未指定则自动生成) | - |
| `--weight-path <WEIGHT_PATH>` | 本地模型权重路径(必选) | - |
**示例:**
```bash
# VoxCPM1.5 文字转语音
aha run -m voxcpm1.5 -in "太阳当空照" -out output.wav --weight-path /path/to/model
# VoxCPM1.5 文字转语音(单个输入)
aha run -m voxcpm1.5 -i "太阳当空照" -o output.wav --weight-path /path/to/model
# VoxCPM1.5 从文件读取输入
aha run -m voxcpm1.5 -in "file://./input.txt" --weight-path /path/to/model
# VoxCPM1.5 从文件读取输入(单个输入)
aha run -m voxcpm1.5 -i "file://./input.txt" --weight-path /path/to/model
# MiniCPM4 文本生成
aha run -m minicpm4-0.5b -in "你好" --weight-path /path/to/model
# MiniCPM4 文本生成(单个输入)
aha run -m minicpm4-0.5b -i "你好" --weight-path /path/to/model
# DeepSeek OCR 图片识别
aha run -m deepseek-ocr -in "image.jpg" --weight-path /path/to/model
# DeepSeek OCR 图片识别(单个输入)
aha run -m deepseek-ocr -i "image.jpg" --weight-path /path/to/model
# RMBG2.0 背景移除
aha run -m RMBG2.0 -in "photo.png" -out "no_bg.png" --weight-path /path/to/model
# RMBG2.0 背景移除(单个输入)
aha run -m RMBG2.0 -i "photo.png" -o "no_bg.png" --weight-path /path/to/model
# GLM-ASR 语音识别
aha run -m glm-asr-nano-2512 -in "audio.wav" -in "请转写这段音频" --weight-path /path/to/model
# GLM-ASR 语音识别(两个输入:提示文本 + 音频文件)
aha run -m glm-asr-nano-2512 -i "请转写这段音频" -i "audio.wav" --weight-path /path/to/model
# Fun-ASR 语音识别(两个输入:提示文本 + 音频文件)
aha run -m fun-asr-nano-2512 -i "语音转写:" -i "audio.wav" --weight-path /path/to/model
# qwen3 文本生成(单个输入)
aha run -m qwen3-0.6b -i "你好" --weight-path /path/to/model
# qwen2.5vl 图像理解(两个输入:提示文本 + 图片文件)
aha run -m qwen2.5vl-3b -i "请分析图片并提取所有可见文本内容,按从左到右、从上到下的布局,返回纯文本" -i "image.jpg" --weight-path /path/to/model
```
### serv - 启动服务
+17 -7
View File
@@ -1,18 +1,24 @@
//! DeepSeek-OCR exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, deepseek_ocr::generate::DeepseekOCRGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
pub struct DeepSeekORExec;
impl ExecModel for DeepSeekORExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let input_path = if input.starts_with("file://") {
input.to_string()
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let url = &input[0];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", input)
format!("file://{}", url)
};
let i_start = Instant::now();
@@ -32,12 +38,16 @@ impl ExecModel for DeepSeekORExec {
"image_url": {{
"url": "{}"
}}
}},
{{
"type": "text",
"text": "<image>\nConvert the document to markdown. "
}}
]
}}
]
}}"#,
input_path
input_url
);
let mes = serde_json::from_str(&message)?;
+18 -9
View File
@@ -1,19 +1,24 @@
//! Fun-ASR-Nano-2512 exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, fun_asr_nano::generate::FunAsrNanoGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
use crate::utils::get_file_path;
pub struct FunASRNanoExec;
impl ExecModel for FunASRNanoExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
// let path = &input[7..];
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let i_start = Instant::now();
@@ -22,10 +27,14 @@ impl ExecModel for FunASRNanoExec {
println!("Time elapsed in load model is: {:?}", i_duration);
// Create ChatCompletionParameters for ASR
let input_url = if input.starts_with("http://") || input.starts_with("https://") || input.starts_with("file://") {
input.to_string()
let url = &input[1];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", input)
format!("file://{}", url)
};
let message = format!(
+18 -9
View File
@@ -1,19 +1,24 @@
//! GLM-ASR-Nano-2512 exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, glm_asr_nano::generate::GlmAsrNanoGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
use crate::utils::get_file_path;
pub struct GlmASRNanoExec;
impl ExecModel for GlmASRNanoExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
// let path = &input[7..];
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let i_start = Instant::now();
@@ -23,10 +28,14 @@ impl ExecModel for GlmASRNanoExec {
// Create ChatCompletionParameters for ASR
// Input should be an audio file path
let input_url = if input.starts_with("http://") || input.starts_with("https://") || input.starts_with("file://") {
input.to_string()
let url = &input[1];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", input)
format!("file://{}", url)
};
let message = format!(
+17 -7
View File
@@ -1,18 +1,24 @@
//! Hunyuan-OCR exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, hunyuan_ocr::generate::HunyuanOCRGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
pub struct HunyuanORExec;
impl ExecModel for HunyuanORExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let input_path = if input.starts_with("file://") {
input.to_string()
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let url = &input[0];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", input)
format!("file://{}", url)
};
let i_start = Instant::now();
@@ -32,12 +38,16 @@ impl ExecModel for HunyuanORExec {
"image_url": {{
"url": "{}"
}}
}},
{{
"type": "text",
"text": "检测并识别图片中的文字,将文本坐标格式化输出。"
}}
]
}}
]
}}"#,
input_path
input_url
);
let mes = serde_json::from_str(&message)?;
+11 -6
View File
@@ -1,19 +1,24 @@
//! MiniCPM4-0.5B exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, minicpm4::generate::MiniCPMGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
use crate::utils::get_file_path;
pub struct MiniCPM4Exec;
impl ExecModel for MiniCPM4Exec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
// let path = &input[7..];
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.to_string()
};
let i_start = Instant::now();
+1 -1
View File
@@ -33,5 +33,5 @@ pub trait ExecModel {
/// # Returns
/// * `Ok(())` on success
/// * `Err(anyhow::Error)` on failure
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()>;
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()>;
}
+17 -7
View File
@@ -1,18 +1,24 @@
//! PaddleOCR-VL exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, paddleocr_vl::generate::PaddleOCRVLGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
pub struct PaddleOVLExec;
impl ExecModel for PaddleOVLExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let input_path = if input.starts_with("file://") {
input.to_string()
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let url = &input[0];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", input)
format!("file://{}", url)
};
let i_start = Instant::now();
@@ -32,12 +38,16 @@ impl ExecModel for PaddleOVLExec {
"image_url": {{
"url": "{}"
}}
}},
{{
"type": "text",
"text": "OCR:"
}}
]
}}
]
}}"#,
input_path
input_url
);
let mes = serde_json::from_str(&message)?;
+32 -9
View File
@@ -1,21 +1,33 @@
//! Qwen2.5VL-3B exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, qwen2_5vl::generate::Qwen2_5VLGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
use crate::utils::get_file_path;
pub struct Qwen2_5vlExec;
impl ExecModel for Qwen2_5vlExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let url = &input[1];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", url)
};
let i_start = Instant::now();
let mut model = Qwen2_5VLGenerateModel::init(weight_path, None, None)?;
let i_duration = i_start.elapsed();
@@ -27,11 +39,22 @@ impl ExecModel for Qwen2_5vlExec {
"messages": [
{{
"role": "user",
"content": "{}"
"content": [
{{
"type": "image",
"image_url": {{
"url": "{}"
}}
}},
{{
"type": "text",
"text": "{}"
}}
]
}}
]
}}"#,
target_text.replace('"', "\\\"")
input_url, target_text
);
let mes = serde_json::from_str(&message)?;
+11 -6
View File
@@ -1,19 +1,24 @@
//! Qwen3-0.6B exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, qwen3::generate::Qwen3GenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
use crate::utils::get_file_path;
pub struct Qwen3Exec;
impl ExecModel for Qwen3Exec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
// let path = &input[7..];
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let i_start = Instant::now();
+62 -12
View File
@@ -1,38 +1,88 @@
//! Qwen3VL-2B exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::{GenerateModel, qwen3vl::generate::Qwen3VLGenerateModel};
use anyhow::{Ok, Result};
use std::time::Instant;
use crate::utils::get_file_path;
pub struct Qwen3vlExec;
impl ExecModel for Qwen3vlExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let i_start = Instant::now();
let mut model = Qwen3VLGenerateModel::init(weight_path, None, None)?;
let i_duration = i_start.elapsed();
println!("Time elapsed in load model is: {:?}", i_duration);
let message = format!(
r#"{{
let url = &input[1];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", url)
};
let message = if input_url.ends_with("mp4") {
format!(
r#"{{
"model": "qwen3vl",
"messages": [
{{
"role": "user",
"content": "{}"
"content": [
{{
"type": "video",
"video_url":
{{
"url": "{}"
}}
}},
{{
"type": "text",
"text": "{}"
}}
]
}}
]
}}"#,
target_text.replace('"', "\\\"")
);
input_url, target_text
)
} else {
format!(
r#"{{
"model": "qwen2.5vl",
"messages": [
{{
"role": "user",
"content": [
{{
"type": "image",
"image_url": {{
"url": "{}"
}}
}},
{{
"type": "text",
"text": "{}"
}}
]
}}
]
}}"#,
input_url, target_text
)
};
let mes = serde_json::from_str(&message)?;
let i_start = Instant::now();
+13 -7
View File
@@ -1,18 +1,24 @@
//! RMBG2.0 exec implementation for CLI `run` subcommand
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::exec::ExecModel;
use crate::models::rmbg2_0::generate::RMBG2_0Model;
use anyhow::{Ok, Result};
use std::time::Instant;
pub struct RMBG2_0Exec;
impl ExecModel for RMBG2_0Exec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let input_path = if input.starts_with("file://") {
input.to_string()
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let url = &input[0];
let input_url = if url.starts_with("http://")
|| url.starts_with("https://")
|| url.starts_with("file://")
{
url.clone()
} else {
format!("file://{}", input)
format!("file://{}", url)
};
let i_start = Instant::now();
@@ -38,7 +44,7 @@ impl ExecModel for RMBG2_0Exec {
}}
]
}}"#,
input_path
input_url
);
let mes = serde_json::from_str(&message)?;
+14 -10
View File
@@ -1,19 +1,23 @@
//! VoxCPM exec implementation for CLI `run` subcommand
use crate::exec::ExecModel;
use crate::models::voxcpm::generate::VoxCPMGenerate;
use anyhow::{Ok, Result};
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::models::voxcpm::generate::VoxCPMGenerate;
use crate::{exec::ExecModel, utils::get_file_path};
pub struct VoxCPMExec;
impl ExecModel for VoxCPMExec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
// let path = &input[7..];
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let i_start = Instant::now();
@@ -24,10 +28,10 @@ impl ExecModel for VoxCPMExec {
let i_start = Instant::now();
let audio = voxcpm_generate.inference(
target_text,
Some("啥子小师叔,打狗还要看主人,你再要继续,我就是你的对手".to_string()), //todo args
Some("file://./assets/audio/voice_01.wav".to_string()), //todo args
Some("啥子小师叔,打狗还要看主人,你再要继续,我就是你的对手".to_string()), // todo args
Some("file://./assets/audio/voice_01.wav".to_string()), // todo args
2,
100, // max_len (voxcpm uses 100 vs voxcpm1.5's 4096)
100, // max_len (voxcpm uses 100 vs voxcpm1.5's 4096)
10,
2.0,
6.0,
+13 -9
View File
@@ -5,20 +5,24 @@
//! - Input can be text content or a file path (with `file://` prefix)
//! - Output can be a file path or will be auto-generated if not specified
use crate::exec::ExecModel;
use crate::models::voxcpm::generate::VoxCPMGenerate;
use anyhow::{Ok, Result};
use std::time::Instant;
use anyhow::{Ok, Result};
use crate::models::voxcpm::generate::VoxCPMGenerate;
use crate::{exec::ExecModel, utils::get_file_path};
pub struct VoxCPM1_5Exec;
impl ExecModel for VoxCPM1_5Exec {
fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()> {
let target_text = if input.starts_with("file://") {
let path = &input[7..];
fn run(input: &[String], output: Option<&str>, weight_path: &str) -> Result<()> {
let input_text = &input[0];
let target_text = if input_text.starts_with("file://") {
// let path = &input[7..];
let path = get_file_path(input_text)?;
std::fs::read_to_string(path)?
} else {
input.to_string()
input_text.clone()
};
let i_start = Instant::now();
@@ -29,8 +33,8 @@ impl ExecModel for VoxCPM1_5Exec {
let i_start = Instant::now();
let audio = voxcpm_generate.inference(
target_text,
Some("啥子小师叔,打狗还要看主人,你再要继续,我就是你的对手".to_string()), //todo args
Some("file://./assets/audio/voice_01.wav".to_string()), //todo args
Some("啥子小师叔,打狗还要看主人,你再要继续,我就是你的对手".to_string()), // todo args
Some("file://./assets/audio/voice_01.wav".to_string()), // todo args
2,
4096,
10,
+25 -8
View File
@@ -129,8 +129,8 @@ struct RunArgs {
model: WhichModel,
/// Input text or file path
#[arg(short, long)]
input: String,
#[arg(short, long, num_args = 1..=2, value_delimiter = ' ')]
input: Vec<String>,
/// Output file path (optional)
#[arg(short, long)]
@@ -219,7 +219,7 @@ fn run_list() -> anyhow::Result<()> {
println!("Available models:");
println!();
println!("{:<30} {}", "Model Name", "ModelScope ID");
println!("{:<30} ModelScope ID", "Model Name");
println!("{}", "-".repeat(80));
for model in models {
let possible_value = model.to_possible_value().unwrap();
@@ -233,7 +233,12 @@ fn run_list() -> anyhow::Result<()> {
/// Run the 'cli' subcommand: download model (if needed) and start service
async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
let CliArgs { common, weight_path, save_dir, download_retries } = args;
let CliArgs {
common,
weight_path,
save_dir,
download_retries,
} = args;
let model_id = get_model_id(common.model);
let model_path = match weight_path {
@@ -257,7 +262,10 @@ async fn run_cli(args: CliArgs) -> anyhow::Result<()> {
/// Run the 'serv' subcommand: start service only (no download)
async fn run_serv(args: ServArgs) -> anyhow::Result<()> {
let ServArgs { common, weight_path } = args;
let ServArgs {
common,
weight_path,
} = args;
init(common.model, weight_path)?;
start_http_server(common.address, common.port).await?;
@@ -267,7 +275,11 @@ async fn run_serv(args: ServArgs) -> anyhow::Result<()> {
/// Run the 'download' subcommand: download model only (no server)
async fn run_download(args: DownloadArgs) -> anyhow::Result<()> {
let DownloadArgs { model, save_dir, download_retries } = args;
let DownloadArgs {
model,
save_dir,
download_retries,
} = args;
let model_id = get_model_id(model);
let save_dir = match save_dir {
@@ -285,7 +297,12 @@ async fn run_download(args: DownloadArgs) -> anyhow::Result<()> {
fn run_run(args: RunArgs) -> anyhow::Result<()> {
use aha::exec::ExecModel;
let RunArgs { model, input, output, weight_path } = args;
let RunArgs {
model,
input,
output,
weight_path,
} = args;
match model {
WhichModel::MiniCPM4_0_5B => {
@@ -405,4 +422,4 @@ pub(crate) async fn start_http_server(address: String, port: u16) -> anyhow::Res
builder.launch().await?;
Ok(())
}
}
+15 -1
View File
@@ -3,7 +3,7 @@ pub mod img_utils;
pub mod tensor_utils;
pub mod video_utils;
use std::{fs, process::Command};
use std::{fs, path::PathBuf, process::Command};
use aha_openai_dive::v1::resources::{
chat::{
@@ -490,3 +490,17 @@ pub fn get_default_save_dir() -> Option<String> {
path.to_string_lossy().to_string()
})
}
pub fn get_file_path(file: &str) -> Result<PathBuf> {
let path = url::Url::parse(file)?;
let path = path.to_file_path();
let path = match path {
Ok(path) => path,
Err(_) => {
let mut path = file.to_owned();
path = path.split_off(7);
PathBuf::from(path)
}
};
Ok(path)
}