From 370d79ce04e038844d7d9513a5c039db4c31f892 Mon Sep 17 00:00:00 2001 From: Tim Janik Date: Sat, 25 Jul 2026 23:29:12 +0200 Subject: [PATCH 1/2] add -o/--output flag to write transcription text to a file When -o is given, the CLI writes only the transcribed text (no metadata, no timestamps, no timings) to that file. Stdout is unchanged. In batch mode each utterance's text goes on its own line. examples/cli/main.cpp | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) --- examples/cli/main.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index df820403..5803f0d0 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -204,6 +204,7 @@ struct cli_args { bool quiet = false; bool list_devices = false; // --list-devices: print devices and exit bool batch_jsonl = false; // --batch-jsonl: output JSONL + std::string output_path; // -o: write raw text here int repeat = 1; int n_threads = 0; // 0 = library default (all cores) int n_ctx = 0; // 0 = model's true max; >0 lowers the cap @@ -282,6 +283,7 @@ void print_usage(const char * argv0) { " --target-language ISO target language for translation (e.g. de, es, fr)\n" " -q, --quiet suppress library log output\n" " -r, --repeat N run N times per file (benchmark)\n" + " -o, --output PATH write transcribed text to PATH (stdout unchanged)\n" " --threads N CPU threads (default: all cores)\n" " --n-ctx N session context/KV cap in tokens (bounds decoder\n" " KV memory; cannot extend the model): 0 = model\n" @@ -528,6 +530,12 @@ bool parse_args(int argc, char ** argv, cli_args & out) { std::fprintf(stderr, "error: --batch-size must be >= 0\n"); return false; } + } else if (a == "-o" || a == "--output") { + const char * v = take_value(a.c_str()); + if (!v) { + return false; + } + out.output_path = v; } else if (a == "--initial-prompt") { const char * v = take_value(a.c_str()); if (!v) { @@ -688,6 +696,20 @@ void log_cb(transcribe_log_level level, const char * msg, void * userdata) { std::fprintf(stderr, "%s %s%s", prefix, msg, (msg && *msg && msg[std::strlen(msg) - 1] == '\n') ? "" : "\n"); } +// Write transcription text to the output file if -o was given. +// Appends so it works for both single-file and batch mode. +static void write_output_file(const std::string & path, const char * text) { + if (path.empty() || text == nullptr || text[0] == '\0') { + return; + } + std::ofstream fout(path, std::ios::binary | std::ios::app); + if (!fout) { + std::fprintf(stderr, "error: cannot open %s for writing\n", path.c_str()); + } else { + fout << text << '\n'; + } +} + } // namespace int main(int argc, char ** argv) { @@ -957,6 +979,7 @@ int main(int argc, char ** argv) { std::printf(" ERROR: %s\n", transcribe_status_string(ust)); } } + write_output_file(args.output_path, text); std::fflush(stdout); } } @@ -1095,6 +1118,7 @@ int main(int argc, char ** argv) { std::printf(" ERROR: %s\n", transcribe_status_string(run_st)); } } + write_output_file(args.output_path, text); std::fflush(stdout); } } @@ -1341,6 +1365,7 @@ int main(int argc, char ** argv) { if (result_present) { const char * text = transcribe_full_text(ctx); std::printf("text: %s\n", (text && *text) ? text : "(empty)"); + write_output_file(args.output_path, text); // A truncated decode hit the model's context/output budget before // end-of-stream; the text above is incomplete. From 19be2a1eb54b3fcb04a0362cf57fdbab43da1685 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Thu, 30 Jul 2026 18:37:32 +0800 Subject: [PATCH 2/2] tweaks --- examples/cli/main.cpp | 61 ++++++++++++++++++++++-------------- tests/CMakeLists.txt | 7 +++++ tests/cli_output_smoke.cmake | 36 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 tests/cli_output_smoke.cmake diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 5803f0d0..1258b19c 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -204,14 +204,14 @@ struct cli_args { bool quiet = false; bool list_devices = false; // --list-devices: print devices and exit bool batch_jsonl = false; // --batch-jsonl: output JSONL - std::string output_path; // -o: write raw text here - int repeat = 1; - int n_threads = 0; // 0 = library default (all cores) - int n_ctx = 0; // 0 = model's true max; >0 lowers the cap - transcribe_kv_type kv_type = TRANSCRIBE_KV_TYPE_AUTO; - transcribe_backend_request backend = TRANSCRIBE_BACKEND_AUTO; - int gpu_device = 0; // --device N: 0 = auto, >0 = registry index - transcribe_timestamp_kind timestamps = TRANSCRIBE_TIMESTAMPS_AUTO; + std::string output_path; // -o/--output: write raw text here + int repeat = 1; + int n_threads = 0; // 0 = library default (all cores) + int n_ctx = 0; // 0 = model's true max; >0 lowers the cap + transcribe_kv_type kv_type = TRANSCRIBE_KV_TYPE_AUTO; + transcribe_backend_request backend = TRANSCRIBE_BACKEND_AUTO; + int gpu_device = 0; // --device N: 0 = auto, >0 = registry index + transcribe_timestamp_kind timestamps = TRANSCRIBE_TIMESTAMPS_AUTO; // Whisper-family knobs. Ignored for non-Whisper models. std::string initial_prompt; // --initial-prompt TEXT @@ -696,18 +696,21 @@ void log_cb(transcribe_log_level level, const char * msg, void * userdata) { std::fprintf(stderr, "%s %s%s", prefix, msg, (msg && *msg && msg[std::strlen(msg) - 1] == '\n') ? "" : "\n"); } -// Write transcription text to the output file if -o was given. -// Appends so it works for both single-file and batch mode. -static void write_output_file(const std::string & path, const char * text) { - if (path.empty() || text == nullptr || text[0] == '\0') { - return; +bool write_output_file(std::ofstream * output, const std::string & path, const char * text) { + if (output == nullptr) { + return true; } - std::ofstream fout(path, std::ios::binary | std::ios::app); - if (!fout) { - std::fprintf(stderr, "error: cannot open %s for writing\n", path.c_str()); - } else { - fout << text << '\n'; + const char * value = text != nullptr ? text : ""; + *output << value; + if (value[0] == '\0' || value[std::strlen(value) - 1] != '\n') { + *output << '\n'; } + output->flush(); + if (!*output) { + std::fprintf(stderr, "error: cannot write %s\n", path.c_str()); + return false; + } + return true; } } // namespace @@ -735,6 +738,18 @@ int main(int argc, char ** argv) { transcribe_log_set(log_cb, nullptr); } + std::ofstream output_file; + std::ofstream * output = nullptr; + if (!args.output_path.empty()) { + output_file.open(args.output_path, std::ios::binary | std::ios::trunc); + if (!output_file) { + std::fprintf(stderr, "error: cannot open %s for writing\n", args.output_path.c_str()); + return EXIT_FAILURE; + } + output = &output_file; + } + bool output_ok = true; + // Batch mode: --batch reads a file list, one wav path per line. Loads // the model ONCE and reuses the context across all files. Outputs one // JSONL line per file to stdout when --batch-jsonl is set, otherwise @@ -979,7 +994,7 @@ int main(int argc, char ** argv) { std::printf(" ERROR: %s\n", transcribe_status_string(ust)); } } - write_output_file(args.output_path, text); + output_ok = write_output_file(output, args.output_path, text) && output_ok; std::fflush(stdout); } } @@ -1118,7 +1133,7 @@ int main(int argc, char ** argv) { std::printf(" ERROR: %s\n", transcribe_status_string(run_st)); } } - write_output_file(args.output_path, text); + output_ok = write_output_file(output, args.output_path, text) && output_ok; std::fflush(stdout); } } @@ -1132,7 +1147,7 @@ int main(int argc, char ** argv) { transcribe_model_free(model); // OUTPUT_TRUNCATED is result-bearing and does not fail the batch, but // hard per-utterance failures must remain visible to automation. - return n_fail > 0 ? EXIT_FAILURE : EXIT_SUCCESS; + return n_fail > 0 || !output_ok ? EXIT_FAILURE : EXIT_SUCCESS; } // Single-file mode. @@ -1365,7 +1380,7 @@ int main(int argc, char ** argv) { if (result_present) { const char * text = transcribe_full_text(ctx); std::printf("text: %s\n", (text && *text) ? text : "(empty)"); - write_output_file(args.output_path, text); + output_ok = write_output_file(output, args.output_path, text) && output_ok; // A truncated decode hit the model's context/output budget before // end-of-stream; the text above is incomplete. @@ -1443,7 +1458,7 @@ int main(int argc, char ** argv) { transcribe_session_free(ctx); transcribe_model_free(model); - if (run_st != TRANSCRIBE_OK) { + if (run_st != TRANSCRIBE_OK || !output_ok) { return EXIT_FAILURE; } } else { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bec7501c..b463c276 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1234,4 +1234,11 @@ if(TRANSCRIBE_BUILD_EXAMPLES) ${CMAKE_SOURCE_DIR}/samples/jfk.wav) set_tests_properties(transcribe_cli_smoke PROPERTIES PASS_REGULAR_EXPRESSION "duration:") + + add_test(NAME transcribe_cli_output_smoke + COMMAND ${CMAKE_COMMAND} + -DCLI=$ + -DWAV=${CMAKE_SOURCE_DIR}/samples/jfk.wav + -DTEST_DIR=${CMAKE_CURRENT_BINARY_DIR}/cli-output-smoke + -P ${CMAKE_CURRENT_SOURCE_DIR}/cli_output_smoke.cmake) endif() diff --git a/tests/cli_output_smoke.cmake b/tests/cli_output_smoke.cmake new file mode 100644 index 00000000..699b056a --- /dev/null +++ b/tests/cli_output_smoke.cmake @@ -0,0 +1,36 @@ +foreach(_var CLI WAV TEST_DIR) + if(NOT DEFINED ${_var} OR "${${_var}}" STREQUAL "") + message(FATAL_ERROR "${_var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${TEST_DIR}") +file(MAKE_DIRECTORY "${TEST_DIR}") + +set(_output "${TEST_DIR}/transcript.txt") +file(WRITE "${_output}" "stale output\n") +execute_process( + COMMAND "${CLI}" -q -o "${_output}" "${WAV}" + RESULT_VARIABLE _result + OUTPUT_VARIABLE _stdout + ERROR_VARIABLE _stderr) +if(NOT _result EQUAL 0) + message(FATAL_ERROR "output command failed (${_result}):\n${_stdout}\n${_stderr}") +endif() +file(SIZE "${_output}" _output_size) +if(NOT _output_size EQUAL 0) + message(FATAL_ERROR "--output did not truncate stale output") +endif() + +set(_bad_output "${TEST_DIR}/missing/transcript.txt") +execute_process( + COMMAND "${CLI}" -q -o "${_bad_output}" "${WAV}" + RESULT_VARIABLE _bad_result + OUTPUT_VARIABLE _bad_stdout + ERROR_VARIABLE _bad_stderr) +if(_bad_result EQUAL 0) + message(FATAL_ERROR "unwritable output path unexpectedly succeeded") +endif() +if(EXISTS "${_bad_output}") + message(FATAL_ERROR "unwritable output path unexpectedly created a file") +endif()