@chaffjs/lang-en 0.1.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,gBAAgB,CAAC;AAOpE;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,eAsBrB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,eAAe,EAA0B,MAAM,gBAAgB,CAAC;AAoB5F;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,eA6BrB,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,22 @@
1
1
  import { split, SentenceSplitterSyntax } from "sentence-splitter";
2
2
  import { loadLexicons } from "./lexicons.js";
3
+ import { isReady, prepare, tokenize } from "./pos.js";
3
4
  // chaff からは型だけを取る。実行時の値依存を作らない。アダプタは単体で動く。
4
5
  const LATIN_LETTER = /[a-z]/giu;
5
6
  const COUNTABLE = /\S/gu;
7
+ /**
8
+ * token の span は文ではなく、segment に渡した文字列を基準にする。文の span と同じ座標系。
9
+ * wink は位置を返さないため、文ごとに解析してから文の開始位置を足す。
10
+ */
11
+ const withTokens = (sentence) => {
12
+ const tokens = tokenize(sentence.text);
13
+ if (tokens === undefined)
14
+ return sentence;
15
+ return {
16
+ ...sentence,
17
+ tokens: tokens.map((token) => ({ ...token, span: { start: sentence.span.start + token.span.start, end: sentence.span.start + token.span.end } })),
18
+ };
19
+ };
6
20
  /**
7
21
  * 英語は sentence-splitter の既定で足りる。"Dr." "e.g." "U.S." "$3.50" を
8
22
  * いずれも文末と誤認しない。日本語のような後処理は要らない。spec §7.2。
@@ -14,10 +28,17 @@ export const adapter = {
14
28
  capabilities: {
15
29
  sentenceSplit: true,
16
30
  wordSplit: true,
17
- pos: false,
18
- lemma: false,
31
+ // 「払えばできる」の宣言。実際に払うのは prepare。英語は 130 ms ほど。
32
+ pos: true,
33
+ lemma: true,
19
34
  lengthUnit: "word",
20
35
  },
36
+ prepare: (need) => {
37
+ // 英語の解析器は同期に読める。契約は Promise なので、そこだけ合わせる。
38
+ if (need.pos)
39
+ prepare();
40
+ return Promise.resolve();
41
+ },
21
42
  detect: (source) => {
22
43
  const total = [...source.matchAll(COUNTABLE)].length;
23
44
  if (total === 0)
@@ -25,10 +46,11 @@ export const adapter = {
25
46
  return [...source.matchAll(LATIN_LETTER)].length / total;
26
47
  },
27
48
  lexicons: loadLexicons(),
28
- segment: (text) => ({
29
- sentences: split(text)
49
+ segment: (text) => {
50
+ const sentences = split(text)
30
51
  .filter((node) => node.type === SentenceSplitterSyntax.Sentence)
31
- .map((node) => ({ span: { start: node.range[0], end: node.range[1] }, text: text.slice(node.range[0], node.range[1]) })),
32
- }),
52
+ .map((node) => ({ span: { start: node.range[0], end: node.range[1] }, text: text.slice(node.range[0], node.range[1]) }));
53
+ return { sentences: isReady() ? sentences.map(withTokens) : sentences };
54
+ },
33
55
  };
34
56
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAG7C,2CAA2C;AAE3C,MAAM,YAAY,GAAG,UAAU,CAAC;AAChC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAoB;IACtC,IAAI,EAAE,UAAU;IAChB,EAAE,EAAE,IAAI;IACR,UAAU,EAAE,CAAC;IACb,YAAY,EAAE;QACZ,aAAa,EAAE,IAAI;QACnB,SAAS,EAAE,IAAI;QACf,GAAG,EAAE,KAAK;QACV,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,MAAM;KACnB;IACD,MAAM,EAAE,CAAC,MAAc,EAAU,EAAE;QACjC,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACrD,IAAI,KAAK,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC;IAC3D,CAAC;IACD,QAAQ,EAAE,YAAY,EAAE;IACxB,OAAO,EAAE,CAAC,IAAY,EAAgB,EAAE,CAAC,CAAC;QACxC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC;aACnB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,sBAAsB,CAAC,QAAQ,CAAC;aAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;KAC3H,CAAC;CACH,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGtD,2CAA2C;AAE3C,MAAM,YAAY,GAAG,UAAU,CAAC;AAChC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,QAAkB,EAAY,EAAE;IAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC1C,OAAO;QACL,GAAG,QAAQ;QACX,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;KAClJ,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAoB;IACtC,IAAI,EAAE,UAAU;IAChB,EAAE,EAAE,IAAI;IACR,UAAU,EAAE,CAAC;IACb,YAAY,EAAE;QACZ,aAAa,EAAE,IAAI;QACnB,SAAS,EAAE,IAAI;QACf,6CAA6C;QAC7C,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,MAAM;KACnB;IACD,OAAO,EAAE,CAAC,IAAkB,EAAiB,EAAE;QAC7C,0CAA0C;QAC1C,IAAI,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,CAAC;QACxB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IACD,MAAM,EAAE,CAAC,MAAc,EAAU,EAAE;QACjC,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACrD,IAAI,KAAK,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC;IAC3D,CAAC;IACD,QAAQ,EAAE,YAAY,EAAE;IACxB,OAAO,EAAE,CAAC,IAAY,EAAgB,EAAE;QACtC,MAAM,SAAS,GAAe,KAAK,CAAC,IAAI,CAAC;aACtC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,sBAAsB,CAAC,QAAQ,CAAC;aAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3H,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;IAC1E,CAAC;CACF,CAAC"}
package/dist/pos.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { Token } from "chaffjs/plugin";
2
+ export declare const upos: (tag: string) => string;
3
+ export declare const prepare: () => void;
4
+ export declare const isReady: () => boolean;
5
+ export declare const tokenize: (text: string) => Token[] | undefined;
6
+ //# sourceMappingURL=pos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pos.d.ts","sourceRoot":"","sources":["../src/pos.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AA4E5C,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,KAAG,MAAiE,CAAC;AAqBrG,eAAO,MAAM,OAAO,QAAO,IAE1B,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,OAAoC,CAAC;AAwBhE,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,KAAK,EAAE,GAAG,SAIjD,CAAC"}
package/dist/pos.js ADDED
@@ -0,0 +1,109 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const isRecord = (value) => typeof value === "object" && value !== null;
4
+ const isCallable = (value) => typeof value === "function";
5
+ const isTagged = (value) => isRecord(value) && typeof value["value"] === "string" && typeof value["pos"] === "string";
6
+ const toArray = (value) => (Array.isArray(value) ? value : []);
7
+ /** 取り出した関数をそのまま呼ぶと receiver が外れる。wink の tagSentence は自分の状態を読む。 */
8
+ const callMethod = (owner, name, args) => {
9
+ const method = owner[name];
10
+ if (!isCallable(method))
11
+ throw new Error(`wink-pos-tagger が ${name} を持っていません`);
12
+ return Reflect.apply(method, owner, args);
13
+ };
14
+ const build = () => {
15
+ const factory = require("wink-pos-tagger");
16
+ if (!isCallable(factory))
17
+ throw new Error("wink-pos-tagger が関数を export していません");
18
+ const tagger = factory();
19
+ if (!isRecord(tagger))
20
+ throw new Error("wink-pos-tagger が object を返しませんでした");
21
+ return tagger;
22
+ };
23
+ /**
24
+ * Penn Treebank を UPOS に寄せる。detector にアダプタ固有の体系を見せない。spec §6。
25
+ * 引けなかったものは X。誤った品詞を当てるより、分からないと言うほうがまし。
26
+ */
27
+ const BY_TAG = {
28
+ CC: "CCONJ",
29
+ CD: "NUM",
30
+ DT: "DET",
31
+ EX: "PRON",
32
+ FW: "X",
33
+ IN: "ADP",
34
+ JJ: "ADJ",
35
+ JJR: "ADJ",
36
+ JJS: "ADJ",
37
+ MD: "AUX",
38
+ NN: "NOUN",
39
+ NNS: "NOUN",
40
+ NNP: "PROPN",
41
+ NNPS: "PROPN",
42
+ PDT: "DET",
43
+ POS: "PART",
44
+ PRP: "PRON",
45
+ PRP$: "PRON",
46
+ RB: "ADV",
47
+ RBR: "ADV",
48
+ RBS: "ADV",
49
+ RP: "ADP",
50
+ SYM: "SYM",
51
+ TO: "PART",
52
+ UH: "INTJ",
53
+ VB: "VERB",
54
+ VBD: "VERB",
55
+ VBG: "VERB",
56
+ VBN: "VERB",
57
+ VBP: "VERB",
58
+ VBZ: "VERB",
59
+ WDT: "DET",
60
+ WP: "PRON",
61
+ WP$: "PRON",
62
+ WRB: "ADV",
63
+ };
64
+ const PUNCTUATION = new Set([".", ",", ":", "(", ")", "``", "''", "#", "$"]);
65
+ export const upos = (tag) => (PUNCTUATION.has(tag) ? "PUNCT" : (BY_TAG[tag] ?? "X"));
66
+ /**
67
+ * 受動は be + 過去分詞。過去分詞だけでは完了形(has reviewed)と見分けられないので、
68
+ * 直前の be を見る。間に副詞が挟まる(was quickly approved)ぶんだけ遡る。
69
+ */
70
+ const ADVERB_TAGS = new Set(["RB", "RBR", "RBS"]);
71
+ const BE = new Set(["be", "am", "is", "are", "was", "were", "been", "being"]);
72
+ const isBe = (entry) => BE.has(entry.lemma ?? entry.value.toLowerCase()) || BE.has(entry.value.toLowerCase());
73
+ const isPassive = (tagged, at) => {
74
+ if (tagged[at]?.pos !== "VBN")
75
+ return false;
76
+ const before = tagged.slice(0, at).reverse();
77
+ const head = before.find((entry) => !ADVERB_TAGS.has(entry.pos));
78
+ return head !== undefined && isBe(head);
79
+ };
80
+ const state = { ready: undefined };
81
+ export const prepare = () => {
82
+ state.ready ??= build();
83
+ };
84
+ export const isReady = () => state.ready !== undefined;
85
+ /**
86
+ * wink は位置を返さないので、表層を順に照合して復元する。
87
+ * 見つからないものは飛ばし、カーソルは進めない。位置の当てずっぽうを下流に流さない。
88
+ */
89
+ const locate = (text, tagged) => tagged.reduce((acc, entry, at) => {
90
+ const start = text.indexOf(entry.value, acc.cursor);
91
+ if (start === -1)
92
+ return acc;
93
+ const end = start + entry.value.length;
94
+ const token = {
95
+ span: { start, end },
96
+ surface: entry.value,
97
+ pos: upos(entry.pos),
98
+ ...(entry.lemma === undefined ? {} : { lemma: entry.lemma }),
99
+ ...(isPassive(tagged, at) ? { features: { Voice: "Pass" } } : {}),
100
+ };
101
+ return { tokens: [...acc.tokens, token], cursor: end };
102
+ }, { tokens: [], cursor: 0 }).tokens;
103
+ export const tokenize = (text) => {
104
+ const tagger = state.ready;
105
+ if (tagger === undefined)
106
+ return undefined;
107
+ return locate(text, toArray(callMethod(tagger, "tagSentence", [text])).filter(isTagged));
108
+ };
109
+ //# sourceMappingURL=pos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pos.js","sourceRoot":"","sources":["../src/pos.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ # Words that name the actor. A passive carrying one of these says who did it.
2
+ id: agent-marker
3
+ language: en
4
+ entries:
5
+ - pattern: " by "
@@ -0,0 +1,21 @@
1
+ # Signals of generated text. One says nothing, so the weights are summed.
2
+ # Never a verdict on its own. spec §20.2.
3
+ id: ai-tell
4
+ language: en
5
+ entries:
6
+ - pattern: delve into
7
+ weight: 0.9
8
+ - pattern: in today's fast-paced world
9
+ weight: 0.9
10
+ - pattern: tapestry
11
+ weight: 0.6
12
+ - pattern: it's not just
13
+ weight: 0.7
14
+ - pattern: plays a crucial role
15
+ weight: 0.8
16
+ - pattern: navigate the complexities
17
+ weight: 0.8
18
+ - pattern: a testament to
19
+ weight: 0.7
20
+ - pattern: unlock the potential
21
+ weight: 0.8
@@ -0,0 +1,11 @@
1
+ # Softeners. One is courtesy. Several push the point further away.
2
+ id: cushion-phrase
3
+ language: en
4
+ entries:
5
+ - pattern: if you don't mind
6
+ - pattern: sorry to bother
7
+ - pattern: just wanted to
8
+ - pattern: i was wondering if
9
+ - pattern: when you get a chance
10
+ - pattern: no rush but
11
+ - pattern: apologies for
@@ -0,0 +1,12 @@
1
+ # Hedges. Avoiding a claim leaves the reader unsure what was decided.
2
+ id: excessive-hedging
3
+ language: en
4
+ entries:
5
+ - pattern: it may be
6
+ - pattern: it could be
7
+ - pattern: it seems
8
+ - pattern: arguably
9
+ - pattern: to some extent
10
+ - pattern: in some cases
11
+ - pattern: it is possible that
12
+ - pattern: one might argue
@@ -0,0 +1,12 @@
1
+ # Conjunctions that open a paragraph. A run of them makes every paragraph an afterthought.
2
+ id: paragraph-opener
3
+ language: en
4
+ entries:
5
+ - pattern: also
6
+ - pattern: moreover
7
+ - pattern: furthermore
8
+ - pattern: however
9
+ - pattern: additionally
10
+ - pattern: in addition
11
+ - pattern: that said
12
+ - pattern: on the other hand
@@ -0,0 +1,10 @@
1
+ # Conjunctions that open a sentence. One is fine; a run of them makes every
2
+ # sentence read as an afterthought to the last.
3
+ id: sentence-opener
4
+ language: en
5
+ entries:
6
+ - pattern: And
7
+ - pattern: But
8
+ - pattern: So
9
+ - pattern: Yet
10
+ - pattern: Also
@@ -0,0 +1,11 @@
1
+ # Superlatives. They need something to be superlative against.
2
+ id: superlative
3
+ language: en
4
+ entries:
5
+ - pattern: the best
6
+ - pattern: the fastest
7
+ - pattern: the most
8
+ - pattern: world-first
9
+ - pattern: industry-leading
10
+ - pattern: unmatched
11
+ - pattern: unparalleled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaffjs/lang-en",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "English language adapter for chaff",
5
5
  "license": "MIT",
