whisper-macos-cli 0.1.2

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 std::process::Command;

const AFTER_HELP: &str = "\
EXAMPLES:
  whisper-macos-cli transcribe voice.ogg
  whisper-macos-cli transcribe --model base --language pt audio.mp3
  whisper-macos-cli transcribe --timestamps --ndjson *.ogg
  cat audio.wav | whisper-macos-cli transcribe
  whisper-macos-cli models download base
  whisper-macos-cli doctor

ENVIRONMENT:
  WHISPER_MODEL       Override default model (e.g. base, small, medium)
  NO_COLOR            Disable colored output (see https://no-color.org)
  RUST_LOG            Override tracing log level filter

EXIT STATUS:
  0     Success
  2     Usage error (invalid arguments)
  64    No input provided
  65    Invalid input data (corrupt audio, unsupported format)
  66    Input file not found
  69    Service unavailable (download failed, unsupported platform)
  70    Internal error (whisper inference failed)
  74    I/O error
  78    Configuration error (model not found)
  130   Interrupted (SIGINT / Ctrl+C)
  141   Broken pipe (SIGPIPE)

FILES:
  ~/Library/Application Support/whisper-macos-cli/models/
      Downloaded Whisper model files (ggml-*.bin)

SEE ALSO:
  Project:  https://github.com/daniloaguiarbr/whisper-macos-cli
  whisper.cpp: https://github.com/ggerganov/whisper.cpp

BUGS:
  Report bugs at https://github.com/daniloaguiarbr/whisper-macos-cli/issues";

fn main() {
    set_git_env();
    generate_manpages();
}

fn set_git_env() {
    println!("cargo:rerun-if-changed=.git/HEAD");
    println!("cargo:rerun-if-changed=.git/refs");

    let sha = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .ok()
        .filter(|o| o.status.success())
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .unwrap_or_else(|| "unknown".to_string());

    println!("cargo:rustc-env=GIT_SHA={}", sha.trim());
    println!(
        "cargo:rustc-env=TARGET={}",
        std::env::var("TARGET").unwrap_or_default()
    );

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| {
            let secs = d.as_secs();
            let days = secs / 86400;
            let years = 1970 + days / 365;
            let remaining = (days % 365) as u32;
            let months = remaining / 30 + 1;
            let day = remaining % 30 + 1;
            format!("{years}-{months:02}-{day:02}")
        })
        .unwrap_or_else(|_| "unknown".to_string());
    println!("cargo:rustc-env=BUILD_DATE={now}");
}

fn generate_manpages() {
    let out_dir = match std::env::var_os("OUT_DIR") {
        Some(dir) => std::path::PathBuf::from(dir),
        None => return,
    };

    let man_dir = out_dir.join("man");
    std::fs::create_dir_all(&man_dir).ok();

    let cmd = build_command();
    clap_mangen::generate_to(cmd, &man_dir).ok();
}

fn build_command() -> clap::Command {
    use clap::Arg;

    clap::Command::new("whisper-macos-cli")
        .about("macOS-exclusive audio transcription CLI via whisper.cpp with Metal GPU")
        .long_about(
            "Transcribes audio files to text using OpenAI Whisper models with Apple \
             Silicon Metal GPU acceleration. Outputs structured JSON to stdout for \
             seamless integration with AI agents and Unix pipelines.",
        )
        .version(env!("CARGO_PKG_VERSION"))
        .author("Danilo Teixeira")
        .after_help(AFTER_HELP)
        .subcommand(build_transcribe_command())
        .subcommand(build_models_command())
        .subcommand(clap::Command::new("doctor").about("Check system environment"))
        .subcommand(clap::Command::new("schema").about("Print JSON schema of output"))
        .subcommand(
            clap::Command::new("completions")
                .about("Generate shell completions")
                .arg(
                    Arg::new("shell")
                        .help("Shell to generate completions for")
                        .required(true),
                ),
        )
}

fn build_transcribe_command() -> clap::Command {
    use clap::{Arg, ArgAction};

    clap::Command::new("transcribe")
        .about("Transcribe audio files to text")
        .arg(
            Arg::new("files")
                .help("Audio files to transcribe (reads stdin if omitted and not a TTY)")
                .num_args(0..),
        )
        .arg(
            Arg::new("language")
                .short('l')
                .long("language")
                .value_name("LANG")
                .help("Language for transcription (e.g. pt, en, es, auto)"),
        )
        .arg(
            Arg::new("model")
                .short('m')
                .long("model")
                .value_name("MODEL")
                .help("Whisper model to use [tiny, base, small, medium, large-v3]")
                .default_value("large-v3"),
        )
        .arg(
            Arg::new("beam-size")
                .long("beam-size")
                .value_name("N")
                .help("Beam size for BeamSearch decoding [1-16]")
                .default_value("8"),
        )
        .arg(
            Arg::new("timestamps")
                .long("timestamps")
                .help("Include timestamped segments in output")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("ndjson")
                .long("ndjson")
                .help("Emit NDJSON (one JSON object per line per file)")
                .action(ArgAction::SetTrue),
        )
        .arg(
            Arg::new("vad-threshold")
                .long("vad-threshold")
                .value_name("FLOAT")
                .help("VAD threshold [0.0-1.0]")
                .default_value("0.5"),
        )
        .arg(
            Arg::new("concurrency")
                .long("concurrency")
                .value_name("N")
                .help("Maximum parallel transcriptions [1-32]")
                .default_value("2"),
        )
        .arg(
            Arg::new("input-format")
                .long("input-format")
                .value_name("FMT")
                .help("Force input audio format (ogg, mp3, wav, flac)"),
        )
}

fn build_models_command() -> clap::Command {
    clap::Command::new("models")
        .about("Manage Whisper models")
        .subcommand(
            clap::Command::new("download")
                .about("Download a model")
                .arg(
                    clap::Arg::new("model")
                        .value_name("MODEL")
                        .help("Model name (default: large-v3)"),
                ),
        )
        .subcommand(clap::Command::new("list").about("List available and downloaded models"))
        .subcommand(
            clap::Command::new("path")
                .about("Show model file path")
                .arg(
                    clap::Arg::new("model")
                        .value_name("MODEL")
                        .help("Model name (default: large-v3)"),
                ),
        )
        .subcommand(
            clap::Command::new("remove")
                .about("Remove a downloaded model")
                .arg(
                    clap::Arg::new("model")
                        .value_name("MODEL")
                        .help("Model name to remove")
                        .required(true),
                ),
        )
}