Skip to content

Commit 225de85

Browse files
authored
Merge pull request #13 from damageboy/feat/wasm-js-bindings
feat(wasm): add JavaScript/WASM bindings and npm publish pipeline
2 parents 1b90efa + b2e81d7 commit 225de85

11 files changed

Lines changed: 352 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,15 @@ jobs:
2525
- uses: actions/checkout@v4
2626
- run: cargo test
2727
- run: cargo test --features ffi
28+
29+
wasm:
30+
name: WASM
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
- uses: actions/setup-node@v4
35+
with:
36+
node-version: 22
37+
- uses: jetli/wasm-pack-action@v0.4.0
38+
- run: rustup target add wasm32-unknown-unknown
39+
- run: npm run wasm:ci

.github/workflows/npm-publish.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Publish WASM Package
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 22
19+
registry-url: "https://registry.npmjs.org"
20+
21+
- uses: jetli/wasm-pack-action@v0.4.0
22+
23+
- run: rustup target add wasm32-unknown-unknown
24+
- run: npm run wasm:ci
25+
- run: npm run wasm:publish
26+
env:
27+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
/target
2+
/pkg
3+
/pkg-node
4+
/pkg-web

Cargo.lock

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ crate-type = ["lib", "staticlib", "cdylib"]
1313

1414
[dependencies]
1515
lazy_static = "1"
16+
wasm-bindgen = { version = "0.2", optional = true }
17+
console_error_panic_hook = { version = "0.1", optional = true }
1618

1719
[dev-dependencies]
1820
proptest = "1"
1921

2022
[features]
2123
default = []
2224
ffi = [] # Enable C FFI bindings
25+
wasm = ["dep:wasm-bindgen", "dep:console_error_panic_hook"]

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,62 @@ let result = tn_normalize("123");
5252
assert_eq!(result, "one hundred twenty three");
5353
```
5454

55+
### JavaScript (WASM)
56+
57+
Build wasm artifacts:
58+
59+
```bash
60+
npm run wasm:build:node
61+
npm run wasm:build:web
62+
```
63+
64+
Node usage:
65+
66+
```javascript
67+
import * as wasm from "./pkg-node/text_processing_rs.js";
68+
69+
console.log(wasm.normalize("two hundred")); // "200"
70+
console.log(wasm.tnNormalize("$5.50")); // "five dollars and fifty cents"
71+
72+
wasm.addRule("gee pee tee", "GPT");
73+
console.log(wasm.normalize("gee pee tee")); // "GPT"
74+
```
75+
76+
The generated npm package name is `@fluidinference/text-processing-rs`.
77+
78+
Web project usage (Vite / Next.js / webpack):
79+
80+
```bash
81+
npm install @fluidinference/text-processing-rs
82+
```
83+
84+
```javascript
85+
import init, * as wasm from "@fluidinference/text-processing-rs";
86+
87+
async function run() {
88+
// Loads and initializes the .wasm module (required once at startup)
89+
await init();
90+
91+
const itn = wasm.normalize("two hundred");
92+
const tn = wasm.tnNormalize("$5.50");
93+
94+
console.log(itn); // "200"
95+
console.log(tn); // "five dollars and fifty cents"
96+
97+
wasm.addRule("gee pee tee", "GPT");
98+
console.log(wasm.normalize("gee pee tee")); // "GPT"
99+
}
100+
101+
run();
102+
```
103+
104+
If your framework supports top-level `await`, you can initialize at module load time:
105+
106+
```javascript
107+
import init, * as wasm from "@fluidinference/text-processing-rs";
108+
await init();
109+
```
110+
55111
Sentence-level normalization scans for normalizable spans within a larger sentence:
56112

57113
```rust
@@ -163,6 +219,19 @@ cargo build
163219
cargo test
164220
```
165221

222+
### WASM + JavaScript
223+
224+
```bash
225+
# Build + smoke test (Node) + build browser artifact
226+
npm run wasm:ci
227+
228+
# Create a tarball from the browser package
229+
npm run wasm:pack
230+
231+
# Publish browser package to npm (requires npm auth)
232+
npm run wasm:publish
233+
```
234+
166235
### CLI Tools
167236

168237
```bash

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@fluidinference/text-processing-rs",
3+
"version": "0.1.0",
4+
"description": "Inverse Text Normalization (ITN) — convert spoken-form ASR output to written form",
5+
"type": "module",
6+
"main": "pkg-web/text_processing_rs.js",
7+
"types": "pkg-web/text_processing_rs.d.ts",
8+
"files": [
9+
"pkg-web/text_processing_rs.js",
10+
"pkg-web/text_processing_rs.d.ts",
11+
"pkg-web/text_processing_rs_bg.wasm",
12+
"pkg-web/text_processing_rs_bg.wasm.d.ts"
13+
],
14+
"scripts": {
15+
"wasm:build:node": "wasm-pack build --release --target nodejs --features wasm && mkdir -p pkg-node && cp -f pkg/* pkg-node/ && node scripts/set-wasm-package-name.mjs pkg-node",
16+
"wasm:build:web": "wasm-pack build --release --target web --features wasm && mkdir -p pkg-web && cp -f pkg/* pkg-web/ && node scripts/set-wasm-package-name.mjs pkg-web",
17+
"wasm:test:node": "node wasm-tests/node-smoke.mjs",
18+
"wasm:ci": "npm run wasm:build:node && npm run wasm:test:node && npm run wasm:build:web",
19+
"wasm:pack": "npm pack ./pkg-web",
20+
"wasm:publish": "npm publish ./pkg-web --access public"
21+
}
22+
}

