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.
- package/LICENSE +21 -0
- package/README.md +89 -21
- package/builds/compromise-speech.cjs +483 -279
- package/builds/compromise-speech.min.js +1 -1
- package/builds/compromise-speech.mjs +1 -1
- package/package.json +45 -30
- package/src/_version.js +1 -0
- package/src/api.js +5 -5
- package/src/plugin.js +9 -3
- package/src/{compute/soundsLike → sounds-like}/transformations.js +1 -1
- package/src/syllables/index.js +12 -0
- package/src/syllables/split/_lib.js +32 -0
- package/src/syllables/split/find-cuts/00-exceptions.js +81 -0
- package/src/syllables/split/find-cuts/01-vowel-span.js +155 -0
- package/src/syllables/split/find-cuts/02-boundaries.js +86 -0
- package/src/syllables/split/find-cuts/_compound-prefixes.js +22 -0
- package/src/syllables/split/find-cuts/index.js +27 -0
- package/src/syllables/split/index.js +22 -0
- package/types/index.d.cts +9 -0
- package/{index.d.ts → types/index.d.ts} +1 -1
- package/src/compute/index.js +0 -7
- package/src/compute/syllables/index.js +0 -13
- package/src/compute/syllables/postProcess.js +0 -107
- package/src/compute/syllables/syllables.js +0 -90
- /package/src/{compute/soundsLike → sounds-like}/index.js +0 -0
- /package/src/{compute/soundsLike → sounds-like}/metaphone.js +0 -0
|
@@ -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
|
package/src/compute/index.js
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|