diff --git a/docs/cli.md b/docs/cli.md index 9395143..785a0b1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -223,6 +223,37 @@ aha download -m qwen3vl-2b --download-retries 5 aha download -m minicpm4-0.5b -s models ``` +### delete - Delete downloaded model + +Delete a downloaded model from the default location (`~/.aha/{model_id}`). + +**Syntax:** +```bash +aha delete [OPTIONS] --model +``` + +**Options:** + +| Option | Description | Default | +|--------|-------------|---------| +| `-m, --model ` | Model type (required) | - | + +**Examples:** + +```bash +# Delete RMBG2.0 model from default location +aha delete -m rmbg2.0 + +# Delete Qwen3-VL-2B model +aha delete --model qwen3vl-2b +``` + +**Behavior:** +- Displays model information (ID, location, size) before deletion +- Requires confirmation (y/N) before proceeding +- Shows "Model not found" message if the model directory doesn't exist +- Shows "Model deleted successfully" message after completion + ## Supported Models | Model ID | Model Name | Description | diff --git a/docs/cli.zh-CN.md b/docs/cli.zh-CN.md index ad734aa..7ec8f31 100644 --- a/docs/cli.zh-CN.md +++ b/docs/cli.zh-CN.md @@ -223,6 +223,37 @@ aha download -m qwen3vl-2b --download-retries 5 aha download -m minicpm4-0.5b -s models ``` +### delete - 删除已下载的模型 + +删除默认位置(`~/.aha/{model_id}`)的已下载模型。 + +**语法:** +```bash +aha delete [OPTIONS] --model +``` + +**选项:** + +| 选项 | 说明 | 默认值 | +|------|------|--------| +| `-m, --model ` | 模型类型(必选) | - | + +**示例:** + +```bash +# 删除 RMBG2.0 模型 +aha delete -m rmbg2.0 + +# 删除 Qwen3-VL-2B 模型 +aha delete --model qwen3vl-2b +``` + +**行为说明:** +- 删除前会显示模型信息(ID、位置、大小) +- 需要用户确认(y/N)才会执行删除 +- 如果模型目录不存在,显示"模型未找到"消息 +- 删除完成后显示"删除成功"消息 + ## 支持的模型 | 模型标识 | 模型名称 | 说明 | diff --git a/src/main.rs b/src/main.rs index d093419..46bbffd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,8 @@ enum Commands { Serv(ServArgs), /// List all running aha services Ps(ServListArgs), + /// Delete a downloaded model from the default location (~/.aha/{model_id}) + Delete(DeleteArgs), /// Download model only Download(DownloadArgs), /// Run model inference directly @@ -158,6 +160,14 @@ struct RunArgs { weight_path: Option, } +/// Arguments for the 'delete' subcommand (delete model from default location) +#[derive(Args, Debug)] +struct DeleteArgs { + /// Model type (required) + #[arg(short, long)] + model: WhichModel, +} + /// Get the default weight path for a given model /// Returns ~/.aha/{model_id} e.g., ~/.aha/OpenBMB/VoxCPM1.5 fn get_default_weight_path(model: WhichModel) -> String { @@ -408,6 +418,94 @@ fn run_run(args: RunArgs) -> anyhow::Result<()> { Ok(()) } +/// Run the 'delete' subcommand: delete model from default location +fn run_delete(args: DeleteArgs) -> anyhow::Result<()> { + let DeleteArgs { model } = args; + let model_id = model.model_id(); + let save_dir = get_default_save_dir().expect("Failed to get home directory"); + let model_path = format!("{}/{}", save_dir, model_id); + + let path = std::path::Path::new(&model_path); + + if !path.exists() { + println!("Model not found: {} does not exist", model_path); + return Ok(()); + } + + // Show model info + println!("Model ID: {}", model_id); + println!("Location: {}", model_path); + + // Calculate size if possible + if let Ok(metadata) = std::fs::metadata(path) { + if metadata.is_dir() { + if let Ok(total_size) = dir_size(path) { + println!("Size: {}", bytes_to_human(total_size)); + } + } + } + + // Confirm deletion + print!("Are you sure you want to delete this model? (y/N): "); + use std::io::Write; + std::io::stdout().flush()?; + + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + + let input = input.trim().to_lowercase(); + if input != "y" && input != "yes" { + println!("Deletion cancelled."); + return Ok(()); + } + + // Delete the directory + std::fs::remove_dir_all(path)?; + + println!("Model deleted successfully: {}", model_path); + + Ok(()) +} + +/// Calculate total size of a directory recursively +fn dir_size(path: &std::path::Path) -> anyhow::Result { + let mut total = 0; + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let entry_path = entry.path(); + if entry_path.is_dir() { + total += dir_size(&entry_path)?; + } else { + total += entry.metadata()?.len(); + } + } + } else { + total = std::fs::metadata(path)?.len(); + } + Ok(total) +} + +/// Convert bytes to human readable format +fn bytes_to_human(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + const TB: u64 = GB * 1024; + + if bytes >= TB { + format!("{:.2} TB", bytes as f64 / TB as f64) + } else if bytes >= GB { + format!("{:.2} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.2} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.2} KB", bytes as f64 / KB as f64) + } else { + format!("{} B", bytes) + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -416,6 +514,7 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Cli(args)) => run_cli(args).await, Some(Commands::Serv(args)) => run_serv(args).await, Some(Commands::Ps(args)) => run_ps(args), + Some(Commands::Delete(args)) => run_delete(args), Some(Commands::Download(args)) => run_download(args).await, Some(Commands::Run(args)) => run_run(args), Some(Commands::List) => run_list(),