#![allow(
clippy::struct_excessive_bools,
clippy::expect_used,
clippy::panic,
clippy::unwrap_used
)]
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
fn parse_temperature(s: &str) -> Result<f32, String> {
let temp: f32 = s
.parse()
.map_err(|_| format!("'{s}' is not a valid number"))?;
if !(0.0..=1.0).contains(&temp) {
return Err(format!(
"temperature {temp} is out of range (must be between 0.0 and 1.0)"
));
}
Ok(temp)
}
pub fn expand_response_files(args: Vec<String>) -> Result<Vec<String>, std::io::Error> {
let mut result = Vec::with_capacity(args.len());
for arg in args {
if let Some(file_path) = arg.strip_prefix('@') {
let contents = std::fs::read_to_string(file_path)?;
for line in contents.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
result.push(trimmed.to_string());
}
}
} else {
result.push(arg);
}
}
Ok(result)
}
#[derive(Parser, Debug, Clone)]
#[command(name = "whisper-apr")]
#[command(version)]
#[command(about = "WASM-first automatic speech recognition", long_about = None)]
#[command(propagate_version = true)]
#[allow(clippy::struct_excessive_bools)] pub struct Args {
#[command(subcommand)]
pub command: Command,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true, conflicts_with = "verbose")]
pub quiet: bool,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub trace: Option<PathBuf>,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Subcommand, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Command {
Transcribe(TranscribeArgs),
Translate(TranslateArgs),
Summarize(SummarizeArgs),
Stream(StreamArgs),
#[command(alias = "server")]
Serve(ServeArgs),
Record(RecordArgs),
Batch(BatchArgs),
#[command(alias = "folder")]
TranscribeFolder(TranscribeFolderArgs),
Tui,
Test(TestArgs),
Model(ModelArgs),
#[command(alias = "bench")]
Benchmark(BenchmarkArgs),
Validate(ValidateArgs),
Parity(ParityArgs),
Quantize(QuantizeArgs),
Command(CommandArgs),
#[command(alias = "doctor")]
Diagnose(DiagnoseArgs),
Convert(ConvertArgs),
Export(ExportArgs),
Apr(crate::cli::apr_args::AprArgs),
Selftest(SelftestArgs),
Score(ScoreArgs),
}
#[derive(Parser, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct TranscribeArgs {
#[arg(short = 'f', long = "file")]
pub input: PathBuf,
#[arg(short, long, default_value = "tiny")]
pub model: ModelSize,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(short, long, default_value = "auto")]
pub language: String,
#[arg(long)]
pub detect_language: bool,
#[arg(long = "output-file")]
pub output: Option<PathBuf>,
#[arg(short = 'o', long, default_value = "txt")]
pub format: OutputFormatArg,
#[arg(long = "offset-t", default_value = "0")]
pub offset_t: u32,
#[arg(long = "offset-n", default_value = "0")]
pub offset_n: u32,
#[arg(short = 'd', long, default_value = "0")]
pub duration: u32,
#[arg(long = "max-context", default_value = "-1")]
pub max_context: i32,
#[arg(long = "max-len", default_value = "0")]
pub max_len: u32,
#[arg(long = "audio-ctx", default_value = "0")]
pub audio_ctx: u32,
#[arg(long = "best-of", default_value = "2")]
pub best_of: u32,
#[arg(long = "beam-size", default_value = "-1")]
pub beam_size: i32,
#[arg(long = "temperature", default_value = "0.0", value_parser = parse_temperature)]
pub temperature: f32,
#[arg(long = "temperature-inc", default_value = "0.2")]
pub temperature_inc: f32,
#[arg(long = "no-fallback")]
pub no_fallback: bool,
#[arg(long = "split-on-word")]
pub split_on_word: bool,
#[arg(long = "word-thold", default_value = "0.01")]
pub word_thold: f32,
#[arg(long)]
pub word_timestamps: bool,
#[arg(long)]
pub timestamps: bool,
#[arg(long = "no-timestamps")]
pub no_timestamps: bool,
#[arg(long = "entropy-thold", default_value = "2.40")]
pub entropy_thold: f32,
#[arg(long = "logprob-thold", default_value = "-1.0")]
pub logprob_thold: f32,
#[arg(long = "no-speech-thold", default_value = "0.6")]
pub no_speech_thold: f32,
#[arg(long, default_value = "")]
pub prompt: String,
#[arg(long, default_value = "", value_delimiter = ',')]
pub hotwords: Vec<String>,
#[arg(long = "suppress-regex", default_value = "")]
pub suppress_regex: String,
#[arg(long, default_value = "")]
pub grammar: String,
#[arg(long = "grammar-rule", default_value = "")]
pub grammar_rule: String,
#[arg(long = "grammar-penalty", default_value = "100.0")]
pub grammar_penalty: f32,
#[arg(long)]
pub vad: bool,
#[arg(long = "vad-model")]
pub vad_model: Option<PathBuf>,
#[arg(long = "vad-threshold", default_value = "0.5")]
pub vad_threshold: f32,
#[arg(long = "vad-min-speech-ms", default_value = "250")]
pub vad_min_speech_ms: u32,
#[arg(long = "vad-min-silence-ms", default_value = "100")]
pub vad_min_silence_ms: u32,
#[arg(long = "vad-max-speech-s")]
pub vad_max_speech_s: Option<f32>,
#[arg(long = "vad-pad-ms", default_value = "30")]
pub vad_pad_ms: u32,
#[arg(long = "vad-overlap", default_value = "0.1")]
pub vad_overlap: f32,
#[arg(short = 't', long)]
pub threads: Option<u32>,
#[arg(short = 'p', long, default_value = "1")]
pub processors: u32,
#[arg(long)]
pub gpu: bool,
#[arg(long = "no-gpu")]
pub no_gpu: bool,
#[arg(long = "flash-attn")]
pub flash_attn: bool,
#[arg(long = "no-flash-attn")]
pub no_flash_attn: bool,
#[arg(long = "no-prints")]
pub no_prints: bool,
#[arg(long = "print-special")]
pub print_special: bool,
#[arg(long = "colors")]
pub colors: bool,
#[arg(long = "confidence")]
pub confidence: bool,
#[arg(long = "progress")]
pub progress: bool,
#[arg(long = "print-memory")]
pub print_memory: bool,
#[arg(long)]
pub profile: bool,
#[arg(long = "translate")]
pub translate: bool,
#[arg(long)]
pub hallucination_filter: bool,
#[arg(long = "speed", default_value = "1.0")]
pub speed: f32,
#[arg(long = "cache-dir")]
pub cache_dir: Option<PathBuf>,
#[arg(long = "zram-optimized")]
pub zram_optimized: bool,
#[arg(long)]
pub summarize: bool,
#[arg(long = "lfm2-model")]
pub lfm2_model: Option<PathBuf>,
#[arg(long = "summary-output")]
pub summary_output: Option<PathBuf>,
#[arg(long = "summary-format", default_value = "json")]
pub summary_format: SummarizeFormat,
#[arg(long = "action-items")]
pub action_items: bool,
#[arg(long = "key-points")]
pub key_points: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct TranslateArgs {
#[arg(short = 'f', long = "file")]
pub input: PathBuf,
#[arg(short, long, default_value = "base")]
pub model: ModelSize,
#[arg(long = "output-file")]
pub output: Option<PathBuf>,
#[arg(short = 'o', long, default_value = "txt")]
pub format: OutputFormatArg,
#[arg(long)]
pub gpu: bool,
#[arg(short = 't', long)]
pub threads: Option<u32>,
}
#[derive(Parser, Debug, Clone)]
pub struct SummarizeArgs {
#[arg(short = 'f', long = "file")]
pub input: Option<PathBuf>,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(long)]
pub tokenizer_path: Option<PathBuf>,
#[arg(short = 'o', long = "output")]
pub output: Option<PathBuf>,
#[arg(long, default_value = "json")]
pub format: SummarizeFormat,
#[arg(long, default_value = "1024")]
pub max_tokens: u32,
#[arg(long, default_value = "0.3")]
pub temperature: f32,
#[arg(long, default_value = "4096")]
pub max_context: u32,
#[arg(long)]
pub webgpu: bool,
#[arg(long)]
pub stream: bool,
#[arg(long)]
pub action_items: bool,
#[arg(long)]
pub key_points: bool,
#[arg(long)]
pub prompt: Option<String>,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SummarizeFormat {
#[default]
Json,
Text,
Markdown,
Bullets,
}
#[derive(Parser, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct StreamArgs {
#[arg(short, long, default_value = "tiny")]
pub model: ModelSize,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(short, long, default_value = "auto")]
pub language: String,
#[arg(long, default_value = "3000")]
pub step: u32,
#[arg(long, default_value = "10000")]
pub length: u32,
#[arg(long, default_value = "200")]
pub keep: u32,
#[arg(short = 'c', long = "capture", default_value = "-1")]
pub capture: i32,
#[arg(long = "max-tokens", default_value = "32")]
pub max_tokens: u32,
#[arg(long = "vad-thold", default_value = "0.6")]
pub vad_thold: f32,
#[arg(long = "freq-thold", default_value = "100.0")]
pub freq_thold: f32,
#[arg(long = "keep-context")]
pub keep_context: bool,
#[arg(long = "save-audio")]
pub save_audio: bool,
#[arg(short = 't', long)]
pub threads: Option<u32>,
#[arg(long)]
pub translate: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct ServeArgs {
#[arg(short, long, default_value = "tiny")]
pub model: ModelSize,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(long, default_value = "127.0.0.1")]
pub host: String,
#[arg(long, default_value = "8080")]
pub port: u16,
#[arg(long)]
pub public: Option<PathBuf>,
#[arg(long = "request-path", default_value = "")]
pub request_path: String,
#[arg(long = "inference-path", default_value = "/inference")]
pub inference_path: String,
#[arg(long)]
pub convert: bool,
#[arg(long = "tmp-dir", default_value = ".")]
pub tmp_dir: PathBuf,
#[arg(short = 't', long)]
pub threads: Option<u32>,
}
#[derive(Parser, Debug, Clone)]
pub struct ParityArgs {
#[arg(short = 'f', long = "file")]
pub input: PathBuf,
#[arg(long = "whisper-cpp")]
pub whisper_cpp: Option<PathBuf>,
#[arg(long = "cpp-model")]
pub cpp_model: Option<PathBuf>,
#[arg(short, long, default_value = "tiny")]
pub model: ModelSize,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(long = "max-wer", default_value = "0.01")]
pub max_wer: f64,
#[arg(long = "timestamp-tolerance", default_value = "50")]
pub timestamp_tolerance_ms: u32,
#[arg(long)]
pub json: bool,
#[arg(long = "include-hf")]
pub include_hf: bool,
#[arg(short, long)]
pub verbose: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct QuantizeArgs {
pub input: PathBuf,
pub output: PathBuf,
#[arg(short = 'Q', long, default_value = "q5-0")]
pub quantize: QuantizeType,
#[arg(short, long)]
pub verbose: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantizeType {
#[value(name = "f32")]
F32,
#[value(name = "f16")]
F16,
#[value(name = "q8-0")]
Q8_0,
#[value(name = "q5-0")]
Q5_0,
#[value(name = "q5-1")]
Q5_1,
#[value(name = "q4-0")]
Q4_0,
#[value(name = "q4-1")]
Q4_1,
}
impl std::fmt::Display for QuantizeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::F32 => write!(f, "f32"),
Self::F16 => write!(f, "f16"),
Self::Q8_0 => write!(f, "q8-0"),
Self::Q5_0 => write!(f, "q5-0"),
Self::Q5_1 => write!(f, "q5-1"),
Self::Q4_0 => write!(f, "q4-0"),
Self::Q4_1 => write!(f, "q4-1"),
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct CommandArgs {
#[arg(short, long, default_value = "tiny")]
pub model: ModelSize,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(short = 'c', long)]
pub commands: Option<PathBuf>,
#[arg(long)]
pub prompt: Option<String>,
#[arg(long)]
pub grammar: Option<PathBuf>,
#[arg(long, default_value = "-1")]
pub capture: i32,
#[arg(long = "vad-thold", default_value = "0.6")]
pub vad_thold: f32,
#[arg(long)]
pub continuous: bool,
#[arg(short = 't', long)]
pub threads: Option<u32>,
}
#[derive(Parser, Debug, Clone)]
pub struct RecordArgs {
#[arg(short, long)]
pub duration: Option<u32>,
#[arg(long)]
pub live: bool,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub device: Option<String>,
#[arg(long, default_value = "16000")]
pub sample_rate: u32,
#[arg(long)]
pub list_devices: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct BatchArgs {
pub inputs: Vec<PathBuf>,
#[arg(short, long)]
pub output_dir: Option<PathBuf>,
#[arg(short, long)]
pub parallel: Option<usize>,
#[arg(short, long)]
pub recursive: bool,
#[arg(long)]
pub pattern: Option<String>,
#[arg(long)]
pub skip_existing: bool,
#[arg(short, long, default_value = "tiny")]
pub model: ModelSize,
#[arg(short, long, default_value = "txt")]
pub format: OutputFormatArg,
#[arg(long = "cache-dir")]
pub cache_dir: Option<PathBuf>,
#[arg(long = "zram-optimized")]
pub zram_optimized: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct TranscribeFolderArgs {
#[arg(long = "input-dir", short = 'i')]
pub input_dir: PathBuf,
#[arg(long = "output-dir", short = 'o')]
pub output_dir: PathBuf,
#[arg(long, short = 'f', default_value = "json")]
pub format: OutputFormatArg,
#[arg(long, short = 'r')]
pub recursive: bool,
#[arg(long, short = 'w')]
pub workers: Option<usize>,
#[arg(long, short = 'm', default_value = "tiny")]
pub model: ModelSize,
#[arg(long)]
pub model_path: Option<PathBuf>,
#[arg(long, short = 'l', default_value = "auto")]
pub language: String,
#[arg(long)]
pub skip_existing: bool,
#[arg(long)]
pub profile: bool,
#[arg(long)]
pub strict_budget: bool,
#[arg(long)]
pub trace_anomalies: bool,
#[arg(long)]
pub report: Option<PathBuf>,
#[arg(long)]
pub gpu: bool,
#[arg(long, short = 't')]
pub threads: Option<u32>,
#[arg(long = "cache-dir")]
pub cache_dir: Option<PathBuf>,
#[arg(long = "zram-optimized")]
pub zram_optimized: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct TestArgs {
#[arg(short, long, default_value = "all")]
pub backend: BackendArg,
#[arg(long)]
pub demo: Option<String>,
#[arg(long)]
pub pipeline: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct ModelArgs {
#[command(subcommand)]
pub action: ModelAction,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ModelAction {
List,
Download {
model: ModelSize,
},
Convert {
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
},
Info {
file: PathBuf,
},
WasmCheck {
#[arg(short = 'm', long, default_value = "lfm2")]
family: String,
#[arg(short = 'Q', long, default_value = "int4-awq")]
quantization: String,
#[arg(short, long, default_value = "4096")]
context: usize,
#[arg(short = 'w', long, default_value = "2048")]
sliding_window: usize,
},
}
#[derive(Parser, Debug, Clone)]
pub struct BenchmarkArgs {
#[arg(default_value = "tiny")]
pub model: ModelSize,
#[arg(short, long, default_value = "simd")]
pub backend: BackendArg,
#[arg(short, long, default_value = "3")]
pub iterations: usize,
#[arg(long)]
pub lfm2: bool,
#[arg(long, default_value = "all")]
pub component: String,
#[arg(long, default_value = "128")]
pub seq_len: usize,
#[arg(long)]
pub full_size: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct ValidateArgs {
pub file: PathBuf,
#[arg(long)]
pub quick: bool,
#[arg(short, long)]
pub detailed: bool,
#[arg(long, default_value = "23")]
pub min_score: u8,
#[arg(short, long, default_value = "text")]
pub format: ValidateOutputFormat,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ValidateOutputFormat {
#[default]
Text,
Json,
Markdown,
}
#[derive(Parser, Debug, Clone)]
pub struct ScoreArgs {
#[arg(short = 'f', long = "file")]
pub input: PathBuf,
#[arg(long, default_value = "60")]
pub min_score: u32,
#[arg(long)]
pub duration: Option<f64>,
}
#[derive(Parser, Debug, Clone)]
pub struct DiagnoseArgs {
#[arg(short, long)]
pub model: Option<PathBuf>,
#[arg(long)]
pub tokenizer_only: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub full: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct SelftestArgs {
#[arg(long)]
pub model: Option<PathBuf>,
#[arg(long)]
pub audio: Option<PathBuf>,
#[arg(long)]
pub expect: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct ConvertArgs {
#[arg(short, long)]
pub input: PathBuf,
#[arg(short, long)]
pub output: PathBuf,
#[arg(long, default_value = "lfm2")]
pub family: ModelFamilyArg,
#[arg(short = 'Q', long, default_value = "f32")]
pub quantize: QuantizeMethodArg,
#[arg(long, default_value = "128")]
pub group_size: u32,
#[arg(short, long)]
pub verbose: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct ExportArgs {
pub input: PathBuf,
#[arg(short, long)]
pub output: PathBuf,
#[arg(long, default_value = "safetensors")]
pub format: ExportFormatArg,
#[arg(long)]
pub with_metadata: bool,
#[arg(short, long)]
pub verbose: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExportFormatArg {
#[default]
Safetensors,
Ggml,
}
impl std::fmt::Display for ExportFormatArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Safetensors => write!(f, "safetensors"),
Self::Ggml => write!(f, "ggml"),
}
}
}
#[derive(Parser, Debug, Clone)]
pub struct PublishArgs {
#[arg(short, long)]
pub input: PathBuf,
#[arg(short, long)]
pub repo: String,
#[arg(long, default_value = "Whisper APR")]
pub model_name: String,
#[arg(long, default_value = "tiny")]
pub model_size: ModelSize,
#[arg(long, default_value = "both")]
pub format: PublishFormatArg,
#[arg(long, default_value = "Upload via whisper-apr publish")]
pub message: String,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub skip_verify: bool,
#[arg(long, default_value = "mit")]
pub license: String,
#[arg(long)]
pub model_card: Option<PathBuf>,
#[arg(short, long)]
pub verbose: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PublishFormatArg {
Apr,
Safetensors,
#[default]
Both,
}
impl std::fmt::Display for PublishFormatArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Apr => write!(f, "apr"),
Self::Safetensors => write!(f, "safetensors"),
Self::Both => write!(f, "both"),
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelFamilyArg {
Lfm2,
Llama,
Whisper,
}
impl std::fmt::Display for ModelFamilyArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Lfm2 => write!(f, "lfm2"),
Self::Llama => write!(f, "llama"),
Self::Whisper => write!(f, "whisper"),
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantizeMethodArg {
F32,
F16,
Bf16,
Int8,
Int4,
Int4Awq,
Int4Gptq,
}
impl std::fmt::Display for QuantizeMethodArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::F32 => write!(f, "f32"),
Self::F16 => write!(f, "f16"),
Self::Bf16 => write!(f, "bf16"),
Self::Int8 => write!(f, "int8"),
Self::Int4 => write!(f, "int4"),
Self::Int4Awq => write!(f, "int4-awq"),
Self::Int4Gptq => write!(f, "int4-gptq"),
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelSize {
Tiny,
Base,
Small,
Medium,
Large,
#[value(name = "large-v3-turbo")]
LargeV3Turbo,
MoonshineTiny,
MoonshineBase,
}
impl ModelSize {
#[must_use]
pub fn is_moonshine(&self) -> bool {
matches!(self, Self::MoonshineTiny | Self::MoonshineBase)
}
}
impl std::fmt::Display for ModelSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tiny => write!(f, "tiny"),
Self::Base => write!(f, "base"),
Self::Small => write!(f, "small"),
Self::Medium => write!(f, "medium"),
Self::Large => write!(f, "large"),
Self::LargeV3Turbo => write!(f, "large-v3-turbo"),
Self::MoonshineTiny => write!(f, "moonshine-tiny"),
Self::MoonshineBase => write!(f, "moonshine-base"),
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormatArg {
Txt,
Srt,
Vtt,
Json,
JsonFull,
Csv,
Lrc,
Wts,
Md,
}
impl std::fmt::Display for OutputFormatArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Txt => write!(f, "txt"),
Self::Srt => write!(f, "srt"),
Self::Vtt => write!(f, "vtt"),
Self::Json => write!(f, "json"),
Self::JsonFull => write!(f, "json-full"),
Self::Csv => write!(f, "csv"),
Self::Lrc => write!(f, "lrc"),
Self::Wts => write!(f, "wts"),
Self::Md => write!(f, "md"),
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendArg {
All,
Simd,
Wasm,
Cuda,
}
impl std::fmt::Display for BackendArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::All => write!(f, "all"),
Self::Simd => write!(f, "simd"),
Self::Wasm => write!(f, "wasm"),
Self::Cuda => write!(f, "cuda"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_args_command_factory_valid() {
Args::command().debug_assert();
}
#[test]
fn test_parse_transcribe_minimal() {
let args = Args::try_parse_from(["whisper-apr", "transcribe", "-f", "test.wav"]);
assert!(args.is_ok(), "Should parse minimal transcribe command");
let args = args.expect("test parse should succeed");
match args.command {
Command::Transcribe(t) => {
assert_eq!(t.input, PathBuf::from("test.wav"));
assert_eq!(t.model, ModelSize::Tiny);
assert_eq!(t.language, "auto");
assert!(!t.timestamps);
}
_ => panic!("Expected Transcribe command"),
}
}
#[test]
fn test_parse_transcribe_all_options() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe",
"-f",
"audio.mp3",
"--model",
"base",
"--language",
"en",
"--output-file",
"out.srt",
"--format",
"srt",
"--timestamps",
"--word-timestamps",
"--vad",
"--vad-threshold",
"0.7",
"--gpu",
"--threads",
"4",
"--beam-size",
"3",
"--temperature",
"0.2",
"--hallucination-filter",
]);
assert!(args.is_ok(), "Should parse all transcribe options");
let args = args.expect("test parse should succeed");
match args.command {
Command::Transcribe(t) => {
assert_eq!(t.model, ModelSize::Base);
assert_eq!(t.language, "en");
assert_eq!(t.output, Some(PathBuf::from("out.srt")));
assert_eq!(t.format, OutputFormatArg::Srt);
assert!(t.timestamps);
assert!(t.word_timestamps);
assert!(t.vad);
assert!((t.vad_threshold - 0.7).abs() < 0.01);
assert!(t.gpu);
assert_eq!(t.threads, Some(4));
assert_eq!(t.beam_size, 3);
assert!((t.temperature - 0.2).abs() < 0.01);
assert!(t.hallucination_filter);
}
_ => panic!("Expected Transcribe command"),
}
}
#[test]
fn test_parse_translate_minimal() {
let args = Args::try_parse_from(["whisper-apr", "translate", "-f", "german.wav"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Translate(t) => {
assert_eq!(t.input, PathBuf::from("german.wav"));
assert_eq!(t.model, ModelSize::Base);
}
_ => panic!("Expected Translate command"),
}
}
#[test]
fn test_parse_record_with_duration() {
let args = Args::try_parse_from([
"whisper-apr",
"record",
"--duration",
"30",
"--output",
"recording.wav",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Record(r) => {
assert_eq!(r.duration, Some(30));
assert_eq!(r.output, Some(PathBuf::from("recording.wav")));
assert!(!r.live);
}
_ => panic!("Expected Record command"),
}
}
#[test]
fn test_parse_record_live() {
let args = Args::try_parse_from(["whisper-apr", "record", "--live"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Record(r) => {
assert!(r.live);
}
_ => panic!("Expected Record command"),
}
}
#[test]
fn test_parse_record_list_devices() {
let args = Args::try_parse_from(["whisper-apr", "record", "--list-devices"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Record(r) => {
assert!(r.list_devices);
}
_ => panic!("Expected Record command"),
}
}
#[test]
fn test_parse_batch_minimal() {
let args = Args::try_parse_from(["whisper-apr", "batch", "file1.wav", "file2.wav"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Batch(b) => {
assert_eq!(b.inputs.len(), 2);
}
_ => panic!("Expected Batch command"),
}
}
#[test]
fn test_parse_batch_with_options() {
let args = Args::try_parse_from([
"whisper-apr",
"batch",
"*.wav",
"--output-dir",
"transcripts",
"--parallel",
"4",
"--recursive",
"--skip-existing",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Batch(b) => {
assert_eq!(b.output_dir, Some(PathBuf::from("transcripts")));
assert_eq!(b.parallel, Some(4));
assert!(b.recursive);
assert!(b.skip_existing);
}
_ => panic!("Expected Batch command"),
}
}
#[test]
fn test_parse_tui() {
let args = Args::try_parse_from(["whisper-apr", "tui"]);
assert!(args.is_ok());
assert!(matches!(
args.expect("test parse should succeed").command,
Command::Tui
));
}
#[test]
fn test_parse_test_all_backends() {
let args = Args::try_parse_from(["whisper-apr", "test"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Test(t) => {
assert_eq!(t.backend, BackendArg::All);
}
_ => panic!("Expected Test command"),
}
}
#[test]
fn test_parse_test_specific_backend() {
let args = Args::try_parse_from(["whisper-apr", "test", "--backend", "cuda"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Test(t) => {
assert_eq!(t.backend, BackendArg::Cuda);
}
_ => panic!("Expected Test command"),
}
}
#[test]
fn test_parse_model_list() {
let args = Args::try_parse_from(["whisper-apr", "model", "list"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Model(m) => {
assert!(matches!(m.action, ModelAction::List));
}
_ => panic!("Expected Model command"),
}
}
#[test]
fn test_parse_model_download() {
let args = Args::try_parse_from(["whisper-apr", "model", "download", "base"]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Model(m) => match m.action {
ModelAction::Download { model } => {
assert_eq!(model, ModelSize::Base);
}
_ => panic!("Expected Download action"),
},
_ => panic!("Expected Model command"),
}
}
#[test]
fn test_parse_model_convert() {
let args = Args::try_parse_from([
"whisper-apr",
"model",
"convert",
"input.pt",
"--output",
"output.apr",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Model(m) => match m.action {
ModelAction::Convert { input, output } => {
assert_eq!(input, PathBuf::from("input.pt"));
assert_eq!(output, PathBuf::from("output.apr"));
}
_ => panic!("Expected Convert action"),
},
_ => panic!("Expected Model command"),
}
}
#[test]
fn test_parse_benchmark() {
let args = Args::try_parse_from([
"whisper-apr",
"benchmark",
"base",
"--backend",
"simd",
"--iterations",
"5",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Benchmark(b) => {
assert_eq!(b.model, ModelSize::Base);
assert_eq!(b.backend, BackendArg::Simd);
assert_eq!(b.iterations, 5);
}
_ => panic!("Expected Benchmark command"),
}
}
#[test]
fn test_global_verbose_flag() {
let args = Args::try_parse_from(["whisper-apr", "-v", "transcribe", "-f", "test.wav"]);
assert!(args.is_ok());
assert!(args.expect("test parse should succeed").verbose);
}
#[test]
fn test_global_quiet_flag() {
let args = Args::try_parse_from(["whisper-apr", "-q", "transcribe", "-f", "test.wav"]);
assert!(args.is_ok());
assert!(args.expect("test parse should succeed").quiet);
}
#[test]
fn test_global_json_flag() {
let args = Args::try_parse_from(["whisper-apr", "--json", "transcribe", "-f", "test.wav"]);
assert!(args.is_ok());
assert!(args.expect("test parse should succeed").json);
}
#[test]
fn test_global_trace_flag() {
let args = Args::try_parse_from([
"whisper-apr",
"--trace",
"trace.json",
"transcribe",
"-f",
"test.wav",
]);
assert!(args.is_ok());
assert_eq!(
args.expect("test parse should succeed").trace,
Some(PathBuf::from("trace.json"))
);
}
#[test]
fn test_global_no_color_flag() {
let args =
Args::try_parse_from(["whisper-apr", "--no-color", "transcribe", "-f", "test.wav"]);
assert!(args.is_ok());
assert!(args.expect("test parse should succeed").no_color);
}
#[test]
fn test_model_size_display() {
assert_eq!(ModelSize::Tiny.to_string(), "tiny");
assert_eq!(ModelSize::Base.to_string(), "base");
assert_eq!(ModelSize::Small.to_string(), "small");
assert_eq!(ModelSize::Medium.to_string(), "medium");
assert_eq!(ModelSize::Large.to_string(), "large");
assert_eq!(ModelSize::LargeV3Turbo.to_string(), "large-v3-turbo");
}
#[test]
fn test_output_format_display() {
assert_eq!(OutputFormatArg::Txt.to_string(), "txt");
assert_eq!(OutputFormatArg::Srt.to_string(), "srt");
assert_eq!(OutputFormatArg::Vtt.to_string(), "vtt");
assert_eq!(OutputFormatArg::Json.to_string(), "json");
assert_eq!(OutputFormatArg::Csv.to_string(), "csv");
assert_eq!(OutputFormatArg::Md.to_string(), "md");
}
#[test]
fn test_backend_display() {
assert_eq!(BackendArg::All.to_string(), "all");
assert_eq!(BackendArg::Simd.to_string(), "simd");
assert_eq!(BackendArg::Wasm.to_string(), "wasm");
assert_eq!(BackendArg::Cuda.to_string(), "cuda");
}
#[test]
fn test_parse_invalid_command() {
let args = Args::try_parse_from(["whisper-apr", "invalid"]);
assert!(args.is_err());
}
#[test]
fn test_parse_missing_input() {
let args = Args::try_parse_from(["whisper-apr", "transcribe"]);
assert!(args.is_err());
}
#[test]
fn test_parse_invalid_model() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe",
"test.wav",
"--model",
"invalid",
]);
assert!(args.is_err());
}
#[test]
fn test_parse_invalid_format() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe",
"test.wav",
"--format",
"invalid",
]);
assert!(args.is_err());
}
#[test]
fn test_parse_invalid_backend() {
let args = Args::try_parse_from(["whisper-apr", "test", "--backend", "invalid"]);
assert!(args.is_err());
}
#[test]
fn test_parse_summarize_minimal() {
let args = Args::try_parse_from(["whisper-apr", "summarize", "-f", "transcript.txt"]);
assert!(args.is_ok(), "Should parse minimal summarize command");
let args = args.expect("test parse should succeed");
match args.command {
Command::Summarize(s) => {
assert_eq!(s.input, Some(PathBuf::from("transcript.txt")));
assert_eq!(s.format, SummarizeFormat::Json);
assert_eq!(s.max_tokens, 1024);
assert!(!s.stream);
}
_ => panic!("Expected Summarize command"),
}
}
#[test]
fn test_parse_summarize_all_options() {
let args = Args::try_parse_from([
"whisper-apr",
"summarize",
"-f",
"meeting.txt",
"--model-path",
"./lfm2.apr2",
"-o",
"summary.json",
"--format",
"markdown",
"--max-tokens",
"2048",
"--temperature",
"0.5",
"--max-context",
"8192",
"--webgpu",
"--stream",
"--action-items",
"--key-points",
]);
assert!(args.is_ok(), "Should parse all summarize options");
let args = args.expect("test parse should succeed");
match args.command {
Command::Summarize(s) => {
assert_eq!(s.model_path, Some(PathBuf::from("./lfm2.apr2")));
assert_eq!(s.output, Some(PathBuf::from("summary.json")));
assert_eq!(s.format, SummarizeFormat::Markdown);
assert_eq!(s.max_tokens, 2048);
assert!((s.temperature - 0.5).abs() < 0.01);
assert_eq!(s.max_context, 8192);
assert!(s.webgpu);
assert!(s.stream);
assert!(s.action_items);
assert!(s.key_points);
}
_ => panic!("Expected Summarize command"),
}
}
#[test]
fn test_parse_summarize_stdin() {
let args = Args::try_parse_from(["whisper-apr", "summarize"]);
assert!(args.is_ok(), "Should parse summarize without input (stdin)");
let args = args.expect("test parse should succeed");
match args.command {
Command::Summarize(s) => {
assert!(s.input.is_none());
}
_ => panic!("Expected Summarize command"),
}
}
#[test]
fn test_parse_summarize_with_prompt() {
let args = Args::try_parse_from([
"whisper-apr",
"summarize",
"-f",
"transcript.txt",
"--prompt",
"Summarize this meeting in 3 bullet points:",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Summarize(s) => {
assert_eq!(
s.prompt,
Some("Summarize this meeting in 3 bullet points:".to_string())
);
}
_ => panic!("Expected Summarize command"),
}
}
#[test]
fn test_parse_transcribe_with_zram() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe",
"-f",
"test.wav",
"--cache-dir",
"/mnt/whisper-cache",
"--zram-optimized",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Transcribe(t) => {
assert_eq!(t.cache_dir, Some(PathBuf::from("/mnt/whisper-cache")));
assert!(t.zram_optimized);
}
_ => panic!("Expected Transcribe command"),
}
}
#[test]
fn test_parse_batch_with_zram() {
let args = Args::try_parse_from([
"whisper-apr",
"batch",
"*.wav",
"--cache-dir",
"/mnt/whisper-cache",
"--zram-optimized",
]);
assert!(args.is_ok());
let args = args.expect("test parse should succeed");
match args.command {
Command::Batch(b) => {
assert_eq!(b.cache_dir, Some(PathBuf::from("/mnt/whisper-cache")));
assert!(b.zram_optimized);
}
_ => panic!("Expected Batch command"),
}
}
#[test]
fn test_parse_transcribe_folder_minimal() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe-folder",
"--input-dir",
"./audio",
"--output-dir",
"./transcripts",
]);
assert!(args.is_ok(), "Should parse minimal transcribe-folder");
let args = args.expect("test parse should succeed");
match args.command {
Command::TranscribeFolder(tf) => {
assert_eq!(tf.input_dir, PathBuf::from("./audio"));
assert_eq!(tf.output_dir, PathBuf::from("./transcripts"));
assert_eq!(tf.format, OutputFormatArg::Json);
assert_eq!(tf.model, ModelSize::Tiny);
assert!(!tf.recursive);
assert!(!tf.profile);
}
_ => panic!("Expected TranscribeFolder command"),
}
}
#[test]
fn test_parse_transcribe_folder_alias() {
let args = Args::try_parse_from([
"whisper-apr",
"folder",
"-i",
"./audio",
"-o",
"./transcripts",
]);
assert!(args.is_ok(), "Should parse 'folder' alias");
let args = args.expect("test parse should succeed");
assert!(matches!(args.command, Command::TranscribeFolder(_)));
}
#[test]
fn test_parse_transcribe_folder_all_options() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe-folder",
"--input-dir",
"./raw_audio",
"--output-dir",
"./trans",
"--format",
"json",
"--recursive",
"--workers",
"4",
"--model",
"base",
"--language",
"en",
"--skip-existing",
"--profile",
"--strict-budget",
"--trace-anomalies",
"--report",
"profile-report.json",
"--gpu",
"--threads",
"8",
"--zram-optimized",
]);
assert!(args.is_ok(), "Should parse all transcribe-folder options");
let args = args.expect("test parse should succeed");
match args.command {
Command::TranscribeFolder(tf) => {
assert_eq!(tf.input_dir, PathBuf::from("./raw_audio"));
assert_eq!(tf.output_dir, PathBuf::from("./trans"));
assert_eq!(tf.format, OutputFormatArg::Json);
assert!(tf.recursive);
assert_eq!(tf.workers, Some(4));
assert_eq!(tf.model, ModelSize::Base);
assert_eq!(tf.language, "en");
assert!(tf.skip_existing);
assert!(tf.profile);
assert!(tf.strict_budget);
assert!(tf.trace_anomalies);
assert_eq!(tf.report, Some(PathBuf::from("profile-report.json")));
assert!(tf.gpu);
assert_eq!(tf.threads, Some(8));
assert!(tf.zram_optimized);
}
_ => panic!("Expected TranscribeFolder command"),
}
}
#[test]
fn test_parse_transcribe_folder_short_flags() {
let args = Args::try_parse_from([
"whisper-apr",
"transcribe-folder",
"-i",
"./audio",
"-o",
"./out",
"-f",
"txt",
"-r",
"-w",
"2",
"-m",
"small",
"-l",
"fr",
"-t",
"4",
]);
assert!(args.is_ok(), "Should parse short flags");
let args = args.expect("test parse should succeed");
match args.command {
Command::TranscribeFolder(tf) => {
assert_eq!(tf.input_dir, PathBuf::from("./audio"));
assert_eq!(tf.output_dir, PathBuf::from("./out"));
assert_eq!(tf.format, OutputFormatArg::Txt);
assert!(tf.recursive);
assert_eq!(tf.workers, Some(2));
assert_eq!(tf.model, ModelSize::Small);
assert_eq!(tf.language, "fr");
assert_eq!(tf.threads, Some(4));
}
_ => panic!("Expected TranscribeFolder command"),
}
}
#[test]
fn test_parse_transcribe_folder_missing_input_dir() {
let args =
Args::try_parse_from(["whisper-apr", "transcribe-folder", "--output-dir", "./out"]);
assert!(args.is_err(), "Should fail without --input-dir");
}
#[test]
fn test_parse_transcribe_folder_missing_output_dir() {
let args =
Args::try_parse_from(["whisper-apr", "transcribe-folder", "--input-dir", "./audio"]);
assert!(args.is_err(), "Should fail without --output-dir");
}
#[test]
fn test_parse_selftest_minimal() {
let args = Args::try_parse_from(["whisper-apr", "selftest"]);
assert!(args.is_ok(), "Should parse minimal selftest command");
let args = args.expect("test parse should succeed");
match args.command {
Command::Selftest(s) => {
assert!(s.model.is_none());
assert!(s.audio.is_none());
assert!(s.expect.is_none());
}
_ => panic!("Expected Selftest command"),
}
}
#[test]
fn test_parse_score_minimal() {
let args = Args::try_parse_from(["whisper-apr", "score", "-f", "transcript.srt"]);
assert!(args.is_ok(), "Should parse minimal score command");
let args = args.expect("test parse should succeed");
match args.command {
Command::Score(s) => {
assert_eq!(s.input, PathBuf::from("transcript.srt"));
assert_eq!(s.min_score, 60);
assert!(s.duration.is_none());
}
_ => panic!("Expected Score command"),
}
}
#[test]
fn test_parse_score_all_options() {
let args = Args::try_parse_from([
"whisper-apr",
"score",
"-f",
"video.srt",
"--min-score",
"75",
"--duration",
"120.5",
]);
assert!(args.is_ok(), "Should parse score with all options");
let args = args.expect("test parse should succeed");
match args.command {
Command::Score(s) => {
assert_eq!(s.input, PathBuf::from("video.srt"));
assert_eq!(s.min_score, 75);
assert!((s.duration.unwrap() - 120.5).abs() < 0.01);
}
_ => panic!("Expected Score command"),
}
}
#[test]
fn test_parse_score_with_json_flag() {
let args = Args::try_parse_from(["whisper-apr", "--json", "score", "-f", "transcript.srt"]);
assert!(args.is_ok(), "Should parse score with global --json");
let args = args.expect("test parse should succeed");
assert!(args.json);
assert!(matches!(args.command, Command::Score(_)));
}
#[test]
fn test_parse_selftest_full_args() {
let args = Args::try_parse_from([
"whisper-apr",
"selftest",
"--model",
"tiny.apr",
"--audio",
"test.wav",
"--expect",
"birds",
]);
assert!(args.is_ok(), "Should parse selftest with all args");
let args = args.expect("test parse should succeed");
match args.command {
Command::Selftest(s) => {
assert_eq!(s.model, Some(PathBuf::from("tiny.apr")));
assert_eq!(s.audio, Some(PathBuf::from("test.wav")));
assert_eq!(s.expect, Some("birds".to_string()));
}
_ => panic!("Expected Selftest command"),
}
}
}