@echogarden/text-segmentation 0.2.1 → 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.
package/src/Test.ts CHANGED
@@ -1,12 +1,25 @@
1
- import { addMissingPunctuationWordsToWordSequence, SegmentationResult, segmentText, splitToWords, WordSequence } from "./TextSegmentation.js"
1
+ import { addMissingPunctuationWordsToWordSequence, SegmentationOptions, SegmentationResult, segmentText, segmentWordSequence, splitToWords, WordSequence } from "./TextSegmentation.js"
2
2
  import { Timer } from "./utilities/Timer.js"
3
3
 
4
4
  const log = console.log
5
5
 
6
6
  async function test1() {
7
7
  {
8
- const wordSequence = await splitToWords('Hello world! Привет мир! 你好世界!')
9
- console.log(JSON.stringify(wordSequence.words))
8
+ //const text = 'Hello! 1. Say who? 2. How are you? This is v2.0 that good. I have 2 344 234ms C# is good games'
9
+ const text = 'The time is 06:12'
10
+ //const text = 'Hello world! 23rd? 123% 3/4/7 Привет мир! 你好世界!'
11
+
12
+ const options: SegmentationOptions = {
13
+ language: 'en'
14
+ }
15
+
16
+ const wordSequence = await splitToWords(text, options)
17
+
18
+ console.log(JSON.stringify(wordSequence.wordArray))
19
+
20
+ const segmentedText = await segmentWordSequence(wordSequence)
21
+
22
+ const x = 0
10
23
  }
11
24
 
12
25
  const { readFileSync, writeFileSync } = await import('fs')
@@ -16,7 +29,7 @@ async function test1() {
16
29
 
17
30
  let result: SegmentationResult
18
31
 
19
- for (let i = 0; i < 3; i++) {
32
+ for (let i = 0; i < 5; i++) {
20
33
  result = await segmentText(text, {
21
34
  language: 'en',
22
35
  customSuppressions: [],
@@ -39,7 +52,7 @@ async function test1() {
39
52
  for (let phraseIndex = 0; phraseIndex < phrases.length; phraseIndex++) {
40
53
  const phrase = phrases[phraseIndex]
41
54
 
42
- segmentedText += phrase.wordSequence.words.join(' | ')
55
+ segmentedText += phrase.words.wordArray.join(' | ')
43
56
 
44
57
  if (phraseIndex < phrases.length - 1) {
45
58
  segmentedText += `\n${'-'.repeat(100)}\n`
@@ -1,15 +1,16 @@
1
- import { buildRegExp } from 'regexp-composer'
2
- import { buildWordOrNumberPattern as buildWordSplitterPattern, phraseSeparatorRegExp, sentenceSeparatorTrailingPunctuationRegExp, sentenceSeparatorRegExp, whitespacePatternRegExp } from './Patterns.js'
3
- import { cldrSuppressions, leadingApostropheContractionSuppressions, nounSuppressions, tldSuppressions } from './Suppressions.js'
1
+ import { buildWordOrNumberPattern as buildWordSplitterPattern, phraseSeparatorRegExp, sentenceSeparatorTrailingPunctuationRegExp, sentenceSeparatorRegExp, whitespacePatternRegExp, letterPatternGlobalRegExp } from './Patterns.js'
2
+ import { cldrSuppressions, additionalSuppressions, leadingApostropheContractionSuppressions, nounSuppressions, tldSuppressions } from './Suppressions.js'
4
3
  import { eastAsianCharRangesRegExp } from './EastAsianCharacterPatterns.js'
5
4
  import { WordSequence } from './WordSequence.js'
6
-
7
- export { cldrSuppressions } from './Suppressions.js'
5
+ import { getShortLanguageCode } from './utilities/Utilities.js'
8
6
  export { WordSequence, type WordEntry } from './WordSequence.js'
9
7
 
10
- export async function segmentText(text: string, options?: SegmentationOptions) {
11
- options = { ...defaultSegmentationOptions, ...(options ?? {}) }
8
+ import { buildRegExp } from 'regexp-composer'
12
9
 
10
+ ////////////////////////////////////////////////////////////////////////////////////////////////
11
+ // Exported methods
12
+ ////////////////////////////////////////////////////////////////////////////////////////////////
13
+ export async function segmentText(text: string, options?: SegmentationOptions) {
13
14
  const wordSequence = await splitToWords(text, options)
14
15
 
15
16
  return segmentWordSequence(wordSequence)
@@ -18,12 +19,27 @@ export async function segmentText(text: string, options?: SegmentationOptions) {
18
19
  export async function segmentWordSequence(wordSequence: WordSequence) {
19
20
  const sentenceWordRanges: Range[] = []
20
21
 
22
+ const minimumSentenceLetterCount = 2
23
+
21
24
  let sentenceStartWordOffset = 0
25
+ let currentSentenceLetterCount = 0
22
26
 
23
27
  for (let wordIndex = 0; wordIndex < wordSequence.length; wordIndex++) {
24
28
  const word = wordSequence.getWordAt(wordIndex)
25
29
 
26
- if (sentenceSeparatorRegExp.test(word)) {
30
+ if (currentSentenceLetterCount < minimumSentenceLetterCount) {
31
+ const matches = word.matchAll(letterPatternGlobalRegExp)
32
+
33
+ for (const match of matches) {
34
+ currentSentenceLetterCount += 1
35
+
36
+ if (currentSentenceLetterCount >= minimumSentenceLetterCount) {
37
+ break
38
+ }
39
+ }
40
+ }
41
+
42
+ if (currentSentenceLetterCount >= minimumSentenceLetterCount && sentenceSeparatorRegExp.test(word)) {
27
43
  while (wordIndex < wordSequence.length - 1) {
28
44
  const nextWord = wordSequence.getWordAt(wordIndex + 1)
29
45
 
@@ -40,6 +56,7 @@ export async function segmentWordSequence(wordSequence: WordSequence) {
40
56
  })
41
57
 
42
58
  sentenceStartWordOffset = wordIndex + 1
59
+ currentSentenceLetterCount = 0
43
60
  }
44
61
  }
45
62
 
@@ -106,7 +123,7 @@ export async function segmentWordSequence(wordSequence: WordSequence) {
106
123
  }
107
124
 
108
125
  const result: SegmentationResult = {
109
- wordSequence,
126
+ words: wordSequence,
110
127
  sentences,
111
128
  }
112
129
 
@@ -122,6 +139,10 @@ export async function splitToWords(text: string, options?: SegmentationOptions)
122
139
 
123
140
  options = { ...defaultSegmentationOptions, ...options }
124
141
 
142
+ if (options.language) {
143
+ options.language = getShortLanguageCode(options.language)
144
+ }
145
+
125
146
  const optionsAsJson = JSON.stringify(options)
126
147
 
127
148
  let wordSplitterRegExp = cachedWordSplitterRegExps.get(optionsAsJson)
@@ -142,7 +163,9 @@ export async function splitToWords(text: string, options?: SegmentationOptions)
142
163
 
143
164
  function addPunctuationWordIfNeeded() {
144
165
  if (charOffset > punctuationWordStartOffset) {
145
- wordSequence.addWordEntry(text, punctuationWordStartOffset, charOffset, true)
166
+ const wordText = text.substring(punctuationWordStartOffset, charOffset)
167
+ wordSequence.addWord(wordText, punctuationWordStartOffset, true)
168
+
146
169
  punctuationWordStartOffset = charOffset
147
170
  }
148
171
  }
@@ -178,7 +201,8 @@ export async function splitToWords(text: string, options?: SegmentationOptions)
178
201
  addPunctuationWordsBetween(lastMatchEndOffset, matchStartOffset)
179
202
  }
180
203
 
181
- wordSequence.addWordEntry(text, matchStartOffset, matchEndOffset, false)
204
+ const wordText = text.substring(matchStartOffset, matchEndOffset)
205
+ wordSequence.addWord(wordText, matchStartOffset, false)
182
206
 
183
207
  lastMatchEndOffset = matchEndOffset
184
208
  }
@@ -220,47 +244,27 @@ async function postprocessEastAsianWords(containingText: string, wordSequence: W
220
244
  const wordBreaks = [...icuSegmentation.createWordBreakIterator(word)]
221
245
 
222
246
  for (let i = 0; i < wordBreaks.length - 1; i++) {
223
- newWordSequence.addWordEntry(
224
- containingText,
225
- wordStartOffset + wordBreaks[i],
226
- wordStartOffset + wordBreaks[i + 1],
227
- false
247
+ const subwordStartOffset = wordStartOffset + wordBreaks[i]
248
+ const subwordEndOffset = wordStartOffset + wordBreaks[i + 1]
249
+
250
+ const subwordText = containingText.substring(subwordStartOffset, subwordEndOffset)
251
+
252
+ newWordSequence.addWord(
253
+ subwordText,
254
+ subwordStartOffset,
255
+ false,
228
256
  )
229
257
  }
230
258
  } else {
231
- newWordSequence.addWordEntry(containingText, wordEntry.startOffset, wordEntry.endOffset, wordEntry.isPunctuation)
259
+ const wordText = containingText.substring(wordEntry.startOffset, wordEntry.endOffset)
260
+
261
+ newWordSequence.addWord(wordText, wordEntry.startOffset, wordEntry.isPunctuation)
232
262
  }
233
263
  }
234
264
 
235
265
  return newWordSequence
236
266
  }
237
267
 
238
- function buildWordSplitterRegExpForOptions(options: SegmentationOptions) {
239
- const cldrSuppressionsForLang = cldrSuppressions[options.language ?? ''] ?? []
240
- const contractionSuppressionsForLang = leadingApostropheContractionSuppressions[options.language ?? ''] ?? []
241
- const contractionSuppressionsForLangWithSingleQuote = contractionSuppressionsForLang.map(str => str.replaceAll(`'`, `’`))
242
- const customSuppressions = options.customSuppressions ?? []
243
-
244
- let suppressions = [
245
- ...customSuppressions,
246
- ...cldrSuppressionsForLang,
247
- ...contractionSuppressionsForLang,
248
- ...contractionSuppressionsForLangWithSingleQuote,
249
- ...nounSuppressions,
250
- ...tldSuppressions,
251
- ]
252
-
253
- const wordPattern = buildWordSplitterPattern([
254
- ...suppressions,
255
- ...suppressions.map(word => word.toLocaleLowerCase()),
256
- ...suppressions.map(word => word.toLocaleUpperCase()),
257
- ])
258
-
259
- const wordSplitterRegExp = buildRegExp(wordPattern, { global: true })
260
-
261
- return wordSplitterRegExp
262
- }
263
-
264
268
  // Add any missing punctuation words to a word sequence
265
269
  export function addMissingPunctuationWordsToWordSequence(wordSequence: WordSequence, sourceText: string) {
266
270
  const originalWordsReverseMapping = new Map<number, number>()
@@ -280,7 +284,9 @@ export function addMissingPunctuationWordsToWordSequence(wordSequence: WordSeque
280
284
  wordSequenceWithPunctuation.lastEntry.text += ' '
281
285
  wordSequenceWithPunctuation.lastEntry.endOffset = charEndOffset
282
286
  } else {
283
- wordSequenceWithPunctuation.addWordEntry(sourceText, charOffset, charEndOffset, true)
287
+ const wordText = sourceText.substring(charOffset, charEndOffset)
288
+
289
+ wordSequenceWithPunctuation.addWord(wordText, charOffset, true)
284
290
  }
285
291
 
286
292
  charOffset = charEndOffset
@@ -316,6 +322,37 @@ export function addMissingPunctuationWordsToWordSequence(wordSequence: WordSeque
316
322
  return { wordSequenceWithPunctuation, originalWordsReverseMapping }
317
323
  }
318
324
 
325
+ ////////////////////////////////////////////////////////////////////////////////////////////////
326
+ // Helper methods
327
+ ////////////////////////////////////////////////////////////////////////////////////////////////
328
+ function buildWordSplitterRegExpForOptions(options: SegmentationOptions) {
329
+ const cldrSuppressionsForLang = cldrSuppressions[options.language ?? ''] ?? []
330
+ const extendedSuppressionsForLang = additionalSuppressions[options.language ?? ''] ?? []
331
+ const contractionSuppressionsForLang = leadingApostropheContractionSuppressions[options.language ?? ''] ?? []
332
+ const contractionSuppressionsForLangWithSingleQuote = contractionSuppressionsForLang.map(str => str.replaceAll(`'`, `’`))
333
+ const customSuppressions = options.customSuppressions ?? []
334
+
335
+ let suppressions = [
336
+ ...customSuppressions,
337
+ ...cldrSuppressionsForLang,
338
+ ...extendedSuppressionsForLang,
339
+ ...contractionSuppressionsForLang,
340
+ ...contractionSuppressionsForLangWithSingleQuote,
341
+ ...nounSuppressions,
342
+ ...tldSuppressions,
343
+ ]
344
+
345
+ const wordPattern = buildWordSplitterPattern([
346
+ ...suppressions,
347
+ ...suppressions.map(word => word.toLocaleLowerCase()),
348
+ ...suppressions.map(word => word.toLocaleUpperCase()),
349
+ ])
350
+
351
+ const wordSplitterRegExp = buildRegExp(wordPattern, { global: true })
352
+
353
+ return wordSplitterRegExp
354
+ }
355
+
319
356
  async function getIcuSegmentation() {
320
357
  try {
321
358
  const icuSegmentation = await import('@echogarden/icu-segmentation-wasm')
@@ -326,28 +363,31 @@ async function getIcuSegmentation() {
326
363
  }
327
364
  }
328
365
 
366
+ ////////////////////////////////////////////////////////////////////////////////////////////////
367
+ // Types
368
+ ////////////////////////////////////////////////////////////////////////////////////////////////
329
369
  export interface SegmentationResult {
330
- wordSequence: WordSequence
370
+ words: WordSequence
331
371
  sentences: Sentence[]
332
372
  }
333
373
 
334
374
  export class TextFragment {
335
375
  wordRange: Range
336
- wordSequence: WordSequence
376
+ words: WordSequence
337
377
 
338
- constructor(wordRange: Range, wordSequence: WordSequence) {
378
+ constructor(wordRange: Range, words: WordSequence) {
339
379
  this.wordRange = wordRange
340
- this.wordSequence = wordSequence
380
+ this.words = words
341
381
  }
342
382
 
343
383
  get text() {
344
- return this.wordSequence.text
384
+ return this.words.text
345
385
  }
346
386
 
347
387
  get charRange(): Range {
348
388
  return {
349
- start: this.wordSequence.firstEntry.startOffset,
350
- end: this.wordSequence.lastEntry.endOffset
389
+ start: this.words.firstEntry.startOffset,
390
+ end: this.words.lastEntry.endOffset
351
391
  }
352
392
  }
353
393
  }
@@ -1,11 +1,12 @@
1
1
  export class WordSequence {
2
2
  entries: WordEntry[] = []
3
3
 
4
- addWordEntry(containingText: string, startOffset: number, endOffset: number, isPunctuation: boolean) {
5
- const wordText = containingText.substring(startOffset, endOffset)
4
+ addWord(text: string, textStartOffset: number, isPunctuation: boolean) {
5
+ const startOffset = textStartOffset
6
+ const endOffset = startOffset + text.length
6
7
 
7
8
  this.entries.push({
8
- text: wordText,
9
+ text,
9
10
  startOffset,
10
11
  endOffset,
11
12
  isPunctuation
@@ -13,7 +14,7 @@ export class WordSequence {
13
14
  }
14
15
 
15
16
  getWordRange(startIndex: number, endIndex: number) {
16
- return [...this.iterateWordRange(startIndex, endIndex)]
17
+ return this.getEntryRange(startIndex, endIndex).map(entry => entry.text)
17
18
  }
18
19
 
19
20
  *iterateWordRange(startIndex: number, endIndex: number) {
@@ -52,7 +53,7 @@ export class WordSequence {
52
53
  return slicedSequence
53
54
  }
54
55
 
55
- get words() {
56
+ get wordArray() {
56
57
  return this.entries.map(entry => entry.text)
57
58
  }
58
59
 
@@ -72,6 +73,14 @@ export class WordSequence {
72
73
  return this.entries[this.length - 1]
73
74
  }
74
75
 
76
+ get punctuationEntries() {
77
+ return this.entries.filter(entry => entry.isPunctuation === true)
78
+ }
79
+
80
+ get punctuationWords() {
81
+ return this.entries.filter(entry => entry.isPunctuation === true).map(entry => entry.text)
82
+ }
83
+
75
84
  get nonPunctuationEntries() {
76
85
  return this.entries.filter(entry => entry.isPunctuation === false)
77
86
  }
@@ -81,7 +90,7 @@ export class WordSequence {
81
90
  }
82
91
 
83
92
  get text() {
84
- return this.words.join('')
93
+ return this.wordArray.join('')
85
94
  }
86
95
 
87
96
  get length() {
@@ -91,8 +100,9 @@ export class WordSequence {
91
100
 
92
101
  export interface WordEntry {
93
102
  text: string
94
- isPunctuation: boolean
95
103
 
96
104
  startOffset: number
97
105
  endOffset: number
106
+
107
+ isPunctuation: boolean
98
108
  }
@@ -31,3 +31,13 @@ export function extractSuppressions(entries: { suppression: string}[]) {
31
31
 
32
32
  return suppressions
33
33
  }
34
+
35
+ export function getShortLanguageCode(langCode: string) {
36
+ const dashIndex = langCode.indexOf('-')
37
+
38
+ if (dashIndex == -1) {
39
+ return langCode
40
+ }
41
+
42
+ return langCode.substring(0, dashIndex).toLowerCase()
43
+ }