From 861c411ba2848fa95d6ee83c6ea6c1fb0b062a34 Mon Sep 17 00:00:00 2001 From: XiaoYang Date: Tue, 20 Jan 2026 22:15:45 +0800 Subject: [PATCH 1/6] feat: CLI Subcommand Support - **CLI Subcommand Support**: Added three new subcommands for better command organization: - `aha cli` - Download model and start HTTP service (default, backward compatible) - `aha serv` - Start HTTP service only (requires `--weight-path`) - `aha download` - Download model only (no service start) --- CHANGELOG.md | 109 +++++++++++++++++++++ docs/CLI_USAGE.md | 242 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 193 ++++++++++++++++++++++++++++++------ 3 files changed, 516 insertions(+), 28 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/CLI_USAGE.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8e4ba4f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- **CLI Subcommand Support**: Added three new subcommands for better command organization: + - `aha cli` - Download model and start HTTP service (default, backward compatible) + - `aha serv` - Start HTTP service only (requires `--weight-path`) + - `aha download` - Download model only (no service start) + +### Changed + +- **CLI Structure**: Refactored CLI to use clap's Subcommand feature while maintaining backward compatibility +- **Backward Compatibility**: Commands without subcommand now default to `cli` subcommand: + - `aha -m qwen3vl-2b` is equivalent to `aha cli -m qwen3vl-2b` + - All existing parameter options and defaults remain unchanged + +### Technical Details + +**Subcommand Parameters:** + +`aha cli`: +- `-a, --address
` - Server address (default: 127.0.0.1) +- `-p, --port ` - Server port (default: 10100) +- `-m, --model ` - Model to use (required) +- `--weight-path ` - Local model weight path (optional) +- `--save-dir ` - Directory to save downloaded model (optional) +- `--download-retries ` - Download retry attempts (default: 3) + +`aha serv`: +- `-a, --address
` - Server address (default: 127.0.0.1) +- `-p, --port ` - Server port (default: 10100) +- `-m, --model ` - Model to use (required) +- `--weight-path ` - Local model weight path (required) + +`aha download`: +- `-m, --model ` - Model to download (required) +- `-s, --save-dir ` - Directory to save downloaded model (optional) +- `--download-retries ` - Download retry attempts (default: 3) + +**Code Changes:** +- Modified `src/main.rs` only +- Extracted common functions: `get_model_id()`, `start_http_server()` +- Reused existing `download_model()` and `init()` functions +- No changes to other modules or dependencies + +## [0.1.8] - 2025-01-20 + +### Added + +- Support for Fun-ASR-Nano-2512 model +- Support for Qwen3-0.6B model + +## [0.1.7] - 2024-XX-XX + +### Added + +- Support for GLM-ASR-Nano-2512 model + +## [0.1.6] - 2024-XX-XX + +### Added + +- Support for RMBG-2.0 model (background removal) + +## [0.1.5] - 2024-XX-XX + +### Added + +- Support for VoxCPM1.5 model + +## [0.1.4] - 2024-XX-XX + +### Added + +- Support for PaddleOCR-VL model + +## [0.1.3] - 2024-XX-XX + +### Added + +- Support for Hunyuan-OCR model + +## [0.1.2] - 2024-XX-XX + +### Added + +- Support for DeepSeek-OCR model + +## [0.1.1] - 2024-XX-XX + +### Added + +- Support for Qwen3VL model family (2B, 4B, 8B, 32B) + +## [0.1.0] - 2024-XX-XX + +### Added + +- Initial release +- Support for Qwen2.5VL models (3B, 7B) +- Support for MiniCPM4-0.5B model +- Support for VoxCPM-0.5B model diff --git a/docs/CLI_USAGE.md b/docs/CLI_USAGE.md new file mode 100644 index 0000000..85e54d8 --- /dev/null +++ b/docs/CLI_USAGE.md @@ -0,0 +1,242 @@ +# AHA 命令行使用说明 + +## 概述 + +AHA 是一个基于 Candle 框架的高性能模型推理库,支持多种多模态模型,包括视觉、语言和语音模型。 + +```bash +aha [COMMAND] [OPTIONS] +``` + +## 全局选项 + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `-a, --address
` | 服务监听地址 | 127.0.0.1 | +| `-p, --port ` | 服务监听端口 | 10100 | +| `-m, --model ` | 模型类型(必选) | - | +| `--weight-path ` | 本地模型权重路径 | - | +| `--save-dir ` | 模型下载保存目录 | ~/.aha/ | +| `--download-retries ` | 下载重试次数 | 3 | +| `-h, --help` | 显示帮助信息 | - | +| `-V, --version` | 显示版本号 | - | + +## 子命令 + +### cli - 下载模型并启动服务(默认) + +下载指定的模型并启动 HTTP 服务。当不指定子命令时,默认使用此命令。 + +**语法:** +```bash +aha cli [OPTIONS] --model +``` + +**选项:** + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `-a, --address
` | 服务监听地址 | 127.0.0.1 | +| `-p, --port ` | 服务监听端口 | 10100 | +| `-m, --model ` | 模型类型(必选) | - | +| `--weight-path ` | 本地模型权重路径(如指定则跳过下载) | - | +| `--save-dir ` | 模型下载保存目录 | ~/.aha/ | +| `--download-retries ` | 下载重试次数 | 3 | + +**示例:** + +```bash +# 下载模型并启动服务(默认端口 10100) +aha cli -m qwen3vl-2b + +# 指定端口和保存目录 +aha cli -m qwen3vl-2b -p 8080 --save-dir /data/models + +# 使用本地模型(不下载) +aha cli -m qwen3vl-2b --weight-path /path/to/model + +# 向后兼容方式(等同于 cli 子命令) +aha -m qwen3vl-2b +``` + +### serv - 启动服务 + +仅启动 HTTP 服务,不下载模型。必须通过 `--weight-path` 指定本地模型路径。 + +**语法:** +```bash +aha serv [OPTIONS] --model --weight-path +``` + +**选项:** + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `-a, --address
` | 服务监听地址 | 127.0.0.1 | +| `-p, --port ` | 服务监听端口 | 10100 | +| `-m, --model ` | 模型类型(必选) | - | +| `--weight-path ` | 本地模型权重路径(必选) | - | + +**示例:** + +```bash +# 使用本地模型启动服务 +aha serv -m qwen3vl-2b --weight-path /path/to/model + +# 指定端口启动 +aha serv -m qwen3vl-2b --weight-path /path/to/model -p 8080 + +# 指定监听地址 +aha serv -m qwen3vl-2b --weight-path /path/to/model -a 0.0.0.0 +``` + +### download - 下载模型 + +仅下载指定模型,不启动服务。 + +**语法:** +```bash +aha download [OPTIONS] --model +``` + +**选项:** + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `-m, --model ` | 模型类型(必选) | - | +| `-s, --save-dir ` | 模型下载保存目录 | ~/.aha/ | +| `--download-retries ` | 下载重试次数 | 3 | + +**示例:** + +```bash +# 下载模型到默认目录 +aha download -m qwen3vl-2b + +# 指定保存目录 +aha download -m qwen3vl-2b -s /data/models + +# 指定下载重试次数 +aha download -m qwen3vl-2b --download-retries 5 + +# 下载 MiniCPM4-0.5B 模型 +aha download -m minicpm4-0.5b -s models +``` + +## 支持的模型 + +| 模型标识 | 模型名称 | 说明 | +|---------|---------|------| +| `minicpm4-0.5b` | OpenBMB/MiniCPM4-0.5B | 面壁智能 MiniCPM4 0.5B 模型 | +| `qwen2.5vl-3b` | Qwen/Qwen2.5-VL-3B-Instruct | 通义千问 2.5 VL 3B 模型 | +| `qwen2.5vl-7b` | Qwen/Qwen2.5-VL-7B-Instruct | 通义千问 2.5 VL 7B 模型 | +| `qwen3-0.6b` | Qwen/Qwen3-0.6B | 通义千问 3 0.6B 模型 | +| `qwen3vl-2b` | Qwen/Qwen3-VL-2B-Instruct | 通义千问 3 VL 2B 模型 | +| `qwen3vl-4b` | Qwen/Qwen3-VL-4B-Instruct | 通义千问 3 VL 4B 模型 | +| `qwen3vl-8b` | Qwen/Qwen3-VL-8B-Instruct | 通义千问 3 VL 8B 模型 | +| `qwen3vl-32b` | Qwen/Qwen3-VL-32B-Instruct | 通义千问 3 VL 32B 模型 | +| `deepseek-ocr` | deepseek-ai/DeepSeek-OCR | DeepSeek OCR 模型 | +| `hunyuan-ocr` | Tencent-Hunyuan/HunyuanOCR | 腾讯混元 OCR 模型 | +| `paddleocr-vl` | PaddlePaddle/PaddleOCR-VL | 百度飞桨 OCR VL 模型 | +| `RMBG2.0` | AI-ModelScope/RMBG-2.0 | RMBG 2.0 背景移除模型 | +| `voxcpm` | OpenBMB/VoxCPM-0.5B | 面壁智能 VoxCPM 0.5B 语音生成模型 | +| `voxcpm1.5` | OpenBMB/VoxCPM1.5 | 面壁智能 VoxCPM 1.5 语音生成模型 | +| `glm-asr-nano-2512` | ZhipuAI/GLM-ASR-Nano-2512 | 智谱 AI ASR Nano 2512 语音识别模型 | +| `fun-asr-nano-2512` | FunAudioLLM/Fun-ASR-Nano-2512 | 通义百聆 ASR Nano 2512 语音识别模型 | + +## 常见使用场景 + +### 场景 1:快速启动推理服务 + +```bash +# 一条命令下载并启动服务 +aha -m qwen3vl-2b +``` + +### 场景 2:使用已有模型启动服务 + +```bash +# 假设模型已下载到 /data/models/Qwen/Qwen3-VL-2B-Instruct +aha serv -m qwen3vl-2b --weight-path /data/models/Qwen/Qwen3-VL-2B-Instruct +``` + +### 场景 3:预先下载模型 + +```bash +# 下载模型到指定目录,稍后使用 +aha download -m qwen3vl-2b -s /data/models + +# 后续启动时直接使用 +aha serv -m qwen3vl-2b --weight-path /data/models/Qwen/Qwen3-VL-2B-Instruct +``` + +### 场景 4:自定义服务端口和地址 + +```bash +# 在 0.0.0.0:8080 启动服务,允许外部访问 +aha -m qwen3vl-2b -a 0.0.0.0 -p 8080 +``` + +## API 接口 + +服务启动后,提供以下 API 接口: + +### 对话接口 +- **端点**: `POST /chat/completions` +- **功能**: 多模态对话和文本生成 +- **支持模型**: Qwen2.5VL, Qwen3, Qwen3VL, DeepSeekOCR, GLM-ASR-Nano-2512, Fun-ASR-Nano-2512 等 +- **格式**: OpenAI Chat Completion 格式 +- **流式支持**: 支持 + +### 图像处理接口 +- **端点**: `POST /images/remove_background` +- **功能**: 图像背景移除 +- **支持模型**: RMBG-2.0 +- **格式**: OpenAI Chat Completion 格式 +- **流式支持**: 不支持 + +### 语音生成接口 +- **端点**: `POST /audio/speech` +- **功能**: 语音合成和生成 +- **支持模型**: VoxCPM, VoxCPM1.5 +- **格式**: OpenAI Chat Completion 格式 +- **流式支持**: 不支持 + +## 向后兼容性 + +为了保持与旧版本的兼容性,以下两种使用方式是等效的: + +```bash +# 新方式(推荐) +aha cli -m qwen3vl-2b + +# 旧方式(向后兼容) +aha -m qwen3vl-2b +``` + +## 注意事项 + +1. **serv 子命令必须指定 `--weight-path`**:由于 `serv` 子命令不下载模型,必须通过 `--weight-path` 指定已下载的模型路径。 + +2. **下载重试机制**:默认重试 3 次,每次失败后等待 2 秒再重试。可通过 `--download-retries` 调整重试次数。 + +3. **默认保存目录**:模型默认保存到 `~/.aha/` 目录下,可通过 `--save-dir` 或 `-d` 参数自定义。 + +4. **端口占用**:启动服务前确保指定的端口未被占用,默认端口为 10100。 + +5. **权限问题**:如果保存到系统目录(如 `/data/models`),确保有相应的写入权限。 + +## 获取帮助 + +```bash +# 查看主帮助 +aha --help + +# 查看子命令帮助 +aha cli --help +aha serv --help +aha download --help + +# 查看版本信息 +aha --version +``` diff --git a/src/main.rs b/src/main.rs index be1118e..43fc48a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{net::IpAddr, str::FromStr, time::Duration}; use aha::{models::WhichModel, utils::get_default_save_dir}; -use clap::Parser; +use clap::{Args, Parser, Subcommand}; use modelscope::ModelScope; use rocket::{ Config, @@ -14,26 +14,109 @@ use crate::api::init; mod api; #[derive(Parser, Debug)] +#[command(name = "aha")] #[command(version, about, long_about = None)] -struct Args { +struct Cli { + /// Service listen address #[arg(short, long, default_value = "127.0.0.1")] - address: String, - - #[arg(short, long, default_value_t = 10100)] - port: u16, + address: Option, + /// Service listen port #[arg(short, long)] - model: WhichModel, + port: Option, + /// Model type (required for backward compatibility) + #[arg(short, long)] + model: Option, + + /// Local model weight path #[arg(long)] weight_path: Option, + /// Model download save directory #[arg(long)] save_dir: Option, + /// Download retry count + #[arg(long)] + download_retries: Option, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Download model and start service (default) + Cli(CliArgs), + /// Start service only (requires --weight-path) + Serv(ServArgs), + /// Download model only + Download(DownloadArgs), +} + +/// Common/shared arguments for server operations +#[derive(Args, Debug)] +struct CommonArgs { + /// Service listen address + #[arg(short, long, default_value = "127.0.0.1")] + address: String, + + /// Service listen port + #[arg(short, long, default_value_t = 10100)] + port: u16, + + /// Model type (required) + #[arg(short, long)] + model: WhichModel, +} + +/// Arguments for the 'cli' subcommand (download + serve) +#[derive(Args, Debug)] +struct CliArgs { + #[command(flatten)] + common: CommonArgs, + + /// Local model weight path (skip download if provided) + #[arg(long)] + weight_path: Option, + + /// Model download save directory + #[arg(long)] + save_dir: Option, + + /// Download retry count #[arg(long)] download_retries: Option, } + +/// Arguments for the 'serv' subcommand (serve only) +#[derive(Args, Debug)] +struct ServArgs { + #[command(flatten)] + common: CommonArgs, + + /// Local model weight path (required) + #[arg(long, required = true)] + weight_path: String, +} + +/// Arguments for the 'download' subcommand (download only) +#[derive(Args, Debug)] +struct DownloadArgs { + /// Model type (required) + #[arg(short, long)] + model: WhichModel, + + /// Model download save directory + #[arg(short, long)] + save_dir: Option, + + /// Download retry count + #[arg(long)] + download_retries: Option, +} + async fn download_model(model_id: &str, save_dir: &str, max_retries: u32) -> anyhow::Result<()> { let mut attempts = 0u32; loop { @@ -67,10 +150,9 @@ async fn download_model(model_id: &str, save_dir: &str, max_retries: u32) -> any } } -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - let model_id = match &args.model { +/// Get the ModelScope model ID for a given WhichModel variant +fn get_model_id(model: WhichModel) -> &'static str { + match model { WhichModel::MiniCPM4_0_5B => "OpenBMB/MiniCPM4-0.5B", WhichModel::Qwen2_5vl3B => "Qwen/Qwen2.5-VL-3B-Instruct", WhichModel::Qwen2_5vl7B => "Qwen/Qwen2.5-VL-7B-Instruct", @@ -87,30 +169,89 @@ async fn main() -> anyhow::Result<()> { WhichModel::VoxCPM1_5 => "OpenBMB/VoxCPM1.5", WhichModel::GlmASRNano2512 => "ZhipuAI/GLM-ASR-Nano-2512", WhichModel::FunASRNano2512 => "FunAudioLLM/Fun-ASR-Nano-2512", - }; - let model_path = match &args.weight_path { - Some(path) => path.clone(), + } +} + +/// 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 model_id = get_model_id(common.model); + + let model_path = match weight_path { + Some(path) => path, None => { - let save_dir = match &args.save_dir { - Some(dir) => dir.clone(), + let save_dir = match save_dir { + Some(dir) => dir, None => get_default_save_dir().expect("Failed to get home directory"), }; - let max_retries = args.download_retries.unwrap_or(3); + let max_retries = download_retries.unwrap_or(3); download_model(model_id, &save_dir, max_retries).await?; save_dir + "/" + model_id } }; - // println!("-------------------download path: {}", model_path); - init(args.model, model_path)?; - start_http_server(&args).await?; + + init(common.model, model_path)?; + start_http_server(common.address, common.port).await?; Ok(()) } -pub(crate) async fn start_http_server(args: &Args) -> 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; + + init(common.model, weight_path)?; + start_http_server(common.address, common.port).await?; + + Ok(()) +} + +/// 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 model_id = get_model_id(model); + + let save_dir = match save_dir { + Some(dir) => dir, + None => get_default_save_dir().expect("Failed to get home directory"), + }; + let max_retries = download_retries.unwrap_or(3); + + download_model(model_id, &save_dir, max_retries).await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Some(Commands::Cli(args)) => run_cli(args).await, + Some(Commands::Serv(args)) => run_serv(args).await, + Some(Commands::Download(args)) => run_download(args).await, + None => { + // Backward compatibility: when no subcommand is provided, use 'cli' behavior + let model = cli.model.expect("Model is required (use -m or --model)"); + let args = CliArgs { + common: CommonArgs { + address: cli.address.unwrap_or_else(|| "127.0.0.1".to_string()), + port: cli.port.unwrap_or(10100), + model, + }, + weight_path: cli.weight_path, + save_dir: cli.save_dir, + download_retries: cli.download_retries, + }; + run_cli(args).await + } + } +} + +pub(crate) async fn start_http_server(address: String, port: u16) -> anyhow::Result<()> { let mut builder = rocket::build().configure(Config { - address: IpAddr::from_str(&args.address)?, - port: args.port, + address: IpAddr::from_str(&address)?, + port, limits: Limits::default() .limit("string", ByteUnit::Mebibyte(5)) .limit("json", ByteUnit::Mebibyte(5)) @@ -127,8 +268,4 @@ pub(crate) async fn start_http_server(args: &Args) -> anyhow::Result<()> { builder.launch().await?; Ok(()) -} - -// fn main() { -// println!("Hello, world!"); -// } +} \ No newline at end of file From f010087e97ca540b97436a8b5a05fbc1b698930e Mon Sep 17 00:00:00 2001 From: XiaoYang Date: Wed, 21 Jan 2026 18:41:11 +0800 Subject: [PATCH 2/6] ``` feat(cli): add direct model inference via new run subcommand - Add `aha run` CLI subcommand for direct model inference without HTTP service - Support multiple models including Qwen series, OCR models, ASR models, and voice generation - Implement input/output handling with file path support and auto-generation - Add comprehensive documentation in CLI_USAGE.md with examples - Include performance timing for model loading and inference operations - Add macOS build target to Makefile with Metal support ``` --- CHANGELOG.md | 15 +++--- Makefile | 4 ++ docs/CLI_USAGE.md | 40 ++++++++++++++++ src/exec/deepseek_ocr.rs | 58 +++++++++++++++++++++++ src/exec/fun_asr_nano.rs | 70 ++++++++++++++++++++++++++++ src/exec/glm_asr_nano.rs | 71 ++++++++++++++++++++++++++++ src/exec/hunyuan_ocr.rs | 58 +++++++++++++++++++++++ src/exec/minicpm4.rs | 55 ++++++++++++++++++++++ src/exec/mod.rs | 37 +++++++++++++++ src/exec/paddleocr_vl.rs | 58 +++++++++++++++++++++++ src/exec/qwen2_5vl.rs | 52 +++++++++++++++++++++ src/exec/qwen3.rs | 52 +++++++++++++++++++++ src/exec/qwen3vl.rs | 52 +++++++++++++++++++++ src/exec/rmbg2_0.rs | 72 +++++++++++++++++++++++++++++ src/exec/voxcpm.rs | 54 ++++++++++++++++++++++ src/exec/voxcpm1_5.rs | 59 ++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 99 ++++++++++++++++++++++++++++++++++++++++ 18 files changed, 899 insertions(+), 8 deletions(-) create mode 100644 src/exec/deepseek_ocr.rs create mode 100644 src/exec/fun_asr_nano.rs create mode 100644 src/exec/glm_asr_nano.rs create mode 100644 src/exec/hunyuan_ocr.rs create mode 100644 src/exec/minicpm4.rs create mode 100644 src/exec/mod.rs create mode 100644 src/exec/paddleocr_vl.rs create mode 100644 src/exec/qwen2_5vl.rs create mode 100644 src/exec/qwen3.rs create mode 100644 src/exec/qwen3vl.rs create mode 100644 src/exec/rmbg2_0.rs create mode 100644 src/exec/voxcpm.rs create mode 100644 src/exec/voxcpm1_5.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e4ba4f..196cfc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,17 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added -- **CLI Subcommand Support**: Added three new subcommands for better command organization: - - `aha cli` - Download model and start HTTP service (default, backward compatible) - - `aha serv` - Start HTTP service only (requires `--weight-path`) - - `aha download` - Download model only (no service start) +- **CLI `run` Subcommand**: Direct model inference from CLI without HTTP service overhead: + - `aha run` - Run model inference directly + - `-m, --model ` - Specify which model to use + - `-in, --input ` - Input text or file path (model-specific interpretation) + - `-out, --output ` - Output file path (optional, auto-generated if not specified) + - `--weight-path ` - Local model weight path (required) + ### Changed diff --git a/Makefile b/Makefile index 6369fb8..a69a21e 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ build: @echo "Building project..." @cargo build +build_mac: + @echo "Building project for macOS..." + @cargo build --features metal --release + test: @echo "Running tests..." @cargo test diff --git a/docs/CLI_USAGE.md b/docs/CLI_USAGE.md index 85e54d8..80d0476 100644 --- a/docs/CLI_USAGE.md +++ b/docs/CLI_USAGE.md @@ -59,6 +59,46 @@ aha cli -m qwen3vl-2b --weight-path /path/to/model aha -m qwen3vl-2b ``` +### run - 直接模型推理 + +直接运行模型推理,无需启动 HTTP 服务。适用于一次性推理任务或批处理。 + +**语法:** +```bash +aha run [OPTIONS] --model --input --weight-path +``` + +**选项:** + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `-m, --model ` | 模型类型(必选) | - | +| `-in, --input ` | 输入文本或文件路径(模型特定解释) | - | +| `-out, --output ` | 输出文件路径(可选,未指定则自动生成) | - | +| `--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 -in "file://./input.txt" --weight-path /path/to/model + +# MiniCPM4 文本生成 +aha run -m minicpm4-0.5b -in "你好" --weight-path /path/to/model + +# DeepSeek OCR 图片识别 +aha run -m deepseek-ocr -in "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 + +# GLM-ASR 语音识别 +aha run -m glm-asr-nano-2512 -in "audio.wav" -in "请转写这段音频" --weight-path /path/to/model +``` + ### serv - 启动服务 仅启动 HTTP 服务,不下载模型。必须通过 `--weight-path` 指定本地模型路径。 diff --git a/src/exec/deepseek_ocr.rs b/src/exec/deepseek_ocr.rs new file mode 100644 index 0000000..1449633 --- /dev/null +++ b/src/exec/deepseek_ocr.rs @@ -0,0 +1,58 @@ +//! DeepSeek-OCR exec implementation for CLI `run` subcommand + +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() + } else { + format!("file://{}", input) + }; + + let i_start = Instant::now(); + let mut model = DeepseekOCRGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "model": "deepseek-ocr", + "messages": [ + {{ + "role": "user", + "content": [ + {{ + "type": "image", + "image_url": {{ + "url": "{}" + }} + }} + ] + }} + ] + }}"#, + input_path + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/fun_asr_nano.rs b/src/exec/fun_asr_nano.rs new file mode 100644 index 0000000..02fe650 --- /dev/null +++ b/src/exec/fun_asr_nano.rs @@ -0,0 +1,70 @@ +//! Fun-ASR-Nano-2512 exec implementation for CLI `run` subcommand + +use crate::exec::ExecModel; +use crate::models::{GenerateModel, fun_asr_nano::generate::FunAsrNanoGenerateModel}; +use anyhow::{Ok, Result}; +use std::time::Instant; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut model = FunAsrNanoGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + 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() + } else { + format!("file://{}", input) + }; + + let message = format!( + r#"{{ + "model": "fun-asr-nano", + "messages": [ + {{ + "role": "user", + "content": [ + {{ + "type": "audio", + "audio_url": {{ + "url": "{}" + }} + }}, + {{ + "type": "text", + "text": "{}" + }} + ] + }} + ] + }}"#, + input_url, target_text + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let res = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", res); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", res))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/glm_asr_nano.rs b/src/exec/glm_asr_nano.rs new file mode 100644 index 0000000..d21b0c8 --- /dev/null +++ b/src/exec/glm_asr_nano.rs @@ -0,0 +1,71 @@ +//! GLM-ASR-Nano-2512 exec implementation for CLI `run` subcommand + +use crate::exec::ExecModel; +use crate::models::{GenerateModel, glm_asr_nano::generate::GlmAsrNanoGenerateModel}; +use anyhow::{Ok, Result}; +use std::time::Instant; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut model = GlmAsrNanoGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + // 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() + } else { + format!("file://{}", input) + }; + + let message = format!( + r#"{{ + "model": "glm-asr-nano", + "messages": [ + {{ + "role": "user", + "content": [ + {{ + "type": "audio", + "audio_url": {{ + "url": "{}" + }} + }}, + {{ + "type": "text", + "text": "{}" + }} + ] + }} + ] + }}"#, + input_url, target_text + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let res = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", res); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", res))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/hunyuan_ocr.rs b/src/exec/hunyuan_ocr.rs new file mode 100644 index 0000000..f632836 --- /dev/null +++ b/src/exec/hunyuan_ocr.rs @@ -0,0 +1,58 @@ +//! Hunyuan-OCR exec implementation for CLI `run` subcommand + +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() + } else { + format!("file://{}", input) + }; + + let i_start = Instant::now(); + let mut model = HunyuanOCRGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "model": "hunyuan-ocr", + "messages": [ + {{ + "role": "user", + "content": [ + {{ + "type": "image", + "image_url": {{ + "url": "{}" + }} + }} + ] + }} + ] + }}"#, + input_path + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/minicpm4.rs b/src/exec/minicpm4.rs new file mode 100644 index 0000000..880a56c --- /dev/null +++ b/src/exec/minicpm4.rs @@ -0,0 +1,55 @@ +//! MiniCPM4-0.5B exec implementation for CLI `run` subcommand + +use crate::exec::ExecModel; +use crate::models::{GenerateModel, minicpm4::generate::MiniCPMGenerateModel}; +use anyhow::{Ok, Result}; +use std::time::Instant; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut model = MiniCPMGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "temperature": 0.3, + "top_p": 0.8, + "model": "minicpm4", + "messages": [ + {{ + "role": "user", + "content": "{}" + }} + ] + }}"#, + target_text.replace('"', "\\\"") + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + // Print result + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/mod.rs b/src/exec/mod.rs new file mode 100644 index 0000000..76c6e76 --- /dev/null +++ b/src/exec/mod.rs @@ -0,0 +1,37 @@ +//! CLI exec module for direct model inference +//! +//! This module provides model-specific exec implementations for the `run` subcommand. +//! Each model has its own exec module that handles input/output parsing and model invocation. + +pub mod deepseek_ocr; +pub mod fun_asr_nano; +pub mod glm_asr_nano; +pub mod hunyuan_ocr; +pub mod minicpm4; +pub mod paddleocr_vl; +pub mod qwen2_5vl; +pub mod qwen3; +pub mod qwen3vl; +pub mod rmbg2_0; +pub mod voxcpm; +pub mod voxcpm1_5; + +use anyhow::Result; + +/// Trait for model exec implementations +/// +/// Each model exec module implements this trait to provide +/// model-specific inference logic for CLI `run` commands. +pub trait ExecModel { + /// Run inference with the given input and output parameters + /// + /// # Arguments + /// * `input` - Input text or file path (interpretation is model-specific) + /// * `output` - Optional output file path (if None, model will auto-generate) + /// * `weight_path` - Path to the model weights + /// + /// # Returns + /// * `Ok(())` on success + /// * `Err(anyhow::Error)` on failure + fn run(input: &str, output: Option<&str>, weight_path: &str) -> Result<()>; +} diff --git a/src/exec/paddleocr_vl.rs b/src/exec/paddleocr_vl.rs new file mode 100644 index 0000000..edc0500 --- /dev/null +++ b/src/exec/paddleocr_vl.rs @@ -0,0 +1,58 @@ +//! PaddleOCR-VL exec implementation for CLI `run` subcommand + +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() + } else { + format!("file://{}", input) + }; + + let i_start = Instant::now(); + let mut model = PaddleOCRVLGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "model": "paddleocr-vl", + "messages": [ + {{ + "role": "user", + "content": [ + {{ + "type": "image", + "image_url": {{ + "url": "{}" + }} + }} + ] + }} + ] + }}"#, + input_path + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/qwen2_5vl.rs b/src/exec/qwen2_5vl.rs new file mode 100644 index 0000000..94848f3 --- /dev/null +++ b/src/exec/qwen2_5vl.rs @@ -0,0 +1,52 @@ +//! Qwen2.5VL-3B exec implementation for CLI `run` subcommand + +use crate::exec::ExecModel; +use crate::models::{GenerateModel, qwen2_5vl::generate::Qwen2_5VLGenerateModel}; +use anyhow::{Ok, Result}; +use std::time::Instant; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut model = Qwen2_5VLGenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "model": "qwen2.5vl", + "messages": [ + {{ + "role": "user", + "content": "{}" + }} + ] + }}"#, + target_text.replace('"', "\\\"") + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/qwen3.rs b/src/exec/qwen3.rs new file mode 100644 index 0000000..3abe628 --- /dev/null +++ b/src/exec/qwen3.rs @@ -0,0 +1,52 @@ +//! Qwen3-0.6B exec implementation for CLI `run` subcommand + +use crate::exec::ExecModel; +use crate::models::{GenerateModel, qwen3::generate::Qwen3GenerateModel}; +use anyhow::{Ok, Result}; +use std::time::Instant; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut model = Qwen3GenerateModel::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let message = format!( + r#"{{ + "model": "qwen3", + "messages": [ + {{ + "role": "user", + "content": "{}" + }} + ] + }}"#, + target_text.replace('"', "\\\"") + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/qwen3vl.rs b/src/exec/qwen3vl.rs new file mode 100644 index 0000000..5276744 --- /dev/null +++ b/src/exec/qwen3vl.rs @@ -0,0 +1,52 @@ +//! Qwen3VL-2B exec implementation for CLI `run` subcommand + +use crate::exec::ExecModel; +use crate::models::{GenerateModel, qwen3vl::generate::Qwen3VLGenerateModel}; +use anyhow::{Ok, Result}; +use std::time::Instant; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + 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#"{{ + "model": "qwen3vl", + "messages": [ + {{ + "role": "user", + "content": "{}" + }} + ] + }}"#, + target_text.replace('"', "\\\"") + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + println!("Result: {:?}", result); + + if let Some(out) = output { + std::fs::write(out, format!("{:?}", result))?; + println!("Output saved to: {}", out); + } + + Ok(()) + } +} diff --git a/src/exec/rmbg2_0.rs b/src/exec/rmbg2_0.rs new file mode 100644 index 0000000..86488f3 --- /dev/null +++ b/src/exec/rmbg2_0.rs @@ -0,0 +1,72 @@ +//! RMBG2.0 exec implementation for CLI `run` subcommand + +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() + } else { + format!("file://{}", input) + }; + + let i_start = Instant::now(); + let model = RMBG2_0Model::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + // Create ChatCompletionParameters for image background removal + let message = format!( + r#"{{ + "model": "rmbg2.0", + "messages": [ + {{ + "role": "user", + "content": [ + {{ + "type": "image", + "image_url": {{ + "url": "{}" + }} + }} + ] + }} + ] + }}"#, + input_path + ); + let mes = serde_json::from_str(&message)?; + + let i_start = Instant::now(); + let result = model.inference(mes)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + let output_path = if let Some(out) = output { + out.to_string() + } else { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + format!("rmbg_{}.png", timestamp) + }; + + // Save all result images + for (i, img) in result.iter().enumerate() { + let path = if result.len() == 1 { + output_path.clone() + } else { + format!("{}_{}.png", output_path.trim_end_matches(".png"), i) + }; + img.save(&path)?; + println!("Output saved to: {}", path); + } + + Ok(()) + } +} diff --git a/src/exec/voxcpm.rs b/src/exec/voxcpm.rs new file mode 100644 index 0000000..a509cf1 --- /dev/null +++ b/src/exec/voxcpm.rs @@ -0,0 +1,54 @@ +//! 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; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut voxcpm_generate = VoxCPMGenerate::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + 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 + 2, + 100, // max_len (voxcpm uses 100 vs voxcpm1.5's 4096) + 10, + 2.0, + 6.0, + )?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + let output_path = if let Some(out) = output { + out.to_string() + } else { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + format!("voxcpm_{}.wav", timestamp) + }; + + let sample_rate = voxcpm_generate.sample_rate(); + crate::utils::audio_utils::save_wav(&audio, &output_path, sample_rate as u32)?; + + println!("Output saved to: {}", output_path); + + Ok(()) + } +} diff --git a/src/exec/voxcpm1_5.rs b/src/exec/voxcpm1_5.rs new file mode 100644 index 0000000..6e1e400 --- /dev/null +++ b/src/exec/voxcpm1_5.rs @@ -0,0 +1,59 @@ +//! VoxCPM1.5 exec implementation for CLI `run` subcommand +//! +//! This module handles VoxCPM1.5 model inference for direct CLI execution. +//! Input/output parameter interpretation is handled here as per the design: +//! - 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; + +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..]; + std::fs::read_to_string(path)? + } else { + input.to_string() + }; + + let i_start = Instant::now(); + let mut voxcpm_generate = VoxCPMGenerate::init(weight_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + 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 + 2, + 4096, + 10, + 2.0, + 6.0, + )?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + + let output_path = if let Some(out) = output { + out.to_string() + } else { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + format!("voxcpm1_5_{}.wav", timestamp) + }; + + let sample_rate = voxcpm_generate.sample_rate(); + crate::utils::audio_utils::save_wav(&audio, &output_path, sample_rate as u32)?; + + println!("Output saved to: {}", output_path); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 49d7fff..52d096c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod chat_template; +pub mod exec; pub mod models; pub mod position_embed; pub mod tokenizer; diff --git a/src/main.rs b/src/main.rs index 43fc48a..c2fccbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,8 @@ enum Commands { Serv(ServArgs), /// Download model only Download(DownloadArgs), + /// Run model inference directly + Run(RunArgs), } /// Common/shared arguments for server operations @@ -117,6 +119,26 @@ struct DownloadArgs { download_retries: Option, } +/// Arguments for the 'run' subcommand (direct inference) +#[derive(Args, Debug)] +struct RunArgs { + /// Model type (required) + #[arg(short, long)] + model: WhichModel, + + /// Input text or file path + #[arg(short, long)] + input: String, + + /// Output file path (optional) + #[arg(short, long)] + output: Option, + + /// Local model weight path (required) + #[arg(long, required = true)] + weight_path: String, +} + async fn download_model(model_id: &str, save_dir: &str, max_retries: u32) -> anyhow::Result<()> { let mut attempts = 0u32; loop { @@ -222,6 +244,82 @@ async fn run_download(args: DownloadArgs) -> anyhow::Result<()> { Ok(()) } +/// Run the 'run' subcommand: direct model inference +fn run_run(args: RunArgs) -> anyhow::Result<()> { + use aha::exec::ExecModel; + + let RunArgs { model, input, output, weight_path } = args; + + match model { + WhichModel::MiniCPM4_0_5B => { + use aha::exec::minicpm4::MiniCPM4Exec; + MiniCPM4Exec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen2_5vl3B => { + use aha::exec::qwen2_5vl::Qwen2_5vlExec; + Qwen2_5vlExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen2_5vl7B => { + use aha::exec::qwen2_5vl::Qwen2_5vlExec; + Qwen2_5vlExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen3_0_6B => { + use aha::exec::qwen3::Qwen3Exec; + Qwen3Exec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen3vl2B => { + use aha::exec::qwen3vl::Qwen3vlExec; + Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen3vl4B => { + use aha::exec::qwen3vl::Qwen3vlExec; + Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen3vl8B => { + use aha::exec::qwen3vl::Qwen3vlExec; + Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::Qwen3vl32B => { + use aha::exec::qwen3vl::Qwen3vlExec; + Qwen3vlExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::DeepSeekOCR => { + use aha::exec::deepseek_ocr::DeepSeekORExec; + DeepSeekORExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::HunyuanOCR => { + use aha::exec::hunyuan_ocr::HunyuanORExec; + HunyuanORExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::PaddleOCRVL => { + use aha::exec::paddleocr_vl::PaddleOVLExec; + PaddleOVLExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::RMBG2_0 => { + use aha::exec::rmbg2_0::RMBG2_0Exec; + RMBG2_0Exec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::VoxCPM => { + use aha::exec::voxcpm::VoxCPMExec; + VoxCPMExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::VoxCPM1_5 => { + use aha::exec::voxcpm1_5::VoxCPM1_5Exec; + VoxCPM1_5Exec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::GlmASRNano2512 => { + use aha::exec::glm_asr_nano::GlmASRNanoExec; + GlmASRNanoExec::run(&input, output.as_deref(), &weight_path)?; + } + WhichModel::FunASRNano2512 => { + use aha::exec::fun_asr_nano::FunASRNanoExec; + FunASRNanoExec::run(&input, output.as_deref(), &weight_path)?; + } + } + + Ok(()) +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -230,6 +328,7 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Cli(args)) => run_cli(args).await, Some(Commands::Serv(args)) => run_serv(args).await, Some(Commands::Download(args)) => run_download(args).await, + Some(Commands::Run(args)) => run_run(args), None => { // Backward compatibility: when no subcommand is provided, use 'cli' behavior let model = cli.model.expect("Model is required (use -m or --model)"); From da6229abee0aba3f0d17958eb3def29bfffaa89e Mon Sep 17 00:00:00 2001 From: XiaoYang Date: Thu, 22 Jan 2026 17:09:03 +0800 Subject: [PATCH 3/6] ## feat(cli): add **list** subcommand to show supported models - Add 'list' subcommand to display all available models with their ModelScope IDs - Implement run_list() function to print formatted table of models - Update main() to handle the new List command variant - Add ValueEnum import for clap functionality style(models): hide internal model variants from CLI help - Add hide = true attribute to all WhichModel enum variants to prevent showing internal model names in CLI help output - Keep the functionality intact while cleaning up user interface --- src/main.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/models/mod.rs | 32 ++++++++++++++++---------------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index c2fccbc..6be5f4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{net::IpAddr, str::FromStr, time::Duration}; use aha::{models::WhichModel, utils::get_default_save_dir}; -use clap::{Args, Parser, Subcommand}; +use clap::{Args, Parser, Subcommand, ValueEnum}; use modelscope::ModelScope; use rocket::{ Config, @@ -55,6 +55,8 @@ enum Commands { Download(DownloadArgs), /// Run model inference directly Run(RunArgs), + /// List all supported models + List, } /// Common/shared arguments for server operations @@ -194,6 +196,41 @@ fn get_model_id(model: WhichModel) -> &'static str { } } +/// List all supported models +fn run_list() -> anyhow::Result<()> { + let models = [ + WhichModel::MiniCPM4_0_5B, + WhichModel::Qwen2_5vl3B, + WhichModel::Qwen2_5vl7B, + WhichModel::Qwen3_0_6B, + WhichModel::Qwen3vl2B, + WhichModel::Qwen3vl4B, + WhichModel::Qwen3vl8B, + WhichModel::Qwen3vl32B, + WhichModel::DeepSeekOCR, + WhichModel::HunyuanOCR, + WhichModel::PaddleOCRVL, + WhichModel::RMBG2_0, + WhichModel::VoxCPM, + WhichModel::VoxCPM1_5, + WhichModel::GlmASRNano2512, + WhichModel::FunASRNano2512, + ]; + + println!("Available models:"); + println!(); + println!("{:<30} {}", "Model Name", "ModelScope ID"); + println!("{}", "-".repeat(80)); + for model in models { + let possible_value = model.to_possible_value().unwrap(); + let name = possible_value.get_name(); + let id = get_model_id(model); + println!("{:<30} {}", name, id); + } + + Ok(()) +} + /// 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; @@ -329,6 +366,7 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Serv(args)) => run_serv(args).await, Some(Commands::Download(args)) => run_download(args).await, Some(Commands::Run(args)) => run_run(args), + Some(Commands::List) => run_list(), None => { // Backward compatibility: when no subcommand is provided, use 'cli' behavior let model = cli.model.expect("Model is required (use -m or --model)"); diff --git a/src/models/mod.rs b/src/models/mod.rs index 4165669..d5a135a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -29,37 +29,37 @@ use crate::models::{ #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum WhichModel { - #[value(name = "minicpm4-0.5b")] + #[value(name = "minicpm4-0.5b", hide = true)] MiniCPM4_0_5B, - #[value(name = "qwen2.5vl-3b")] + #[value(name = "qwen2.5vl-3b", hide = true)] Qwen2_5vl3B, - #[value(name = "qwen2.5vl-7b")] + #[value(name = "qwen2.5vl-7b", hide = true)] Qwen2_5vl7B, - #[value(name = "qwen3-0.6b")] + #[value(name = "qwen3-0.6b", hide = true)] Qwen3_0_6B, - #[value(name = "qwen3vl-2b")] + #[value(name = "qwen3vl-2b", hide = true)] Qwen3vl2B, - #[value(name = "qwen3vl-4b")] + #[value(name = "qwen3vl-4b", hide = true)] Qwen3vl4B, - #[value(name = "qwen3vl-8b")] + #[value(name = "qwen3vl-8b", hide = true)] Qwen3vl8B, - #[value(name = "qwen3vl-32b")] + #[value(name = "qwen3vl-32b", hide = true)] Qwen3vl32B, - #[value(name = "deepseek-ocr")] + #[value(name = "deepseek-ocr", hide = true)] DeepSeekOCR, - #[value(name = "hunyuan-ocr")] + #[value(name = "hunyuan-ocr", hide = true)] HunyuanOCR, - #[value(name = "paddleocr-vl")] + #[value(name = "paddleocr-vl", hide = true)] PaddleOCRVL, - #[value(name = "RMBG2.0")] + #[value(name = "RMBG2.0", hide = true)] RMBG2_0, - #[value(name = "voxcpm")] + #[value(name = "voxcpm", hide = true)] VoxCPM, - #[value(name = "voxcpm1.5")] + #[value(name = "voxcpm1.5", hide = true)] VoxCPM1_5, - #[value(name = "glm-asr-nano-2512")] + #[value(name = "glm-asr-nano-2512", hide = true)] GlmASRNano2512, - #[value(name = "fun-asr-nano-2512")] + #[value(name = "fun-asr-nano-2512", hide = true)] FunASRNano2512, } From 292dfd31e4e8a6b9078f4e709b5b97a7133f98da Mon Sep 17 00:00:00 2001 From: jhqxxx <18280426169@163.com> Date: Fri, 30 Jan 2026 22:17:00 +0800 Subject: [PATCH 4/6] merge pr/go-restream/20 --- src/main.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/models/mod.rs | 32 ++++++++++++++++---------------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index c2fccbc..6be5f4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{net::IpAddr, str::FromStr, time::Duration}; use aha::{models::WhichModel, utils::get_default_save_dir}; -use clap::{Args, Parser, Subcommand}; +use clap::{Args, Parser, Subcommand, ValueEnum}; use modelscope::ModelScope; use rocket::{ Config, @@ -55,6 +55,8 @@ enum Commands { Download(DownloadArgs), /// Run model inference directly Run(RunArgs), + /// List all supported models + List, } /// Common/shared arguments for server operations @@ -194,6 +196,41 @@ fn get_model_id(model: WhichModel) -> &'static str { } } +/// List all supported models +fn run_list() -> anyhow::Result<()> { + let models = [ + WhichModel::MiniCPM4_0_5B, + WhichModel::Qwen2_5vl3B, + WhichModel::Qwen2_5vl7B, + WhichModel::Qwen3_0_6B, + WhichModel::Qwen3vl2B, + WhichModel::Qwen3vl4B, + WhichModel::Qwen3vl8B, + WhichModel::Qwen3vl32B, + WhichModel::DeepSeekOCR, + WhichModel::HunyuanOCR, + WhichModel::PaddleOCRVL, + WhichModel::RMBG2_0, + WhichModel::VoxCPM, + WhichModel::VoxCPM1_5, + WhichModel::GlmASRNano2512, + WhichModel::FunASRNano2512, + ]; + + println!("Available models:"); + println!(); + println!("{:<30} {}", "Model Name", "ModelScope ID"); + println!("{}", "-".repeat(80)); + for model in models { + let possible_value = model.to_possible_value().unwrap(); + let name = possible_value.get_name(); + let id = get_model_id(model); + println!("{:<30} {}", name, id); + } + + Ok(()) +} + /// 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; @@ -329,6 +366,7 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Serv(args)) => run_serv(args).await, Some(Commands::Download(args)) => run_download(args).await, Some(Commands::Run(args)) => run_run(args), + Some(Commands::List) => run_list(), None => { // Backward compatibility: when no subcommand is provided, use 'cli' behavior let model = cli.model.expect("Model is required (use -m or --model)"); diff --git a/src/models/mod.rs b/src/models/mod.rs index 4165669..d5a135a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -29,37 +29,37 @@ use crate::models::{ #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum WhichModel { - #[value(name = "minicpm4-0.5b")] + #[value(name = "minicpm4-0.5b", hide = true)] MiniCPM4_0_5B, - #[value(name = "qwen2.5vl-3b")] + #[value(name = "qwen2.5vl-3b", hide = true)] Qwen2_5vl3B, - #[value(name = "qwen2.5vl-7b")] + #[value(name = "qwen2.5vl-7b", hide = true)] Qwen2_5vl7B, - #[value(name = "qwen3-0.6b")] + #[value(name = "qwen3-0.6b", hide = true)] Qwen3_0_6B, - #[value(name = "qwen3vl-2b")] + #[value(name = "qwen3vl-2b", hide = true)] Qwen3vl2B, - #[value(name = "qwen3vl-4b")] + #[value(name = "qwen3vl-4b", hide = true)] Qwen3vl4B, - #[value(name = "qwen3vl-8b")] + #[value(name = "qwen3vl-8b", hide = true)] Qwen3vl8B, - #[value(name = "qwen3vl-32b")] + #[value(name = "qwen3vl-32b", hide = true)] Qwen3vl32B, - #[value(name = "deepseek-ocr")] + #[value(name = "deepseek-ocr", hide = true)] DeepSeekOCR, - #[value(name = "hunyuan-ocr")] + #[value(name = "hunyuan-ocr", hide = true)] HunyuanOCR, - #[value(name = "paddleocr-vl")] + #[value(name = "paddleocr-vl", hide = true)] PaddleOCRVL, - #[value(name = "RMBG2.0")] + #[value(name = "RMBG2.0", hide = true)] RMBG2_0, - #[value(name = "voxcpm")] + #[value(name = "voxcpm", hide = true)] VoxCPM, - #[value(name = "voxcpm1.5")] + #[value(name = "voxcpm1.5", hide = true)] VoxCPM1_5, - #[value(name = "glm-asr-nano-2512")] + #[value(name = "glm-asr-nano-2512", hide = true)] GlmASRNano2512, - #[value(name = "fun-asr-nano-2512")] + #[value(name = "fun-asr-nano-2512", hide = true)] FunASRNano2512, } From 16e3aefaa7ddac207dc306273a12fa1c65ce98d5 Mon Sep 17 00:00:00 2001 From: jhqxxx <18280426169@163.com> Date: Sat, 31 Jan 2026 18:37:28 +0800 Subject: [PATCH 5/6] fix aha run multiple inputs bug --- docs/CLI_USAGE.md | 39 +++++++++++++-------- src/exec/deepseek_ocr.rs | 24 +++++++++---- src/exec/fun_asr_nano.rs | 27 ++++++++++----- src/exec/glm_asr_nano.rs | 27 ++++++++++----- src/exec/hunyuan_ocr.rs | 24 +++++++++---- src/exec/minicpm4.rs | 17 +++++---- src/exec/mod.rs | 2 +- src/exec/paddleocr_vl.rs | 24 +++++++++---- src/exec/qwen2_5vl.rs | 41 +++++++++++++++++----- src/exec/qwen3.rs | 17 +++++---- src/exec/qwen3vl.rs | 74 +++++++++++++++++++++++++++++++++------- src/exec/rmbg2_0.rs | 20 +++++++---- src/exec/voxcpm.rs | 24 +++++++------ src/exec/voxcpm1_5.rs | 22 +++++++----- src/main.rs | 33 +++++++++++++----- src/utils/mod.rs | 16 ++++++++- 16 files changed, 308 insertions(+), 123 deletions(-) diff --git a/docs/CLI_USAGE.md b/docs/CLI_USAGE.md index 80d0476..7a5a0db 100644 --- a/docs/CLI_USAGE.md +++ b/docs/CLI_USAGE.md @@ -65,7 +65,7 @@ aha -m qwen3vl-2b **语法:** ```bash -aha run [OPTIONS] --model --input --weight-path +aha run [OPTIONS] --model --input [--input ] --weight-path ``` **选项:** @@ -73,30 +73,39 @@ aha run [OPTIONS] --model --input --weight-path | 选项 | 说明 | 默认值 | |------|------|--------| | `-m, --model ` | 模型类型(必选) | - | -| `-in, --input ` | 输入文本或文件路径(模型特定解释) | - | -| `-out, --output ` | 输出文件路径(可选,未指定则自动生成) | - | +| `-i, --input ` | 输入文本或文件路径(模型特定解释,支持1-2个参数, input1: 提示文本, input2: 文件地址) | - | +| `-o, --output ` | 输出文件路径(可选,未指定则自动生成) | - | | `--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 - 启动服务 diff --git a/src/exec/deepseek_ocr.rs b/src/exec/deepseek_ocr.rs index 1449633..ae46595 100644 --- a/src/exec/deepseek_ocr.rs +++ b/src/exec/deepseek_ocr.rs @@ -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": "\nConvert the document to markdown. " }} ] }} ] }}"#, - input_path + input_url ); let mes = serde_json::from_str(&message)?; diff --git a/src/exec/fun_asr_nano.rs b/src/exec/fun_asr_nano.rs index 02fe650..1af00fe 100644 --- a/src/exec/fun_asr_nano.rs +++ b/src/exec/fun_asr_nano.rs @@ -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!( diff --git a/src/exec/glm_asr_nano.rs b/src/exec/glm_asr_nano.rs index d21b0c8..70c1d3a 100644 --- a/src/exec/glm_asr_nano.rs +++ b/src/exec/glm_asr_nano.rs @@ -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!( diff --git a/src/exec/hunyuan_ocr.rs b/src/exec/hunyuan_ocr.rs index f632836..3eb5a7b 100644 --- a/src/exec/hunyuan_ocr.rs +++ b/src/exec/hunyuan_ocr.rs @@ -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)?; diff --git a/src/exec/minicpm4.rs b/src/exec/minicpm4.rs index 880a56c..5e36a4f 100644 --- a/src/exec/minicpm4.rs +++ b/src/exec/minicpm4.rs @@ -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(); diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 76c6e76..67d9735 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -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<()>; } diff --git a/src/exec/paddleocr_vl.rs b/src/exec/paddleocr_vl.rs index edc0500..803f0eb 100644 --- a/src/exec/paddleocr_vl.rs +++ b/src/exec/paddleocr_vl.rs @@ -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)?; diff --git a/src/exec/qwen2_5vl.rs b/src/exec/qwen2_5vl.rs index 94848f3..ecaaa2e 100644 --- a/src/exec/qwen2_5vl.rs +++ b/src/exec/qwen2_5vl.rs @@ -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)?; diff --git a/src/exec/qwen3.rs b/src/exec/qwen3.rs index 3abe628..7a7d460 100644 --- a/src/exec/qwen3.rs +++ b/src/exec/qwen3.rs @@ -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(); diff --git a/src/exec/qwen3vl.rs b/src/exec/qwen3vl.rs index 5276744..a336d24 100644 --- a/src/exec/qwen3vl.rs +++ b/src/exec/qwen3vl.rs @@ -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(); diff --git a/src/exec/rmbg2_0.rs b/src/exec/rmbg2_0.rs index 86488f3..a93128a 100644 --- a/src/exec/rmbg2_0.rs +++ b/src/exec/rmbg2_0.rs @@ -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)?; diff --git a/src/exec/voxcpm.rs b/src/exec/voxcpm.rs index a509cf1..7a2e2d3 100644 --- a/src/exec/voxcpm.rs +++ b/src/exec/voxcpm.rs @@ -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, diff --git a/src/exec/voxcpm1_5.rs b/src/exec/voxcpm1_5.rs index 6e1e400..b6c7d6b 100644 --- a/src/exec/voxcpm1_5.rs +++ b/src/exec/voxcpm1_5.rs @@ -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, diff --git a/src/main.rs b/src/main.rs index 6be5f4a..add92ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, /// 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(()) -} \ No newline at end of file +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 32995f0..bc85e95 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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 { path.to_string_lossy().to_string() }) } + +pub fn get_file_path(file: &str) -> Result { + 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) +} From 901324cd1add23fd29517d50fd3461ab11a2e157 Mon Sep 17 00:00:00 2001 From: jhqxxx <18280426169@163.com> Date: Sat, 31 Jan 2026 18:57:42 +0800 Subject: [PATCH 6/6] fix qwen3vl-thinking startswith bug --- src/chat_template/mod.rs | 8 +++---- tests/test_qwen3vl.rs | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/chat_template/mod.rs b/src/chat_template/mod.rs index 48aefb4..0cbf6d1 100644 --- a/src/chat_template/mod.rs +++ b/src/chat_template/mod.rs @@ -19,12 +19,12 @@ pub fn get_template(path: String) -> Result { // 修复模板中的问题行 let fixed_template = chat_template .replace( - "message.content.startswith('')", - "message.content is startingwith('')", // 使用minijinja中的 is startingwith 替换 + "content.startswith('')", + "content is startingwith('')", // 使用minijinja中的 is startingwith 替换 ) .replace( - "message.content.endswith('')", - "message.content is endingwith('')", // 使用minijinja中的 is endingwith 替换 + "content.endswith('')", + "content is endingwith('')", // 使用minijinja中的 is endingwith 替换 ) .replace( "content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n')", diff --git a/tests/test_qwen3vl.rs b/tests/test_qwen3vl.rs index 9c81761..7f0f733 100644 --- a/tests/test_qwen3vl.rs +++ b/tests/test_qwen3vl.rs @@ -5,6 +5,58 @@ use aha_openai_dive::v1::resources::chat::ChatCompletionParameters; use anyhow::Result; use rocket::futures::StreamExt; +#[test] +fn qwen3vl_thinking_generate() -> Result<()> { + // test with cuda: RUST_BACKTRACE=1 cargo test -F cuda qwen3vl_thinking_generate -r -- --nocapture + + let save_dir = + aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?; + let model_path = format!("{}/Qwen/Qwen3-VL-2B-Thinking/", save_dir); + + let message = r#" + { + "model": "qwen3vl-thinking", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image", + "image_url": + { + "url": "file://./assets/img/ocr_test1.png" + } + }, + { + "type": "text", + "text": "请分析图片并提取所有可见文本内容,按从左到右、从上到下的布局,返回纯文本" + } + ] + } + ], + "max_tokens": 10240 + } + "#; + let mes: ChatCompletionParameters = serde_json::from_str(message)?; + let i_start = Instant::now(); + let mut qwen3vl = Qwen3VLGenerateModel::init(&model_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + + let i_start = Instant::now(); + let res = qwen3vl.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("generate: \n {:?}", res); + if res.usage.is_some() { + let num_token = res.usage.as_ref().unwrap().total_tokens; + let duration_secs = i_duration.as_secs_f64(); + let tps = num_token as f64 / duration_secs; + println!("Tokens per second (TPS): {:.2}", tps); + } + println!("Time elapsed in generate is: {:?}", i_duration); + Ok(()) +} + #[test] fn qwen3vl_generate() -> Result<()> { // test with cuda: RUST_BACKTRACE=1 cargo test -F cuda,ffmpeg qwen3vl_generate -r -- --nocapture