[feat] enhance CLI flags with --quiet
and --no-interaction
; update logging to respect verbosity and quiet modes; refactor log macros and add related tests
This commit is contained in:
181
src/main.rs
181
src/main.rs
@@ -1,38 +1,17 @@
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
// whisper-rs is used from the library crate
|
||||
use polyscribe::backend::{BackendKind, select_backend};
|
||||
|
||||
static VERBOSE: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
macro_rules! vlog {
|
||||
($lvl:expr, $($arg:tt)*) => {
|
||||
let v = VERBOSE.load(Ordering::Relaxed);
|
||||
let needed = match $lvl { 0u8 => true, 1u8 => v >= 1, 2u8 => v >= 2, _ => true };
|
||||
if needed { eprintln!("INFO: {}", format!($($arg)*)); }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! warnlog {
|
||||
($($arg:tt)*) => {
|
||||
eprintln!("WARN: {}", format!($($arg)*));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! errorlog {
|
||||
($($arg:tt)*) => {
|
||||
eprintln!("ERROR: {}", format!($($arg)*));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum AuxCommands {
|
||||
@@ -64,10 +43,18 @@ enum GpuBackendCli {
|
||||
about = "Merge JSON transcripts or transcribe audio using native whisper"
|
||||
)]
|
||||
struct Args {
|
||||
/// Increase verbosity (-v, -vv). Logs go to stderr.
|
||||
/// Increase verbosity (-v, -vv). Repeat to increase. Debug logs appear with -v; very verbose with -vv. Logs go to stderr.
|
||||
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true)]
|
||||
verbose: u8,
|
||||
|
||||
/// Quiet mode: suppress non-error logging on stderr (overrides -v). Does not suppress interactive prompts or stdout output.
|
||||
#[arg(short = 'q', long = "quiet", global = true)]
|
||||
quiet: bool,
|
||||
|
||||
/// Non-interactive mode: never prompt; use defaults instead.
|
||||
#[arg(long = "no-interaction", global = true)]
|
||||
no_interaction: bool,
|
||||
|
||||
/// Optional auxiliary subcommands (completions, man)
|
||||
#[command(subcommand)]
|
||||
aux: Option<AuxCommands>,
|
||||
@@ -146,6 +133,10 @@ fn prompt_speaker_name_for_path(path: &Path, default_name: &str, enabled: bool)
|
||||
if !enabled {
|
||||
return default_name.to_string();
|
||||
}
|
||||
if polyscribe::is_no_interaction() {
|
||||
// Explicitly non-interactive: never prompt
|
||||
return default_name.to_string();
|
||||
}
|
||||
let display_owned: String = path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
@@ -203,18 +194,37 @@ impl Drop for LastModelCleanup {
|
||||
if let Err(e) = std::fs::remove_file(&self.path) {
|
||||
// Best-effort cleanup; ignore missing file; warn for other errors
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
warnlog!("Failed to remove {}: {}", self.path.display(), e);
|
||||
polyscribe::wlog!("Failed to remove {}: {}", self.path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn with_quiet_stdio_if_needed<F, R>(_quiet: bool, f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
// Quiet mode no longer redirects stdio globally; only logging is silenced.
|
||||
f()
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn with_quiet_stdio_if_needed<F, R>(_quiet: bool, f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
f()
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
// Parse CLI
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize verbosity
|
||||
VERBOSE.store(args.verbose, Ordering::Relaxed);
|
||||
// Initialize runtime flags
|
||||
polyscribe::set_verbose(args.verbose);
|
||||
polyscribe::set_quiet(args.quiet);
|
||||
polyscribe::set_no_interaction(args.no_interaction);
|
||||
|
||||
// Handle auxiliary subcommands that write to stdout and exit early
|
||||
if let Some(aux) = &args.aux {
|
||||
@@ -254,12 +264,12 @@ fn run() -> Result<()> {
|
||||
GpuBackendCli::Vulkan => BackendKind::Vulkan,
|
||||
};
|
||||
let sel = select_backend(requested, args.verbose > 0)?;
|
||||
vlog!(0, "Using backend: {:?}", sel.chosen);
|
||||
polyscribe::dlog!(1, "Using backend: {:?}", sel.chosen);
|
||||
|
||||
// If requested, run the interactive model downloader first. If no inputs were provided, exit after downloading.
|
||||
if args.download_models {
|
||||
if let Err(e) = polyscribe::models::run_interactive_model_downloader() {
|
||||
errorlog!("Model downloader failed: {:#}", e);
|
||||
polyscribe::elog!("Model downloader failed: {:#}", e);
|
||||
}
|
||||
if args.inputs.is_empty() {
|
||||
return Ok(());
|
||||
@@ -269,7 +279,7 @@ fn run() -> Result<()> {
|
||||
// If requested, update local models and exit unless inputs provided to continue
|
||||
if args.update_models {
|
||||
if let Err(e) = polyscribe::models::update_local_models() {
|
||||
errorlog!("Model update failed: {:#}", e);
|
||||
polyscribe::elog!("Model update failed: {:#}", e);
|
||||
return Err(e);
|
||||
}
|
||||
// if only updating models and no inputs, exit
|
||||
@@ -279,7 +289,7 @@ fn run() -> Result<()> {
|
||||
}
|
||||
|
||||
// Determine inputs and optional output path
|
||||
vlog!(1, "Parsed {} input(s)", args.inputs.len());
|
||||
polyscribe::dlog!(1, "Parsed {} input(s)", args.inputs.len());
|
||||
let mut inputs = args.inputs;
|
||||
let mut output_path = args.output;
|
||||
if output_path.is_none() && inputs.len() >= 2 {
|
||||
@@ -308,7 +318,7 @@ fn run() -> Result<()> {
|
||||
}
|
||||
|
||||
if args.merge_and_separate {
|
||||
vlog!(1, "Mode: merge-and-separate; output_dir={:?}", output_path);
|
||||
polyscribe::dlog!(1, "Mode: merge-and-separate; output_dir={:?}", output_path);
|
||||
// Combined mode: write separate outputs per input and also a merged output set
|
||||
// Require an output directory
|
||||
let out_dir = match output_path.as_ref() {
|
||||
@@ -336,13 +346,28 @@ fn run() -> Result<()> {
|
||||
// Collect entries per file and extend merged
|
||||
let mut entries: Vec<OutputEntry> = Vec::new();
|
||||
if is_audio_file(path) {
|
||||
let items = sel.backend.transcribe(
|
||||
path,
|
||||
&speaker,
|
||||
lang_hint.as_deref(),
|
||||
args.gpu_layers,
|
||||
)?;
|
||||
entries.extend(items.into_iter());
|
||||
// Progress log to stderr (suppressed by -q); avoid partial lines
|
||||
polyscribe::ilog!("Processing file: {} ...", path.display());
|
||||
let res = with_quiet_stdio_if_needed(args.quiet, || {
|
||||
sel.backend.transcribe(
|
||||
path,
|
||||
&speaker,
|
||||
lang_hint.as_deref(),
|
||||
args.gpu_layers,
|
||||
)
|
||||
});
|
||||
match res {
|
||||
Ok(items) => {
|
||||
polyscribe::ilog!("done");
|
||||
entries.extend(items.into_iter());
|
||||
}
|
||||
Err(e) => {
|
||||
if !(polyscribe::is_no_interaction() || !polyscribe::stdin_is_tty()) {
|
||||
polyscribe::elog!("{:#}", e);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
} else if is_json_file(path) {
|
||||
let mut buf = String::new();
|
||||
File::open(path)
|
||||
@@ -461,7 +486,7 @@ fn run() -> Result<()> {
|
||||
.with_context(|| format!("Failed to create output file: {}", m_srt.display()))?;
|
||||
ms.write_all(m_srt_str.as_bytes())?;
|
||||
} else if args.merge {
|
||||
vlog!(1, "Mode: merge; output_base={:?}", output_path);
|
||||
polyscribe::dlog!(1, "Mode: merge; output_base={:?}", output_path);
|
||||
// MERGED MODE (previous default)
|
||||
let mut entries: Vec<OutputEntry> = Vec::new();
|
||||
for input_path in &inputs {
|
||||
@@ -476,16 +501,31 @@ fn run() -> Result<()> {
|
||||
|
||||
let mut buf = String::new();
|
||||
if is_audio_file(path) {
|
||||
let items = sel.backend.transcribe(
|
||||
path,
|
||||
&speaker,
|
||||
lang_hint.as_deref(),
|
||||
args.gpu_layers,
|
||||
)?;
|
||||
for e in items {
|
||||
entries.push(e);
|
||||
// Progress log to stderr (suppressed by -q)
|
||||
polyscribe::ilog!("Processing file: {} ...", path.display());
|
||||
let res = with_quiet_stdio_if_needed(args.quiet, || {
|
||||
sel.backend.transcribe(
|
||||
path,
|
||||
&speaker,
|
||||
lang_hint.as_deref(),
|
||||
args.gpu_layers,
|
||||
)
|
||||
});
|
||||
match res {
|
||||
Ok(items) => {
|
||||
polyscribe::ilog!("done");
|
||||
for e in items {
|
||||
entries.push(e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
if !(polyscribe::is_no_interaction() || !polyscribe::stdin_is_tty()) {
|
||||
polyscribe::elog!("{:#}", e);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if is_json_file(path) {
|
||||
File::open(path)
|
||||
.with_context(|| format!("Failed to open: {}", input_path))?
|
||||
@@ -577,7 +617,7 @@ fn run() -> Result<()> {
|
||||
writeln!(&mut handle)?;
|
||||
}
|
||||
} else {
|
||||
vlog!(1, "Mode: separate; output_dir={:?}", output_path);
|
||||
polyscribe::dlog!(1, "Mode: separate; output_dir={:?}", output_path);
|
||||
// SEPARATE MODE (default now)
|
||||
// If writing to stdout with multiple inputs, not supported
|
||||
if output_path.is_none() && inputs.len() > 1 {
|
||||
@@ -609,13 +649,28 @@ fn run() -> Result<()> {
|
||||
// Collect entries per file
|
||||
let mut entries: Vec<OutputEntry> = Vec::new();
|
||||
if is_audio_file(path) {
|
||||
let items = sel.backend.transcribe(
|
||||
path,
|
||||
&speaker,
|
||||
lang_hint.as_deref(),
|
||||
args.gpu_layers,
|
||||
)?;
|
||||
entries.extend(items);
|
||||
// Progress log to stderr (suppressed by -q)
|
||||
polyscribe::ilog!("Processing file: {} ...", path.display());
|
||||
let res = with_quiet_stdio_if_needed(args.quiet, || {
|
||||
sel.backend.transcribe(
|
||||
path,
|
||||
&speaker,
|
||||
lang_hint.as_deref(),
|
||||
args.gpu_layers,
|
||||
)
|
||||
});
|
||||
match res {
|
||||
Ok(items) => {
|
||||
polyscribe::ilog!("done");
|
||||
entries.extend(items);
|
||||
}
|
||||
Err(e) => {
|
||||
if !(polyscribe::is_no_interaction() || !polyscribe::stdin_is_tty()) {
|
||||
polyscribe::elog!("{:#}", e);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
} else if is_json_file(path) {
|
||||
let mut buf = String::new();
|
||||
File::open(path)
|
||||
@@ -703,11 +758,11 @@ fn run() -> Result<()> {
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
errorlog!("{}", e);
|
||||
if VERBOSE.load(Ordering::Relaxed) >= 1 {
|
||||
polyscribe::elog!("{}", e);
|
||||
if polyscribe::verbose_level() >= 1 {
|
||||
let mut src = e.source();
|
||||
while let Some(s) = src {
|
||||
errorlog!("caused by: {}", s);
|
||||
polyscribe::elog!("caused by: {}", s);
|
||||
src = s.source();
|
||||
}
|
||||
}
|
||||
@@ -897,6 +952,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_backend_auto_order_prefers_cuda_then_hip_then_vulkan_then_cpu() {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
let _guard = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
|
||||
// Clear overrides
|
||||
unsafe {
|
||||
std_env::remove_var("POLYSCRIBE_TEST_FORCE_CUDA");
|
||||
@@ -935,6 +993,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_backend_explicit_missing_errors() {
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
let _guard = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
|
||||
// Ensure all off
|
||||
unsafe {
|
||||
std_env::remove_var("POLYSCRIBE_TEST_FORCE_CUDA");
|
||||
|
Reference in New Issue
Block a user