scripts/set-wasm-package-name.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
const pkgDir = process.argv[2];
5+
if (!pkgDir) {
6+
throw new Error('Usage: node scripts/set-wasm-package-name.mjs <pkg-dir>');
7+
}
8+
9+
const packageJsonPath = path.join(pkgDir, 'package.json');
10+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
11+
pkg.name = '@fluidinference/text-processing-rs';
12+
pkg.keywords = ['asr', 'speech', 'normalization', 'nlp', 'itn', 'tts', 'wasm'];
13+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub mod tn;
2222

2323
#[cfg(feature = "ffi")]
2424
pub mod ffi;
25+
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
26+
pub mod wasm;
2527

2628
use itn::en::{
2729
cardinal, date, decimal, electronic, measure, money, ordinal, punctuation, telephone, time,

src/wasm.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! WebAssembly exports for JavaScript interop.
2+
3+
use wasm_bindgen::prelude::*;
4+
5+
use crate::{
6+
custom_rules, normalize, normalize_sentence, normalize_sentence_with_max_span,
7+
normalize_with_lang, tn_normalize, tn_normalize_lang, tn_normalize_sentence,
8+
tn_normalize_sentence_lang, tn_normalize_sentence_with_max_span,
9+
tn_normalize_sentence_with_max_span_lang,
10+
};
11+
12+
/// Initialize panic hook for better error messages in browser devtools.
13+
#[wasm_bindgen]
14+
pub fn set_panic_hook() {
15+
console_error_panic_hook::set_once();
16+
}
17+
18+
#[wasm_bindgen(js_name = normalize)]
19+
pub fn normalize_js(input: &str) -> String {
20+
normalize(input)
21+
}
22+
23+
#[wasm_bindgen(js_name = normalizeWithLang)]
24+
pub fn normalize_with_lang_js(input: &str, lang: &str) -> String {
25+
normalize_with_lang(input, lang)
26+
}
27+
28+
#[wasm_bindgen(js_name = normalizeSentence)]
29+
pub fn normalize_sentence_js(input: &str) -> String {
30+
normalize_sentence(input)
31+
}
32+
33+
#[wasm_bindgen(js_name = normalizeSentenceWithMaxSpan)]
34+
pub fn normalize_sentence_with_max_span_js(input: &str, max_span_tokens: u32) -> String {
35+
normalize_sentence_with_max_span(input, max_span_tokens as usize)
36+
}
37+
38+
#[wasm_bindgen(js_name = tnNormalize)]
39+
pub fn tn_normalize_js(input: &str) -> String {
40+
tn_normalize(input)
41+
}
42+
43+
#[wasm_bindgen(js_name = tnNormalizeLang)]
44+
pub fn tn_normalize_lang_js(input: &str, lang: &str) -> String {
45+
tn_normalize_lang(input, lang)
46+
}
47+
48+
#[wasm_bindgen(js_name = tnNormalizeSentence)]
49+
pub fn tn_normalize_sentence_js(input: &str) -> String {
50+
tn_normalize_sentence(input)
51+
}
52+
53+
#[wasm_bindgen(js_name = tnNormalizeSentenceLang)]
54+
pub fn tn_normalize_sentence_lang_js(input: &str, lang: &str) -> String {
55+
tn_normalize_sentence_lang(input, lang)
56+
}
57+
58+
#[wasm_bindgen(js_name = tnNormalizeSentenceWithMaxSpan)]
59+
pub fn tn_normalize_sentence_with_max_span_js(input: &str, max_span_tokens: u32) -> String {
60+
tn_normalize_sentence_with_max_span(input, max_span_tokens as usize)
61+
}
62+
63+
#[wasm_bindgen(js_name = tnNormalizeSentenceWithMaxSpanLang)]
64+
pub fn tn_normalize_sentence_with_max_span_lang_js(
65+
input: &str,
66+
lang: &str,
67+
max_span_tokens: u32,
68+
) -> String {
69+
tn_normalize_sentence_with_max_span_lang(input, lang, max_span_tokens as usize)
70+
}
71+
72+
#[wasm_bindgen(js_name = addRule)]
73+
pub fn add_rule_js(spoken: &str, written: &str) {
74+
custom_rules::add_rule(spoken, written);
75+
}
76+
77+
#[wasm_bindgen(js_name = removeRule)]
78+
pub fn remove_rule_js(spoken: &str) -> bool {
79+
custom_rules::remove_rule(spoken)
80+
}
81+
82+
#[wasm_bindgen(js_name = clearRules)]
83+
pub fn clear_rules_js() {
84+
custom_rules::clear_rules();
85+
}
86+
87+
#[wasm_bindgen(js_name = ruleCount)]
88+
pub fn rule_count_js() -> u32 {
89+
custom_rules::rule_count() as u32
90+
}

0 commit comments

Comments
 (0)