garu-ko 0.6.10 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Browser-native Korean morphological analyzer.** No server required.
4
4
 
5
- - **1.6MB model** bundled in npm package (no CDN needed)
5
+ - **1.9MB model** bundled in npm package (no CDN needed)
6
6
  - **93KB WASM** engine -- runs in any modern browser
7
7
  - **F1 91.1%** on human-verified gold testset (vs. Kiwi 89.7%)
8
8
  - **< 1ms** inference per sentence
@@ -13,7 +13,7 @@
13
13
 
14
14
  | | Kiwi | MeCab-ko | garu-ko |
15
15
  |---|---|---|---|
16
- | Model size | ~40MB | ~50MB | **1.6MB** |
16
+ | Model size | ~40MB | ~50MB | **1.9MB** |
17
17
  | npm package | No | No | **Yes** |
18
18
  | F1 (gold testset) | 89.7% | — | **91.1%** |
19
19
  | F1 (NIKL MP) | 87.9% | ~85% | **93.7%** |
@@ -109,6 +109,28 @@ Returns surface-form strings only. Lightweight alternative to `analyze()`.
109
109
 
110
110
  Free WASM memory. Instance is unusable after this call.
111
111
 
112
+ ## Integrations
113
+
114
+ Drop-in tokenizers for popular JS search libraries:
115
+
116
+ - **[`garu-orama-tokenizer`](https://www.npmjs.com/package/garu-orama-tokenizer)** — Korean tokenization for [Orama](https://github.com/oramasearch/orama)
117
+ - **[`garu-minisearch-tokenizer`](https://www.npmjs.com/package/garu-minisearch-tokenizer)** — Korean tokenization for [MiniSearch](https://github.com/lucaong/minisearch)
118
+
119
+ Both solve the same problem: default tokenizers don't handle Korean particles or verb inflections, so `"먹다"` never matches `"먹었다"` and `"학교"` misses `"학교에"`. These adapters run morphological analysis so the inflections fall off before indexing.
120
+
121
+ ```ts
122
+ import { create, insert, search } from '@orama/orama'
123
+ import { createTokenizer } from 'garu-orama-tokenizer'
124
+
125
+ const db = await create({
126
+ schema: { title: 'string' },
127
+ components: { tokenizer: await createTokenizer() }
128
+ })
129
+ await insert(db, { title: '학교에서 점심을 먹었다' })
130
+
131
+ await search(db, { term: '먹다' }) // ← matches
132
+ ```
133
+
112
134
  ## Acknowledgments
113
135
 
114
136
  The morphological analysis model is trained on the **NIKL Morpheme-Tagged Corpus (v1.1)** provided by the National Institute of Korean Language (국립국어원). The model contains only derived frequency statistics, not original text.
@@ -0,0 +1,13 @@
1
+ import { GaruBase } from './core.js';
2
+ import type { LoadOptions } from './core.js';
3
+ export type { POS, Token, AnalyzeResult, AnalyzeOptions, NounsOptions, LoadOptions, ModelInfo, NormalizeOptions, Segment, } from './core.js';
4
+ export { normalizeText, splitSentences } from './normalize.js';
5
+ /**
6
+ * Browser-targeted Garu. Resolves WASM and model assets via `fetch` and
7
+ * `new URL(..., import.meta.url)`. Has no Node-only imports (`fs/promises`,
8
+ * `url`, `path`), so bundlers (Vite, Webpack, Rollup) emit a clean
9
+ * browser-only chunk.
10
+ */
11
+ export declare class Garu extends GaruBase {
12
+ static load(options?: LoadOptions): Promise<Garu>;
13
+ }
@@ -0,0 +1,42 @@
1
+ import { GaruBase } from './core.js';
2
+ export { normalizeText, splitSentences } from './normalize.js';
3
+ /**
4
+ * Browser-targeted Garu. Resolves WASM and model assets via `fetch` and
5
+ * `new URL(..., import.meta.url)`. Has no Node-only imports (`fs/promises`,
6
+ * `url`, `path`), so bundlers (Vite, Webpack, Rollup) emit a clean
7
+ * browser-only chunk.
8
+ */
9
+ export class Garu extends GaruBase {
10
+ static async load(options) {
11
+ // @ts-ignore dynamic WASM import
12
+ const wasmModule = await import('../pkg/garu_wasm.js');
13
+ await wasmModule.default();
14
+ let modelBytes;
15
+ if (options?.modelData) {
16
+ modelBytes = new Uint8Array(options.modelData);
17
+ }
18
+ else if (options?.modelUrl) {
19
+ const response = await fetch(options.modelUrl);
20
+ if (!response.ok) {
21
+ throw new Error(`Failed to fetch model from ${options.modelUrl}: ${response.status} ${response.statusText}`);
22
+ }
23
+ modelBytes = new Uint8Array(await response.arrayBuffer());
24
+ }
25
+ else {
26
+ const url = new URL('../models/base.gmdl', import.meta.url).href;
27
+ const response = await fetch(url);
28
+ if (!response.ok) {
29
+ throw new Error(`Failed to fetch model from ${url}: ${response.status} ${response.statusText}`);
30
+ }
31
+ modelBytes = new Uint8Array(await response.arrayBuffer());
32
+ }
33
+ const cnnUrl = new URL('../models/cnn2.bin', import.meta.url).href;
34
+ const cnnResp = await fetch(cnnUrl);
35
+ if (!cnnResp.ok) {
36
+ throw new Error(`Failed to fetch CNN model from ${cnnUrl}: ${cnnResp.status}`);
37
+ }
38
+ const cnnBytes = new Uint8Array(await cnnResp.arrayBuffer());
39
+ const wasmInstance = new wasmModule.GaruWasm(modelBytes, cnnBytes);
40
+ return new Garu(wasmInstance, modelBytes.byteLength + cnnBytes.byteLength);
41
+ }
42
+ }
@@ -28,19 +28,17 @@ export interface ModelInfo {
28
28
  size: number;
29
29
  accuracy: number;
30
30
  }
31
- export declare class Garu {
32
- private _wasm;
33
- private _loaded;
34
- private _modelSize;
35
- private constructor();
36
- /**
37
- * Load the WASM module and model data, returning a ready-to-use Garu instance.
38
- *
39
- * @param options.modelData - Provide model bytes directly as an ArrayBuffer
40
- * @param options.modelUrl - Fetch model from this URL
41
- * If neither is provided, the model is fetched from the default CDN URL.
42
- */
43
- static load(options?: LoadOptions): Promise<Garu>;
31
+ export declare const EMPTY_RESULT: AnalyzeResult;
32
+ /**
33
+ * Shared analyzer instance. The browser/node entry points subclass this and
34
+ * provide their own `static load()` that resolves WASM and model bytes via
35
+ * environment-appropriate APIs (fetch vs fs).
36
+ */
37
+ export declare class GaruBase {
38
+ protected _wasm: any;
39
+ protected _loaded: boolean;
40
+ protected _modelSize: number;
41
+ protected constructor(wasmInstance: any, modelSize: number);
44
42
  /**
45
43
  * Analyze Korean text, returning morphological tokens with scores.
46
44
  *
package/dist/core.js ADDED
@@ -0,0 +1,97 @@
1
+ export { normalizeText, splitSentences } from './normalize.js';
2
+ export const EMPTY_RESULT = Object.freeze({
3
+ tokens: [],
4
+ score: 0,
5
+ elapsed: 0,
6
+ });
7
+ /**
8
+ * Shared analyzer instance. The browser/node entry points subclass this and
9
+ * provide their own `static load()` that resolves WASM and model bytes via
10
+ * environment-appropriate APIs (fetch vs fs).
11
+ */
12
+ export class GaruBase {
13
+ constructor(wasmInstance, modelSize) {
14
+ this._wasm = wasmInstance;
15
+ this._loaded = true;
16
+ this._modelSize = modelSize;
17
+ }
18
+ /**
19
+ * Analyze Korean text, returning morphological tokens with scores.
20
+ *
21
+ * When `options.topN` is greater than 1, returns an array of N-best results.
22
+ * Otherwise returns a single AnalyzeResult.
23
+ *
24
+ * Note: topN > 1 is not yet fully supported and may return fewer results.
25
+ */
26
+ analyze(text, options) {
27
+ if (!this._loaded) {
28
+ throw new Error('Garu instance has been destroyed');
29
+ }
30
+ const topN = options?.topN ?? 1;
31
+ if (topN > 1) {
32
+ if (text === '') {
33
+ return [{ ...EMPTY_RESULT, tokens: [] }];
34
+ }
35
+ return this._wasm.analyze_topn(text, topN);
36
+ }
37
+ if (text === '') {
38
+ return { ...EMPTY_RESULT, tokens: [] };
39
+ }
40
+ return this._wasm.analyze(text);
41
+ }
42
+ /**
43
+ * Quick tokenisation — returns an array of surface-form strings.
44
+ */
45
+ tokenize(text) {
46
+ if (!this._loaded) {
47
+ throw new Error('Garu instance has been destroyed');
48
+ }
49
+ if (text === '') {
50
+ return [];
51
+ }
52
+ return this._wasm.tokenize(text);
53
+ }
54
+ /**
55
+ * Extract nouns (NNG, NNP) from text.
56
+ * Set `options.includeSL` to also include foreign tokens (SL) like "AI", "BM25".
57
+ */
58
+ nouns(text, options) {
59
+ if (!this._loaded) {
60
+ throw new Error('Garu instance has been destroyed');
61
+ }
62
+ if (text === '') {
63
+ return [];
64
+ }
65
+ const result = this._wasm.analyze(text);
66
+ const includeSL = options?.includeSL ?? false;
67
+ return result.tokens
68
+ .filter((t) => t.pos === 'NNG' || t.pos === 'NNP' || (includeSL && t.pos === 'SL'))
69
+ .map((t) => t.text);
70
+ }
71
+ /**
72
+ * Whether the WASM analyzer is loaded and ready.
73
+ */
74
+ isLoaded() {
75
+ return this._loaded;
76
+ }
77
+ /**
78
+ * Return metadata about the loaded model.
79
+ */
80
+ modelInfo() {
81
+ return {
82
+ version: this._wasm.constructor.version(),
83
+ size: this._modelSize,
84
+ accuracy: 0.939,
85
+ };
86
+ }
87
+ /**
88
+ * Free the WASM instance and mark this Garu as unloaded.
89
+ */
90
+ destroy() {
91
+ if (this._wasm) {
92
+ this._wasm.free();
93
+ this._wasm = null;
94
+ }
95
+ this._loaded = false;
96
+ }
97
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { GaruBase } from './core.js';
2
+ import type { LoadOptions } from './core.js';
3
+ export type { POS, Token, AnalyzeResult, AnalyzeOptions, NounsOptions, LoadOptions, ModelInfo, NormalizeOptions, Segment, } from './core.js';
4
+ export { normalizeText, splitSentences } from './normalize.js';
5
+ /**
6
+ * Node-targeted Garu. Resolves WASM and model bytes via `fs/promises`.
7
+ * Browser code paths must not import this entry — use `garu-ko` directly
8
+ * (resolves via package.json conditional exports) or import from
9
+ * `garu-ko/browser`.
10
+ */
11
+ export declare class Garu extends GaruBase {
12
+ static load(options?: LoadOptions): Promise<Garu>;
13
+ }
package/dist/node.js ADDED
@@ -0,0 +1,39 @@
1
+ import { GaruBase } from './core.js';
2
+ export { normalizeText, splitSentences } from './normalize.js';
3
+ /**
4
+ * Node-targeted Garu. Resolves WASM and model bytes via `fs/promises`.
5
+ * Browser code paths must not import this entry — use `garu-ko` directly
6
+ * (resolves via package.json conditional exports) or import from
7
+ * `garu-ko/browser`.
8
+ */
9
+ export class Garu extends GaruBase {
10
+ static async load(options) {
11
+ // @ts-ignore dynamic WASM import
12
+ const wasmModule = await import('../pkg/garu_wasm.js');
13
+ const { readFile } = await import('fs/promises');
14
+ const { fileURLToPath } = await import('url');
15
+ const { join, dirname } = await import('path');
16
+ const dir = dirname(fileURLToPath(import.meta.url));
17
+ const wasmBytes = await readFile(join(dir, '..', 'pkg', 'garu_wasm_bg.wasm'));
18
+ await wasmModule.default(wasmBytes);
19
+ let modelBytes;
20
+ if (options?.modelData) {
21
+ modelBytes = new Uint8Array(options.modelData);
22
+ }
23
+ else if (options?.modelUrl) {
24
+ const response = await fetch(options.modelUrl);
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to fetch model from ${options.modelUrl}: ${response.status} ${response.statusText}`);
27
+ }
28
+ modelBytes = new Uint8Array(await response.arrayBuffer());
29
+ }
30
+ else {
31
+ const buf = await readFile(join(dir, '..', 'models', 'base.gmdl'));
32
+ modelBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
33
+ }
34
+ const cnnBuf = await readFile(join(dir, '..', 'models', 'cnn2.bin'));
35
+ const cnnBytes = new Uint8Array(cnnBuf.buffer, cnnBuf.byteOffset, cnnBuf.byteLength);
36
+ const wasmInstance = new wasmModule.GaruWasm(modelBytes, cnnBytes);
37
+ return new Garu(wasmInstance, modelBytes.byteLength + cnnBytes.byteLength);
38
+ }
39
+ }
package/models/cnn2.bin CHANGED
Binary file
package/package.json CHANGED
@@ -1,10 +1,27 @@
1
1
  {
2
2
  "name": "garu-ko",
3
- "version": "0.6.10",
4
- "description": "Ultra-lightweight Korean morphological analyzer for the web (1.7MB model, WASM, F1 93.7%)",
3
+ "version": "0.7.0",
4
+ "description": "Ultra-lightweight Korean morphological analyzer for the web (1.9MB model, WASM, F1 93.9%)",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
6
+ "main": "dist/node.js",
7
+ "browser": "dist/browser.js",
8
+ "types": "dist/node.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/node.d.ts",
12
+ "browser": "./dist/browser.js",
13
+ "node": "./dist/node.js",
14
+ "default": "./dist/node.js"
15
+ },
16
+ "./browser": {
17
+ "types": "./dist/browser.d.ts",
18
+ "default": "./dist/browser.js"
19
+ },
20
+ "./node": {
21
+ "types": "./dist/node.d.ts",
22
+ "default": "./dist/node.js"
23
+ }
24
+ },
8
25
  "files": [
9
26
  "dist",
10
27
  "pkg",
package/pkg/garu_wasm.js CHANGED
@@ -85,7 +85,7 @@ export class GaruWasm {
85
85
  }
86
86
  }
87
87
  if (Symbol.dispose) GaruWasm.prototype[Symbol.dispose] = GaruWasm.prototype.free;
88
- import * as import1 from "./snippets/garu-core-e0c86c49c213a331/inline0.js"
88
+ import * as import1 from "./snippets/garu-core-5657584039ad7577/inline0.js"
89
89
 
90
90
  function __wbg_get_imports() {
91
91
  const import0 = {
@@ -146,7 +146,7 @@ function __wbg_get_imports() {
146
146
  return {
147
147
  __proto__: null,
148
148
  "./garu_wasm_bg.js": import0,
149
- "./snippets/garu-core-e0c86c49c213a331/inline0.js": import1,
149
+ "./snippets/garu-core-5657584039ad7577/inline0.js": import1,
150
150
  };
151
151
  }
152
152
 
Binary file
package/pkg/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "garu-wasm",
3
3
  "type": "module",
4
- "version": "0.6.10",
4
+ "version": "0.7.0",
5
5
  "files": [
6
6
  "garu_wasm_bg.wasm",
7
7
  "garu_wasm.js",
@@ -0,0 +1 @@
1
+ export function performance_now() { return performance.now(); }
@@ -0,0 +1 @@
1
+ export function performance_now() { return performance.now(); }
package/dist/index.js DELETED
@@ -1,167 +0,0 @@
1
- export { normalizeText, splitSentences } from './normalize.js';
2
- const isNode = typeof process !== 'undefined' &&
3
- process.versions != null &&
4
- process.versions.node != null;
5
- const EMPTY_RESULT = Object.freeze({
6
- tokens: [],
7
- score: 0,
8
- elapsed: 0,
9
- });
10
- export class Garu {
11
- constructor(wasmInstance, modelSize) {
12
- this._wasm = wasmInstance;
13
- this._loaded = true;
14
- this._modelSize = modelSize;
15
- }
16
- /**
17
- * Load the WASM module and model data, returning a ready-to-use Garu instance.
18
- *
19
- * @param options.modelData - Provide model bytes directly as an ArrayBuffer
20
- * @param options.modelUrl - Fetch model from this URL
21
- * If neither is provided, the model is fetched from the default CDN URL.
22
- */
23
- static async load(options) {
24
- // Dynamic import of the WASM glue module and initialise it
25
- // @ts-ignore dynamic WASM import
26
- const wasmModule = await import('../pkg/garu_wasm.js');
27
- if (isNode) {
28
- const { readFile } = await import('fs/promises');
29
- const { fileURLToPath } = await import('url');
30
- const { join, dirname } = await import('path');
31
- const dir = dirname(fileURLToPath(import.meta.url));
32
- const wasmBytes = await readFile(join(dir, '..', 'pkg', 'garu_wasm_bg.wasm'));
33
- await wasmModule.default(wasmBytes);
34
- }
35
- else {
36
- await wasmModule.default();
37
- }
38
- // Resolve model bytes
39
- let modelBytes;
40
- if (options?.modelData) {
41
- modelBytes = new Uint8Array(options.modelData);
42
- }
43
- else if (options?.modelUrl) {
44
- const response = await fetch(options.modelUrl);
45
- if (!response.ok) {
46
- throw new Error(`Failed to fetch model from ${options.modelUrl}: ${response.status} ${response.statusText}`);
47
- }
48
- modelBytes = new Uint8Array(await response.arrayBuffer());
49
- }
50
- else if (isNode) {
51
- const { readFile } = await import('fs/promises');
52
- const { fileURLToPath } = await import('url');
53
- const { join, dirname } = await import('path');
54
- const dir = dirname(fileURLToPath(import.meta.url));
55
- const buf = await readFile(join(dir, '..', 'models', 'base.gmdl'));
56
- modelBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
57
- }
58
- else {
59
- const url = new URL('../models/base.gmdl', import.meta.url).href;
60
- const response = await fetch(url);
61
- if (!response.ok) {
62
- throw new Error(`Failed to fetch model from ${url}: ${response.status} ${response.statusText}`);
63
- }
64
- modelBytes = new Uint8Array(await response.arrayBuffer());
65
- }
66
- // Load CNN reranker
67
- let cnnBytes;
68
- if (isNode) {
69
- const { readFile } = await import('fs/promises');
70
- const { fileURLToPath } = await import('url');
71
- const { join, dirname } = await import('path');
72
- const dir = dirname(fileURLToPath(import.meta.url));
73
- const buf = await readFile(join(dir, '..', 'models', 'cnn2.bin'));
74
- cnnBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
75
- }
76
- else {
77
- const cnnUrl = new URL('../models/cnn2.bin', import.meta.url).href;
78
- const cnnResp = await fetch(cnnUrl);
79
- if (!cnnResp.ok) {
80
- throw new Error(`Failed to fetch CNN model from ${cnnUrl}: ${cnnResp.status}`);
81
- }
82
- cnnBytes = new Uint8Array(await cnnResp.arrayBuffer());
83
- }
84
- // Construct the WASM analyzer with both models
85
- const wasmInstance = new wasmModule.GaruWasm(modelBytes, cnnBytes);
86
- return new Garu(wasmInstance, modelBytes.byteLength + cnnBytes.byteLength);
87
- }
88
- /**
89
- * Analyze Korean text, returning morphological tokens with scores.
90
- *
91
- * When `options.topN` is greater than 1, returns an array of N-best results.
92
- * Otherwise returns a single AnalyzeResult.
93
- *
94
- * Note: topN > 1 is not yet fully supported and may return fewer results.
95
- */
96
- analyze(text, options) {
97
- if (!this._loaded) {
98
- throw new Error('Garu instance has been destroyed');
99
- }
100
- const topN = options?.topN ?? 1;
101
- if (topN > 1) {
102
- if (text === '') {
103
- return [{ ...EMPTY_RESULT, tokens: [] }];
104
- }
105
- return this._wasm.analyze_topn(text, topN);
106
- }
107
- if (text === '') {
108
- return { ...EMPTY_RESULT, tokens: [] };
109
- }
110
- return this._wasm.analyze(text);
111
- }
112
- /**
113
- * Quick tokenisation — returns an array of surface-form strings.
114
- */
115
- tokenize(text) {
116
- if (!this._loaded) {
117
- throw new Error('Garu instance has been destroyed');
118
- }
119
- if (text === '') {
120
- return [];
121
- }
122
- return this._wasm.tokenize(text);
123
- }
124
- /**
125
- * Extract nouns (NNG, NNP) from text.
126
- * Set `options.includeSL` to also include foreign tokens (SL) like "AI", "BM25".
127
- */
128
- nouns(text, options) {
129
- if (!this._loaded) {
130
- throw new Error('Garu instance has been destroyed');
131
- }
132
- if (text === '') {
133
- return [];
134
- }
135
- const result = this._wasm.analyze(text);
136
- const includeSL = options?.includeSL ?? false;
137
- return result.tokens
138
- .filter((t) => t.pos === 'NNG' || t.pos === 'NNP' || (includeSL && t.pos === 'SL'))
139
- .map((t) => t.text);
140
- }
141
- /**
142
- * Whether the WASM analyzer is loaded and ready.
143
- */
144
- isLoaded() {
145
- return this._loaded;
146
- }
147
- /**
148
- * Return metadata about the loaded model.
149
- */
150
- modelInfo() {
151
- return {
152
- version: this._wasm.constructor.version(),
153
- size: this._modelSize,
154
- accuracy: 0.939,
155
- };
156
- }
157
- /**
158
- * Free the WASM instance and mark this Garu as unloaded.
159
- */
160
- destroy() {
161
- if (this._wasm) {
162
- this._wasm.free();
163
- this._wasm = null;
164
- }
165
- this._loaded = false;
166
- }
167
- }