[feat] integrate cliclack for TTY-aware UI, add summaries and intro/outro helpers

This commit is contained in:
2025-08-12 07:30:54 +02:00
parent 4916aa6224
commit 2cc5e49131
5 changed files with 243 additions and 87 deletions

112
Cargo.lock generated
View File

@@ -285,6 +285,20 @@ dependencies = [
"roff", "roff",
] ]
[[package]]
name = "cliclack"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c420bdc04c123a2df04d9c5a07289195f00007af6e45ab18f55e56dc7e04b8"
dependencies = [
"console",
"indicatif",
"once_cell",
"strsim",
"textwrap",
"zeroize",
]
[[package]] [[package]]
name = "cmake" name = "cmake"
version = "0.1.54" version = "0.1.54"
@@ -300,6 +314,19 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -362,6 +389,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -814,6 +847,19 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "indicatif"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
"portable-atomic",
"unicode-width",
"web-time",
]
[[package]] [[package]]
name = "io-uring" name = "io-uring"
version = "0.7.9" version = "0.7.9"
@@ -980,6 +1026,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.36.7"
@@ -1078,6 +1130,7 @@ dependencies = [
"clap", "clap",
"clap_complete", "clap_complete",
"clap_mangen", "clap_mangen",
"cliclack",
"libc", "libc",
"reqwest", "reqwest",
"serde", "serde",
@@ -1088,6 +1141,12 @@ dependencies = [
"whisper-rs", "whisper-rs",
] ]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.2" version = "0.1.2"
@@ -1406,6 +1465,12 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.0" version = "0.6.0"
@@ -1499,6 +1564,17 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.1"
@@ -1682,6 +1758,18 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-width"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@@ -1828,6 +1916,16 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "whisper-rs" name = "whisper-rs"
version = "0.14.3" version = "0.14.3"
@@ -2147,6 +2245,20 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"

View File

@@ -29,6 +29,7 @@ sha2 = "0.10"
# whisper-rs is always used (CPU-only by default); GPU features map onto it # whisper-rs is always used (CPU-only by default); GPU features map onto it
whisper-rs = { git = "https://github.com/tazz4843/whisper-rs" } whisper-rs = { git = "https://github.com/tazz4843/whisper-rs" }
libc = "0.2" libc = "0.2"
cliclack = "0.3"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"

View File

