From fc496354916bede03c6f030bb768d99938a667c3 Mon Sep 17 00:00:00 2001 From: Lakshya Date: Sat, 4 Jul 2026 18:18:58 +0530 Subject: [PATCH 1/2] stream: add interactive start/stop mode (-i) Press SPACE to start/stop microphone capture and Q to quit, instead of transcribing continuously from launch. Closes #969. Uses non-blocking terminal input (termios on POSIX, conio on Windows), only active with -i and when stdin is a TTY; default behaviour unchanged. --- examples/stream/stream.cpp | 77 +++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/examples/stream/stream.cpp b/examples/stream/stream.cpp index 94f9016e075..72acc492638 100644 --- a/examples/stream/stream.cpp +++ b/examples/stream/stream.cpp @@ -9,11 +9,49 @@ #include #include +#include #include #include #include #include +// --- minimal cross-platform non-blocking keyboard, for interactive start/stop --- +#ifdef _WIN32 +#include +static void kbd_enable() {} +static void kbd_restore() {} +static int kbd_get() { return _kbhit() ? _getch() : -1; } +#else +#include +#include +#include +static struct termios g_kbd_old; +static bool g_kbd_saved = false; +static void kbd_restore() { + if (g_kbd_saved) { tcsetattr(STDIN_FILENO, TCSANOW, &g_kbd_old); g_kbd_saved = false; } +} +static void kbd_enable() { + if (!isatty(STDIN_FILENO) || g_kbd_saved) return; + tcgetattr(STDIN_FILENO, &g_kbd_old); + g_kbd_saved = true; + atexit(kbd_restore); + struct termios tio = g_kbd_old; + tio.c_lflag &= ~(ICANON | ECHO); + tio.c_cc[VMIN] = 0; tio.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSANOW, &tio); +} +static int kbd_get() { + if (!isatty(STDIN_FILENO)) return -1; + fd_set fds; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); + struct timeval tv{0, 0}; + if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) > 0) { + unsigned char c; + if (read(STDIN_FILENO, &c, 1) == 1) return c; + } + return -1; +} +#endif + // command-line parameters struct whisper_params { int32_t n_threads = std::min(4, (int32_t) std::thread::hardware_concurrency()); @@ -37,6 +75,7 @@ struct whisper_params { bool save_audio = false; // save audio to wav file bool use_gpu = true; bool flash_attn = true; + bool interactive = false; // press SPACE to start/stop capture std::string language = "en"; std::string model = "models/ggml-base.en.bin"; @@ -75,6 +114,7 @@ static bool whisper_params_parse(int argc, char ** argv, whisper_params & params else if (arg == "-ng" || arg == "--no-gpu") { params.use_gpu = false; } else if (arg == "-fa" || arg == "--flash-attn") { params.flash_attn = true; } else if (arg == "-nfa" || arg == "--no-flash-attn") { params.flash_attn = false; } + else if (arg == "-i" || arg == "--interactive") { params.interactive = true; } else { fprintf(stderr, "error: unknown argument: %s\n", arg.c_str()); @@ -114,6 +154,7 @@ void whisper_print_usage(int /*argc*/, char ** argv, const whisper_params & para fprintf(stderr, " -ng, --no-gpu [%-7s] disable GPU inference\n", params.use_gpu ? "false" : "true"); fprintf(stderr, " -fa, --flash-attn [%-7s] enable flash attention during inference\n", params.flash_attn ? "true" : "false"); fprintf(stderr, " -nfa, --no-flash-attn [%-7s] disable flash attention during inference\n", params.flash_attn ? "false" : "true"); + fprintf(stderr, " -i, --interactive [%-7s] press SPACE to start/stop capture, Q to quit\n", params.interactive ? "true" : "false"); fprintf(stderr, "\n"); } @@ -230,7 +271,16 @@ int main(int argc, char ** argv) { wavWriter.open(filename, WHISPER_SAMPLE_RATE, 16, 1); } - printf("[Start speaking]\n"); + bool recording = true; + if (params.interactive) { + kbd_enable(); + audio.pause(); // start paused; wait for the user to press SPACE + audio.clear(); + recording = false; + printf("[Interactive mode: press SPACE to start/stop recording, Q to quit]\n"); + } else { + printf("[Start speaking]\n"); + } fflush(stdout); auto t_last = std::chrono::high_resolution_clock::now(); @@ -238,6 +288,30 @@ int main(int argc, char ** argv) { // main audio loop while (is_running) { + // interactive start/stop via keyboard (SPACE toggles capture, Q quits) + if (params.interactive) { + const int key = kbd_get(); + if (key == 'q' || key == 'Q') { + break; + } + if (key == ' ') { + recording = !recording; + if (recording) { + audio.clear(); + audio.resume(); + printf("\n[recording... press SPACE to stop]\n"); + } else { + audio.pause(); + printf("\n[stopped. press SPACE to record]\n"); + } + fflush(stdout); + } + if (!recording) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + continue; + } + } + if (params.save_audio) { wavWriter.write(pcmf32_new.data(), pcmf32_new.size()); } @@ -429,6 +503,7 @@ int main(int argc, char ** argv) { } audio.pause(); + kbd_restore(); whisper_print_timings(ctx); whisper_free(ctx); From b4087913c12af2b5debd535ce79d8abdad3e2c6b Mon Sep 17 00:00:00 2001 From: Lakshya Date: Sat, 4 Jul 2026 18:28:21 +0530 Subject: [PATCH 2/2] stream : document interactive start/stop (-i) in README --- examples/stream/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/stream/README.md b/examples/stream/README.md index f07cfb8915c..c133ade648a 100644 --- a/examples/stream/README.md +++ b/examples/stream/README.md @@ -25,6 +25,20 @@ It's best to tune it to the specific use case, but a value around `0.6` should b When silence is detected, it will transcribe the last `--length` milliseconds of audio and output a transcription block that is suitable for parsing. +## Interactive start/stop + +By default `whisper-stream` transcribes continuously from launch. Pass `-i` (`--interactive`) to +control capture from the keyboard instead: the tool starts paused, **SPACE** starts/stops recording, +and **Q** quits. + +```bash +./build/bin/whisper-stream -m ./models/ggml-base.en.bin -i +``` + +This is handy for dictation / push-to-talk style usage where you only want to transcribe the audio +you explicitly record. It works in both the default and sliding-window (`--step 0`) modes, and only +takes effect when stdin is a terminal (piped/redirected input is unaffected). + ## Building The `whisper-stream` tool depends on SDL2 library to capture audio from the microphone. You can build it like this: