Node.js
litsea-nodejs exposes Litsea to Node.js 20+ through napi-rs. It is published to npm as litsea, with prebuilt native binaries for Linux, macOS, and Windows on x64 and arm64.
For browsers, use litsea-wasm instead.
Installation
npm install litsea
Getting a model
The package contains no models. Download one from the models/ directory and pass its path — see Pre-trained Models. The model file identifies its own kind, so hasPos reports what the loaded model can do and no flag is needed.
Segmentation
import { Segmenter } from 'litsea'
const seg = Segmenter.open('japanese', 'models/japanese.model')
seg.segment('これはテストです。')
// [ 'これ', 'は', 'テスト', 'です', '。' ]
The language name and its ISO 639-1 code are interchangeable ('ja', 'japanese').
For space-delimited languages the whitespace is returned as its own token, so the tokens always reconstruct the input:
Segmenter.open('ko', 'models/korean.model').segment('안녕하세요 반갑습니다')
// [ '안녕하세요', ' ', '반갑습니다' ]
POS tagging
const seg = Segmenter.open('japanese', 'models/japanese_pos.model')
seg.segmentWithPos('これはテストです。')
// [ { surface: 'これ', start: 0, end: 6, pos: 'PRON' },
// { surface: 'は', start: 6, end: 9, pos: 'ADP' },
// { surface: 'テスト', start: 9, end: 18, pos: 'NOUN' },
// { surface: 'です', start: 18, end: 24, pos: 'AUX' },
// { surface: '。', start: 24, end: 27, pos: 'PUNCT' } ]
start and end are byte offsets. JavaScript string indices are UTF-16 code units, so slice with a Buffer:
Buffer.from(text).subarray(token.start, token.end).toString() // === token.surface
pos is undefined on tokens from segmentTokens, which does no tagging.
API
| Call | Returns |
|---|---|
Segmenter.open(language, path) | A segmenter (synchronous) |
Segmenter.fromBytes(language, buffer) | A segmenter (synchronous) |
Segmenter.fromUri(language, uri) | Promise<Segmenter> — downloads off the event loop |
segment(text) | string[] |
segmentBatch(texts) | string[][] |
segmentTokens(text) | Token[] with byte offsets |
segmentWithPos(text) | Token[] with tags and offsets |
segmentWithPosBatch(texts) | Token[][] |
new Extractor(language).extract(...) | Promise<void> |
new Extractor(language).extractTwoStage(...) | Promise<void> |
new Trainer(threshold, iterations, features).train(model, cancel?) | Promise<BinaryMetrics> |
new PerceptronTrainer(epochs, features).train(model, cancel?) | Promise<MulticlassMetrics> |
new TwoStageTrainer(epochs, prefix, dominance?).train(model, cancel?) | Promise<TwoStageMetrics> |
Type definitions are generated by napi-rs and shipped as index.d.ts.
Asynchronous by design
Downloading a model, extracting features, and training all return promises and run on libuv’s threadpool, so the event loop keeps turning. That is what makes cancellation useful:
import { CancelToken, Trainer } from 'litsea'
const cancel = new CancelToken()
setTimeout(() => cancel.cancel(), 60_000)
const metrics = await new Trainer(0.01, 100_000, 'features.txt').train('japanese.model', cancel)
Cancelling is not an error: training stops at its next check point, still writes the partially trained model, and resolves with its metrics. The binding never installs a signal handler.
Segmentation itself is synchronous: it is fast enough that a promise would cost more than the work.
A TwoStageTrainer can only be used once — training collapses stage 1 into an AdaBoost model, which consumes it. available reports the state, and a second train() rejects.
Errors
Every error carries a code, matching the error kinds the other bindings expose. Rejected promises carry the same codes as thrown errors.
err.code | Raised when |
|---|---|
invalid_argument | Unknown language name, unknown feature set, reused trainer |
model | Download failed, or the file is a legacy joint POS model |
io | A file could not be read or written |
parse | The model or training data is malformed |
unsupported | The scheme or operation is unavailable in this build |
pos_unavailable | POS tagging requested from a segmentation-only model |
Because napi::Status is a closed enum, the code reaches JavaScript two ways: synchronous calls use napi’s string-status error, and asynchronous ones rebuild the JavaScript Error object in Task::reject so the property survives the rejection.
Development
make test-litsea-nodejs # cargo test + napi build + node --test
make lint-litsea-nodejs # clippy
make build-litsea-nodejs # release build
index.js and index.d.ts are generated by napi build and committed; CI rebuilds them and fails if the committed copies are stale. The parity tests build the litsea CLI and compare the binding’s output against it.