From f83652bcf3356d6b083d1c4523af453979763b70 Mon Sep 17 00:00:00 2001 From: avionicharshit-byte Date: Sat, 29 Aug 2026 20:51:26 +0530 Subject: [PATCH 1/2] warn when the cli is given an unknown option --- app/cli/args.cpp | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ app/cli/args.h | 4 +++ app/cli/main.cpp | 8 ++++-- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/cli/args.cpp b/app/cli/args.cpp index dbd4c01de..849c75c93 100644 --- a/app/cli/args.cpp +++ b/app/cli/args.cpp @@ -1,10 +1,72 @@ #include "args.h" +#include +#include #include namespace minitts::cli { +namespace { + +// Every option name the CLI looked for, split by whether the lookup consumes the following +// argument. Populated by the lookups below, so a new flag is recognised the moment its lookup +// is added and there is no second list to keep in sync. +std::set & value_names() { + static std::set names; + return names; +} + +std::set & flag_names() { + static std::set names; + return names; +} + +void record_value_query(const std::string & name) { + value_names().insert(name); +} + +void record_flag_query(const std::string & name) { + flag_names().insert(name); +} + +} // namespace + +void report_unused_args(int argc, char ** argv) { + const auto & values = value_names(); + const auto & flags = flag_names(); + std::vector unused; + for (int i = 1; i < argc; ++i) { + const std::string token = argv[i]; + if (token.rfind("--", 0) != 0 || token == "--") { + continue; + } + // Only an option that takes a value consumes the next argument, so only then can the + // next argument be something that merely looks like an option. + if (values.count(token) != 0) { + ++i; + continue; + } + if (flags.count(token) != 0) { + continue; + } + unused.push_back(token); + } + if (unused.empty()) { + return; + } + std::cerr << "audiocpp_cli warning: ignored unknown option"; + if (unused.size() > 1) { + std::cerr << "s"; + } + std::cerr << ":"; + for (const auto & token : unused) { + std::cerr << " " << token; + } + std::cerr << "\n"; +} + std::optional find_arg(int argc, char ** argv, const std::string & name) { + record_value_query(name); for (int i = 1; i + 1 < argc; ++i) { if (argv[i] == name) { return std::string(argv[i + 1]); @@ -14,6 +76,7 @@ std::optional find_arg(int argc, char ** argv, const std::string & } bool has_arg(int argc, char ** argv, const std::string & name) { + record_flag_query(name); for (int i = 1; i < argc; ++i) { if (argv[i] == name) { return true; @@ -23,6 +86,7 @@ bool has_arg(int argc, char ** argv, const std::string & name) { } std::vector collect_args(int argc, char ** argv, const std::string & name) { + record_value_query(name); std::vector values; for (int i = 1; i + 1 < argc; ++i) { if (argv[i] == name) { diff --git a/app/cli/args.h b/app/cli/args.h index d11aa1be3..86b647c14 100644 --- a/app/cli/args.h +++ b/app/cli/args.h @@ -10,6 +10,10 @@ namespace minitts::cli { +// Reports any --flags on the command line that no lookup ever asked for, so a typo like +// --bakend is not silently discarded. Call once, after the run has finished. +void report_unused_args(int argc, char ** argv); + std::optional find_arg(int argc, char ** argv, const std::string & name); bool has_arg(int argc, char ** argv, const std::string & name); std::vector collect_args(int argc, char ** argv, const std::string & name); diff --git a/app/cli/main.cpp b/app/cli/main.cpp index affe12bdf..2a75ce65d 100644 --- a/app/cli/main.cpp +++ b/app/cli/main.cpp @@ -975,7 +975,9 @@ int wmain(int argc, wchar_t ** wargv) { argv.push_back(arg.data()); } argv.push_back(nullptr); - return audiocpp_cli_main(argc, argv.data()); + const int status = audiocpp_cli_main(argc, argv.data()); + minitts::cli::report_unused_args(argc, argv.data()); + return status; } catch (const std::exception & ex) { std::cerr << "audiocpp_cli failed: " << ex.what() << "\n"; return 1; @@ -983,6 +985,8 @@ int wmain(int argc, wchar_t ** wargv) { } #else int main(int argc, char ** argv) { - return audiocpp_cli_main(argc, argv); + const int status = audiocpp_cli_main(argc, argv); + minitts::cli::report_unused_args(argc, argv); + return status; } #endif From 912088c7ffff141c51a51e203d0c27c8ef7c67fd Mon Sep 17 00:00:00 2001 From: avionicharshit-byte Date: Sat, 29 Aug 2026 23:08:59 +0530 Subject: [PATCH 2/2] make an unknown cli option a hard error, checked before the run --- app/cli/args.cpp | 42 +++++++++++++---- app/cli/args.h | 12 +++-- app/cli/batch.cpp | 8 ++-- app/cli/batch.h | 3 +- app/cli/main.cpp | 110 ++++++++++++++++++++++++++------------------ app/cli/request.cpp | 10 ++-- 6 files changed, 116 insertions(+), 69 deletions(-) diff --git a/app/cli/args.cpp b/app/cli/args.cpp index 849c75c93..527733fef 100644 --- a/app/cli/args.cpp +++ b/app/cli/args.cpp @@ -29,12 +29,10 @@ void record_flag_query(const std::string & name) { flag_names().insert(name); } -} // namespace - -void report_unused_args(int argc, char ** argv) { +std::vector args_nobody_asked_for(int argc, char ** argv) { const auto & values = value_names(); const auto & flags = flag_names(); - std::vector unused; + std::vector unknown; for (int i = 1; i < argc; ++i) { const std::string token = argv[i]; if (token.rfind("--", 0) != 0 || token == "--") { @@ -49,17 +47,43 @@ void report_unused_args(int argc, char ** argv) { if (flags.count(token) != 0) { continue; } - unused.push_back(token); + unknown.push_back(token); + } + return unknown; +} + +// Set once the strict check has spoken for this run, so the warning below does not repeat it. +bool strict_check_ran = false; + +} // namespace + +void require_known_args(int argc, char ** argv) { + strict_check_ran = true; + const auto unknown = args_nobody_asked_for(argc, argv); + if (unknown.empty()) { + return; + } + std::string message = unknown.size() > 1 ? "unknown options:" : "unknown option:"; + for (const auto & token : unknown) { + message += " " + token; + } + throw std::runtime_error(message); +} + +void warn_ignored_args(int argc, char ** argv) { + if (strict_check_ran) { + return; } - if (unused.empty()) { + const auto ignored = args_nobody_asked_for(argc, argv); + if (ignored.empty()) { return; } - std::cerr << "audiocpp_cli warning: ignored unknown option"; - if (unused.size() > 1) { + std::cerr << "audiocpp_cli warning: ignored option"; + if (ignored.size() > 1) { std::cerr << "s"; } std::cerr << ":"; - for (const auto & token : unused) { + for (const auto & token : ignored) { std::cerr << " " << token; } std::cerr << "\n"; diff --git a/app/cli/args.h b/app/cli/args.h index 86b647c14..e2c5d23bd 100644 --- a/app/cli/args.h +++ b/app/cli/args.h @@ -10,9 +10,15 @@ namespace minitts::cli { -// Reports any --flags on the command line that no lookup ever asked for, so a typo like -// --bakend is not silently discarded. Call once, after the run has finished. -void report_unused_args(int argc, char ** argv); +// Throws if the command line carries a --flag that no lookup ever asked for, so a typo like +// --bakend is refused instead of being silently discarded. Call once every option has been +// read, which on the run paths is before the model does any work. +void require_known_args(int argc, char ** argv); + +// The same check reported as a warning, for the informational commands. Those return before the +// rest of the options are read, so all that can honestly be said there is that an option was +// ignored, not that it was misspelled. Does nothing if require_known_args already ran. +void warn_ignored_args(int argc, char ** argv); std::optional find_arg(int argc, char ** argv, const std::string & name); bool has_arg(int argc, char ** argv, const std::string & name); diff --git a/app/cli/batch.cpp b/app/cli/batch.cpp index b56bf3ab5..ce39484bd 100644 --- a/app/cli/batch.cpp +++ b/app/cli/batch.cpp @@ -261,7 +261,8 @@ bool has_batch_input(int argc, char ** argv) { minitts::app::AppBatchRequest build_batch_request_from_cli( int argc, char ** argv, - const engine::runtime::TaskRequest & base_request) { + const engine::runtime::TaskRequest & base_request, + const std::string & audio_role) { const auto request_sequence_path = optional_path_arg(argc, argv, "--request-sequence"); const auto batch_text_file = optional_path_arg(argc, argv, "--batch-text-file"); const auto batch_text_dir = optional_path_arg(argc, argv, "--batch-text-dir"); @@ -293,10 +294,7 @@ minitts::app::AppBatchRequest build_batch_request_from_cli( base_request, find_arg(argc, argv, "--language").value_or("")); } - return build_audio_dir_batch( - *batch_audio_dir, - base_request, - find_arg(argc, argv, "--batch-audio-role").value_or("audio")); + return build_audio_dir_batch(*batch_audio_dir, base_request, audio_role); } } // namespace minitts::cli diff --git a/app/cli/batch.h b/app/cli/batch.h index 61a6e9a0d..e643db9a0 100644 --- a/app/cli/batch.h +++ b/app/cli/batch.h @@ -13,6 +13,7 @@ bool has_batch_input(int argc, char ** argv); minitts::app::AppBatchRequest build_batch_request_from_cli( int argc, char ** argv, - const engine::runtime::TaskRequest & base_request); + const engine::runtime::TaskRequest & base_request, + const std::string & audio_role); } // namespace minitts::cli diff --git a/app/cli/main.cpp b/app/cli/main.cpp index 2a75ce65d..ad59f4555 100644 --- a/app/cli/main.cpp +++ b/app/cli/main.cpp @@ -396,10 +396,13 @@ void print_inspection(const engine::runtime::ModelInspection & inspection) { } bool has_vad_chunk_option(int argc, char ** argv) { - return minitts::cli::optional_path_arg(argc, argv, "--vad-chunks-out").has_value() || - minitts::cli::find_arg(argc, argv, "--vad-chunk-max-seconds").has_value() || - minitts::cli::find_arg(argc, argv, "--vad-chunk-merge-gap-seconds").has_value() || - minitts::cli::find_arg(argc, argv, "--vad-chunk-padding-seconds").has_value(); + // Every option is looked up before the ors are taken. Short-circuiting would leave the later + // ones unread, and an option the CLI never asked for is what require_known_args rejects. + const bool chunks_out = minitts::cli::optional_path_arg(argc, argv, "--vad-chunks-out").has_value(); + const bool max_seconds = minitts::cli::find_arg(argc, argv, "--vad-chunk-max-seconds").has_value(); + const bool merge_gap = minitts::cli::find_arg(argc, argv, "--vad-chunk-merge-gap-seconds").has_value(); + const bool padding = minitts::cli::find_arg(argc, argv, "--vad-chunk-padding-seconds").has_value(); + return chunks_out || max_seconds || merge_gap || padding; } int64_t seconds_to_samples(float seconds, int sample_rate, const std::string & name) { @@ -501,8 +504,7 @@ std::optional audio_metrics_info( // Feeds raw PCM from stdin into the session chunk by chunk, so nothing has to be buffered up // front and transcription tracks the input as it arrives. engine::runtime::TaskResult run_streaming_from_stdin( - int argc, - char ** argv, + const std::string & input_format, engine::runtime::IStreamingVoiceTaskSession & streaming, const engine::runtime::TaskRequest & request, const minitts::app::StreamEventSink & sink) { @@ -511,8 +513,7 @@ engine::runtime::TaskResult run_streaming_from_stdin( if (!request.audio_input.has_value()) { throw std::runtime_error("streaming from stdin requires an audio format contract"); } - const auto sample_format = minitts::app::parse_pcm_sample_format( - minitts::cli::find_arg(argc, argv, "--input-format").value_or("s16le")); + const auto sample_format = minitts::app::parse_pcm_sample_format(input_format); const minitts::app::AudioStreamFormat format{ request.audio_input->sample_rate, request.audio_input->channels, @@ -531,8 +532,11 @@ void run_streaming( char ** argv, engine::runtime::IStreamingVoiceTaskSession & streaming, engine::runtime::IVoiceTaskSession & session, - engine::runtime::TaskRequest & request) { - const auto out_dir = minitts::cli::optional_path_arg(argc, argv, "--out-dir"); + engine::runtime::TaskRequest & request, + const minitts::app::FileOutputPolicy & outputs, + const std::string & input_format, + const std::optional & text_out) { + const auto & out_dir = outputs.output_dir; minitts::cli::PartialTextRenderer partial_renderer(stdout_is_terminal()); const minitts::app::StreamEventSink sink = [&](const engine::runtime::StreamEvent & event) { @@ -573,7 +577,7 @@ void run_streaming( }; const auto result = stream_audio_from_stdin(argc, argv) - ? run_streaming_from_stdin(argc, argv, streaming, request, sink) + ? run_streaming_from_stdin(input_format, streaming, request, sink) : minitts::app::run_streaming_task(streaming, request, sink); // Close the transcript line so the summary below starts on a fresh row. std::cout << partial_renderer.finish(); @@ -582,13 +586,13 @@ void run_streaming( std::cout << "mode=" << engine::runtime::to_string(session.run_mode()) << "\n"; minitts::app::emit_task_result( result, - minitts::cli::optional_path_arg(argc, argv, "--out"), + outputs.audio_out, std::nullopt, - minitts::cli::optional_path_arg(argc, argv, "--out-dir"), - minitts::cli::optional_path_arg(argc, argv, "--segments-out"), - minitts::cli::optional_path_arg(argc, argv, "--turns-out"), - minitts::cli::optional_path_arg(argc, argv, "--words-out")); - if (const auto text_out = minitts::cli::optional_path_arg(argc, argv, "--text-out")) { + outputs.output_dir, + outputs.segments_base, + outputs.turns_base, + outputs.words_base); + if (text_out.has_value()) { write_text_output(result, *text_out, "text_out"); } } @@ -639,6 +643,11 @@ int audiocpp_cli_main(int argc, char ** argv) { registry_config ? std::optional(std::filesystem::path(*registry_config)) : std::nullopt); auto pipeline_registry = minitts::app::make_default_pipeline_registry(); const bool help_requested = has_arg(argc, argv, "--help"); + // Read before the command branches below. An option that no lookup ever runs for cannot + // be told apart from a misspelling, which is what require_known_args rejects. + const bool json_output = has_arg(argc, argv, "--json"); + const auto task_name = find_arg(argc, argv, "--task"); + const auto mode_name = find_arg(argc, argv, "--mode").value_or("offline"); if (has_arg(argc, argv, "--list-pipelines")) { const auto ids = pipeline_registry.ids(); std::cout << "registered_pipelines=" << ids.size() << "\n"; @@ -653,7 +662,7 @@ int audiocpp_cli_main(int argc, char ** argv) { } if (has_arg(argc, argv, "--list-loaders")) { const auto advertisements = registry.advertise_loaders(); - if (has_arg(argc, argv, "--json")) { + if (json_output) { engine::io::json::Value::Object loaders_object; for (const auto & row : advertisements) { engine::io::json::Value::Object tasks_object; @@ -719,6 +728,11 @@ int audiocpp_cli_main(int argc, char ** argv) { const auto model_arg = find_arg(argc, argv, "--model"); const auto pipeline_arg = find_arg(argc, argv, "--pipeline"); + // Read up front for the same reason as the options above. + const auto workflow_inputs = collect_key_value_args(argc, argv, "--workflow-input"); + const auto audio_converter = find_arg(argc, argv, "--audio-converter").value_or("ffmpeg"); + const auto batch_merge_audio = find_arg(argc, argv, "--batch-merge-audio").value_or("none"); + const auto batch_audio_role = find_arg(argc, argv, "--batch-audio-role").value_or("audio"); if (pipeline_arg.has_value()) { const int threads = parse_int_arg(argc, argv, "--threads", 4); if (threads <= 0) { @@ -740,15 +754,15 @@ int audiocpp_cli_main(int argc, char ** argv) { optional_path_arg(argc, argv, "--out"), collect_key_value_args(argc, argv, "--load-option"), collect_key_value_args(argc, argv, "--session-option"), - collect_key_value_args(argc, argv, "--workflow-input"), + workflow_inputs, optional_path_arg(argc, argv, "--model-spec-override"), - find_arg(argc, argv, "--audio-converter").value_or("ffmpeg"), + audio_converter, }); return 0; } if (help_requested && !model_arg.has_value()) { - if (const auto task = find_arg(argc, argv, "--task")) { - print_task_help(registry, *task); + if (task_name.has_value()) { + print_task_help(registry, *task_name); } else { print_task_list_help(); } @@ -782,11 +796,9 @@ int audiocpp_cli_main(int argc, char ** argv) { return 0; } - const auto task_name = find_arg(argc, argv, "--task"); if (!task_name.has_value()) { throw std::runtime_error("missing required --task argument"); } - const auto mode_name = find_arg(argc, argv, "--mode").value_or("offline"); const engine::runtime::TaskSpec task_spec{ engine::runtime::parse_voice_task_kind(*task_name), engine::runtime::parse_run_mode(mode_name), @@ -816,6 +828,15 @@ int audiocpp_cli_main(int argc, char ** argv) { const auto text_out = optional_path_arg(argc, argv, "--text-out"); const auto words_out = optional_path_arg(argc, argv, "--words-out"); const auto vad_chunks_out = optional_path_arg(argc, argv, "--vad-chunks-out"); + const minitts::app::FileOutputPolicy outputs{ + optional_path_arg(argc, argv, "--out"), + optional_path_arg(argc, argv, "--out-dir"), + optional_path_arg(argc, argv, "--segments-out"), + optional_path_arg(argc, argv, "--turns-out"), + words_out, + optional_path_arg(argc, argv, "--batch-manifest-out"), + }; + const auto input_format = find_arg(argc, argv, "--input-format").value_or("s16le"); if (has_batch_input(argc, argv)) { if (task_spec.mode != engine::runtime::RunMode::Offline) { @@ -827,9 +848,8 @@ int audiocpp_cli_main(int argc, char ** argv) { if (has_vad_chunk_option(argc, argv)) { throw std::runtime_error("VAD chunk output options are not supported with batch inputs"); } - const auto merge_mode = minitts::app::parse_audio_merge_mode( - find_arg(argc, argv, "--batch-merge-audio").value_or("none")); - if (optional_path_arg(argc, argv, "--out").has_value() && + const auto merge_mode = minitts::app::parse_audio_merge_mode(batch_merge_audio); + if (outputs.audio_out.has_value() && merge_mode == minitts::app::AudioMergeMode::None) { throw std::runtime_error("batch --out requires --batch-merge-audio concat"); } @@ -844,20 +864,13 @@ int audiocpp_cli_main(int argc, char ** argv) { if (words_out.has_value()) { base_request.options["return_timestamps"] = "true"; } - auto batch_request = build_batch_request_from_cli(argc, argv, base_request); + auto batch_request = build_batch_request_from_cli(argc, argv, base_request, batch_audio_role); if (words_out.has_value()) { for (auto & item : batch_request.requests) { item.request.options["return_timestamps"] = "true"; } } - const minitts::app::FileOutputPolicy output_policy{ - optional_path_arg(argc, argv, "--out"), - optional_path_arg(argc, argv, "--out-dir"), - optional_path_arg(argc, argv, "--segments-out"), - optional_path_arg(argc, argv, "--turns-out"), - words_out, - optional_path_arg(argc, argv, "--batch-manifest-out"), - }; + require_known_args(argc, argv); std::cout << "family=" << session->family() << "\n"; std::cout << "task=" << engine::runtime::to_string(session->task_kind()) << "\n"; std::cout << "mode=" << engine::runtime::to_string(session->run_mode()) << "\n"; @@ -867,7 +880,7 @@ int audiocpp_cli_main(int argc, char ** argv) { batch_request, merge_mode, [&](size_t index, const minitts::app::AppRequestResult & item) { - minitts::app::emit_batch_item_result(index, item, output_policy); + minitts::app::emit_batch_item_result(index, item, outputs); if (metrics_requested) { minitts::app::emit_task_metrics( item.result, @@ -882,7 +895,7 @@ int audiocpp_cli_main(int argc, char ** argv) { write_text_output(item.result, path, "text_out[" + request_id + "]"); } }); - minitts::app::emit_batch_summary(batch_result, output_policy); + minitts::app::emit_batch_summary(batch_result, outputs); return 0; } @@ -911,6 +924,7 @@ int audiocpp_cli_main(int argc, char ** argv) { } request.options["pocket_tts.export_voice_state_path"] = voice_state_out->string(); } + require_known_args(argc, argv); const auto session_start = std::chrono::steady_clock::now(); session->prepare(engine::runtime::build_preparation_request(request)); if (voice_state_out.has_value()) { @@ -931,11 +945,11 @@ int audiocpp_cli_main(int argc, char ** argv) { std::cout << "mode=" << engine::runtime::to_string(session->run_mode()) << "\n"; minitts::app::emit_task_result( result, - optional_path_arg(argc, argv, "--out"), - optional_path_arg(argc, argv, "--out-dir"), - optional_path_arg(argc, argv, "--out-dir"), - optional_path_arg(argc, argv, "--segments-out"), - optional_path_arg(argc, argv, "--turns-out"), + outputs.audio_out, + outputs.output_dir, + outputs.output_dir, + outputs.segments_base, + outputs.turns_base, words_out); if (vad_chunks_out.has_value()) { write_vad_chunks_output( @@ -957,7 +971,7 @@ int audiocpp_cli_main(int argc, char ** argv) { if (streaming == nullptr) { throw std::runtime_error("selected task session does not support streaming execution"); } - run_streaming(argc, argv, *streaming, *session, request); + run_streaming(argc, argv, *streaming, *session, request, outputs, input_format, text_out); return 0; } catch (const std::exception & ex) { std::cerr << "audiocpp_cli failed: " << ex.what() << "\n"; @@ -976,7 +990,9 @@ int wmain(int argc, wchar_t ** wargv) { } argv.push_back(nullptr); const int status = audiocpp_cli_main(argc, argv.data()); - minitts::cli::report_unused_args(argc, argv.data()); + if (status == 0) { + minitts::cli::warn_ignored_args(argc, argv.data()); + } return status; } catch (const std::exception & ex) { std::cerr << "audiocpp_cli failed: " << ex.what() << "\n"; @@ -986,7 +1002,9 @@ int wmain(int argc, wchar_t ** wargv) { #else int main(int argc, char ** argv) { const int status = audiocpp_cli_main(argc, argv); - minitts::cli::report_unused_args(argc, argv); + if (status == 0) { + minitts::cli::warn_ignored_args(argc, argv); + } return status; } #endif diff --git a/app/cli/request.cpp b/app/cli/request.cpp index 71d591b0f..da30494b6 100644 --- a/app/cli/request.cpp +++ b/app/cli/request.cpp @@ -272,15 +272,15 @@ engine::runtime::TaskRequest build_request_from_cli(int argc, char ** argv) { if (const auto text = find_arg(argc, argv, "--text")) { request.text_input = engine::runtime::Transcript{*text, language}; } + // Read outside the branch below: only a live stdin source uses them, but an option the CLI + // never looks up cannot be told apart from a misspelling. + const int input_rate = parse_int_arg(argc, argv, "--input-rate", 16000); + const int input_channels = parse_int_arg(argc, argv, "--input-channels", 1); if (const auto audio_path = find_arg(argc, argv, "--audio")) { if (is_stdin_audio_source(*audio_path)) { // Live PCM arrives chunk by chunk, so only the format contract is known up front. // The samples stay empty; the streaming driver pulls them from stdin instead. - request.audio_input = engine::runtime::AudioBuffer{ - parse_int_arg(argc, argv, "--input-rate", 16000), - parse_int_arg(argc, argv, "--input-channels", 1), - {}, - }; + request.audio_input = engine::runtime::AudioBuffer{input_rate, input_channels, {}}; } else { request.audio_input = read_audio_buffer(std::filesystem::path(*audio_path)); }