6
6
  "author": "isamu",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "sentence-splitter": "^5.0.1",
30
+ "wink-pos-tagger": "^2.2.2",
30
31
  "yaml": "^2.9.0"
31
32
  },
32
33
  "repository": {
package/src/index.ts CHANGED
@@ -1,12 +1,26 @@
1
1
  import { split, SentenceSplitterSyntax } from "sentence-splitter";
2
2
  import { loadLexicons } from "./lexicons.ts";
3
- import type { LanguageAdapter, Segmentation } from "chaffjs/plugin";
3
+ import { isReady, prepare, tokenize } from "./pos.ts";
4
+ import type { AdapterNeeds, LanguageAdapter, Segmentation, Sentence } from "chaffjs/plugin";
4
5
 
5
6
  // chaff からは型だけを取る。実行時の値依存を作らない。アダプタは単体で動く。
6
7
 
7
8
  const LATIN_LETTER = /[a-z]/giu;
8
9
  const COUNTABLE = /\S/gu;
9
10
 
11
+ /**
12
+ * token の span は文ではなく、segment に渡した文字列を基準にする。文の span と同じ座標系。
13
+ * wink は位置を返さないため、文ごとに解析してから文の開始位置を足す。
14
+ */
15
+ const withTokens = (sentence: Sentence): Sentence => {
16
+ const tokens = tokenize(sentence.text);
17
+ if (tokens === undefined) return sentence;
18
+ return {
19
+ ...sentence,
20
+ tokens: tokens.map((token) => ({ ...token, span: { start: sentence.span.start + token.span.start, end: sentence.span.start + token.span.end } })),
21
+ };
22
+ };
23
+
10
24
  /**
11
25
  * 英語は sentence-splitter の既定で足りる。"Dr." "e.g." "U.S." "$3.50" を
12
26
  * いずれも文末と誤認しない。日本語のような後処理は要らない。spec §7.2。
@@ -18,19 +32,26 @@ export const adapter: LanguageAdapter = {
18
32
  capabilities: {
19
33
  sentenceSplit: true,
20
34
  wordSplit: true,
21
- pos: false,
22
- lemma: false,
35
+ // 「払えばできる」の宣言。実際に払うのは prepare。英語は 130 ms ほど。
36
+ pos: true,
37
+ lemma: true,
23
38
  lengthUnit: "word",
24
39
  },
40
+ prepare: (need: AdapterNeeds): Promise<void> => {
41
+ // 英語の解析器は同期に読める。契約は Promise なので、そこだけ合わせる。
42
+ if (need.pos) prepare();
43
+ return Promise.resolve();
44
+ },
25
45
  detect: (source: string): number => {
26
46
  const total = [...source.matchAll(COUNTABLE)].length;
27
47
  if (total === 0) return 0;
28
48
  return [...source.matchAll(LATIN_LETTER)].length / total;
29
49
  },
30
50
  lexicons: loadLexicons(),
31
- segment: (text: string): Segmentation => ({
32
- sentences: split(text)
51
+ segment: (text: string): Segmentation => {
52
+ const sentences: Sentence[] = split(text)
33
53
  .filter((node) => node.type === SentenceSplitterSyntax.Sentence)
34
- .map((node) => ({ span: { start: node.range[0], end: node.range[1] }, text: text.slice(node.range[0], node.range[1]) })),
35
- }),
54
+ .map((node) => ({ span: { start: node.range[0], end: node.range[1] }, text: text.slice(node.range[0], node.range[1]) }));
55
+ return { sentences: isReady() ? sentences.map(withTokens) : sentences };
56
+ },
36
57
  };
package/src/pos.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { createRequire } from "node:module";
2
+ import type { Token } from "chaffjs/plugin";
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ /** wink は CommonJS で、辞書を同期に持つ。初期化は 130 ms ほど。 */
7
+ type Tagged = { readonly value: string; readonly pos: string; readonly lemma?: string };
8
+
9
+ type Tagger = Record<string, unknown>;
10
+
11
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
12
+
13
+ const isCallable = (value: unknown): value is (...args: readonly unknown[]) => unknown => typeof value === "function";
14
+
15
+ const isTagged = (value: unknown): value is Tagged => isRecord(value) && typeof value["value"] === "string" && typeof value["pos"] === "string";
16
+
17
+ const toArray = (value: unknown): readonly unknown[] => (Array.isArray(value) ? value : []);
18
+
19
+ /** 取り出した関数をそのまま呼ぶと receiver が外れる。wink の tagSentence は自分の状態を読む。 */
20
+ const callMethod = (owner: Record<string, unknown>, name: string, args: readonly unknown[]): unknown => {
21
+ const method: unknown = owner[name];
22
+ if (!isCallable(method)) throw new Error(`wink-pos-tagger が ${name} を持っていません`);
23
+ return Reflect.apply(method, owner, args);
24
+ };
25
+
26
+ const build = (): Tagger => {
27
+ const factory: unknown = require("wink-pos-tagger");
28
+ if (!isCallable(factory)) throw new Error("wink-pos-tagger が関数を export していません");
29
+ const tagger: unknown = factory();
30
+ if (!isRecord(tagger)) throw new Error("wink-pos-tagger が object を返しませんでした");
31
+ return tagger;
32
+ };
33
+
34
+ /**
35
+ * Penn Treebank を UPOS に寄せる。detector にアダプタ固有の体系を見せない。spec §6。
36
+ * 引けなかったものは X。誤った品詞を当てるより、分からないと言うほうがまし。
37
+ */
38
+ const BY_TAG: Readonly<Record<string, string>> = {
39
+ CC: "CCONJ",
40
+ CD: "NUM",
41
+ DT: "DET",
42
+ EX: "PRON",
43
+ FW: "X",
44
+ IN: "ADP",
45
+ JJ: "ADJ",
46
+ JJR: "ADJ",
47
+ JJS: "ADJ",
48
+ MD: "AUX",
49
+ NN: "NOUN",
50
+ NNS: "NOUN",
51
+ NNP: "PROPN",
52
+ NNPS: "PROPN",
53
+ PDT: "DET",
54
+ POS: "PART",
55
+ PRP: "PRON",
56
+ PRP$: "PRON",
57
+ RB: "ADV",
58
+ RBR: "ADV",
59
+ RBS: "ADV",
60
+ RP: "ADP",
61
+ SYM: "SYM",
62
+ TO: "PART",
63
+ UH: "INTJ",
64
+ VB: "VERB",
65
+ VBD: "VERB",
66
+ VBG: "VERB",
67
+ VBN: "VERB",
68
+ VBP: "VERB",
69
+ VBZ: "VERB",
70
+ WDT: "DET",
71
+ WP: "PRON",
72
+ WP$: "PRON",
73
+ WRB: "ADV",
74
+ };
75
+
76
+ const PUNCTUATION = new Set([".", ",", ":", "(", ")", "``", "''", "#", "$"]);
77
+
78
+ export const upos = (tag: string): string => (PUNCTUATION.has(tag) ? "PUNCT" : (BY_TAG[tag] ?? "X"));
79
+
80
+ /**
81
+ * 受動は be + 過去分詞。過去分詞だけでは完了形(has reviewed)と見分けられないので、
82
+ * 直前の be を見る。間に副詞が挟まる(was quickly approved)ぶんだけ遡る。
83
+ */
84
+ const ADVERB_TAGS = new Set(["RB", "RBR", "RBS"]);
85
+
86
+ const BE = new Set(["be", "am", "is", "are", "was", "were", "been", "being"]);
87
+
88
+ const isBe = (entry: Tagged): boolean => BE.has(entry.lemma ?? entry.value.toLowerCase()) || BE.has(entry.value.toLowerCase());
89
+
90
+ const isPassive = (tagged: readonly Tagged[], at: number): boolean => {
91
+ if (tagged[at]?.pos !== "VBN") return false;
92
+ const before = tagged.slice(0, at).reverse();
93
+ const head = before.find((entry) => !ADVERB_TAGS.has(entry.pos));
94
+ return head !== undefined && isBe(head);
95
+ };
96
+
97
+ const state: { ready: Tagger | undefined } = { ready: undefined };
98
+
99
+ export const prepare = (): void => {
100
+ state.ready ??= build();
101
+ };
102
+
103
+ export const isReady = (): boolean => state.ready !== undefined;
104
+
105
+ /**
106
+ * wink は位置を返さないので、表層を順に照合して復元する。
107
+ * 見つからないものは飛ばし、カーソルは進めない。位置の当てずっぽうを下流に流さない。
108
+ */
109
+ const locate = (text: string, tagged: readonly Tagged[]): Token[] =>
110
+ tagged.reduce<{ tokens: Token[]; cursor: number }>(
111
+ (acc, entry, at) => {
112
+ const start = text.indexOf(entry.value, acc.cursor);
113
+ if (start === -1) return acc;
114
+ const end = start + entry.value.length;
115
+ const token = {
116
+ span: { start, end },
117
+ surface: entry.value,
118
+ pos: upos(entry.pos),
119
+ ...(entry.lemma === undefined ? {} : { lemma: entry.lemma }),
120
+ ...(isPassive(tagged, at) ? { features: { Voice: "Pass" } } : {}),
121
+ };
122
+ return { tokens: [...acc.tokens, token], cursor: end };
123
+ },
124
+ { tokens: [], cursor: 0 },
125
+ ).tokens;
126
+
127
+ export const tokenize = (text: string): Token[] | undefined => {
128
+ const tagger = state.ready;
129
+ if (tagger === undefined) return undefined;
130
+ return locate(text, toArray(callMethod(tagger, "tagSentence", [text])).filter(isTagged));
131
+ };