use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint};
pub fn long_version() -> &'static str {
concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("GIT_SHA"),
" ",
env!("BUILD_DATE"),
" ",
env!("TARGET"),
")"
)
}
#[derive(Debug, Parser)]
#[command(
name = "whisper-macos-cli",
version,
long_version = long_version(),
propagate_version = true,
arg_required_else_help = true,
max_term_width = 100,
after_help = "\
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
whisper-macos-cli commands --format json
ENVIRONMENT:
WHISPER_MODEL Override default model (e.g. base, small, medium)
WHISPER_LANGUAGE Override default language (e.g. pt, en, es, auto)
NO_COLOR Disable colored output (see https://no-color.org)
CI Disable all interactive prompts when set to true
RUST_LOG Override tracing log level filter
SOURCE_DATE_EPOCH Unix timestamp for reproducible builds
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)
143 Terminated (SIGTERM)
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/ggml-org/whisper.cpp
BUGS:
Report bugs at https://github.com/daniloaguiarbr/whisper-macos-cli/issues"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(long, global = true, env = "QUIET")]
pub quiet: bool,
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long, global = true)]
pub print_schema: bool,
#[arg(long, global = true)]
pub print_config: bool,
#[arg(long, global = true, env = "NO_INPUT")]
pub no_input: bool,
#[arg(long, global = true, value_name = "WHEN", default_value = "auto")]
pub color: ColorChoice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ColorChoice {
Auto,
Always,
Never,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Transcribe(TranscribeArgs),
Models {
#[command(subcommand)]
action: ModelsAction,
},
Doctor,
Schema,
Config,
Completions {
#[arg(value_name = "SHELL")]
shell: clap_complete::Shell,
},
Commands {
#[arg(long, value_name = "FMT", default_value = "json")]
format: CommandsFormat,
},
Init {
#[arg(long, value_name = "DIR", default_value = ".")]
target: PathBuf,
},
Licenses,
Resume {
#[arg(value_name = "WORKFLOW_ID")]
workflow_id: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum CommandsFormat {
Json,
Yaml,
}
#[derive(Debug, Args)]
pub struct TranscribeArgs {
#[arg(value_hint = ValueHint::FilePath)]
pub files: Vec<PathBuf>,
#[arg(
short,
long,
value_name = "LANG",
env = "WHISPER_LANGUAGE",
help_heading = "Transcription"
)]
pub language: Option<String>,
#[arg(
short,
long,
value_name = "MODEL",
env = "WHISPER_MODEL",
default_value = "large-v3",
help_heading = "Transcription"
)]
pub model: WhisperModel,
#[arg(long, value_name = "N", default_value_t = 8, value_parser = parse_beam_size, help_heading = "Transcription")]
pub beam_size: i32,
#[arg(long, help_heading = "Output")]
pub timestamps: bool,
#[arg(long, help_heading = "Output", conflicts_with = "output_format")]
pub ndjson: bool,
#[arg(long, value_name = "FMT", help_heading = "Output")]
pub output_format: Option<OutputFormat>,
#[arg(long, value_name = "FLOAT", default_value_t = 0.5, value_parser = parse_vad_threshold, help_heading = "Transcription")]
pub vad_threshold: f32,
#[arg(long, value_name = "N", default_value_t = 2, value_parser = parse_concurrency, help_heading = "Transcription")]
pub concurrency: usize,
#[arg(long, value_name = "FMT", help_heading = "Input")]
pub input_format: Option<String>,
#[arg(long, help_heading = "Execution")]
pub dry_run: bool,
#[arg(long, value_name = "SECS", value_parser = parse_timeout_secs, help_heading = "Execution")]
pub timeout: Option<u64>,
#[arg(long, value_name = "N", value_parser = parse_retry_count, help_heading = "Execution")]
pub retry_count: Option<u32>,
#[arg(long, value_name = "SECS", value_parser = parse_retry_elapsed, help_heading = "Execution")]
pub retry_max_elapsed: Option<u64>,
#[arg(long, help_heading = "Execution")]
pub offline: bool,
#[arg(long, value_name = "WORKFLOW_ID", help_heading = "Execution")]
pub resume: Option<String>,
}
impl TranscribeArgs {
pub fn is_ndjson(&self) -> bool {
self.ndjson || matches!(self.output_format, Some(OutputFormat::Ndjson))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Json,
Ndjson,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum WhisperModel {
Tiny,
Base,
Small,
Medium,
#[value(name = "large-v3")]
LargeV3,
}
impl WhisperModel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Tiny => "tiny",
Self::Base => "base",
Self::Small => "small",
Self::Medium => "medium",
Self::LargeV3 => "large-v3",
}
}
}
impl std::fmt::Display for WhisperModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Subcommand)]
pub enum ModelsAction {
Download {
#[arg(value_name = "MODEL")]
model: Option<WhisperModel>,
},
List,
Path {
#[arg(value_name = "MODEL")]
model: Option<WhisperModel>,
},
Remove {
#[arg(value_name = "MODEL")]
model: WhisperModel,
#[arg(long)]
dry_run: bool,
},
}
fn parse_beam_size(s: &str) -> Result<i32, String> {
let val: i32 = s.parse().map_err(|e| format!("invalid integer: {e}"))?;
if !(1..=16).contains(&val) {
return Err(format!("beam size must be between 1 and 16, got {val}"));
}
Ok(val)
}
fn parse_vad_threshold(s: &str) -> Result<f32, String> {
let val: f32 = s.parse().map_err(|e| format!("invalid float: {e}"))?;
if !(0.0..=1.0).contains(&val) {
return Err(format!(
"VAD threshold must be between 0.0 and 1.0, got {val}"
));
}
Ok(val)
}
fn parse_concurrency(s: &str) -> Result<usize, String> {
let val: usize = s.parse().map_err(|e| format!("invalid integer: {e}"))?;
if !(1..=32).contains(&val) {
return Err(format!("concurrency must be between 1 and 32, got {val}"));
}
Ok(val)
}
fn parse_timeout_secs(s: &str) -> Result<u64, String> {
let val: u64 = s.parse().map_err(|e| format!("invalid integer: {e}"))?;
if !(1..=3600).contains(&val) {
return Err(format!(
"timeout must be between 1 and 3600 seconds, got {val}"
));
}
Ok(val)
}
fn parse_retry_count(s: &str) -> Result<u32, String> {
let val: u32 = s.parse().map_err(|e| format!("invalid integer: {e}"))?;
if val > 10 {
return Err(format!("retry count must be between 0 and 10, got {val}"));
}
Ok(val)
}
fn parse_retry_elapsed(s: &str) -> Result<u64, String> {
let val: u64 = s.parse().map_err(|e| format!("invalid integer: {e}"))?;
if !(1..=3600).contains(&val) {
return Err(format!(
"retry max elapsed must be between 1 and 3600 seconds, got {val}"
));
}
Ok(val)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn cli_debug_assert() {
Cli::command().debug_assert();
}
}