From a9a1bee4809c7eb007198d858321d2e0d3e1787c Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Wed, 25 Jan 2023 14:48:53 -0500 Subject: [PATCH 01/11] chore: make it pretty --- js/package.json | 67 +++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/js/package.json b/js/package.json index 61d55d4e..c5f9c786 100644 --- a/js/package.json +++ b/js/package.json @@ -1,35 +1,36 @@ { - "name": "soundswallower", - "version": "0.5.0", - "description": "An even smaller speech recognizer", - "main": "soundswallower.js", - "scripts": { - "test": "mocha soundswallower.spec", - "tstest": "npx tsc && node test_typescript", - "webtest": "xdg-open http://localhost:8000/test_web.html && python server.py" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/ReadAlongs/SoundSwallower.git" - }, - "keywords": [ - "speech", - "audio", - "nlp" - ], - "author": "David Huggins-Daines ", - "license": "MIT", - "bugs": { - "url": "https://github.com/ReadAlongs/SoundSwallower/issues" - }, - "homepage": "https://github.com/ReadAlongs/SoundSwallower#readme", - "devDependencies": { - "@types/node": "^18.0.3", - "jsdoc": "^3.6.10", - "mocha": "^10.0.0", - "typescript": "^4.7.4" - }, - "dependencies": { - "@types/emscripten": "^1.39.6" - } + "name": "soundswallower", + "version": "0.5.1", + "description": "An even smaller speech recognizer", + "exports": {}, + "main": "soundswallower.js", + "scripts": { + "test": "mocha soundswallower.spec", + "tstest": "npx tsc && node test_typescript", + "webtest": "xdg-open http://localhost:8000/test_web.html && python server.py" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ReadAlongs/SoundSwallower.git" + }, + "keywords": [ + "speech", + "audio", + "nlp" + ], + "author": "David Huggins-Daines ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ReadAlongs/SoundSwallower/issues" + }, + "homepage": "https://github.com/ReadAlongs/SoundSwallower#readme", + "devDependencies": { + "@types/node": "^18.0.3", + "jsdoc": "^3.6.10", + "mocha": "^10.0.0", + "typescript": "^4.7.4" + }, + "dependencies": { + "@types/emscripten": "^1.39.6" + } } From 501e430bc898dadc8a55e4b783de7289264fb059 Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Thu, 26 Jan 2023 11:50:03 -0500 Subject: [PATCH 02/11] chore: bump versions --- CMakeLists.txt | 2 +- js/package.json | 10 +++++++--- setup.cfg | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cb4a6a90..bccca9ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ include(CheckSymbolExists) include(CheckLibraryExists) include(TestBigEndian) -project(soundswallower VERSION 0.5.0 +project(soundswallower VERSION 0.6.0 DESCRIPTION "An even smaller speech recognizer") if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) diff --git a/js/package.json b/js/package.json index c5f9c786..9b9c0d1c 100644 --- a/js/package.json +++ b/js/package.json @@ -1,9 +1,12 @@ { "name": "soundswallower", - "version": "0.5.1", + "version": "0.6.0", "description": "An even smaller speech recognizer", - "exports": {}, - "main": "soundswallower.js", + "exports": { + "node": "./soundswallower.node.js", + "default": "./soundswallower.web.js" + }, + "type": "module", "scripts": { "test": "mocha soundswallower.spec", "tstest": "npx tsc && node test_typescript", @@ -26,6 +29,7 @@ "homepage": "https://github.com/ReadAlongs/SoundSwallower#readme", "devDependencies": { "@types/node": "^18.0.3", + "chai": "^4.3.7", "jsdoc": "^3.6.10", "mocha": "^10.0.0", "typescript": "^4.7.4" diff --git a/setup.cfg b/setup.cfg index 06ca2b23..2a293011 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = soundswallower -version = 0.5.1 +version = 0.6.0 description = An even smaller speech recognizer long_description = file: README.md long_description_content_type = text/markdown From a0a7848016be9d5ff001a601c0843bc4e99fe411 Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Thu, 26 Jan 2023 11:53:21 -0500 Subject: [PATCH 03/11] feat: separate node and web builds and use es6 only --- js/CMakeLists.txt | 55 +++++++++++++++++++----------- js/api-node-pre.js | 9 +++++ js/api-node.js | 46 +++++++++++++++++++++++++ js/api-web-pre.js | 11 ++++++ js/api-web.js | 51 ++++++++++++++++++++++++++++ js/api.js | 83 ++-------------------------------------------- 6 files changed, 155 insertions(+), 100 deletions(-) create mode 100644 js/api-node-pre.js create mode 100644 js/api-node.js create mode 100644 js/api-web-pre.js create mode 100644 js/api-web.js diff --git a/js/CMakeLists.txt b/js/CMakeLists.txt index 7037b26e..ddfc33ce 100644 --- a/js/CMakeLists.txt +++ b/js/CMakeLists.txt @@ -1,34 +1,49 @@ -add_executable(ssjs soundswallower.c) -set_property(TARGET ssjs PROPERTY OUTPUT_NAME soundswallower) -set_property(TARGET ssjs PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -set_property(TARGET ssjs PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -set_property(TARGET ssjs PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -# Have to use --whole-archive due to -# https://github.com/emscripten-core/emscripten/issues/6233 (and see -# also -# https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#binding-libraries) -target_link_libraries(ssjs -Wl,--whole-archive soundswallower -Wl,--no-whole-archive) -target_compile_options(soundswallower PRIVATE -Oz -sSUPPORT_LONGJMP=0 -sSTRICT=1) -target_compile_options(ssjs PRIVATE -Oz -sSUPPORT_LONGJMP=0 -sSTRICT=1) +add_executable(soundswallower.web soundswallower.c) +add_executable(soundswallower.node soundswallower.c) +foreach(JSBUILD soundswallower.web soundswallower.node) + set_property(TARGET ${JSBUILD} PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + set_property(TARGET ${JSBUILD} PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + set_property(TARGET ${JSBUILD} PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + # Have to use --whole-archive due to + # https://github.com/emscripten-core/emscripten/issues/6233 (and see + # also + # https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#binding-libraries) + target_link_libraries(${JSBUILD} -Wl,--whole-archive soundswallower -Wl,--no-whole-archive) + target_compile_options(soundswallower PRIVATE -Oz -sSUPPORT_LONGJMP=0 -sSTRICT=1) + target_compile_options(${JSBUILD} PRIVATE -Oz -sSUPPORT_LONGJMP=0 -sSTRICT=1) + target_include_directories( + ${JSBUILD} PRIVATE ${PYTHON_INCLUDE_DIR} + ${JSBUILD} PRIVATE ${PROJECT_SOURCE_DIR}/src + ${JSBUILD} PRIVATE ${PROJECT_SOURCE_DIR}/include + ${JSBUILD} PRIVATE ${CMAKE_BINARY_DIR} # for config.h + ) +endforeach() # Cannot, will not set lists of functions directly on the command line # # https://github.com/emscripten-core/emscripten/pull/17403 # https://stackoverflow.com/questions/68775616/how-not-to-have-dollar-sign-in-target-link-options-mangled # https://discourse.cmake.org/t/how-not-to-have-dollar-sign-in-target-link-options-mangled/3939/3 -target_link_options(ssjs PRIVATE +target_link_options(soundswallower.web PRIVATE @${CMAKE_SOURCE_DIR}/js/linker_options.txt + --extern-pre-js ${CMAKE_SOURCE_DIR}/js/api-web-pre.js + -sENVIRONMENT=web + -sEXPORT_ES6=1 + -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=@${CMAKE_SOURCE_DIR}/js/library_funcs.txt + -sEXPORTED_FUNCTIONS=@${CMAKE_SOURCE_DIR}/js/exported_functions.txt) +# Sadly, it seems they cannot share the same wasm file, for no good reason +target_link_options(soundswallower.node PRIVATE + @${CMAKE_SOURCE_DIR}/js/linker_options.txt + --extern-pre-js ${CMAKE_SOURCE_DIR}/js/api-node-pre.js + -sENVIRONMENT=node + -sEXPORT_ES6=1 -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=@${CMAKE_SOURCE_DIR}/js/library_funcs.txt -sEXPORTED_FUNCTIONS=@${CMAKE_SOURCE_DIR}/js/exported_functions.txt) # See # https://github.com/emscripten-core/emscripten/blob/main/cmake/Modules/Platform/Emscripten.cmake # ...sure would be nice if this were documented -em_link_post_js(ssjs api.js) -target_include_directories( - ssjs PRIVATE ${PYTHON_INCLUDE_DIR} - ssjs PRIVATE ${PROJECT_SOURCE_DIR}/src - ssjs PRIVATE ${PROJECT_SOURCE_DIR}/include - ssjs PRIVATE ${CMAKE_BINARY_DIR} # for config.h - ) +em_link_post_js(soundswallower.web api-web.js api.js) +em_link_post_js(soundswallower.node api-node.js api.js) + # Copy model into build directory add_custom_command(OUTPUT model COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/js/api-node-pre.js b/js/api-node-pre.js new file mode 100644 index 00000000..2d9dda6b --- /dev/null +++ b/js/api-node-pre.js @@ -0,0 +1,9 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +/** + * Async read some binary data + */ +export async function load_binary_file(path) { + return readFile(path); +} diff --git a/js/api-node.js b/js/api-node.js new file mode 100644 index 00000000..344fc331 --- /dev/null +++ b/js/api-node.js @@ -0,0 +1,46 @@ +/** + * Async read some JSON (maybe there is a built-in that does this?) + */ +async function load_json(path) { + const data = await readFile(path, { encoding: "utf8" }); + return JSON.parse(data); +} + +/** + * Load a file from disk or Internet and make it into an s3file_t. + */ +async function load_to_s3file(path) { + let blob_u8; + // FIXME: Should read directly to emscripten memory... how? + const blob = await readFile(path); + blob_u8 = new Uint8Array(blob.buffer); + const blob_len = blob_u8.length + 1; + const blob_addr = Module._malloc(blob_len); + if (blob_addr == 0) + throw new Error("Failed to allocate " + blob_len + " bytes for " + path); + writeArrayToMemory(blob_u8, blob_addr); + // Ensure it is NUL-terminated in case someone treats it as a string + HEAP8[blob_addr + blob_len] = 0; + // But exclude the trailing NUL from file size so it works normally + return Module._s3file_init(blob_addr, blob_len - 1); +} + +/** + * Get a model or model file from the built-in model path. + * + * The base path can be set by modifying the `modelBase` property of + * the module object, at initialization or any other time. Or you can + * also just override this function if you have special needs. + * + * This function is used by `Decoder` to find the default model, which + * is equivalent to `Model.modelBase + Model.defaultModel`. + * + * @param {string} subpath path to model directory or parameter + * file, e.g. "en-us", "en-us/variances", etc + * @returns {string} concatenated path. Note this is a simple string + * concatenation on the Web, so ensure that `modelBase` has a trailing + * slash if it is a directory. + */ +function get_model_path(subpath) { + return join(Module.modelBase, subpath); +} diff --git a/js/api-web-pre.js b/js/api-web-pre.js new file mode 100644 index 00000000..d7024a6e --- /dev/null +++ b/js/api-web-pre.js @@ -0,0 +1,11 @@ +/** + * Async read some binary data + */ +export async function load_binary_file(path) { + const response = await fetch(path); + if (response.ok) { + let blob = await response.blob(); + return new Uint8Array(await blob.arrayBuffer()); + } else + throw new Error("Failed to fetch " + path + " :" + response.statusText); +} diff --git a/js/api-web.js b/js/api-web.js new file mode 100644 index 00000000..e4d5d95b --- /dev/null +++ b/js/api-web.js @@ -0,0 +1,51 @@ +/** + * Async read some JSON (maybe there is a built-in that does this?) + */ +async function load_json(path) { + const response = await fetch(path); + if (response.ok) return response.json(); + else throw new Error("Failed to fetch " + path + " :" + response.statusText); +} + +/** + * Load a file from disk or Internet and make it into an s3file_t. + */ +async function load_to_s3file(path) { + let blob_u8; + const response = await fetch(path); + if (response.ok) { + const blob = await response.blob(); + const blob_buf = await blob.arrayBuffer(); + blob_u8 = new Uint8Array(blob_buf); + } else + throw new Error("Failed to fetch " + path + " :" + response.statusText); + const blob_len = blob_u8.length + 1; + const blob_addr = Module._malloc(blob_len); + if (blob_addr == 0) + throw new Error("Failed to allocate " + blob_len + " bytes for " + path); + writeArrayToMemory(blob_u8, blob_addr); + // Ensure it is NUL-terminated in case someone treats it as a string + HEAP8[blob_addr + blob_len] = 0; + // But exclude the trailing NUL from file size so it works normally + return Module._s3file_init(blob_addr, blob_len - 1); +} + +/** + * Get a model or model file from the built-in model path. + * + * The base path can be set by modifying the `modelBase` property of + * the module object, at initialization or any other time. Or you can + * also just override this function if you have special needs. + * + * This function is used by `Decoder` to find the default model, which + * is equivalent to `Model.modelBase + Model.defaultModel`. + * + * @param {string} subpath path to model directory or parameter + * file, e.g. "en-us", "en-us/variances", etc + * @returns {string} concatenated path. Note this is a simple string + * concatenation on the Web, so ensure that `modelBase` has a trailing + * slash if it is a directory. + */ +function get_model_path(subpath) { + return Module.modelBase + subpath; +} diff --git a/js/api.js b/js/api.js index 3869230d..d3d0574b 100644 --- a/js/api.js +++ b/js/api.js @@ -6,18 +6,12 @@ const ARG_BOOLEAN = 1 << 4; const DEFAULT_MODEL = "en-us"; -// User can specify a default model, or none at all +if (typeof Module.modelBase === "undefined") { + Module.modelBase = "model/"; +} if (typeof Module.defaultModel === "undefined") { Module.defaultModel = DEFAULT_MODEL; } -// User can also specify the base URL for models -if (typeof Module.modelBase === "undefined") { - if (ENVIRONMENT_IS_WEB) { - Module.modelBase = "model/"; - } else { - Module.modelBase = require("./model/index.js"); - } -} /** * Speech recognizer object. @@ -729,77 +723,6 @@ class Endpointer { } } -/** - * Async read some JSON (maybe there is a built-in that does this?) - */ -async function load_json(path) { - if (ENVIRONMENT_IS_WEB) { - const response = await fetch(path); - if (response.ok) return response.json(); - else - throw new Error("Failed to fetch " + path + " :" + response.statusText); - } else { - const fs = require("fs/promises"); - const data = await fs.readFile(path, { encoding: "utf8" }); - return JSON.parse(data); - } -} - -/** - * Load a file from disk or Internet and make it into an s3file_t. - */ -async function load_to_s3file(path) { - let blob_u8; - if (ENVIRONMENT_IS_WEB) { - const response = await fetch(path); - if (response.ok) { - const blob = await response.blob(); - const blob_buf = await blob.arrayBuffer(); - blob_u8 = new Uint8Array(blob_buf); - } else - throw new Error("Failed to fetch " + path + " :" + response.statusText); - } else { - const fs = require("fs/promises"); - // FIXME: Should read directly to emscripten memory... how? - const blob = await fs.readFile(path); - blob_u8 = new Uint8Array(blob.buffer); - } - const blob_len = blob_u8.length + 1; - const blob_addr = Module._malloc(blob_len); - if (blob_addr == 0) - throw new Error("Failed to allocate " + blob_len + " bytes for " + path); - writeArrayToMemory(blob_u8, blob_addr); - // Ensure it is NUL-terminated in case someone treats it as a string - HEAP8[blob_addr + blob_len] = 0; - // But exclude the trailing NUL from file size so it works normally - return Module._s3file_init(blob_addr, blob_len - 1); -} - -/** - * Get a model or model file from the built-in model path. - * - * The base path can be set by modifying the `modelBase` property of - * the module object, at initialization or any other time. Or you can - * also just override this function if you have special needs. - * - * This function is used by `Decoder` to find the default model, which - * is equivalent to `Model.modelBase + Model.defaultModel`. - * - * @param {string} subpath path to model directory or parameter - * file, e.g. "en-us", "en-us/variances", etc - * @returns {string} concatenated path. Note this is a simple string - * concatenation on the Web, so ensure that `modelBase` has a trailing - * slash if it is a directory. - */ -function get_model_path(subpath) { - if (ENVIRONMENT_IS_WEB) { - return Module.modelBase + subpath; - } else { - const path = require("path"); - return path.join(Module.modelBase, subpath); - } -} - Module.get_model_path = get_model_path; Module.load_json = load_json; Module.Decoder = Decoder; From a3f03d9dec89d60adac66e414afe355802e276d9 Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Thu, 26 Jan 2023 11:53:53 -0500 Subject: [PATCH 04/11] docs: update for es6 --- docs/source/soundswallower.js.rst | 30 +++------ js/README.md | 102 +++++++++--------------------- 2 files changed, 39 insertions(+), 93 deletions(-) diff --git a/docs/source/soundswallower.js.rst b/docs/source/soundswallower.js.rst index df051b98..2b07b535 100644 --- a/docs/source/soundswallower.js.rst +++ b/docs/source/soundswallower.js.rst @@ -37,10 +37,11 @@ and placed them under ``/model`` in your web server root: .. code-block:: javascript + import createModule from "soundswallower"; // Avoid loading the default model - const ssjs = { defaultModel: null }; - await require('soundswallower')(ssjs); - const decoder = new ssjs.Decoder({hmm: "/model/cmusphinx-pt-br-5.2", + const soundswallower = { defaultModel: null }; + await createModule(soundswallower); + const decoder = new soundswallower.Decoder({hmm: "/model/cmusphinx-pt-br-5.2", dict: "/model/br-pt.dic"}); await decoder.initialize(); @@ -64,19 +65,6 @@ make the rules: { from: modelDir, to: "model"}, ], - // Eliminate webpack's node junk when using webpack - resolve: { - fallback: { - crypto: false, - fs: false, - path: false, - }, - }, - node: { - global: false, - __filename: false, - __dirname: false, - }, For a more elaborate example, see [the soundswallower-demo code](https://github.com/dhdaines/soundswallower-demo). @@ -98,10 +86,11 @@ Now run this with ``node``: .. code-block:: javascript + import createModule from "soundswallower"; + import load_binary_file from "soundswallower"; (async () => { // Wrap everything in an async function call - // Load the library and pre-load the default model - const ssjs = await require("soundswallower")(); - const decoder = new ssjs.Decoder(); + const soundswallower = createModule(); + const decoder = new soundswallower.Decoder(); // Initialization is asynchronous await decoder.initialize(); const grammar = decoder.set_jsgf(`#JSGF V1.0; @@ -110,8 +99,7 @@ Now run this with ``node``: = one | two | three | four | five | six | seven | eight | nine | ten | eleven;`); // It goes to eleven // Default input is 16kHz, 32-bit floating-point PCM - const fs = require("fs/promises"); - let pcm = await fs.readFile("digits.raw"); + let pcm = await load_binary_file("digits.raw"); // Start speech processing decoder.start(); // Takes a typed array, as returned by readFile diff --git a/js/README.md b/js/README.md index 6822f600..c9ae1fdb 100644 --- a/js/README.md +++ b/js/README.md @@ -19,71 +19,25 @@ SoundSwallower can be installed in your NPM project: # From the Internets npm install soundswallower -You can also build and install it from source, provided you have -Emscripten and CMake installed: +You can also build and install it from source, provided you have a _very_ recent +version of Emscripten and CMake installed: # From top-level soundswallower directory emcmake cmake -S . -B jsbuild - cmake --build jsbuild + cd jsbuild && make && npm install # From your project's directory cd /path/to/my/project npm link /path/to/soundswallower/jsbuild -For use in Node.js, no other particular action is required on your -part. +To run the tests, from the `jsbuild` directory: -Because Webpack is blissfully unaware of all things -Emscripten-related, you will need a number of extra incantations in -your `webpack.config.js` if you want to include SoundSwallower in an -application that uses it, and there currently seems to be [no way -around this problem](https://github.com/webpack/webpack/issues/7352). -You will need to add this to the top of your `webpack.config.js`: + npm test + npm run tstest + npm run webtest -```js -const CopyPlugin = require("copy-webpack-plugin"); -const modelDir = require("soundswallower/model"); -``` - -Then to the `plugins` section in `config` or `module.exports` -(depending on how your config is structured): - -```js -new CopyPlugin({ - patterns: [ - { from: "node_modules/soundswallower/soundswallower.wasm*", - to: "[name][ext]"}, - { from: modelDir, - to: "model"}, - ], -}), -``` - -Next to the `node` section, to prevent Webpack from mocking some -Node.js global variables that will break WASM loading: - -```js - node: { - global: false, - __filename: false, - __dirname: false, - }, -``` - -And finally to the `resolve` section: - -```js -resolve: { - fallback: { - crypto: false, - fs: false, - path: false, - } -} -``` - -Look at the [SoundSwallower-Demo -repository](https://github.com/dhdaines/soundswallower-demo) for an -example. +For an "end-to-end" example of using SoundSwallower on the Web, look at the +[SoundSwallower-Demo +repository](https://github.com/dhdaines/soundswallower-demo). ## Basic Usage @@ -93,24 +47,26 @@ can rebuild it yourself using [the full source code from GitHub](https://github.com/ReadAlongs/SoundSwallower) which also includes C and Python implementations. -It follows the usual conventions of Emscripten, meaning that you must -require it as a CommonJS or Node.js module, which returns a function -that returns a promise that is fulfilled with the actual module once -the WASM code is fully loaded: +As of version 0.6.0, we _only_ provide an ES6 module. This means you must have +Node.js 14 or higher. It also means that you might need to use a bleeding-edge +version of `emcc` if you compile from source. Sorry about this, but the +JavaScript "ecosystem" is a toxic waste dump. + +It uses WebAssembly, so must be imported asynchronously. But... because, even +though ES6 `import` is async, you can't directly `import` WebAssembly with it, +you have to use this bogus incantation: ```js -const ssjs = await require("soundswallower")(); +import createModule from "soundswallower"; // or whatever name you like +const soundswallower = await createModule(); ``` -Once you figure out how to get the module, you can try to initialize -the recognizer and recognize some speech. - -Great, so let's initialize the recognizer. This possibly involves some long I/O -operations so it's asynchronous. We follow the construct-then-initialize -pattern. You can use `Promise`s too of course. +Because it needs access to the compiled WebAssembly code, all of the actual API is +contained within this object that you get back from `createModule`. Now you can +try to initialize a recognizer and recognize some speech. ```js -let decoder = new ssjs.Decoder({ +let decoder = new soundswallower.Decoder({ loglevel: "INFO", backtrace: true, }); @@ -191,10 +147,11 @@ is also available, which you can load by default by setting the `defaultModel` property in the module object before loading: ```js -const ssjs = { +import createModule from "soundswallower"; +const soundswallower = { defaultModel: "fr-fr", }; -await require("soundswallower")(ssjs); +await createModule(soundswallower); ``` The default model is expected to live under the `model/` directory @@ -203,11 +160,12 @@ module (in Node.js). You can modify this by setting the `modelBase` property in the module object when loading, e.g.: ```js -const ssjs = { +import createModule from "soundswallower"; +const soundswallower = { modelBase: "/assets/models/" /* Trailing slash is necessary */, defaultModel: "fr-fr", }; -await require("soundswallower")(ssjs); +await createModule(soundswallower); ``` This is simply concatenated to the model name, so you should make sure From 758247e820350f52247714327d92d72387cae7b4 Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Thu, 26 Jan 2023 11:54:04 -0500 Subject: [PATCH 05/11] feat: update build for node/web split and es6 --- js/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/js/CMakeLists.txt b/js/CMakeLists.txt index ddfc33ce..6af0f468 100644 --- a/js/CMakeLists.txt +++ b/js/CMakeLists.txt @@ -66,6 +66,7 @@ set(JSFILES test_web.html soundswallower.spec.js soundswallower.d.ts + tests.js tsconfig.json ) list(TRANSFORM JSFILES PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/) From 64614ffa993d594a767c0bf192cad20c42725a9e Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Thu, 26 Jan 2023 11:54:35 -0500 Subject: [PATCH 06/11] tests: update tests for es6 (with profanity) --- js/soundswallower.spec.js | 308 +------------------------------------- js/test_typescript.ts | 17 ++- js/test_web.html | 21 ++- js/tests.js | 294 ++++++++++++++++++++++++++++++++++++ js/tsconfig.json | 14 +- 5 files changed, 330 insertions(+), 324 deletions(-) create mode 100644 js/tests.js diff --git a/js/soundswallower.spec.js b/js/soundswallower.spec.js index 2da4a84f..4a6a2b96 100644 --- a/js/soundswallower.spec.js +++ b/js/soundswallower.spec.js @@ -1,305 +1,3 @@ -/* A bunch of junk to make this work in Node and Web */ -const ENVIRONMENT_IS_WEB = typeof window !== "undefined"; -let assert, load_binary_file; -if (ENVIRONMENT_IS_WEB) { - load_binary_file = async function (path) { - const response = await fetch(path); - if (response.ok) { - let blob = await response.blob(); - return new Uint8Array(await blob.arrayBuffer()); - } else - throw new Error("Failed to fetch " + path + " :" + response.statusText); - }; - assert = chai.assert; -} else { - load_binary_file = async function (path) { - const fs = require("fs/promises"); - return fs.readFile(path); - }; - assert = require("assert"); -} - -function check_alignment(hypseg, text) { - let hypseg_words = []; - let prev = -1; - for (const { t, b, d } of hypseg.w) { - assert.ok(d > 0); - assert.ok(b > prev); - prev = b; - if (t != "" && t != "(NULL)") hypseg_words.push(t); - } - assert.equal(hypseg_words.join(" "), text); -} - -const soundswallower = {}; -before(async () => { - if (ENVIRONMENT_IS_WEB) { - await createModule(soundswallower); - } else { - await require("./soundswallower.js")(soundswallower); - } -}); -describe("Test initialization", () => { - it("Should load the WASM module", () => { - assert.ok(soundswallower); - }); -}); -describe("Test decoder initialization", () => { - it("Should initialize the decoder", async () => { - let decoder = new soundswallower.Decoder({ - fsg: "testdata/goforward.fsg", - samprate: 16000, - }); - assert.ok(decoder); - assert.equal( - decoder.get_config("hmm"), - soundswallower.get_model_path(soundswallower.defaultModel) - ); - decoder.delete(); - }); -}); -describe("Test configuration as JSON", () => { - it("Should contain default configuration", () => { - let decoder = new soundswallower.Decoder({ - fsg: "testdata/goforward.fsg", - samprate: 16000, - }); - assert.ok(decoder); - let json = decoder.get_config_json(); - let config = JSON.parse(json); - assert.ok(config); - assert.equal( - config.hmm, - soundswallower.get_model_path(soundswallower.defaultModel) - ); - assert.equal(config.loglevel, "WARN"); - assert.equal(config.fsg, "testdata/goforward.fsg"); - }); -}); -describe("Test acoustic model loading", () => { - it("Should load acoustic model", async () => { - let decoder = new soundswallower.Decoder(); - await decoder.init_featparams(); - await decoder.init_fe(); - await decoder.init_feat(); - await decoder.init_acmod(); - await decoder.load_acmod_files(); - assert.ok(decoder); - decoder.delete(); - }); -}); -describe("Test decoding", () => { - it('Should recognize "go forward ten meters"', async () => { - let decoder = new soundswallower.Decoder({ - fsg: "testdata/goforward.fsg", - samprate: 16000, - }); - await decoder.initialize(); - let pcm = await load_binary_file("testdata/goforward-float32.raw"); - decoder.start(); - // 128-sample buffers, because fuck you, that's why - for (let pos = 0; pos < pcm.length; pos += 128) { - let len = pcm.length - pos; - if (len > 128) len = 128; - decoder.process_audio(pcm.subarray(pos, pos + len), false, false); - } - decoder.stop(); - assert.equal("go forward ten meters", decoder.get_text()); - check_alignment(decoder.get_alignment(), "go forward ten meters"); - decoder.delete(); - }); - it("Should accept Float32Array as well as UInt8Array", async () => { - let decoder = new soundswallower.Decoder({ - fsg: "testdata/goforward.fsg", - samprate: 16000, - }); - await decoder.initialize(); - let pcm = await load_binary_file("testdata/goforward-float32.raw"); - let pcm32 = new Float32Array(pcm.buffer); - decoder.start(); - decoder.process_audio(pcm32, false, true); - decoder.stop(); - assert.equal("go forward ten meters", decoder.get_text()); - decoder.delete(); - }); - it('Should align "go forward ten meters"', async () => { - let decoder = new soundswallower.Decoder({ - samprate: 16000, - }); - await decoder.initialize(); - let pcm = await load_binary_file("testdata/goforward-float32.raw"); - decoder.set_align_text("go forward ten meters"); - decoder.start(); - decoder.process_audio(pcm, false, true); - decoder.stop(); - assert.equal("go forward ten meters", decoder.get_text()); - check_alignment(decoder.get_alignment(), "go forward ten meters"); - - let result = decoder.get_alignment({ align_level: 1 }); - assert.ok(result); - assert.equal(result.t, "go forward ten meters"); - let json_words = []; - let json_phones = []; - for (const word of result.w) { - if (word.t == "") continue; - json_words.push(word.t); - for (const phone of word.w) { - json_phones.push(phone.t); - } - } - assert.equal(json_words.join(" "), "go forward ten meters"); - assert.equal( - json_phones.join(" "), - "G OW F AO R W ER D T EH N M IY T ER Z" - ); - - decoder.delete(); - }); -}); -describe("Test dictionary and FSG", () => { - it('Should recognize "_go _forward _ten _meters"', async () => { - let decoder = new soundswallower.Decoder({ samprate: 16000 }); - decoder.unset_config("dict"); - await decoder.initialize(); - decoder.add_words( - ["_go", "G OW"], - ["_forward", "F AO R W ER D"], - ["_ten", "T EH N"], - ["_meters", "M IY T ER Z"] - ); - decoder.set_align_text("_go _forward _ten _meters"); - let pcm = await load_binary_file("testdata/goforward-float32.raw"); - decoder.start(); - decoder.process_audio(pcm, false, true); - decoder.stop(); - assert.equal("_go _forward _ten _meters", decoder.get_text()); - decoder.delete(); - }); -}); -describe("Test reinitialization", () => { - it('Should recognize "go forward ten meters"', async () => { - let decoder = new soundswallower.Decoder({ samprate: 16000 }); - await decoder.initialize(); - decoder.add_words( - ["_go", "G OW"], - ["_forward", "F AO R W ER D"], - ["_ten", "T EH N"], - ["_meters", "M IY T ER Z"] - ); - decoder.set_align_text("_go _forward _ten _meters"); - decoder.set_config( - "dict", - soundswallower.get_model_path(soundswallower.defaultModel) + "/dict.txt" - ); - decoder.set_config("fsg", "testdata/goforward.fsg"); - await decoder.initialize(); - let pcm = await load_binary_file("testdata/goforward-float32.raw"); - decoder.start(); - decoder.process_audio(pcm, false, true); - decoder.stop(); - assert.equal("go forward ten meters", decoder.get_text()); - decoder.delete(); - }); -}); -describe("Test loading model for other language", () => { - it('Should recognize "avance de dix mètres"', async () => { - let decoder = new soundswallower.Decoder({ - hmm: soundswallower.get_model_path("fr-fr"), - samprate: 16000, - }); - await decoder.initialize(); - decoder.set_grammar(`#JSGF V1.0; -grammar avance; -public = (avance | recule) (d' | de) (mètre | mètres); - = un | deux | trois | quatre | cinq | six | sept | huit | neuf | dix; -`); - let pcm = await load_binary_file("testdata/goforward_fr-float32.raw"); - decoder.start(); - decoder.process_audio(pcm, false, true); - decoder.stop(); - assert.equal("avance de dix mètres", decoder.get_text()); - decoder.delete(); - }); -}); -describe("Test JSGF", () => { - it('Should recognize "yo gimme four large all dressed pizzas"', async () => { - let decoder = new soundswallower.Decoder({ - jsgf: "testdata/pizza.gram", - samprate: 16000, - }); - await decoder.initialize(); - let pcm = await load_binary_file("testdata/pizza-float32.raw"); - decoder.start(); - decoder.process_audio(pcm, false, true); - decoder.stop(); - assert.equal("yo gimme four large all dressed pizzas", decoder.get_text()); - decoder.delete(); - }); -}); -describe("Test JSGF string", () => { - it('Should recognize "yo gimme four large all dressed pizzas"', async () => { - let decoder = new soundswallower.Decoder({ samprate: 16000 }); - await decoder.initialize(); - decoder.set_grammar(`#JSGF V1.0; -grammar pizza; -public = [] [] [] [] [