4444#include < cstdint>
4545#include < cstdlib>
4646#include < cstring>
47+ #include < filesystem>
4748#include < fstream>
4849#include < iostream>
4950#include < mutex>
@@ -222,6 +223,7 @@ struct Word {
222223
223224int main (int argc, char ** argv) {
224225 std::string model_path;
226+ std::string vad_model_path;
225227 std::string host = " 127.0.0.1" ;
226228 bool host_from_flag = false ;
227229 bool force_cpu = false ;
@@ -231,6 +233,7 @@ int main(int argc, char** argv) {
231233 for (int i = 1 ; i < argc; ++i) {
232234 const std::string a = argv[i];
233235 if (a == " --model" && i + 1 < argc) model_path = argv[++i];
236+ else if (a == " --vad-model" && i + 1 < argc) vad_model_path = argv[++i];
234237 else if (a == " --host" && i + 1 < argc) { host = argv[++i]; host_from_flag = true ; }
235238 else if (a == " --port" && i + 1 < argc) port = std::atoi (argv[++i]);
236239 else if (a == " --threads" && i + 1 < argc) threads = std::atoi (argv[++i]);
@@ -241,6 +244,10 @@ int main(int argc, char** argv) {
241244 if (model_path.empty ()) {
242245 if (const char * p = std::getenv (" OPENSCREEN_WHISPER_MODEL" )) model_path = p;
243246 }
247+ if (vad_model_path.empty ()) {
248+ if (const char * p = std::getenv (" OPENSCREEN_VAD_MODEL" )) vad_model_path = p;
249+ else if (const char * p = std::getenv (" OPENSCREEN_WHISPER_VAD_MODEL" )) vad_model_path = p;
250+ }
244251 if (port == 0 ) {
245252 if (const char * p = std::getenv (" OPENSCREEN_WHISPER_PORT" )) port = std::atoi (p);
246253 }
@@ -264,7 +271,9 @@ int main(int argc, char** argv) {
264271 " OPENSCREEN_WHISPER_MODEL is required" << std::endl;
265272 return 2 ;
266273 }
267- log (" boot: model=" + model_path + " host=" + host +
274+ log (" boot: model=" + model_path +
275+ (!vad_model_path.empty () ? (" vad_model=" + vad_model_path) : " " ) +
276+ " host=" + host +
268277 " port=" + (port > 0 ? std::to_string (port) : " (any)" ) +
269278 " threads=" + std::to_string (threads));
270279
@@ -293,6 +302,25 @@ int main(int argc, char** argv) {
293302 const std::string active_backend = cparams.use_gpu ? detect_active_backend () : " whispercpp-cpu" ;
294303 log (" model loaded; backend=" + active_backend);
295304
305+ // ---- Init VAD context (Silero VAD v6.2.0) ----
306+ struct whisper_vad_context * vctx = nullptr ;
307+ if (!vad_model_path.empty ()) {
308+ struct whisper_vad_context_params vad_ctx_params = whisper_vad_default_context_params ();
309+ vad_ctx_params.n_threads = threads;
310+ vad_ctx_params.use_gpu = !force_cpu;
311+ vctx = whisper_vad_init_from_file_with_params (vad_model_path.c_str (), vad_ctx_params);
312+ if (!vctx && vad_ctx_params.use_gpu ) {
313+ log (" GPU VAD initialization failed; retrying with CPU inference" );
314+ vad_ctx_params.use_gpu = false ;
315+ vctx = whisper_vad_init_from_file_with_params (vad_model_path.c_str (), vad_ctx_params);
316+ }
317+ if (vctx) {
318+ log (" VAD model loaded; path=" + vad_model_path);
319+ } else {
320+ log (" WARNING: failed to load VAD model from " + vad_model_path);
321+ }
322+ }
323+
296324 // ---- HTTP server ----
297325 httplib::Server svr;
298326 std::mutex infer_mu; // whisper contexts are not thread-safe
@@ -366,6 +394,11 @@ int main(int argc, char** argv) {
366394 wparams.print_realtime = false ;
367395 wparams.print_timestamps = false ;
368396 wparams.n_threads = threads;
397+ if (vctx && !vad_model_path.empty ()) {
398+ wparams.vad = true ;
399+ wparams.vad_model_path = vad_model_path.c_str ();
400+ wparams.vad_params = whisper_vad_default_params ();
401+ }
369402
370403 const auto t0 = std::chrono::steady_clock::now ();
371404 const int rc = whisper_full (ctx, wparams, pcm.data (), static_cast <int >(pcm.size ()));
@@ -540,12 +573,80 @@ int main(int argc, char** argv) {
540573 res.set_content (reply.dump (), " application/json" );
541574 });
542575
576+ // POST /vad — multipart form with `file` (WAV: 16 kHz mono PCM16).
577+ // Runs Silero VAD segmentation and returns speech intervals [start, end] in seconds.
578+ svr.Post (" /vad" , [&](const httplib::Request& req, httplib::Response& res) {
579+ if (!vctx) {
580+ res.status = 400 ;
581+ res.set_content (R"( {"error":"VAD model was not loaded on server startup"})" , " application/json" );
582+ return ;
583+ }
584+ auto it = req.files .find (" file" );
585+ if (it == req.files .end ()) {
586+ res.status = 400 ;
587+ res.set_content (R"( {"error":"missing 'file' form field"})" , " application/json" );
588+ return ;
589+ }
590+ const auto & file_entry = it->second ;
591+
592+ static std::atomic<uint64_t > tmp_vad_counter{0 };
593+ const auto tmp_vad_id = tmp_vad_counter.fetch_add (1 , std::memory_order_relaxed);
594+ const auto tmp_vad_ts = std::chrono::high_resolution_clock::now ().time_since_epoch ().count ();
595+ const std::string tmp_wav = (std::filesystem::temp_directory_path () /
596+ (" openscreen-vad-" + std::to_string (tmp_vad_ts) +
597+ " -" + std::to_string (tmp_vad_id) + " .wav" )).string ();
598+ {
599+ std::ofstream out (tmp_wav, std::ios::binary);
600+ out.write (file_entry.content .data (),
601+ static_cast <std::streamsize>(file_entry.content .size ()));
602+ }
603+ std::vector<float > pcm;
604+ int sample_rate = 0 , channels = 0 ;
605+ const bool ok = read_wav_pcm16 (tmp_wav, pcm, sample_rate, channels);
606+ std::error_code ec;
607+ std::filesystem::remove (tmp_wav, ec);
608+ if (!ok) {
609+ res.status = 400 ;
610+ res.set_content (R"( {"error":"failed to parse WAV"})" , " application/json" );
611+ return ;
612+ }
613+ if (sample_rate != 16000 || channels != 1 ) {
614+ res.status = 400 ;
615+ res.set_content (
616+ R"( {"error":"expected 16 kHz mono PCM16 WAV"})" ,
617+ " application/json" );
618+ return ;
619+ }
620+
621+ const std::lock_guard<std::mutex> lk (infer_mu);
622+ struct whisper_vad_params vad_params = whisper_vad_default_params ();
623+ struct whisper_vad_segments * vad_segments =
624+ whisper_vad_segments_from_samples (vctx, vad_params, pcm.data (), static_cast <int >(pcm.size ()));
625+
626+ nlohmann::json reply;
627+ reply[" segments" ] = nlohmann::json::array ();
628+ if (vad_segments) {
629+ const int n_segs = whisper_vad_segments_n_segments (vad_segments);
630+ for (int i = 0 ; i < n_segs; ++i) {
631+ const float t0 = whisper_vad_segments_get_segment_t0 (vad_segments, i);
632+ const float t1 = whisper_vad_segments_get_segment_t1 (vad_segments, i);
633+ reply[" segments" ].push_back ({
634+ {" start" , static_cast <double >(t0)},
635+ {" end" , static_cast <double >(t1)}
636+ });
637+ }
638+ whisper_vad_free_segments (vad_segments);
639+ }
640+ res.set_content (reply.dump (), " application/json" );
641+ });
642+
543643 // ---- bind + listen ----
544644 int bound_port = port;
545645 if (bound_port == 0 ) {
546646 bound_port = svr.bind_to_any_port (host);
547647 } else if (!svr.bind_to_port (host, bound_port)) {
548648 std::cerr << " FATAL: bind_to_port(" << host << " :" << bound_port << " ) failed" << std::endl;
649+ if (vctx) whisper_vad_free (vctx);
549650 whisper_free (ctx);
550651 return 4 ;
551652 }
@@ -554,6 +655,7 @@ int main(int argc, char** argv) {
554655 if (rc != 0 ) {
555656 std::cerr << " FATAL: listen_after_bind failed" << std::endl;
556657 }
658+ if (vctx) whisper_vad_free (vctx);
557659 whisper_free (ctx);
558660 return rc;
559661}
0 commit comments