compromise-speech 0.1.1 → 0.2.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.
@@ -0,0 +1,86 @@
1
+ import compoundPrefixes from './_compound-prefixes.js'
2
+
3
+ // Turn vowel spans into cut positions by assigning the consonants between them.
4
+ // Consonant clusters allowed to start a chunk; the picker checks longest first.
5
+ const onset = new Set([
6
+ 'bl', 'br', 'ch', 'cl', 'cr', 'dr', 'fl', 'fr', 'gl', 'gr',
7
+ 'ph', 'pl', 'pr', 'qu', 'sh', 'sk', 'sl', 'sm', 'sn', 'sp',
8
+ 'st', 'sw', 'th', 'tr', 'tw', 'wh', 'scr', 'shr', 'spl', 'spr', 'str', 'thr',
9
+ ])
10
+
11
+ // Applied in order after onset placement; later rules take precedence.
12
+ // left/right are offsets from the surrounding vowel groups' edges.
13
+ // minLeft only moves a boundary forward, preserving a longer consonant cluster.
14
+ // gap matches intervening letters; tail matches the complete remaining spelling.
15
+ const adjustments = [
16
+ { minGap: 1, maxGap: 1, gap: /^x$/, left: 1 }, // box-es
17
+ { minGap: 2, gap: /^(.)\1/, minLeft: 1 }, // rab-bit
18
+ { minGap: 2, tail: /^ed$/, right: -1 }, // res-ted
19
+ { minGap: 2, gap: /^sh$/, tail: /^(es|ing)$/, right: 0 }, // wish-ing
20
+ { minGap: 2, gap: /l$/, tail: /^e(?:[sd]|['’](?:s|d|ll|re|ve))?$/, right: -2 }, // bot-tle, ta-bles, bub-bled
21
+ { minGap: 2, gap: /l$/, tail: /^e(?:ments?|ships?|ness(?:es)?|less|ful(?:ly)?|ly)(?:['’]s)?$/, right: -2 }, // set-tle-ment, bat-tle-ships
22
+ { minGap: 1, gap: /l$/, tail: /^ess(?:ly|ness(?:es)?)?$/, right: -1 }, // help-less: keep the suffix intact
23
+ { minGap: 3, gap: /^ssl$/, tail: /^y$/, right: -1 }, // help-less-ly, not help-les-sly
24
+ { minGap: 1, gap: /^(?:s|th)$/, tail: /^m(?:s|['’]s)?$/, right: 0 }, // chas-m, rhyth-m
25
+ { minGap: 1, tail: /^n['’]t$/, right: 0 }, // could-n't: keep the base consonants together
26
+ { minGap: 1, gap: /^[-‐‑–—]+$/, right: 0 }, // re-enter: keep the hyphen on the left
27
+ ]
28
+
29
+ // Choose one cut between each pair of vowel spans, preserving their count.
30
+ const pickBoundaries = (word, vowelSpans) => {
31
+ const cuts = []
32
+ // Keep recognized compound components intact, such as side-walk and some-one.
33
+ const compoundCuts = compoundPrefixes.flatMap(pattern => {
34
+ const match = pattern.exec(word)
35
+ return match ? [match[0].length] : []
36
+ })
37
+ for (let i = 1; i < vowelSpans.length; i += 1) {
38
+ // The gap contains the letters not assigned to either vowel span.
39
+ const left = vowelSpans[i - 1].end
40
+ const right = vowelSpans[i].start
41
+ const gap = word.slice(left, right)
42
+ // Match full suffixes so long endings are not mistaken for shorter ones.
43
+ const tail = word.slice(right)
44
+ // Adjacent vowels split directly; a single consonant starts the next chunk.
45
+ let cut = left
46
+ if (gap.length > 1) {
47
+ cut = right - 1
48
+ // Prefer the longest recognized consonant cluster at the next chunk's start.
49
+ for (let size = Math.min(3, gap.length); size > 1; size -= 1) {
50
+ if (onset.has(gap.slice(-size))) {
51
+ cut = right - size
52
+ break
53
+ }
54
+ }
55
+ }
56
+ // Apply every matching adjustment in order, allowing specific rules to override.
57
+ adjustments.forEach(rule => {
58
+ if (
59
+ gap.length < rule.minGap ||
60
+ (rule.maxGap !== undefined && gap.length > rule.maxGap) ||
61
+ (rule.gap && !rule.gap.test(gap)) ||
62
+ (rule.tail && !rule.tail.test(tail))
63
+ ) {
64
+ return
65
+ }
66
+ // Exact offsets replace the cut; minLeft only advances an earlier cut.
67
+ if (rule.left !== undefined) {
68
+ cut = left + rule.left
69
+ } else if (rule.right !== undefined) {
70
+ cut = right + rule.right
71
+ } else {
72
+ cut = Math.max(cut, left + rule.minLeft)
73
+ }
74
+ })
75
+ // A compound boundary overrides ordinary consonant placement within this gap.
76
+ compoundCuts.forEach(boundary => {
77
+ if (boundary >= left && boundary <= right) {
78
+ cut = boundary
79
+ }
80
+ })
81
+ cuts.push(cut)
82
+ }
83
+ return cuts
84
+ }
85
+
86
+ export default pickBoundaries
@@ -0,0 +1,22 @@
1
+ // Match a compound's left component, including its silent e.
2
+ // Both vowel counting and boundary placement use these same spelling rules.
3
+ const compoundPrefixes = [
4
+ /^note(?=book)/, // note-book: silent e and a boundary after the complete base
5
+ /^side(?=kick|line|show|step|stream|track|walk|water|way|winder|wise)/,
6
+ /^some(?=bod|one|time|where)/,
7
+ /^none(?=such)/, // none-such retains the silent e of none
8
+ /^goose(?=berr)/, // goose-berry and goose-berries
9
+ /^above(?=board|ground)/,
10
+ /^a?fore(?=clos|taste|said|mentioned)/,
11
+ /^ware(?=house|housing)/,
12
+ /^space(?=port|ship)/,
13
+ /^race(?=horse)/,
14
+ /^lake(?=side)/,
15
+ /^white(?=side)/,
16
+ /^state(?=side)/,
17
+ /^juke(?=box)/,
18
+ /^police(?=woman|women|man|men)/,
19
+ /^(?:sale|spoke)s?(?=woman|women|man|men)/,
20
+ ]
21
+
22
+ export default compoundPrefixes
@@ -0,0 +1,27 @@
1
+ import checkExceptions from './00-exceptions.js'
2
+ import vowelSpan from './01-vowel-span.js'
3
+ import pickBoundaries from './02-boundaries.js'
4
+
5
+ const findCuts = str => {
6
+ // Exact exceptions and their plural forms take priority over spelling rules.
7
+ const found = checkExceptions(str)
8
+ if (found !== null) {
9
+ return found
10
+ }
11
+ // Keep apostrophe suffixes intact so vowel rules can recognize an extra syllable.
12
+ const vowelSpans = vowelSpan(str)
13
+ const cuts = pickBoundaries(str, vowelSpans)
14
+ // An exception may supply base-word cuts without overriding a syllabic suffix.
15
+ const key = str.replace(/['’](?:s|d|ll|re|ve)$/, '')
16
+ // Without a suffix, the initial lookup already checked this word.
17
+ if (key === str) {
18
+ return cuts
19
+ }
20
+ const baseCuts = checkExceptions(key)
21
+ if (baseCuts !== null) {
22
+ return baseCuts.concat(cuts.filter(cut => cut >= key.length))
23
+ }
24
+ return cuts
25
+ }
26
+
27
+ export default findCuts
@@ -0,0 +1,22 @@
1
+ import findCuts from './find-cuts/index.js'
2
+ import { applyCuts, normalize, pushCutsBack } from './_lib.js'
3
+
4
+ // Pass the original term text through; the tokenizer owns pre/post punctuation.
5
+ const splitTerm = (term, options = {}) => {
6
+ if (!term.text) {
7
+ return []
8
+ }
9
+ let str = term.text
10
+ if (options.normalize) {
11
+ str = term.normal || term.text
12
+ }
13
+ // we can split on a normalized string
14
+ const { clean, prefix } = normalize(str)
15
+ let cuts = findCuts(clean)
16
+ if (prefix && prefix.length > 0) {
17
+ cuts = pushCutsBack(cuts, prefix)
18
+ }
19
+ return applyCuts(str, cuts)
20
+ }
21
+
22
+ export default splitTerm
@@ -0,0 +1,9 @@
1
+ import type esmPlugin from './index.d.ts' with { 'resolution-mode': 'import' }
2
+ import type * as types from './index.d.ts' with { 'resolution-mode': 'import' }
3
+
4
+ declare const plugin: typeof esmPlugin
5
+ declare namespace plugin {
6
+ export type SpeechMethods = types.SpeechMethods
7
+ }
8
+
9
+ export = plugin
@@ -8,6 +8,6 @@ export interface SpeechMethods {
8
8
  }
9
9
 
10
10
  /** extended compromise lib **/
11
- declare const nlpSpeech: nlp.TypedPlugin<SpeechMethods>
11
+ declare const nlpSpeech: nlp.TypedPlugin<SpeechMethods> & { version: string }
12
12
 
13
13
  export default nlpSpeech
@@ -1,7 +0,0 @@
1
- import soundsLike from './soundsLike/index.js'
2
- import syllables from './syllables/index.js'
3
-
4
- export default {
5
- soundsLike,
6
- syllables
7
- }
@@ -1,13 +0,0 @@
1
- import getSyllables from './syllables.js'
2
-
3
- // const defaultObj = { normal: true, text: true, terms: false }
4
-
5
- const syllables = function (view) {
6
- view.docs.forEach(terms => {
7
- terms.forEach(term => {
8
- term.syllables = getSyllables(term.normal || term.text)
9
- })
10
- })
11
- }
12
-
13
- export default syllables
@@ -1,107 +0,0 @@
1
- const starts_with_single_vowel_combos = /^(eu)/i
2
- const joining_consonant_vowel = /^[^aeiou]e([^d]|$)/
3
- const cvcv_same_consonant = /^([^aeiouy])[aeiouy]\1[aeiouy]/
4
- const cvcv_same_vowel = /^[^aeiouy]([aeiouy])[^aeiouy]\1/
5
- const cvcv_known_consonants = /^([tg][aeiouy]){2}/
6
- const only_one_or_more_c = /^[^aeiouy]+$/
7
-
8
- const ends_with_vowel = /[aeiouy]$/
9
- const starts_with_consonant_vowel = /^[^aeiouy]h?[aeiouy]/
10
-
11
- const ones = [
12
- /^[^aeiou]?ion/,
13
- /^[^aeiou]?ised/,
14
- /^[^aeiou]?iled/,
15
-
16
- // -ing, -ent
17
- /[aeiou]n[gt]$/,
18
-
19
- // -ate, -age
20
- /\wa[gt]e$/,
21
- ]
22
-
23
- //suffix fixes
24
- const postprocess = function (arr) {
25
- //trim whitespace
26
- arr = arr.map(function (w) {
27
- return w.trim()
28
- })
29
- arr = arr.filter(function (w) {
30
- return w !== ''
31
- })
32
- // if (arr.length > 2) {
33
- // return arr;
34
- // }
35
- let l = arr.length
36
- if (l > 1) {
37
- let suffix = arr[l - 2] + arr[l - 1]
38
- for (let i = 0; i < ones.length; i++) {
39
- if (suffix.match(ones[i])) {
40
- arr[l - 2] = arr[l - 2] + arr[l - 1]
41
- arr.pop()
42
- }
43
- }
44
- }
45
-
46
- // since the open syllable detection is overzealous,
47
- // sometimes need to rejoin incorrect splits
48
- if (arr.length > 1) {
49
- let first_is_open =
50
- (arr[0].length === 1 || arr[0].match(starts_with_consonant_vowel)) &&
51
- arr[0].match(ends_with_vowel)
52
- let second_is_joining = arr[1].match(joining_consonant_vowel)
53
-
54
- if (first_is_open && second_is_joining) {
55
- let possible_combination = arr[0] + arr[1]
56
- let probably_separate_syllables =
57
- possible_combination.match(cvcv_same_consonant) ||
58
- possible_combination.match(cvcv_same_vowel) ||
59
- possible_combination.match(cvcv_known_consonants)
60
-
61
- if (!probably_separate_syllables) {
62
- arr[0] = arr[0] + arr[1]
63
- arr.splice(1, 1)
64
- }
65
- }
66
- }
67
-
68
- if (arr.length > 1) {
69
- let second_to_last_is_open =
70
- arr[arr.length - 2].match(starts_with_consonant_vowel) &&
71
- arr[arr.length - 2].match(ends_with_vowel)
72
- let last_is_joining =
73
- arr[arr.length - 1].match(joining_consonant_vowel) &&
74
- ones.every(re => !arr[arr.length - 1].match(re))
75
-
76
- if (second_to_last_is_open && last_is_joining) {
77
- let possible_combination = arr[arr.length - 2] + arr[arr.length - 1]
78
- let probably_separate_syllables =
79
- possible_combination.match(cvcv_same_consonant) ||
80
- possible_combination.match(cvcv_same_vowel) ||
81
- possible_combination.match(cvcv_known_consonants)
82
-
83
- if (!probably_separate_syllables) {
84
- arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1]
85
- arr.splice(arr.length - 1, 1)
86
- }
87
- }
88
- }
89
-
90
- if (arr.length > 1) {
91
- let single = arr[0] + arr[1]
92
- if (single.match(starts_with_single_vowel_combos)) {
93
- arr[0] = single
94
- arr.splice(1, 1)
95
- }
96
- }
97
-
98
- if (arr.length > 1) {
99
- if (arr[arr.length - 1].match(only_one_or_more_c)) {
100
- arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1]
101
- arr.splice(arr.length - 1, 1)
102
- }
103
- }
104
-
105
- return arr
106
- }
107
- export default postprocess
@@ -1,90 +0,0 @@
1
- //chop a string into pronounced syllables
2
- import postProcess from './postProcess.js'
3
-
4
- const all_spaces = / +/g
5
- const ends_with_vowel = /[aeiouy]$/
6
- const starts_with_consonant_vowel = /^[^aeiouy]h?[aeiouy]/
7
- const starts_with_e_then_specials = /^e[sm]/
8
- const starts_with_e = /^e/
9
- const ends_with_noisy_vowel_combos = /(eo|eu|ia|oa|ua|ui)$/i
10
- const aiouy = /[aiouy]/
11
- const ends_with_ee = /ee$/
12
- // const whitespace_dash = /\s\-/
13
-
14
- //method is nested because it's called recursively
15
- const doWord = function (w) {
16
- let all = []
17
- let chars = w.split('')
18
- let before = ''
19
- let after = ''
20
- let current = ''
21
- for (let i = 0; i < chars.length; i++) {
22
- before = chars.slice(0, i).join('')
23
- current = chars[i]
24
- after = chars.slice(i + 1, chars.length).join('')
25
- let candidate = before + chars[i]
26
-
27
- //it's a consonant that comes after a vowel
28
- if (before.match(ends_with_vowel) && !current.match(ends_with_vowel)) {
29
- if (after.match(starts_with_e_then_specials)) {
30
- candidate += 'e'
31
- after = after.replace(starts_with_e, '')
32
- }
33
- all.push(candidate)
34
- return all.concat(doWord(after))
35
- }
36
-
37
- //unblended vowels ('noisy' vowel combinations)
38
- if (candidate.match(ends_with_noisy_vowel_combos)) {
39
- //'io' is noisy, not in 'ion'
40
- all.push(before)
41
- all.push(current)
42
- return all.concat(doWord(after)) //recursion
43
- }
44
-
45
- // if candidate is followed by a CV, assume consecutive open syllables
46
- if (candidate.match(ends_with_vowel) && after.match(starts_with_consonant_vowel)) {
47
- all.push(candidate)
48
- return all.concat(doWord(after))
49
- }
50
- }
51
- //if still running, end last syllable
52
- if (w.match(aiouy) || w.match(ends_with_ee)) {
53
- //allow silent trailing e
54
- all.push(w)
55
- } else if (w) {
56
- let last = all.length - 1
57
- if (last < 0) {
58
- last = 0
59
- }
60
- all[last] = (all[last] || '') + w //append it to the last one
61
- }
62
- return all
63
- }
64
-
65
- let syllables = function (str) {
66
- let all = []
67
- if (!str) {
68
- return all
69
- }
70
- str = str.replace(/[.,?]/g, '')
71
- str.split(all_spaces).map(s => {
72
- all = all.concat(doWord(s))
73
- })
74
-
75
- // str.split(whitespace_dash).forEach(doWord)
76
- all = postProcess(all)
77
-
78
- //for words like 'tree' and 'free'
79
- if (all.length === 0) {
80
- all = [str]
81
- }
82
- //filter blanks
83
- all = all.filter(s => s)
84
-
85
- return all
86
- }
87
-
88
- // console.log(syllables('civilised'))
89
-
90
- export default syllables