whisper-macos-cli 0.1.0

Transcribe audio files locally on Apple Silicon via whisper.cpp with Metal GPU acceleration, exposing a strict stdin/stdout JSON contract for AI agents and Unix pipelines.
Documentation
use serde_json::json;

use crate::cli::{ModelsAction, WhisperModel};
use crate::error::Error;
use crate::model::{download, registry, storage};
use crate::output;

pub fn run(action: &ModelsAction, correlation_id: &str) -> Result<(), Error> {
    match action {
        ModelsAction::Download { model } => run_download(*model, correlation_id),
        ModelsAction::List => run_list(correlation_id),
        ModelsAction::Path { model } => run_path(*model, correlation_id),
        ModelsAction::Remove { model, dry_run } => run_remove(*model, *dry_run, correlation_id),
    }
}

fn run_download(name: Option<WhisperModel>, correlation_id: &str) -> Result<(), Error> {
    let model = match name {
        Some(m) => registry::get_model(m.as_str()).ok_or_else(|| Error::ModelNotFound {
            name: m.as_str().to_string(),
        })?,
        None => registry::default_model(),
    };

    if storage::is_model_downloaded(model)? {
        let value = json!({
            "schema_version": env!("CARGO_PKG_VERSION"),
            "correlation_id": correlation_id,
            "model": model.name,
            "action": "already_downloaded",
            "path": null,
        });
        output::write_json_value(&value).map_err(Error::Io)?;
        return Ok(());
    }

    let dest = storage::model_path(model)?;
    tracing::info!(
        model = model.name,
        bytes = model.size_bytes,
        "downloading model"
    );
    let etag = download::download_model(model.url, &dest, model.size_bytes, model.min_size_bytes)?;
    let value = json!({
        "schema_version": env!("CARGO_PKG_VERSION"),
        "correlation_id": correlation_id,
        "model": model.name,
        "action": "downloaded",
        "path": dest.display().to_string(),
        "etag": etag,
    });
    output::write_json_value(&value).map_err(Error::Io)?;
    Ok(())
}

fn run_list(correlation_id: &str) -> Result<(), Error> {
    let models: Vec<serde_json::Value> = registry::all_models()
        .iter()
        .map(|model| {
            let downloaded = storage::is_model_downloaded(model).unwrap_or(false);
            json!({
                "name": model.name,
                "size_bytes": model.size_bytes,
                "min_size_bytes": model.min_size_bytes,
                "downloaded": downloaded,
                "description": model.description,
            })
        })
        .collect();

    let envelope = json!({
        "schema_version": env!("CARGO_PKG_VERSION"),
        "correlation_id": correlation_id,
        "models": models,
        "total": models.len(),
    });
    output::write_json_value(&envelope).map_err(Error::Io)
}

fn run_path(name: Option<WhisperModel>, correlation_id: &str) -> Result<(), Error> {
    let model = match name {
        Some(m) => registry::get_model(m.as_str()).ok_or_else(|| Error::ModelNotFound {
            name: m.as_str().to_string(),
        })?,
        None => registry::default_model(),
    };

    let path = storage::model_path(model)?;
    let downloaded = storage::is_model_downloaded(model)?;

    let value = json!({
        "schema_version": env!("CARGO_PKG_VERSION"),
        "correlation_id": correlation_id,
        "model": model.name,
        "path": path.display().to_string(),
        "downloaded": downloaded,
    });
    output::write_json_value(&value).map_err(Error::Io)
}

fn run_remove(name: WhisperModel, dry_run: bool, correlation_id: &str) -> Result<(), Error> {
    let model = registry::get_model(name.as_str()).ok_or_else(|| Error::ModelNotFound {
        name: name.as_str().to_string(),
    })?;

    let path = storage::model_path(model)?;

    if dry_run {
        let value = json!({
            "schema_version": env!("CARGO_PKG_VERSION"),
            "correlation_id": correlation_id,
            "model": model.name,
            "action": "would_remove",
            "path": path.display().to_string(),
            "size_bytes": model.size_bytes,
        });
        output::write_json_value(&value).map_err(Error::Io)?;
        return Ok(());
    }

    storage::remove_model(model)?;
    let value = json!({
        "schema_version": env!("CARGO_PKG_VERSION"),
        "correlation_id": correlation_id,
        "model": model.name,
        "action": "removed",
        "path": path.display().to_string(),
    });
    output::write_json_value(&value).map_err(Error::Io)
}