Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 49 additions & 9 deletions examples/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +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
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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -688,6 +696,23 @@ 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");
}

bool write_output_file(std::ofstream * output, const std::string & path, const char * text) {
if (output == nullptr) {
return true;
}
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

int main(int argc, char ** argv) {
Expand All @@ -713,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
Expand Down Expand Up @@ -957,6 +994,7 @@ int main(int argc, char ** argv) {
std::printf(" ERROR: %s\n", transcribe_status_string(ust));
}
}
output_ok = write_output_file(output, args.output_path, text) && output_ok;
std::fflush(stdout);
}
}
Expand Down Expand Up @@ -1095,6 +1133,7 @@ int main(int argc, char ** argv) {
std::printf(" ERROR: %s\n", transcribe_status_string(run_st));
}
}
output_ok = write_output_file(output, args.output_path, text) && output_ok;
std::fflush(stdout);
}
}
Expand All @@ -1108,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.
Expand Down Expand Up @@ -1341,6 +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)");
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.
Expand Down Expand Up @@ -1418,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 {
Expand Down
7 changes: 7 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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=$<TARGET_FILE:transcribe-cli>
-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()
36 changes: 36 additions & 0 deletions tests/cli_output_smoke.cmake
Original file line number Diff line number Diff line change
@@ -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()