Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Node.js

litsea-nodejsnapi-rs を用いて Litsea を Node.js 20 以降へ公開するバインディングです。npm では litsea として配布し、Linux・macOS・Windows の x64 / arm64 向けにビルド済みバイナリを提供します。

ブラウザ向けには litsea-wasm を使ってください。

インストール

npm install litsea

モデルの入手

パッケージにモデルは含まれません。models/ から取得してパスを渡してください(事前学習済みモデルを参照)。モデルファイル自身が種別を持つため、フラグの指定は不要で、読み込んだモデルで何ができるかは hasPos が示します。

分割

import { Segmenter } from 'litsea'

const seg = Segmenter.open('japanese', 'models/japanese.model')

seg.segment('これはテストです。')
// [ 'これ', 'は', 'テスト', 'です', '。' ]

言語名と ISO 639-1 コードのどちらも使えます('ja' / 'japanese')。

空白区切りの言語では空白自体が 1 トークンとして返るため、トークンを連結すると常に入力が復元されます。

Segmenter.open('ko', 'models/korean.model').segment('안녕하세요 반갑습니다')
// [ '안녕하세요', ' ', '반갑습니다' ]

POS タグ付け

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' } ]

startendバイトオフセットです。JavaScript の文字列インデックスは UTF-16 コードユニットなので、切り出しには Buffer を使ってください。

Buffer.from(text).subarray(token.start, token.end).toString()   // === token.surface

タグ付けを行わない segmentTokens のトークンでは posundefined になります。

API

呼び出し戻り値
Segmenter.open(language, path)セグメンタ(同期)
Segmenter.fromBytes(language, buffer)セグメンタ(同期)
Segmenter.fromUri(language, uri)Promise<Segmenter>(イベントループ外でダウンロード)
segment(text)string[]
segmentBatch(texts)string[][]
segmentTokens(text)バイトオフセット付き Token[]
segmentWithPos(text)タグとオフセット付き Token[]
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>

型定義は napi-rs が生成し、index.d.ts として同梱されます。

非同期設計

モデルのダウンロード・特徴量抽出・学習はいずれも Promise を返し、libuv のスレッドプールで実行されるため、イベントループは止まりません。これがキャンセルを有効にしている理由でもあります。

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)

キャンセルはエラーではありません。次のチェックポイントで停止し、部分的に学習されたモデルを保存してメトリクスで resolve します。バインディングはシグナルハンドラを登録しません。

分割自体は同期処理です。Promise のコストの方が処理そのものより大きいためです。

TwoStageTrainer は 1 度しか使えません。学習時に stage 1 が AdaBoost モデルへ collapse され、トレーナが消費されるためです。状態は available が示し、2 回目の train() は reject されます。

エラー

すべてのエラーは、他のバインディングと同じ種別を表す code を持ちます。reject された Promise も throw されたエラーと同じ code を持ちます。

err.code発生条件
invalid_argument未知の言語名、未知の feature set、使用済みトレーナ
modelダウンロード失敗、または旧 joint POS モデル
ioファイルの読み書き失敗
parseモデルまたは学習データの形式不正
unsupportedこのビルドでは利用できないスキームや操作
pos_unavailable分割専用モデルに対する POS タグ付けの要求

napi::Status が閉じた enum のため、code は 2 つの経路で JavaScript に渡ります。同期呼び出しは napi の文字列ステータス付きエラーを使い、非同期呼び出しは Task::reject で JavaScript の Error オブジェクトを再構築してプロパティを reject 後も保持させています。

開発

make test-litsea-nodejs    # cargo test + napi build + node --test
make lint-litsea-nodejs    # clippy
make build-litsea-nodejs   # リリースビルド

index.jsindex.d.tsnapi build が生成したものをコミットしており、CI で再生成して内容が古い場合は失敗させます。パリティテストは litsea CLI をビルドし、その出力とバインディングの出力を突き合わせます。