https://user-images.githubusercontent.com/1991296/197385372-962a6dea-bca1-4d50-bf96-1d8c27b98c81.mp4
-You can also easily make your own offline voice assistant application:
+You can also easily make your own offline voice assistant application: [command](examples/command)
https://user-images.githubusercontent.com/1991296/204038393-2f846eae-c255-4099-a76d-5735c25c49da.mp4
if (EMSCRIPTEN)
add_subdirectory(whisper.wasm)
add_subdirectory(stream.wasm)
+ add_subdirectory(command.wasm)
add_subdirectory(talk.wasm)
else()
add_subdirectory(main)
--- /dev/null
+#
+# libcommand
+#
+
+set(TARGET libcommand)
+
+add_executable(${TARGET}
+ emscripten.cpp
+ )
+
+target_link_libraries(${TARGET} PRIVATE
+ whisper
+ )
+
+unset(EXTRA_FLAGS)
+
+if (WHISPER_WASM_SINGLE_FILE)
+ set(EXTRA_FLAGS "-s SINGLE_FILE=1")
+ message(STATUS "Embedding WASM inside command.js")
+
+ add_custom_command(
+ TARGET ${TARGET} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy
+ ${CMAKE_BINARY_DIR}/bin/libcommand.js
+ ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/command.wasm/command.js
+ )
+endif()
+
+set_target_properties(${TARGET} PROPERTIES LINK_FLAGS " \
+ --bind \
+ -s USE_PTHREADS=1 \
+ -s PTHREAD_POOL_SIZE=8 \
+ -s INITIAL_MEMORY=1024MB \
+ -s TOTAL_MEMORY=1024MB \
+ -s FORCE_FILESYSTEM=1 \
+ -s EXPORTED_RUNTIME_METHODS=\"['print', 'printErr', 'ccall', 'cwrap']\" \
+ ${EXTRA_FLAGS} \
+ ")
+
+#
+# command.wasm
+#
+
+set(TARGET command.wasm)
+
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/index-tmpl.html ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/index.html @ONLY)
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/../helpers.js ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/helpers.js @ONLY)
--- /dev/null
+# command.wasm
+
+This is a basic Voice Assistant example that accepts voice commands from the microphone.
+It runs in fully in the browser via WebAseembly.
+
+Online demo: https://whisper.ggerganov.com/command/
+
+Terminal version: https://github.com/ggerganov/whisper.cpp/examples/command
+
+## Build instructions
+
+```bash
+# build using Emscripten (v3.1.2)
+git clone https://github.com/ggerganov/whisper.cpp
+cd whisper.cpp
+mkdir build-em && cd build-em
+emcmake cmake ..
+make -j
+
+# copy the produced page to your HTTP path
+cp bin/command.wasm/* /path/to/html/
+cp bin/libcommand.worker.js /path/to/html/
+```
--- /dev/null
+#include "ggml.h"
+#include "whisper.h"
+
+#include <emscripten.h>
+#include <emscripten/bind.h>
+
+#include <atomic>
+#include <cmath>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+#include <regex>
+
+constexpr int N_THREAD = 8;
+
+std::vector<struct whisper_context *> g_contexts(4, nullptr);
+
+std::mutex g_mutex;
+std::thread g_worker;
+
+std::atomic<bool> g_running(false);
+
+std::string g_status = "";
+std::string g_status_forced = "";
+std::string g_transcribed = "";
+
+std::vector<float> g_pcmf32;
+
+static std::string trim(const std::string & s) {
+ std::regex e("^\\s+|\\s+$");
+ return std::regex_replace(s, e, "");
+}
+
+static void high_pass_filter(std::vector<float> & data, float cutoff, float sample_rate) {
+ const float rc = 1.0f / (2.0f * M_PI * cutoff);
+ const float dt = 1.0f / sample_rate;
+ const float alpha = dt / (rc + dt);
+
+ float y = data[0];
+
+ for (size_t i = 1; i < data.size(); i++) {
+ y = alpha * (y + data[i] - data[i - 1]);
+ data[i] = y;
+ }
+}
+
+// compute similarity between two strings using Levenshtein distance
+static float similarity(const std::string & s0, const std::string & s1) {
+ const size_t len0 = s0.size() + 1;
+ const size_t len1 = s1.size() + 1;
+
+ std::vector<int> col(len1, 0);
+ std::vector<int> prevCol(len1, 0);
+
+ for (size_t i = 0; i < len1; i++) {
+ prevCol[i] = i;
+ }
+
+ for (size_t i = 0; i < len0; i++) {
+ col[0] = i;
+ for (size_t j = 1; j < len1; j++) {
+ col[j] = std::min(std::min(1 + col[j - 1], 1 + prevCol[j]), prevCol[j - 1] + (s0[i - 1] == s1[j - 1] ? 0 : 1));
+ }
+ col.swap(prevCol);
+ }
+
+ const float dist = prevCol[len1 - 1];
+
+ return 1.0f - (dist / std::max(s0.size(), s1.size()));
+}
+
+void command_set_status(const std::string & status) {
+ std::lock_guard<std::mutex> lock(g_mutex);
+ g_status = status;
+}
+
+bool command_vad_simple(std::vector<float> & pcmf32, int sample_rate, int last_ms, float vad_thold, float freq_thold, bool verbose) {
+ const int n_samples = pcmf32.size();
+ const int n_samples_last = (sample_rate * last_ms) / 1000;
+
+ if (n_samples_last >= n_samples) {
+ // not enough samples - assume no speech
+ return false;
+ }
+
+ if (freq_thold > 0.0f) {
+ high_pass_filter(pcmf32, freq_thold, sample_rate);
+ }
+
+ float energy_all = 0.0f;
+ float energy_last = 0.0f;
+
+ for (size_t i = 0; i < n_samples; i++) {
+ energy_all += fabsf(pcmf32[i]);
+
+ if (i >= n_samples - n_samples_last) {
+ energy_last += fabsf(pcmf32[i]);
+ }
+ }
+
+ energy_all /= n_samples;
+ energy_last /= n_samples_last;
+
+ if (verbose) {
+ fprintf(stderr, "%s: energy_all: %f, energy_last: %f, vad_thold: %f, freq_thold: %f\n", __func__, energy_all, energy_last, vad_thold, freq_thold);
+ }
+
+ if (energy_last > vad_thold*energy_all) {
+ return false;
+ }
+
+ return true;
+}
+
+std::string command_transcribe(whisper_context * ctx, const whisper_full_params & wparams, const std::vector<float> & pcmf32, float & prob, int64_t & t_ms) {
+ const auto t_start = std::chrono::high_resolution_clock::now();
+
+ prob = 0.0f;
+ t_ms = 0;
+
+ if (whisper_full(ctx, wparams, pcmf32.data(), pcmf32.size()) != 0) {
+ return "";
+ }
+
+ int prob_n = 0;
+ std::string result;
+
+ const int n_segments = whisper_full_n_segments(ctx);
+ for (int i = 0; i < n_segments; ++i) {
+ const char * text = whisper_full_get_segment_text(ctx, i);
+
+ result += text;
+
+ const int n_tokens = whisper_full_n_tokens(ctx, i);
+ for (int j = 0; j < n_tokens; ++j) {
+ const auto token = whisper_full_get_token_data(ctx, i, j);
+
+ prob += token.p;
+ ++prob_n;
+ }
+ }
+
+ if (prob_n > 0) {
+ prob /= prob_n;
+ }
+
+ const auto t_end = std::chrono::high_resolution_clock::now();
+ t_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count();
+
+ return result;
+}
+
+void command_get_audio(int ms, int sample_rate, std::vector<float> & audio) {
+ const int64_t n_samples = (ms * sample_rate) / 1000;
+
+ int64_t n_take = 0;
+ if (g_pcmf32.size() < n_samples) {
+ n_take = g_pcmf32.size();
+ } else {
+ n_take = n_samples;
+ }
+
+ audio.resize(n_take);
+ std::copy(g_pcmf32.end() - n_take, g_pcmf32.end(), audio.begin());
+}
+
+void command_main(size_t index) {
+ command_set_status("loading data ...");
+
+ struct whisper_full_params wparams = whisper_full_default_params(whisper_sampling_strategy::WHISPER_SAMPLING_GREEDY);
+
+ wparams.n_threads = std::min(N_THREAD, (int) std::thread::hardware_concurrency());
+ wparams.offset_ms = 0;
+ wparams.translate = false;
+ wparams.no_context = true;
+ wparams.single_segment = true;
+ wparams.print_realtime = false;
+ wparams.print_progress = false;
+ wparams.print_timestamps = true;
+ wparams.print_special = false;
+
+ wparams.max_tokens = 32;
+ wparams.audio_ctx = 768; // partial encoder context for better performance
+
+ wparams.language = "en";
+
+ printf("command: using %d threads\n", wparams.n_threads);
+
+ bool is_running = true;
+ bool have_prompt = false;
+ bool ask_prompt = true;
+ bool print_energy = false;
+
+ float prob0 = 0.0f;
+ float prob = 0.0f;
+
+ std::vector<float> pcmf32_cur;
+ std::vector<float> pcmf32_prompt;
+
+ const std::string k_prompt = "Ok Whisper, start listening for commands.";
+
+ // whisper context
+ auto & ctx = g_contexts[index];
+
+ const int32_t vad_ms = 2000;
+ const int32_t prompt_ms = 5000;
+ const int32_t command_ms = 4000;
+
+ const float vad_thold = 0.1f;
+ const float freq_thold = -1.0f;
+
+ while (g_running) {
+ // delay
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ if (ask_prompt) {
+ fprintf(stdout, "\n");
+ fprintf(stdout, "%s: Say the following phrase: '%s%s%s'\n", __func__, "\033[1m", k_prompt.c_str(), "\033[0m");
+ fprintf(stdout, "\n");
+
+ {
+ char txt[1024];
+ snprintf(txt, sizeof(txt), "Say the following phrase: '%s'", k_prompt.c_str());
+ command_set_status(txt);
+ }
+
+ ask_prompt = false;
+ }
+
+ int64_t t_ms = 0;
+
+ {
+ command_get_audio(vad_ms, WHISPER_SAMPLE_RATE, pcmf32_cur);
+
+ if (command_vad_simple(pcmf32_cur, WHISPER_SAMPLE_RATE, 1000, vad_thold, freq_thold, print_energy)) {
+ fprintf(stdout, "%s: Speech detected! Processing ...\n", __func__);
+ command_set_status("Speech detected! Processing ...");
+
+ if (!have_prompt) {
+ command_get_audio(prompt_ms, WHISPER_SAMPLE_RATE, pcmf32_cur);
+
+ const auto txt = ::trim(::command_transcribe(ctx, wparams, pcmf32_cur, prob0, t_ms));
+
+ fprintf(stdout, "%s: Heard '%s%s%s', (t = %d ms)\n", __func__, "\033[1m", txt.c_str(), "\033[0m", (int) t_ms);
+
+ const float sim = similarity(txt, k_prompt);
+
+ if (txt.length() < 0.8*k_prompt.length() || txt.length() > 1.2*k_prompt.length() || sim < 0.8f) {
+ fprintf(stdout, "%s: WARNING: prompt not recognized, try again\n", __func__);
+ ask_prompt = true;
+ } else {
+ fprintf(stdout, "\n");
+ fprintf(stdout, "%s: The prompt has been recognized!\n", __func__);
+ fprintf(stdout, "%s: Waiting for voice commands ...\n", __func__);
+ fprintf(stdout, "\n");
+
+ {
+ char txt[1024];
+ snprintf(txt, sizeof(txt), "Success! Waiting for voice commands ...");
+ command_set_status(txt);
+ }
+
+ // save the audio for the prompt
+ pcmf32_prompt = pcmf32_cur;
+ have_prompt = true;
+ }
+ } else {
+ command_get_audio(command_ms, WHISPER_SAMPLE_RATE, pcmf32_cur);
+
+ // prepend the prompt audio
+ pcmf32_cur.insert(pcmf32_cur.begin(), pcmf32_prompt.begin(), pcmf32_prompt.end());
+
+ const auto txt = ::trim(::command_transcribe(ctx, wparams, pcmf32_cur, prob, t_ms));
+
+ prob = 100.0f*(prob - prob0);
+
+ fprintf(stdout, "%s: heard '%s'\n", __func__, txt.c_str());
+
+ // find the prompt in the text
+ float best_sim = 0.0f;
+ size_t best_len = 0;
+ for (int n = 0.8*k_prompt.size(); n <= 1.2*k_prompt.size(); ++n) {
+ const auto prompt = txt.substr(0, n);
+
+ const float sim = similarity(prompt, k_prompt);
+
+ //fprintf(stderr, "%s: prompt = '%s', sim = %f\n", __func__, prompt.c_str(), sim);
+
+ if (sim > best_sim) {
+ best_sim = sim;
+ best_len = n;
+ }
+ }
+
+ const std::string command = ::trim(txt.substr(best_len));
+
+ fprintf(stdout, "%s: Command '%s%s%s', (t = %d ms)\n", __func__, "\033[1m", command.c_str(), "\033[0m", (int) t_ms);
+ fprintf(stdout, "\n");
+
+ {
+ char txt[1024];
+ snprintf(txt, sizeof(txt), "Command '%s', (t = %d ms)", command.c_str(), (int) t_ms);
+ command_set_status(txt);
+ }
+ {
+ std::lock_guard<std::mutex> lock(g_mutex);
+ g_transcribed = command;
+ }
+ }
+
+ g_pcmf32.clear();
+ }
+ }
+ }
+
+ if (index < g_contexts.size()) {
+ whisper_free(g_contexts[index]);
+ g_contexts[index] = nullptr;
+ }
+}
+
+EMSCRIPTEN_BINDINGS(command) {
+ emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
+ for (size_t i = 0; i < g_contexts.size(); ++i) {
+ if (g_contexts[i] == nullptr) {
+ g_contexts[i] = whisper_init(path_model.c_str());
+ if (g_contexts[i] != nullptr) {
+ g_running = true;
+ if (g_worker.joinable()) {
+ g_worker.join();
+ }
+ g_worker = std::thread([i]() {
+ command_main(i);
+ });
+
+ return i + 1;
+ } else {
+ return (size_t) 0;
+ }
+ }
+ }
+
+ return (size_t) 0;
+ }));
+
+ emscripten::function("free", emscripten::optional_override([](size_t index) {
+ if (g_running) {
+ g_running = false;
+ }
+ }));
+
+ emscripten::function("set_audio", emscripten::optional_override([](size_t index, const emscripten::val & audio) {
+ --index;
+
+ if (index >= g_contexts.size()) {
+ return -1;
+ }
+
+ if (g_contexts[index] == nullptr) {
+ return -2;
+ }
+
+ {
+ std::lock_guard<std::mutex> lock(g_mutex);
+ const int n = audio["length"].as<int>();
+
+ emscripten::val heap = emscripten::val::module_property("HEAPU8");
+ emscripten::val memory = heap["buffer"];
+
+ g_pcmf32.resize(n);
+
+ emscripten::val memoryView = audio["constructor"].new_(memory, reinterpret_cast<uintptr_t>(g_pcmf32.data()), n);
+ memoryView.call<void>("set", audio);
+ }
+
+ return 0;
+ }));
+
+ emscripten::function("get_transcribed", emscripten::optional_override([]() {
+ std::string transcribed;
+
+ {
+ std::lock_guard<std::mutex> lock(g_mutex);
+ transcribed = std::move(g_transcribed);
+ }
+
+ return transcribed;
+ }));
+
+ emscripten::function("get_status", emscripten::optional_override([]() {
+ std::string status;
+
+ {
+ std::lock_guard<std::mutex> lock(g_mutex);
+ status = g_status_forced.empty() ? g_status : g_status_forced;
+ }
+
+ return status;
+ }));
+
+ emscripten::function("set_status", emscripten::optional_override([](const std::string & status) {
+ {
+ std::lock_guard<std::mutex> lock(g_mutex);
+ g_status_forced = status;
+ }
+ }));
+}
--- /dev/null
+<!doctype html>
+<html lang="en-us">
+ <head>
+ <title>command : Voice assistant example using Whisper + WebAssembly</title>
+
+ <style>
+ #output {
+ width: 100%;
+ height: 100%;
+ margin: 0 auto;
+ margin-top: 10px;
+ border-left: 0px;
+ border-right: 0px;
+ padding-left: 0px;
+ padding-right: 0px;
+ display: block;
+ background-color: black;
+ color: white;
+ font-size: 10px;
+ font-family: 'Lucida Console', Monaco, monospace;
+ outline: none;
+ white-space: pre;
+ overflow-wrap: normal;
+ overflow-x: scroll;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="main-container">
+ <b>command : Voice assistant example using Whisper + WebAssembly</b>
+
+ <br><br>
+
+ You can find more about this project on <a href="https://github.com/ggerganov/whisper.cpp/tree/master/examples/command.wasm">GitHub</a>.
+
+ <br><br>
+
+ <hr>
+
+ Select the model you would like to use, click the "Start" button and follow the instructions.
+
+ <br><br>
+
+ <div id="model-whisper">
+ Whisper model: <span id="model-whisper-status"></span>
+ <button id="fetch-whisper-tiny-en" onclick="loadWhisper('tiny.en')">tiny.en (75 MB)</button>
+ <button id="fetch-whisper-base-en" onclick="loadWhisper('base.en')">base.en (142 MB)</button>
+ <span id="fetch-whisper-progress"></span>
+
+ <!--
+ <input type="file" id="file" name="file" onchange="loadFile(event, 'whisper.bin')" />
+ -->
+ </div>
+
+ <br>
+
+ <div id="input">
+ <button id="start" onclick="onStart()" disabled>Start</button>
+ <button id="stop" onclick="onStop()" disabled>Stop</button>
+ <button id="clear" onclick="clearCache()">Clear Cache</button>
+ </div>
+
+ <br>
+
+ <div id="state">
+ Status: <b><span id="state-status">not started</span></b>
+
+ <pre id="state-transcribed">[The recognized voice commands will be displayed here]</pre>
+ </div>
+
+ <hr>
+
+ Debug output:
+ <textarea id="output" rows="20"></textarea>
+
+ <br>
+
+ <b>Troubleshooting</b>
+
+ <br><br>
+
+ The page does some heavy computations, so make sure:
+
+ <ul>
+ <li>To use a modern web browser (e.g. Chrome, Firefox)</li>
+ <li>To use a fast desktop or laptop computer (i.e. not a mobile phone)</li>
+ <li>Your browser supports WASM <a href="https://webassembly.org/roadmap/">Fixed-width SIMD</a></li>
+ </ul>
+
+ <div class="cell-version">
+ <span>
+ |
+ Build time: <span class="nav-link">@GIT_DATE@</span> |
+ Commit hash: <a class="nav-link" href="https://github.com/ggerganov/whisper.cpp/commit/@GIT_SHA1@">@GIT_SHA1@</a> |
+ Commit subject: <span class="nav-link">@GIT_COMMIT_SUBJECT@</span> |
+ <a class="nav-link" href="https://github.com/ggerganov/whisper.cpp/tree/master/examples/command.wasm">Source Code</a> |
+ </span>
+ </div>
+ </div>
+
+ <script type="text/javascript" src="helpers.js"></script>
+ <script type='text/javascript'>
+ // web audio context
+ var context = null;
+
+ // audio data
+ var audio = null;
+ var audio0 = null;
+
+ // the command instance
+ var instance = null;
+
+ // model name
+ var model_whisper = null;
+
+ var Module = {
+ print: printTextarea,
+ printErr: printTextarea,
+ setStatus: function(text) {
+ printTextarea('js: ' + text);
+ },
+ monitorRunDependencies: function(left) {
+ },
+ preRun: function() {
+ printTextarea('js: Preparing ...');
+ },
+ postRun: function() {
+ printTextarea('js: Initialized successfully!');
+ }
+ };
+
+ //
+ // fetch models
+ //
+
+ let dbVersion = 1
+ let dbName = 'whisper.ggerganov.com';
+ let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
+
+ function storeFS(fname, buf) {
+ // write to WASM file using FS_createDataFile
+ // if the file exists, delete it
+ try {
+ Module.FS_unlink(fname);
+ } catch (e) {
+ // ignore
+ }
+
+ Module.FS_createDataFile("/", fname, buf, true, true);
+
+ printTextarea('storeFS: stored model: ' + fname + ' size: ' + buf.length);
+
+ document.getElementById('model-whisper-status').innerHTML = 'loaded "' + model_whisper + '"!';
+
+ if (model_whisper != null) {
+ document.getElementById('start').disabled = false;
+ document.getElementById('stop' ).disabled = true;
+ }
+ }
+
+ function loadWhisper(model) {
+ let urls = {
+ 'tiny.en': 'https://whisper.ggerganov.com/ggml-model-whisper-tiny.en.bin',
+ 'base.en': 'https://whisper.ggerganov.com/ggml-model-whisper-base.en.bin',
+ };
+
+ let sizes = {
+ 'tiny.en': 75,
+ 'base.en': 142,
+ };
+
+ let url = urls[model];
+ let dst = 'whisper.bin';
+ let size_mb = sizes[model];
+
+ model_whisper = model;
+
+ document.getElementById('fetch-whisper-tiny-en').style.display = 'none';
+ document.getElementById('fetch-whisper-base-en').style.display = 'none';
+ document.getElementById('model-whisper-status').innerHTML = 'loading "' + model + '" ... ';
+
+ cbProgress = function(p) {
+ let el = document.getElementById('fetch-whisper-progress');
+ el.innerHTML = Math.round(100*p) + '%';
+ };
+
+ cbCancel = function() {
+ var el;
+ el = document.getElementById('fetch-whisper-tiny-en'); if (el) el.style.display = 'inline-block';
+ el = document.getElementById('fetch-whisper-base-en'); if (el) el.style.display = 'inline-block';
+ el = document.getElementById('model-whisper-status'); if (el) el.innerHTML = '';
+ };
+
+ loadRemote(url, dst, size_mb, cbProgress, storeFS, cbCancel, printTextarea);
+ }
+
+ //
+ // microphone
+ //
+
+ const kSampleRate = 16000;
+ const kRestartRecording_s = 120;
+ const kIntervalAudio_ms = 250; // pass the recorded audio to the C++ instance at this rate
+
+ var mediaRecorder = null;
+ var doRecording = false;
+ var startTime = 0;
+
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
+
+ function stopRecording() {
+ Module.set_status("paused");
+ doRecording = false;
+ audio0 = null;
+ audio = null;
+ context = null;
+ }
+
+ function startRecording() {
+ if (!context) {
+ context = new AudioContext({
+ sampleRate: kSampleRate,
+ channelCount: 1,
+ echoCancellation: false,
+ autoGainControl: true,
+ noiseSuppression: true,
+ });
+ }
+
+ Module.set_status("");
+
+ document.getElementById('start').disabled = true;
+ document.getElementById('stop').disabled = false;
+
+ doRecording = true;
+ startTime = Date.now();
+
+ var chunks = [];
+ var stream = null;
+
+ navigator.mediaDevices.getUserMedia({audio: true, video: false})
+ .then(function(s) {
+ stream = s;
+ mediaRecorder = new MediaRecorder(stream);
+ mediaRecorder.ondataavailable = function(e) {
+ chunks.push(e.data);
+
+ var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' });
+ var reader = new FileReader();
+
+ reader.onload = function(event) {
+ var buf = new Uint8Array(reader.result);
+
+ if (!context) {
+ return;
+ }
+ context.decodeAudioData(buf.buffer, function(audioBuffer) {
+ var offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate);
+ var source = offlineContext.createBufferSource();
+ source.buffer = audioBuffer;
+ source.connect(offlineContext.destination);
+ source.start(0);
+
+ offlineContext.startRendering().then(function(renderedBuffer) {
+ audio = renderedBuffer.getChannelData(0);
+
+ //printTextarea('js: audio recorded, size: ' + audio.length + ', old size: ' + (audio0 == null ? 0 : audio0.length));
+
+ var audioAll = new Float32Array(audio0 == null ? audio.length : audio0.length + audio.length);
+ if (audio0 != null) {
+ audioAll.set(audio0, 0);
+ }
+ audioAll.set(audio, audio0 == null ? 0 : audio0.length);
+
+ if (instance) {
+ Module.set_audio(instance, audioAll);
+ }
+ });
+ }, function(e) {
+ audio = null;
+ });
+ }
+
+ reader.readAsArrayBuffer(blob);
+ };
+
+ mediaRecorder.onstop = function(e) {
+ if (doRecording) {
+ setTimeout(function() {
+ startRecording();
+ });
+ }
+ };
+
+ mediaRecorder.start(kIntervalAudio_ms);
+ })
+ .catch(function(err) {
+ printTextarea('js: error getting audio stream: ' + err);
+ });
+
+ var interval = setInterval(function() {
+ if (!doRecording) {
+ clearInterval(interval);
+ mediaRecorder.stop();
+ stream.getTracks().forEach(function(track) {
+ track.stop();
+ });
+
+ document.getElementById('start').disabled = false;
+ document.getElementById('stop').disabled = true;
+
+ mediaRecorder = null;
+ }
+
+ // if audio length is more than kRestartRecording_s seconds, restart recording
+ if (audio != null && audio.length > kSampleRate*kRestartRecording_s) {
+ if (doRecording) {
+ //printTextarea('js: restarting recording');
+
+ clearInterval(interval);
+ audio0 = audio;
+ audio = null;
+ mediaRecorder.stop();
+ stream.getTracks().forEach(function(track) {
+ track.stop();
+ });
+ }
+ }
+ }, 100);
+ }
+
+ //
+ // main
+ //
+
+ var nLines = 0;
+ var intervalUpdate = null;
+ var transcribedAll = '';
+
+ function onStart() {
+ if (!instance) {
+ instance = Module.init('whisper.bin');
+
+ if (instance) {
+ printTextarea("js: whisper initialized, instance: " + instance);
+ }
+ }
+
+ if (!instance) {
+ printTextarea("js: failed to initialize whisper");
+ return;
+ }
+
+ startRecording();
+
+ intervalUpdate = setInterval(function() {
+ var transcribed = Module.get_transcribed();
+
+ if (transcribed != null && transcribed.length > 1) {
+ transcribedAll += transcribed + '<br>';
+ nLines++;
+
+ // if more than 10 lines, remove the first line
+ if (nLines > 10) {
+ var i = transcribedAll.indexOf('<br>');
+ if (i > 0) {
+ transcribedAll = transcribedAll.substring(i + 4);
+ nLines--;
+ }
+ }
+ }
+
+ document.getElementById('state-status').innerHTML = Module.get_status();
+ document.getElementById('state-transcribed').innerHTML = transcribedAll;
+ }, 100);
+ }
+
+ function onStop() {
+ stopRecording();
+ }
+
+ </script>
+ <script type="text/javascript" src="command.js"></script>
+ </body>
+</html>
\r
https://user-images.githubusercontent.com/1991296/204038393-2f846eae-c255-4099-a76d-5735c25c49da.mp4\r
\r
+Web version: https://github.com/ggerganov/whisper.cpp/examples/command.wasm\r
+\r
## Building\r
\r
The `command` tool depends on SDL2 library to capture audio from the microphone. You can build it like this:\r
bool is_running = true;
bool have_prompt = false;
- bool ask_prompt = true;
+ bool ask_prompt = true;
float prob0 = 0.0f;
float prob = 0.0f;
<script type="text/javascript" src="helpers.js"></script>
<script type='text/javascript'>
- const kRestartRecording_s = 15;
- const kSampleRate = 16000;
-
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
-
// web audio context
var context = null;
// microphone
//
+ const kSampleRate = 16000;
+ const kRestartRecording_s = 120;
+ const kIntervalAudio_ms = 5000; // pass the recorded audio to the C++ instance at this rate
+
var mediaRecorder = null;
var doRecording = false;
var startTime = 0;
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
+
function stopRecording() {
Module.set_status("paused");
doRecording = false;
function startRecording() {
if (!context) {
context = new AudioContext({
- sampleRate: 16000,
+ sampleRate: kSampleRate,
channelCount: 1,
echoCancellation: false,
autoGainControl: true,
}
};
- mediaRecorder.start(5000);
+ mediaRecorder.start(kIntervalAudio_ms);
})
.catch(function(err) {
printTextarea('js: error getting audio stream: ' + err);
});
}
}
- }, 250);
+ }, 100);
}
//
<script type="text/javascript" src="helpers.js"></script>
<script type='text/javascript'>
- const kRestartRecording_s = 15;
- const kSampleRate = 16000;
-
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
-
// web audio context
var context = null;
// microphone
//
+ const kSampleRate = 16000;
+ const kRestartRecording_s = 120;
+ const kIntervalAudio_ms = 250; // pass the recorded audio to the C++ instance at this rate
+
var mediaRecorder = null;
var doRecording = false;
var startTime = 0;
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
+
function stopRecording() {
Module.set_status("paused");
doRecording = false;
function startRecording() {
if (!context) {
context = new AudioContext({
- sampleRate: 16000,
+ sampleRate: kSampleRate,
channelCount: 1,
echoCancellation: false,
autoGainControl: true,
}
};
- mediaRecorder.start(250);
+ mediaRecorder.start(kIntervalAudio_ms);
})
.catch(function(err) {
printTextarea('js: error getting audio stream: ' + err);
});
}
}
- }, 250);
+ }, 100);
}
//
}
};
- const kMaxAudio_s = 120;
- const kSampleRate = 16000;
-
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
-
// web audio context
var context = null;
// audio file
//
+ const kMaxAudio_s = 120;
+ const kSampleRate = 16000;
+
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
+ window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
+
function loadAudio(event) {
if (!context) {
- context = new AudioContext({sampleRate: 16000});
+ context = new AudioContext({
+ sampleRate: kSampleRate,
+ channelCount: 1,
+ echoCancellation: false,
+ autoGainControl: true,
+ noiseSuppression: true,
+ });
}
var file = event.target.files[0] || null;
// update progress information
function startRecording() {
if (!context) {
- context = new AudioContext({sampleRate: 16000});
+ context = new AudioContext({
+ sampleRate: kSampleRate,
+ channelCount: 1,
+ echoCancellation: false,
+ autoGainControl: true,
+ noiseSuppression: true,
+ });
}
document.getElementById('start').disabled = true;
--- /dev/null
+#!/bin/bash
+#
+# This is a helper script to deploy all WebAssembly examples to my node
+# Run from the build directory:
+#
+# cd build-em
+# ../extra/deploy-wasm.sh
+#
+
+# check if emcmake is available
+if ! command -v emcmake &> /dev/null
+then
+ echo "Error: emscripten environment is not set up"
+ exit
+fi
+
+emcmake cmake .. && make -j
+if [ $? -ne 0 ]; then
+ echo "Error: build failed"
+ exit
+fi
+
+# copy all wasm files to the node
+scp bin/whisper.wasm/* root@linode0:/var/www/html/whisper/ && scp bin/libwhisper.worker.js root@linode0:/var/www/html/whisper/
+scp bin/stream.wasm/* root@linode0:/var/www/html/whisper/stream/ && scp bin/libstream.worker.js root@linode0:/var/www/html/whisper/stream/
+scp bin/command.wasm/* root@linode0:/var/www/html/whisper/command/ && scp bin/libcommand.worker.js root@linode0:/var/www/html/whisper/command/
+scp bin/talk.wasm/* root@linode0:/var/www/html/whisper/talk/ && scp bin/libtalk.worker.js root@linode0:/var/www/html/whisper/talk/
+
+echo "Done"
+exit