@@ -173,50 +173,82 @@ where
} }
} }
/// Centralized UI helpers (TTY-aware, quiet/verbose-aware)
pub mod ui {
use std::io;
// Prefer cliclack for all user-visible messages to ensure consistent, TTY-aware output.
// Falls back to stderr printing if needed.
/// Startup intro/banner (suppressed when quiet).
pub fn intro(msg: impl AsRef<str>) {
if crate::is_quiet() { return; }
// Use cliclack intro to render a nice banner when TTY
let _ = cliclack::intro(msg.as_ref());
}
/// Print an informational line (suppressed when quiet).
pub fn info(msg: impl AsRef<str>) {
if crate::is_quiet() { return; }
let _ = cliclack::log::info(msg.as_ref());
}
/// Print a warning (always printed).
pub fn warn(msg: impl AsRef<str>) {
// cliclack provides a warning-level log utility
let _ = cliclack::log::warning(msg.as_ref());
}
/// Print an error (always printed).
pub fn error(msg: impl AsRef<str>) {
let _ = cliclack::log::error(msg.as_ref());
}
/// Print a line above any progress bars (maps to cliclack log; synchronized).
pub fn println_above_bars(msg: impl AsRef<str>) {
if crate::is_quiet() { return; }
// cliclack logs are synchronized with its spinners/bars
let _ = cliclack::log::info(msg.as_ref());
}
/// Final outro/summary printed below any progress indicators (suppressed when quiet).
pub fn outro(msg: impl AsRef<str>) {
if crate::is_quiet() { return; }
let _ = cliclack::outro(msg.as_ref());
}
/// Prompt the user (TTY-aware via cliclack) and read a line from stdin. Returns the raw line with trailing newline removed.
pub fn prompt_line(prompt: &str) -> io::Result<String> {
// Route prompt through cliclack to keep consistent styling and avoid direct eprint!/println!
let _ = cliclack::log::info(prompt);
let mut s = String::new();
io::stdin().read_line(&mut s)?;
Ok(s)
}
}
/// Logging macros and helpers /// Logging macros and helpers
/// Log an error to stderr (always printed). Recommended for user-visible errors. /// Log an error using the UI helper (always printed). Recommended for user-visible errors.
#[macro_export] #[macro_export]
macro_rules! elog { macro_rules! elog {
($($arg:tt)*) => {{ ($($arg:tt)*) => {{
eprintln!("ERROR: {}", format!($($arg)*)); $crate::ui::error(format!($($arg)*));
}}
}
/// Internal helper macro used by other logging macros to centralize the
/// common behavior: build formatted message, check quiet/verbose flags,
/// and print to stderr with a label.
#[macro_export]
macro_rules! log_with_level {
($label:expr, $min_lvl:expr, $always:expr, $($arg:tt)*) => {{
let should_print = if $always {
true
} else if let Some(minv) = $min_lvl {
!$crate::is_quiet() && $crate::verbose_level() >= minv
} else {
!$crate::is_quiet()
};
if should_print {
eprintln!("{}: {}", $label, format!($($arg)*));
}
}} }}
} }
/// Log a warning to stderr (printed even in quiet mode). /// Log a warning using the UI helper (printed even in quiet mode).
#[macro_export] #[macro_export]
macro_rules! wlog { macro_rules! wlog {
($($arg:tt)*) => {{ $crate::log_with_level!("WARN", None, true, $($arg)*); }} ($($arg:tt)*) => {{
$crate::ui::warn(format!($($arg)*));
}}
} }
/// Log an informational line to stderr unless quiet mode is enabled. /// Log an informational line using the UI helper unless quiet mode is enabled.
#[macro_export] #[macro_export]
macro_rules! ilog { macro_rules! ilog {
($($arg:tt)*) => {{ $crate::log_with_level!("INFO", None, false, $($arg)*); }} ($($arg:tt)*) => {{
if !$crate::is_quiet() { $crate::ui::info(format!($($arg)*)); }
}}
} }
/// Log a debug/trace line when verbose level is at least the given level (u8). /// Log a debug/trace line when verbose level is at least the given level (u8).
#[macro_export] #[macro_export]
macro_rules! dlog { macro_rules! dlog {
($lvl:expr, $($arg:tt)*) => {{ ($lvl:expr, $($arg:tt)*) => {{
$crate::log_with_level!(&format!("DEBUG{}", &$lvl), Some($lvl), false, $($arg)*); if !$crate::is_quiet() && $crate::verbose_level() >= $lvl { $crate::ui::info(format!("DEBUG{}: {}", $lvl, format!($($arg)*))); }
}} }}
} }
@@ -230,7 +262,6 @@ use anyhow::{Context, Result, anyhow};
use chrono::Local; use chrono::Local;
use std::env; use std::env;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
@@ -462,10 +493,7 @@ pub fn find_model_file() -> Result<PathBuf> {
"No models available and interactive mode is disabled. Please set WHISPER_MODEL or run with --download-models." "No models available and interactive mode is disabled. Please set WHISPER_MODEL or run with --download-models."
)); ));
} }
eprint!("Would you like to download models now? [Y/n]: "); let input = crate::ui::prompt_line("Would you like to download models now? [Y/n]: ").unwrap_or_default();
io::stderr().flush().ok();
let mut input = String::new();
io::stdin().read_line(&mut input).ok();
let ans = input.trim().to_lowercase(); let ans = input.trim().to_lowercase();
if ans.is_empty() || ans == "y" || ans == "yes" { if ans.is_empty() || ans == "y" || ans == "yes" {
if let Err(e) = models::run_interactive_model_downloader() { if let Err(e) = models::run_interactive_model_downloader() {
@@ -519,16 +547,12 @@ pub fn find_model_file() -> Result<PathBuf> {
} }
} }
eprintln!("Multiple Whisper models found in {}:", models_dir.display()); crate::ui::println_above_bars(format!("Multiple Whisper models found in {}:", models_dir.display()));
for (i, p) in candidates.iter().enumerate() { for (i, p) in candidates.iter().enumerate() {
eprintln!(" {}) {}", i + 1, p.display()); crate::ui::println_above_bars(format!(" {}) {}", i + 1, p.display()));
} }
eprint!("Select model by number [1-{}]: ", candidates.len()); let input = crate::ui::prompt_line(&format!("Select model by number [1-{}]: ", candidates.len()))
io::stderr().flush().ok(); .map_err(|_| anyhow!("Failed to read selection"))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read selection")?;
let sel: usize = input let sel: usize = input
.trim() .trim()
.parse() .parse()

View File

@@ -142,25 +142,18 @@ fn prompt_speaker_name_for_path(path: &Path, default_name: &str, enabled: bool)
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string()); .unwrap_or_else(|| path.to_string_lossy().to_string());
eprint!( let buf = polyscribe::ui::prompt_line(&format!(
"Enter speaker name for {display_owned} [default: {default_name}]: " "Enter speaker name for {display_owned} [default: {default_name}]: "
); )).unwrap_or_default();
io::stderr().flush().ok(); let raw = buf.trim();
let mut buf = String::new(); if raw.is_empty() {
match io::stdin().read_line(&mut buf) { return default_name.to_string();
Ok(_) => { }
let raw = buf.trim(); let sanitized = sanitize_speaker_name(raw);
if raw.is_empty() { if sanitized.is_empty() {
return default_name.to_string(); default_name.to_string()
} } else {
let sanitized = sanitize_speaker_name(raw); sanitized
if sanitized.is_empty() {
default_name.to_string()
} else {
sanitized
}
}
Err(_) => default_name.to_string(),
} }
} }
@@ -217,6 +210,7 @@ where
} }
fn run() -> Result<()> { fn run() -> Result<()> {
let _t0 = std::time::Instant::now();
// Parse CLI // Parse CLI
let args = Args::parse(); let args = Args::parse();
@@ -225,6 +219,9 @@ fn run() -> Result<()> {
polyscribe::set_quiet(args.quiet); polyscribe::set_quiet(args.quiet);
polyscribe::set_no_interaction(args.no_interaction); polyscribe::set_no_interaction(args.no_interaction);
// Startup banner via UI (TTY-aware through cliclack), suppressed when quiet
polyscribe::ui::intro(format!("PolyScribe v{}", env!("CARGO_PKG_VERSION")));
// Handle auxiliary subcommands that write to stdout and exit early // Handle auxiliary subcommands that write to stdout and exit early
if let Some(aux) = &args.aux { if let Some(aux) = &args.aux {
use clap::CommandFactory; use clap::CommandFactory;
@@ -266,6 +263,10 @@ fn run() -> Result<()> {
polyscribe::dlog!(1, "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 requested, run the interactive model downloader first. If no inputs were provided, exit after downloading.
let mut summary_inputs_total: usize = 0;
let mut summary_audio_count: usize = 0;
let mut summary_json_count: usize = 0;
let mut summary_segments_total: usize = 0;
if args.download_models { if args.download_models {
if let Err(e) = polyscribe::models::run_interactive_model_downloader() { if let Err(e) = polyscribe::models::run_interactive_model_downloader() {
polyscribe::elog!("Model downloader failed: {:#}", e); polyscribe::elog!("Model downloader failed: {:#}", e);
@@ -290,6 +291,7 @@ fn run() -> Result<()> {
// Determine inputs and optional output path // Determine inputs and optional output path
polyscribe::dlog!(1, "Parsed {} input(s)", args.inputs.len()); polyscribe::dlog!(1, "Parsed {} input(s)", args.inputs.len());
let mut inputs = args.inputs; let mut inputs = args.inputs;
summary_inputs_total = inputs.len();
let mut output_path = args.output; let mut output_path = args.output;
if output_path.is_none() && inputs.len() >= 2 { if output_path.is_none() && inputs.len() >= 2 {
if let Some(last) = inputs.last().cloned() { if let Some(last) = inputs.last().cloned() {
@@ -353,6 +355,7 @@ fn run() -> Result<()> {
// Collect entries per file and extend merged // Collect entries per file and extend merged
let mut entries: Vec<OutputEntry> = Vec::new(); let mut entries: Vec<OutputEntry> = Vec::new();
if is_audio_file(path) { if is_audio_file(path) {
summary_audio_count += 1;
// Progress log to stderr (suppressed by -q); avoid partial lines // Progress log to stderr (suppressed by -q); avoid partial lines
polyscribe::ilog!("Processing file: {} ...", path.display()); polyscribe::ilog!("Processing file: {} ...", path.display());
let res = with_quiet_stdio_if_needed(args.quiet, || { let res = with_quiet_stdio_if_needed(args.quiet, || {
@@ -372,6 +375,7 @@ fn run() -> Result<()> {
} }
} }
} else if is_json_file(path) { } else if is_json_file(path) {
summary_json_count += 1;
let mut buf = String::new(); let mut buf = String::new();
File::open(path) File::open(path)
.with_context(|| format!("Failed to open: {input_path}"))? .with_context(|| format!("Failed to open: {input_path}"))?
@@ -409,6 +413,7 @@ fn run() -> Result<()> {
for (i, e) in entries.iter_mut().enumerate() { for (i, e) in entries.iter_mut().enumerate() {
e.id = i as u64; e.id = i as u64;
} }
summary_segments_total += entries.len();
// Write separate outputs to out_dir // Write separate outputs to out_dir
let out = OutputRoot { let out = OutputRoot {
@@ -498,6 +503,7 @@ fn run() -> Result<()> {
let mut buf = String::new(); let mut buf = String::new();
if is_audio_file(path) { if is_audio_file(path) {
summary_audio_count += 1;
// Progress log to stderr (suppressed by -q) // Progress log to stderr (suppressed by -q)
polyscribe::ilog!("Processing file: {} ...", path.display()); polyscribe::ilog!("Processing file: {} ...", path.display());
let res = with_quiet_stdio_if_needed(args.quiet, || { let res = with_quiet_stdio_if_needed(args.quiet, || {
@@ -520,6 +526,7 @@ fn run() -> Result<()> {
} }
} }
} else if is_json_file(path) { } else if is_json_file(path) {
summary_json_count += 1;
File::open(path) File::open(path)
.with_context(|| format!("Failed to open: {}", input_path))? .with_context(|| format!("Failed to open: {}", input_path))?
.read_to_string(&mut buf) .read_to_string(&mut buf)
@@ -559,6 +566,7 @@ fn run() -> Result<()> {
e.id = i as u64; e.id = i as u64;
} }
let out = OutputRoot { items: entries }; let out = OutputRoot { items: entries };
summary_segments_total = out.items.len();
if let Some(path) = output_path { if let Some(path) = output_path {
let base_path = Path::new(&path); let base_path = Path::new(&path);
@@ -636,6 +644,7 @@ fn run() -> Result<()> {
// Collect entries per file // Collect entries per file
let mut entries: Vec<OutputEntry> = Vec::new(); let mut entries: Vec<OutputEntry> = Vec::new();
if is_audio_file(path) { if is_audio_file(path) {
summary_audio_count += 1;
// Progress log to stderr (suppressed by -q) // Progress log to stderr (suppressed by -q)
polyscribe::ilog!("Processing file: {} ...", path.display()); polyscribe::ilog!("Processing file: {} ...", path.display());
let res = with_quiet_stdio_if_needed(args.quiet, || { let res = with_quiet_stdio_if_needed(args.quiet, || {
@@ -655,6 +664,7 @@ fn run() -> Result<()> {
} }
} }
} else if is_json_file(path) { } else if is_json_file(path) {
summary_json_count += 1;
let mut buf = String::new(); let mut buf = String::new();
File::open(path) File::open(path)
.with_context(|| format!("Failed to open: {input_path}"))? .with_context(|| format!("Failed to open: {input_path}"))?
@@ -692,6 +702,7 @@ fn run() -> Result<()> {
for (i, e) in entries.iter_mut().enumerate() { for (i, e) in entries.iter_mut().enumerate() {
e.id = i as u64; e.id = i as u64;
} }
summary_segments_total += entries.len();
let out = OutputRoot { items: entries }; let out = OutputRoot { items: entries };
if let Some(dir) = &out_dir { if let Some(dir) = &out_dir {
@@ -736,6 +747,20 @@ fn run() -> Result<()> {
} }
} }
// Final summary (TTY-aware via UI), only when not quiet
if !polyscribe::is_quiet() {
let elapsed = _t0.elapsed();
let secs = elapsed.as_secs_f32();
let mut out = String::new();
out.push_str("Summary:\n");
out.push_str(&format!("{:<12} {:>8}\n", "Files:", summary_inputs_total));
out.push_str(&format!("{:<12} {:>8}\n", "Audio:", summary_audio_count));
out.push_str(&format!("{:<12} {:>8}\n", "JSON:", summary_json_count));
out.push_str(&format!("{:<12} {:>8}\n", "Segments:", summary_segments_total));
out.push_str(&format!("{:<12} {:>8.2}s\n", "Time:", secs));
polyscribe::ui::outro(out);
}
Ok(()) Ok(())
} }

View File

@@ -5,7 +5,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::env; use std::env;
use std::fs::{File, create_dir_all}; use std::fs::{File, create_dir_all};
use std::io::{self, Read, Write}; use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
use std::time::Duration; use std::time::Duration;
@@ -326,8 +326,8 @@ fn fetch_all_models(client: &Client) -> Result<Vec<ModelEntry>> {
match hf_fetch_repo_models(client, "akashmjn/tinydiarize-whisper.cpp") { match hf_fetch_repo_models(client, "akashmjn/tinydiarize-whisper.cpp") {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
ilog!( wlog!(
"Warning: failed to fetch optional repo akashmjn/tinydiarize-whisper.cpp: {:#}", "Failed to fetch optional repo akashmjn/tinydiarize-whisper.cpp: {:#}",
e e
); );
Vec::new() Vec::new()
@@ -413,18 +413,16 @@ fn prompt_select_models_two_stage(models: &[ModelEntry]) -> Result<Vec<ModelEntr
return Ok(Vec::new()); return Ok(Vec::new());
} }
// Print base selection on stderr // Print base selection via UI
eprintln!("Available base model families:"); crate::ui::println_above_bars("Available base model families:");
for (i, b) in bases.iter().enumerate() { for (i, b) in bases.iter().enumerate() {
eprintln!(" {}) {}", i + 1, b); crate::ui::println_above_bars(format!(" {}) {}", i + 1, b));
} }
loop { loop {
eprint!("Select base (number or name, 'q' to cancel): "); let mut line = match crate::ui::prompt_line("Select base (number or name, 'q' to cancel): ") {
io::stderr().flush().ok(); Ok(s) => s,
let mut line = String::new(); Err(_) => String::new(),
io::stdin() };
.read_line(&mut line)
.context("Failed to read base selection")?;
let s = line.trim(); let s = line.trim();
if s.eq_ignore_ascii_case("q") if s.eq_ignore_ascii_case("q")
|| s.eq_ignore_ascii_case("quit") || s.eq_ignore_ascii_case("quit")
@@ -450,12 +448,12 @@ fn prompt_select_models_two_stage(models: &[ModelEntry]) -> Result<Vec<ModelEntr
let filtered: Vec<ModelEntry> = let filtered: Vec<ModelEntry> =
models.iter().filter(|m| m.base == base).cloned().collect(); models.iter().filter(|m| m.base == base).cloned().collect();
if filtered.is_empty() { if filtered.is_empty() {
eprintln!("No models found for base '{base}'."); crate::ui::warn(format!("No models found for base '{base}'."));
continue; continue;
} }
// Reuse the formatter but only for the chosen base list // Reuse the formatter but only for the chosen base list
let listing = format_model_list(&filtered); let listing = format_model_list(&filtered);
eprint!("{listing}"); crate::ui::println_above_bars(listing);
// Build index map for filtered list // Build index map for filtered list
let mut index_map: Vec<usize> = Vec::with_capacity(filtered.len()); let mut index_map: Vec<usize> = Vec::with_capacity(filtered.len());
@@ -466,12 +464,8 @@ fn prompt_select_models_two_stage(models: &[ModelEntry]) -> Result<Vec<ModelEntr
} }
// Second prompt: sub-type selection // Second prompt: sub-type selection
loop { loop {
eprint!("Selection: "); let line2 = crate::ui::prompt_line("Selection: ")
io::stderr().flush().ok(); .map_err(|_| anyhow!("Failed to read selection"))?;
let mut line2 = String::new();
io::stdin()
.read_line(&mut line2)
.context("Failed to read selection")?;
let s2 = line2.trim().to_lowercase(); let s2 = line2.trim().to_lowercase();
if s2 == "q" || s2 == "quit" || s2 == "exit" { if s2 == "q" || s2 == "quit" || s2 == "exit" {
return Ok(Vec::new()); return Ok(Vec::new());
@@ -501,7 +495,7 @@ fn prompt_select_models_two_stage(models: &[ModelEntry]) -> Result<Vec<ModelEntr
selected.sort_unstable(); selected.sort_unstable();
selected.dedup(); selected.dedup();
if selected.is_empty() { if selected.is_empty() {
eprintln!("No valid selection. Please try again or 'q' to cancel."); crate::ui::warn("No valid selection. Please try again or 'q' to cancel.");
continue; continue;
} }
let chosen: Vec<ModelEntry> = selected let chosen: Vec<ModelEntry> = selected
@@ -511,10 +505,10 @@ fn prompt_select_models_two_stage(models: &[ModelEntry]) -> Result<Vec<ModelEntr
return Ok(chosen); return Ok(chosen);
} }
} else { } else {
eprintln!( crate::ui::warn(format!(
"Invalid base selection. Please enter a number from 1-{} or a base name.", "Invalid base selection. Please enter a number from 1-{} or a base name.",
bases.len() bases.len()
); ));
} }
} }
} }
@@ -591,8 +585,8 @@ fn download_one_model(client: &Client, models_dir: &Path, entry: &ModelEntry) ->
} }
} }
Err(e) => { Err(e) => {
qlog!( wlog!(
"Warning: failed to hash existing {}: {}. Will re-download to ensure correctness.", "Failed to hash existing {}: {}. Will re-download to ensure correctness.",
final_path.display(), final_path.display(),
e e
); );
@@ -618,8 +612,8 @@ fn download_one_model(client: &Client, models_dir: &Path, entry: &ModelEntry) ->
} }
} }
Err(e) => { Err(e) => {
qlog!( wlog!(
"Warning: failed to stat existing {}: {}. Will re-download to ensure correctness.", "Failed to stat existing {}: {}. Will re-download to ensure correctness.",
final_path.display(), final_path.display(),
e e
); );
@@ -723,8 +717,8 @@ fn download_one_model(client: &Client, models_dir: &Path, entry: &ModelEntry) ->
)); ));
} }
} else { } else {
qlog!( wlog!(
"Warning: no SHA-256 available for {}. Skipping verification.", "No SHA-256 available for {}. Skipping verification.",
entry.name entry.name
); );
} }
@@ -826,7 +820,7 @@ pub fn update_local_models() -> Result<()> {
} }
} }
Err(e) => { Err(e) => {
qlog!("Warning: failed hashing {}: {}. Re-downloading.", fname, e); wlog!("Failed hashing {}: {}. Re-downloading.", fname, e);
} }
} }
download_one_model(&client, models_dir, remote)?; download_one_model(&client, models_dir, remote)?;
@@ -839,7 +833,7 @@ pub fn update_local_models() -> Result<()> {
download_one_model(&client, models_dir, remote)?; download_one_model(&client, models_dir, remote)?;
} }
Err(e) => { Err(e) => {
qlog!("Warning: stat failed for {}: {}. Updating...", fname, e); wlog!("Stat failed for {}: {}. Updating...", fname, e);
download_one_model(&client, models_dir, remote)?; download_one_model(&client, models_dir, remote)?;
} }
} }