chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:48:55 +08:00
commit c728c8e1e1
1067 changed files with 109127 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
<div align="center">
<img src="https://cloud.githubusercontent.com/assets/399657/23590290/ede73772-01aa-11e7-8915-181ef21027bc.png" />
<div>pronounciation metadata plugin for <a href="https://github.com/spencermountain/compromise/">compromise</a></div>
<!-- npm version -->
<a href="https://npmjs.org/package/compromise-speech">
<img src="https://img.shields.io/npm/v/compromise-speech.svg?style=flat-square" />
</a>
<!-- file size -->
<a href="https://unpkg.com/compromise-speech/builds/compromise-speech.min.js">
<img src="https://badge-size.herokuapp.com/spencermountain/compromise/master/plugins/speech/builds/compromise-speech.min.js" />
</a>
<div align="center">
<code>npm install compromise-speech</code>
</div>
</div>
<!-- spacer -->
<img height="30px" src="https://user-images.githubusercontent.com/399657/68221862-17ceb980-ffb8-11e9-87d4-7b30b6488f16.png"/>
### Syllables
Tokenize a word approximately by it's pronounced syllables. We use a custom rule-based alortithm for this.
```js
import plg from 'compromise-speech'
nlp.extend(plg)
let doc = nlp('seventh millenium. white collar')
doc.syllables()
[ [ 'se', 'venth', 'mil', 'le', 'nium' ], [ 'white', 'col', 'lar' ] ]
```
Alternatively, if you'd like to combine syllable information with other properties, you can use `.compute('syllables')`
```js
let doc = nlp('edmonton oilers')
doc.compute('syllables')
doc.json()
/*[{
"terms": [
{
"normal": "edmonton",
"syllables": ["ed", "mon", "ton"]
},
{
"normal": "oilers",
"syllables": ["oi", "lers"]
}
]
}]
*/
```
### SoundsLike
Generate an expected pronounciation for the word, as a normalized string.
We use the [metaphone implementation](https://en.wikipedia.org/wiki/Metaphone), which is a simple rule-based pronounciation system.
```js
import plg from 'compromise-speech'
nlp.extend(plg)
let doc = nlp('seventh millenium. white collar')
doc.soundsLike()
// [ [ 'sefenth', 'milenium' ], [ 'wite', 'kolar' ] ]
```
Alternatively, if you'd like to combine syllable information with other properties, you can use `.compute('syllables')`
```js
let doc = nlp('edmonton oilers')
doc.compute('soundsLike')
doc.json()
/*[{
"terms": [
{
"normal": "edmonton",
"soundsLike": "etmonton"
},
{
"normal": "oilers",
"soundsLike": "oilers"
}
]
}]
*/
```
MIT
+386
View File
@@ -0,0 +1,386 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.compromiseSpeech = factory());
})(this, (function () { 'use strict';
//individual manipulations of the text
const transformations = {
dedup: (s) => {
return s.replace(/([^c])\1/g, '$1')
},
dropInitialLetters: (s) => {
if (s.match(/^(kn|gn|pn|ae|wr)/)) {
return s.substring(1, s.length - 1)
}
return s
},
dropBafterMAtEnd: (s) => {
return s.replace(/mb$/, 'm')
},
cchange: (s) => {
s = s.replace(/([^s]|^)(c)(h)/g, '$1x$3').trim();
s = s.replace(/cia/g, 'xia');
s = s.replace(/c([iey])/g, 's$1');
return s.replace(/c/g, 'k')
},
dchange: (s) => {
s = s.replace(/d(ge|gy|gi)/g, 'j$1');
return s.replace(/d/g, 't')
},
dropG: (s) => {
// Drop 'G' if followed by 'H' and 'H' is not at the end or before a vowel.
s = s.replace(/gh(^$|[^aeiou])/g, 'h$1');//eslint-disable-line
// Drop 'G' if followed by 'N' or 'NED' and is at the end.
return s.replace(/g(n|ned)$/g, '$1')
},
changeG: (s) => {
s = s.replace(/gh/g, 'f');
s = s.replace(/([^g]|^)(g)([iey])/g, '$1j$3');
s = s.replace(/gg/g, 'g');
return s.replace(/g/g, 'k')
},
dropH: (s) => {
return s.replace(/([aeiou])h([^aeiou]|$)/g, '$1$2')
},
changeCK: (s) => {
return s.replace(/ck/g, 'k')
},
changePH: (s) => {
return s.replace(/ph/g, 'f')
},
changeQ: (s) => {
return s.replace(/q/g, 'k')
},
changeS: (s) => {
return s.replace(/s(h|io|ia)/g, 'x$1')
},
changeT: (s) => {
s = s.replace(/t(ia[^n]|io)/g, 'x$1');
// return s.replace(/th/, '0')
return s
},
dropT: (s) => {
return s.replace(/tch/g, 'ch')
},
changeV: (s) => {
return s.replace(/v/g, 'f')
},
changeWH: (s) => {
return s.replace(/^wh/, 'w')
},
dropW: (s) => {
return s.replace(/w([^aeiou]|$)/g, '$1')
},
changeX: (s) => {
s = s.replace(/^x/, 's');
return s.replace(/x/g, 'ks')
},
dropY: (s) => {
return s.replace(/y([^aeiou]|$)/g, '$1')
},
changeZ: (s) => {
return s.replace(/z/, 's')
},
dropVowels: (s) => {
return s //.charAt(0) + s.substr(1, s.length).replace(/[aeiou]/g, '');
},
};
var m = transformations;
//a js version of the metaphone (#1) algorithm
const metaphone = function (s) {
s = m.dedup(s);
s = m.dropInitialLetters(s);
s = m.dropBafterMAtEnd(s);
s = m.changeCK(s);
s = m.cchange(s);
s = m.dchange(s);
s = m.dropG(s);
s = m.changeG(s);
s = m.dropH(s);
s = m.changePH(s);
s = m.changeQ(s);
s = m.changeS(s);
s = m.changeX(s);
s = m.changeT(s);
s = m.dropT(s);
s = m.changeV(s);
s = m.changeWH(s);
s = m.dropW(s);
s = m.dropY(s);
s = m.changeZ(s);
s = m.dropVowels(s);
return s.trim()
};
var metaphone$1 = metaphone;
const soundsLike = function (view) {
view.docs.forEach(terms => {
terms.forEach(term => {
term.soundsLike = metaphone$1(term.normal || term.text);
});
});
};
var soundsLike$1 = soundsLike;
const starts_with_single_vowel_combos = /^(eu)/i;
const joining_consonant_vowel = /^[^aeiou]e([^d]|$)/;
const cvcv_same_consonant = /^([^aeiouy])[aeiouy]\1[aeiouy]/;
const cvcv_same_vowel = /^[^aeiouy]([aeiouy])[^aeiouy]\1/;
const cvcv_known_consonants = /^([tg][aeiouy]){2}/;
const only_one_or_more_c = /^[^aeiouy]+$/;
const ends_with_vowel$1 = /[aeiouy]$/;
const starts_with_consonant_vowel$1 = /^[^aeiouy]h?[aeiouy]/;
const ones = [
/^[^aeiou]?ion/,
/^[^aeiou]?ised/,
/^[^aeiou]?iled/,
// -ing, -ent
/[aeiou]n[gt]$/,
// -ate, -age
/\wa[gt]e$/,
];
//suffix fixes
const postprocess = function (arr) {
//trim whitespace
arr = arr.map(function (w) {
return w.trim()
});
arr = arr.filter(function (w) {
return w !== ''
});
// if (arr.length > 2) {
// return arr;
// }
let l = arr.length;
if (l > 1) {
let suffix = arr[l - 2] + arr[l - 1];
for (let i = 0; i < ones.length; i++) {
if (suffix.match(ones[i])) {
arr[l - 2] = arr[l - 2] + arr[l - 1];
arr.pop();
}
}
}
// since the open syllable detection is overzealous,
// sometimes need to rejoin incorrect splits
if (arr.length > 1) {
let first_is_open =
(arr[0].length === 1 || arr[0].match(starts_with_consonant_vowel$1)) &&
arr[0].match(ends_with_vowel$1);
let second_is_joining = arr[1].match(joining_consonant_vowel);
if (first_is_open && second_is_joining) {
let possible_combination = arr[0] + arr[1];
let probably_separate_syllables =
possible_combination.match(cvcv_same_consonant) ||
possible_combination.match(cvcv_same_vowel) ||
possible_combination.match(cvcv_known_consonants);
if (!probably_separate_syllables) {
arr[0] = arr[0] + arr[1];
arr.splice(1, 1);
}
}
}
if (arr.length > 1) {
let second_to_last_is_open =
arr[arr.length - 2].match(starts_with_consonant_vowel$1) &&
arr[arr.length - 2].match(ends_with_vowel$1);
let last_is_joining =
arr[arr.length - 1].match(joining_consonant_vowel) &&
ones.every(re => !arr[arr.length - 1].match(re));
if (second_to_last_is_open && last_is_joining) {
let possible_combination = arr[arr.length - 2] + arr[arr.length - 1];
let probably_separate_syllables =
possible_combination.match(cvcv_same_consonant) ||
possible_combination.match(cvcv_same_vowel) ||
possible_combination.match(cvcv_known_consonants);
if (!probably_separate_syllables) {
arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1];
arr.splice(arr.length - 1, 1);
}
}
}
if (arr.length > 1) {
let single = arr[0] + arr[1];
if (single.match(starts_with_single_vowel_combos)) {
arr[0] = single;
arr.splice(1, 1);
}
}
if (arr.length > 1) {
if (arr[arr.length - 1].match(only_one_or_more_c)) {
arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1];
arr.splice(arr.length - 1, 1);
}
}
return arr
};
var postProcess = postprocess;
//chop a string into pronounced syllables
const all_spaces = / +/g;
const ends_with_vowel = /[aeiouy]$/;
const starts_with_consonant_vowel = /^[^aeiouy]h?[aeiouy]/;
const starts_with_e_then_specials = /^e[sm]/;
const starts_with_e = /^e/;
const ends_with_noisy_vowel_combos = /(eo|eu|ia|oa|ua|ui)$/i;
const aiouy = /[aiouy]/;
const ends_with_ee = /ee$/;
// const whitespace_dash = /\s\-/
//method is nested because it's called recursively
const doWord = function (w) {
let all = [];
let chars = w.split('');
let before = '';
let after = '';
let current = '';
for (let i = 0; i < chars.length; i++) {
before = chars.slice(0, i).join('');
current = chars[i];
after = chars.slice(i + 1, chars.length).join('');
let candidate = before + chars[i];
//it's a consonant that comes after a vowel
if (before.match(ends_with_vowel) && !current.match(ends_with_vowel)) {
if (after.match(starts_with_e_then_specials)) {
candidate += 'e';
after = after.replace(starts_with_e, '');
}
all.push(candidate);
return all.concat(doWord(after))
}
//unblended vowels ('noisy' vowel combinations)
if (candidate.match(ends_with_noisy_vowel_combos)) {
//'io' is noisy, not in 'ion'
all.push(before);
all.push(current);
return all.concat(doWord(after)) //recursion
}
// if candidate is followed by a CV, assume consecutive open syllables
if (candidate.match(ends_with_vowel) && after.match(starts_with_consonant_vowel)) {
all.push(candidate);
return all.concat(doWord(after))
}
}
//if still running, end last syllable
if (w.match(aiouy) || w.match(ends_with_ee)) {
//allow silent trailing e
all.push(w);
} else if (w) {
let last = all.length - 1;
if (last < 0) {
last = 0;
}
all[last] = (all[last] || '') + w; //append it to the last one
}
return all
};
let syllables$2 = function (str) {
let all = [];
if (!str) {
return all
}
str = str.replace(/[.,?]/g, '');
str.split(all_spaces).map(s => {
all = all.concat(doWord(s));
});
// str.split(whitespace_dash).forEach(doWord)
all = postProcess(all);
//for words like 'tree' and 'free'
if (all.length === 0) {
all = [str];
}
//filter blanks
all = all.filter(s => s);
return all
};
// console.log(syllables('civilised'))
var getSyllables = syllables$2;
// const defaultObj = { normal: true, text: true, terms: false }
const syllables = function (view) {
view.docs.forEach(terms => {
terms.forEach(term => {
term.syllables = getSyllables(term.normal || term.text);
});
});
};
var syllables$1 = syllables;
var compute = {
soundsLike: soundsLike$1,
syllables: syllables$1
};
const api = function (View) {
/** */
View.prototype.syllables = function () {
this.compute('syllables');
let all = [];
this.docs.forEach(terms => {
let some = [];
terms.forEach(term => {
some = some.concat(term.syllables);
});
if (some.length > 0) {
all.push(some);
}
});
return all
};
/** */
View.prototype.soundsLike = function () {
this.compute('soundsLike');
let all = [];
this.docs.forEach(terms => {
let some = [];
terms.forEach(term => {
some = some.concat(term.soundsLike);
});
if (some.length > 0) {
all.push(some);
}
});
return all
};
};
var api$1 = api;
var plugin = {
api: api$1,
compute
};
return plugin;
}));
+1
View File
@@ -0,0 +1 @@
var e,t;e=this,t=function(){var e={dedup:e=>e.replace(/([^c])\1/g,"$1"),dropInitialLetters:e=>e.match(/^(kn|gn|pn|ae|wr)/)?e.substring(1,e.length-1):e,dropBafterMAtEnd:e=>e.replace(/mb$/,"m"),cchange:e=>(e=(e=(e=e.replace(/([^s]|^)(c)(h)/g,"$1x$3").trim()).replace(/cia/g,"xia")).replace(/c([iey])/g,"s$1")).replace(/c/g,"k"),dchange:e=>(e=e.replace(/d(ge|gy|gi)/g,"j$1")).replace(/d/g,"t"),dropG:e=>(e=e.replace(/gh(^$|[^aeiou])/g,"h$1")).replace(/g(n|ned)$/g,"$1"),changeG:e=>(e=(e=(e=e.replace(/gh/g,"f")).replace(/([^g]|^)(g)([iey])/g,"$1j$3")).replace(/gg/g,"g")).replace(/g/g,"k"),dropH:e=>e.replace(/([aeiou])h([^aeiou]|$)/g,"$1$2"),changeCK:e=>e.replace(/ck/g,"k"),changePH:e=>e.replace(/ph/g,"f"),changeQ:e=>e.replace(/q/g,"k"),changeS:e=>e.replace(/s(h|io|ia)/g,"x$1"),changeT:e=>e=e.replace(/t(ia[^n]|io)/g,"x$1"),dropT:e=>e.replace(/tch/g,"ch"),changeV:e=>e.replace(/v/g,"f"),changeWH:e=>e.replace(/^wh/,"w"),dropW:e=>e.replace(/w([^aeiou]|$)/g,"$1"),changeX:e=>(e=e.replace(/^x/,"s")).replace(/x/g,"ks"),dropY:e=>e.replace(/y([^aeiou]|$)/g,"$1"),changeZ:e=>e.replace(/z/,"s"),dropVowels:e=>e},t=function(t){return t=e.dedup(t),t=e.dropInitialLetters(t),t=e.dropBafterMAtEnd(t),t=e.changeCK(t),t=e.cchange(t),t=e.dchange(t),t=e.dropG(t),t=e.changeG(t),t=e.dropH(t),t=e.changePH(t),t=e.changeQ(t),t=e.changeS(t),t=e.changeX(t),t=e.changeT(t),t=e.dropT(t),t=e.changeV(t),t=e.changeWH(t),t=e.dropW(t),t=e.dropY(t),t=e.changeZ(t),(t=e.dropVowels(t)).trim()};const a=/^(eu)/i,c=/^[^aeiou]e([^d]|$)/,n=/^([^aeiouy])[aeiouy]\1[aeiouy]/,l=/^[^aeiouy]([aeiouy])[^aeiouy]\1/,h=/^([tg][aeiouy]){2}/,o=/^[^aeiouy]+$/,i=/[aeiouy]$/,r=/^[^aeiouy]h?[aeiouy]/,g=[/^[^aeiou]?ion/,/^[^aeiou]?ised/,/^[^aeiou]?iled/,/[aeiou]n[gt]$/,/\wa[gt]e$/];var p=function(e){let t=(e=(e=e.map((function(e){return e.trim()}))).filter((function(e){return""!==e}))).length;if(t>1){let a=e[t-2]+e[t-1];for(let c=0;c<g.length;c++)a.match(g[c])&&(e[t-2]=e[t-2]+e[t-1],e.pop())}if(e.length>1){let t=(1===e[0].length||e[0].match(r))&&e[0].match(i),a=e[1].match(c);if(t&&a){let t=e[0]+e[1];t.match(n)||t.match(l)||t.match(h)||(e[0]=e[0]+e[1],e.splice(1,1))}}if(e.length>1){let t=e[e.length-2].match(r)&&e[e.length-2].match(i),a=e[e.length-1].match(c)&&g.every(t=>!e[e.length-1].match(t));if(t&&a){let t=e[e.length-2]+e[e.length-1];t.match(n)||t.match(l)||t.match(h)||(e[e.length-2]=e[e.length-2]+e[e.length-1],e.splice(e.length-1,1))}}if(e.length>1){let t=e[0]+e[1];t.match(a)&&(e[0]=t,e.splice(1,1))}return e.length>1&&e[e.length-1].match(o)&&(e[e.length-2]=e[e.length-2]+e[e.length-1],e.splice(e.length-1,1)),e};const u=/ +/g,s=/[aeiouy]$/,f=/^[^aeiouy]h?[aeiouy]/,d=/^e[sm]/,m=/^e/,y=/(eo|eu|ia|oa|ua|ui)$/i,$=/[aiouy]/,k=/ee$/,b=function(e){let t=[],a=e.split(""),c="",n="",l="";for(let e=0;e<a.length;e++){c=a.slice(0,e).join(""),l=a[e],n=a.slice(e+1,a.length).join("");let h=c+a[e];if(c.match(s)&&!l.match(s))return n.match(d)&&(h+="e",n=n.replace(m,"")),t.push(h),t.concat(b(n));if(h.match(y))return t.push(c),t.push(l),t.concat(b(n));if(h.match(s)&&n.match(f))return t.push(h),t.concat(b(n))}if(e.match($)||e.match(k))t.push(e);else if(e){let a=t.length-1;a<0&&(a=0),t[a]=(t[a]||"")+e}return t};var x=function(e){let t=[];return e?((e=e.replace(/[.,?]/g,"")).split(u).map(e=>{t=t.concat(b(e))}),t=p(t),0===t.length&&(t=[e]),t=t.filter(e=>e),t):t};return{api:function(e){e.prototype.syllables=function(){this.compute("syllables");let e=[];return this.docs.forEach(t=>{let a=[];t.forEach(e=>{a=a.concat(e.syllables)}),a.length>0&&e.push(a)}),e},e.prototype.soundsLike=function(){this.compute("soundsLike");let e=[];return this.docs.forEach(t=>{let a=[];t.forEach(e=>{a=a.concat(e.soundsLike)}),a.length>0&&e.push(a)}),e}},compute:{soundsLike:function(e){e.docs.forEach(e=>{e.forEach(e=>{e.soundsLike=t(e.normal||e.text)})})},syllables:function(e){e.docs.forEach(e=>{e.forEach(e=>{e.syllables=x(e.normal||e.text)})})}}}},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).compromiseSpeech=t();
@@ -0,0 +1 @@
var e={dedup:e=>e.replace(/([^c])\1/g,"$1"),dropInitialLetters:e=>e.match(/^(kn|gn|pn|ae|wr)/)?e.substring(1,e.length-1):e,dropBafterMAtEnd:e=>e.replace(/mb$/,"m"),cchange:e=>(e=(e=(e=e.replace(/([^s]|^)(c)(h)/g,"$1x$3").trim()).replace(/cia/g,"xia")).replace(/c([iey])/g,"s$1")).replace(/c/g,"k"),dchange:e=>(e=e.replace(/d(ge|gy|gi)/g,"j$1")).replace(/d/g,"t"),dropG:e=>(e=e.replace(/gh(^$|[^aeiou])/g,"h$1")).replace(/g(n|ned)$/g,"$1"),changeG:e=>(e=(e=(e=e.replace(/gh/g,"f")).replace(/([^g]|^)(g)([iey])/g,"$1j$3")).replace(/gg/g,"g")).replace(/g/g,"k"),dropH:e=>e.replace(/([aeiou])h([^aeiou]|$)/g,"$1$2"),changeCK:e=>e.replace(/ck/g,"k"),changePH:e=>e.replace(/ph/g,"f"),changeQ:e=>e.replace(/q/g,"k"),changeS:e=>e.replace(/s(h|io|ia)/g,"x$1"),changeT:e=>e=e.replace(/t(ia[^n]|io)/g,"x$1"),dropT:e=>e.replace(/tch/g,"ch"),changeV:e=>e.replace(/v/g,"f"),changeWH:e=>e.replace(/^wh/,"w"),dropW:e=>e.replace(/w([^aeiou]|$)/g,"$1"),changeX:e=>(e=e.replace(/^x/,"s")).replace(/x/g,"ks"),dropY:e=>e.replace(/y([^aeiou]|$)/g,"$1"),changeZ:e=>e.replace(/z/,"s"),dropVowels:e=>e};var a=function(a){return a=e.dedup(a),a=e.dropInitialLetters(a),a=e.dropBafterMAtEnd(a),a=e.changeCK(a),a=e.cchange(a),a=e.dchange(a),a=e.dropG(a),a=e.changeG(a),a=e.dropH(a),a=e.changePH(a),a=e.changeQ(a),a=e.changeS(a),a=e.changeX(a),a=e.changeT(a),a=e.dropT(a),a=e.changeV(a),a=e.changeWH(a),a=e.dropW(a),a=e.dropY(a),a=e.changeZ(a),(a=e.dropVowels(a)).trim()};const t=/^(eu)/i,c=/^[^aeiou]e([^d]|$)/,n=/^([^aeiouy])[aeiouy]\1[aeiouy]/,l=/^[^aeiouy]([aeiouy])[^aeiouy]\1/,h=/^([tg][aeiouy]){2}/,r=/^[^aeiouy]+$/,o=/[aeiouy]$/,g=/^[^aeiouy]h?[aeiouy]/,i=[/^[^aeiou]?ion/,/^[^aeiou]?ised/,/^[^aeiou]?iled/,/[aeiou]n[gt]$/,/\wa[gt]e$/];var p=function(e){let a=(e=(e=e.map((function(e){return e.trim()}))).filter((function(e){return""!==e}))).length;if(a>1){let t=e[a-2]+e[a-1];for(let c=0;c<i.length;c++)t.match(i[c])&&(e[a-2]=e[a-2]+e[a-1],e.pop())}if(e.length>1){let a=(1===e[0].length||e[0].match(g))&&e[0].match(o),t=e[1].match(c);if(a&&t){let a=e[0]+e[1];a.match(n)||a.match(l)||a.match(h)||(e[0]=e[0]+e[1],e.splice(1,1))}}if(e.length>1){let a=e[e.length-2].match(g)&&e[e.length-2].match(o),t=e[e.length-1].match(c)&&i.every(a=>!e[e.length-1].match(a));if(a&&t){let a=e[e.length-2]+e[e.length-1];a.match(n)||a.match(l)||a.match(h)||(e[e.length-2]=e[e.length-2]+e[e.length-1],e.splice(e.length-1,1))}}if(e.length>1){let a=e[0]+e[1];a.match(t)&&(e[0]=a,e.splice(1,1))}return e.length>1&&e[e.length-1].match(r)&&(e[e.length-2]=e[e.length-2]+e[e.length-1],e.splice(e.length-1,1)),e};const u=/ +/g,s=/[aeiouy]$/,d=/^[^aeiouy]h?[aeiouy]/,f=/^e[sm]/,m=/^e/,$=/(eo|eu|ia|oa|ua|ui)$/i,y=/[aiouy]/,k=/ee$/,E=function(e){let a=[],t=e.split(""),c="",n="",l="";for(let e=0;e<t.length;e++){c=t.slice(0,e).join(""),l=t[e],n=t.slice(e+1,t.length).join("");let h=c+t[e];if(c.match(s)&&!l.match(s))return n.match(f)&&(h+="e",n=n.replace(m,"")),a.push(h),a.concat(E(n));if(h.match($))return a.push(c),a.push(l),a.concat(E(n));if(h.match(s)&&n.match(d))return a.push(h),a.concat(E(n))}if(e.match(y)||e.match(k))a.push(e);else if(e){let t=a.length-1;t<0&&(t=0),a[t]=(a[t]||"")+e}return a};var x=function(e){let a=[];return e?((e=e.replace(/[.,?]/g,"")).split(u).map(e=>{a=a.concat(E(e))}),a=p(a),0===a.length&&(a=[e]),a=a.filter(e=>e),a):a};var b={api:function(e){e.prototype.syllables=function(){this.compute("syllables");let e=[];return this.docs.forEach(a=>{let t=[];a.forEach(e=>{t=t.concat(e.syllables)}),t.length>0&&e.push(t)}),e},e.prototype.soundsLike=function(){this.compute("soundsLike");let e=[];return this.docs.forEach(a=>{let t=[];a.forEach(e=>{t=t.concat(e.soundsLike)}),t.length>0&&e.push(t)}),e}},compute:{soundsLike:function(e){e.docs.forEach(e=>{e.forEach(e=>{e.soundsLike=a(e.normal||e.text)})})},syllables:function(e){e.docs.forEach(e=>{e.forEach(e=>{e.syllables=x(e.normal||e.text)})})}}};export{b as default};
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/compromise"></script>
<!-- <script src="https://unpkg.com/compromise-speech"></script> -->
<script src="../builds/compromise-speech.min.js"></script>
</head>
<body>
<p>compromise-speech demo:</p>
<ul>
<kbd>chocolate microscopes </kbd>
<pre id="res">loading</pre>
</ul>
<script defer>
nlp.plugin(compromiseSpeech) // window.compromiseSpeech
let txt = document.querySelector('kbd').innerText
let doc = nlp(txt)
let json = doc.syllables()
document.querySelector('#res').innerHTML = JSON.stringify(json, null, 2)
</script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
import nlp from 'compromise'
export interface SpeechMethods {
/** estimate spoken phenomes */
syllables(): String[][]
/** estimate pronounciation information */
soundsLike(): String[][]
}
/** extended compromise lib **/
declare const nlpSpeech: nlp.TypedPlugin<SpeechMethods>
export default nlpSpeech
+44
View File
@@ -0,0 +1,44 @@
{
"name": "compromise-speech",
"description": "plugin for nlp-compromise",
"version": "0.1.1",
"author": "Spencer Kelly <spencermountain@gmail.com> (http://spencermounta.in)",
"main": "./src/plugin.js",
"unpkg": "./builds/compromise-speech.min.js",
"module": "./builds/compromise-speech.mjs",
"type": "module",
"sideEffects": false,
"types": "./index.d.ts",
"exports": {
".": {
"import": "./src/plugin.js",
"require": "./builds/compromise-speech.cjs",
"types": "./index.d.ts"
}
},
"repository": {
"type": "git",
"url": "git://github.com/spencermountain/compromise.git"
},
"homepage": "https://github.com/spencermountain/compromise/tree/master/plugins/speech",
"scripts": {
"test": "tape \"./tests/**/*.test.js\" | tap-dancer --color always",
"testb": "TESTENV=prod tape \"./tests/**/*.test.js\" | tap-dancer --color always",
"watch": "node --watch ./scratch.js",
"build": "rollup -c --silent"
},
"eslintIgnore": [
"builds/*.js"
],
"files": [
"builds/",
"src/",
"index.d.ts"
],
"peerDependencies": {
"compromise": ">=14.0.0"
},
"dependencies": {},
"devDependencies": {},
"license": "MIT"
}
+17
View File
@@ -0,0 +1,17 @@
/* eslint-disable no-console, no-unused-vars */
import nlp from '../../src/three.js'
import speechPlugin from './src/plugin.js'
nlp.plugin(speechPlugin)
// nlp.verbose(true)
// nlp.verbose('date')
let txt = ''
txt = 'seventh millenium. white collar'
// let doc = nlp(txt).compute(['soundsLike', 'syllables'])
// console.dir(doc.json()[0], { depth: 5 })
const doc = nlp('calgary')
doc.compute('soundsLike')
console.log(JSON.stringify(doc.json()[0], null, 2))
+33
View File
@@ -0,0 +1,33 @@
const api = function (View) {
/** */
View.prototype.syllables = function () {
this.compute('syllables')
const all = []
this.docs.forEach(terms => {
let some = []
terms.forEach(term => {
some = some.concat(term.syllables)
})
if (some.length > 0) {
all.push(some)
}
})
return all
}
/** */
View.prototype.soundsLike = function () {
this.compute('soundsLike')
const all = []
this.docs.forEach(terms => {
let some = []
terms.forEach(term => {
some = some.concat(term.soundsLike)
})
if (some.length > 0) {
all.push(some)
}
})
return all
}
}
export default api
+7
View File
@@ -0,0 +1,7 @@
import soundsLike from './soundsLike/index.js'
import syllables from './syllables/index.js'
export default {
soundsLike,
syllables
}
@@ -0,0 +1,11 @@
import metaphone from './metaphone.js'
const soundsLike = function (view) {
view.docs.forEach(terms => {
terms.forEach(term => {
term.soundsLike = metaphone(term.normal || term.text)
})
})
}
export default soundsLike
@@ -0,0 +1,32 @@
//a js version of the metaphone (#1) algorithm
//adapted from the work of Chris Umbel
// https://github.com/NaturalNode/natural/blob/master/lib/natural/phonetics/metaphone.js
import m from './transformations.js'
const metaphone = function (s) {
s = m.dedup(s)
s = m.dropInitialLetters(s)
s = m.dropBafterMAtEnd(s)
s = m.changeCK(s)
s = m.cchange(s)
s = m.dchange(s)
s = m.dropG(s)
s = m.changeG(s)
s = m.dropH(s)
s = m.changePH(s)
s = m.changeQ(s)
s = m.changeS(s)
s = m.changeX(s)
s = m.changeT(s)
s = m.dropT(s)
s = m.changeV(s)
s = m.changeWH(s)
s = m.dropW(s)
s = m.dropY(s)
s = m.changeZ(s)
s = m.dropVowels(s)
return s.trim()
}
export default metaphone
@@ -0,0 +1,83 @@
//individual manipulations of the text
const transformations = {
dedup: (s) => {
return s.replace(/([^c])\1/g, '$1')
},
dropInitialLetters: (s) => {
if (s.match(/^(kn|gn|pn|ae|wr)/)) {
return s.substring(1, s.length - 1)
}
return s
},
dropBafterMAtEnd: (s) => {
return s.replace(/mb$/, 'm')
},
cchange: (s) => {
s = s.replace(/([^s]|^)(c)(h)/g, '$1x$3').trim()
s = s.replace(/cia/g, 'xia')
s = s.replace(/c([iey])/g, 's$1')
return s.replace(/c/g, 'k')
},
dchange: (s) => {
s = s.replace(/d(ge|gy|gi)/g, 'j$1')
return s.replace(/d/g, 't')
},
dropG: (s) => {
// Drop 'G' if followed by 'H' and 'H' is not at the end or before a vowel.
s = s.replace(/gh(^$|[^aeiou])/g, 'h$1')//eslint-disable-line
// Drop 'G' if followed by 'N' or 'NED' and is at the end.
return s.replace(/g(n|ned)$/g, '$1')
},
changeG: (s) => {
s = s.replace(/gh/g, 'f')
s = s.replace(/([^g]|^)(g)([iey])/g, '$1j$3')
s = s.replace(/gg/g, 'g')
return s.replace(/g/g, 'k')
},
dropH: (s) => {
return s.replace(/([aeiou])h([^aeiou]|$)/g, '$1$2')
},
changeCK: (s) => {
return s.replace(/ck/g, 'k')
},
changePH: (s) => {
return s.replace(/ph/g, 'f')
},
changeQ: (s) => {
return s.replace(/q/g, 'k')
},
changeS: (s) => {
return s.replace(/s(h|io|ia)/g, 'x$1')
},
changeT: (s) => {
s = s.replace(/t(ia[^n]|io)/g, 'x$1')
// return s.replace(/th/, '0')
return s
},
dropT: (s) => {
return s.replace(/tch/g, 'ch')
},
changeV: (s) => {
return s.replace(/v/g, 'f')
},
changeWH: (s) => {
return s.replace(/^wh/, 'w')
},
dropW: (s) => {
return s.replace(/w([^aeiou]|$)/g, '$1')
},
changeX: (s) => {
s = s.replace(/^x/, 's')
return s.replace(/x/g, 'ks')
},
dropY: (s) => {
return s.replace(/y([^aeiou]|$)/g, '$1')
},
changeZ: (s) => {
return s.replace(/z/, 's')
},
dropVowels: (s) => {
return s //.charAt(0) + s.substr(1, s.length).replace(/[aeiou]/g, '');
},
}
export default transformations
@@ -0,0 +1,13 @@
import getSyllables from './syllables.js'
// const defaultObj = { normal: true, text: true, terms: false }
const syllables = function (view) {
view.docs.forEach(terms => {
terms.forEach(term => {
term.syllables = getSyllables(term.normal || term.text)
})
})
}
export default syllables
@@ -0,0 +1,107 @@
const starts_with_single_vowel_combos = /^(eu)/i
const joining_consonant_vowel = /^[^aeiou]e([^d]|$)/
const cvcv_same_consonant = /^([^aeiouy])[aeiouy]\1[aeiouy]/
const cvcv_same_vowel = /^[^aeiouy]([aeiouy])[^aeiouy]\1/
const cvcv_known_consonants = /^([tg][aeiouy]){2}/
const only_one_or_more_c = /^[^aeiouy]+$/
const ends_with_vowel = /[aeiouy]$/
const starts_with_consonant_vowel = /^[^aeiouy]h?[aeiouy]/
const ones = [
/^[^aeiou]?ion/,
/^[^aeiou]?ised/,
/^[^aeiou]?iled/,
// -ing, -ent
/[aeiou]n[gt]$/,
// -ate, -age
/\wa[gt]e$/,
]
//suffix fixes
const postprocess = function (arr) {
//trim whitespace
arr = arr.map(function (w) {
return w.trim()
})
arr = arr.filter(function (w) {
return w !== ''
})
// if (arr.length > 2) {
// return arr;
// }
const l = arr.length
if (l > 1) {
const suffix = arr[l - 2] + arr[l - 1]
for (let i = 0; i < ones.length; i++) {
if (suffix.match(ones[i])) {
arr[l - 2] = arr[l - 2] + arr[l - 1]
arr.pop()
}
}
}
// since the open syllable detection is overzealous,
// sometimes need to rejoin incorrect splits
if (arr.length > 1) {
const first_is_open =
(arr[0].length === 1 || arr[0].match(starts_with_consonant_vowel)) &&
arr[0].match(ends_with_vowel)
const second_is_joining = arr[1].match(joining_consonant_vowel)
if (first_is_open && second_is_joining) {
const possible_combination = arr[0] + arr[1]
const probably_separate_syllables =
possible_combination.match(cvcv_same_consonant) ||
possible_combination.match(cvcv_same_vowel) ||
possible_combination.match(cvcv_known_consonants)
if (!probably_separate_syllables) {
arr[0] = arr[0] + arr[1]
arr.splice(1, 1)
}
}
}
if (arr.length > 1) {
const second_to_last_is_open =
arr[arr.length - 2].match(starts_with_consonant_vowel) &&
arr[arr.length - 2].match(ends_with_vowel)
const last_is_joining =
arr[arr.length - 1].match(joining_consonant_vowel) &&
ones.every(re => !arr[arr.length - 1].match(re))
if (second_to_last_is_open && last_is_joining) {
const possible_combination = arr[arr.length - 2] + arr[arr.length - 1]
const probably_separate_syllables =
possible_combination.match(cvcv_same_consonant) ||
possible_combination.match(cvcv_same_vowel) ||
possible_combination.match(cvcv_known_consonants)
if (!probably_separate_syllables) {
arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1]
arr.splice(arr.length - 1, 1)
}
}
}
if (arr.length > 1) {
const single = arr[0] + arr[1]
if (single.match(starts_with_single_vowel_combos)) {
arr[0] = single
arr.splice(1, 1)
}
}
if (arr.length > 1) {
if (arr[arr.length - 1].match(only_one_or_more_c)) {
arr[arr.length - 2] = arr[arr.length - 2] + arr[arr.length - 1]
arr.splice(arr.length - 1, 1)
}
}
return arr
}
export default postprocess
@@ -0,0 +1,90 @@
//chop a string into pronounced syllables
import postProcess from './postProcess.js'
const all_spaces = / +/g
const ends_with_vowel = /[aeiouy]$/
const starts_with_consonant_vowel = /^[^aeiouy]h?[aeiouy]/
const starts_with_e_then_specials = /^e[sm]/
const starts_with_e = /^e/
const ends_with_noisy_vowel_combos = /(eo|eu|ia|oa|ua|ui)$/i
const aiouy = /[aiouy]/
const ends_with_ee = /ee$/
// const whitespace_dash = /\s\-/
//method is nested because it's called recursively
const doWord = function (w) {
const all = []
const chars = w.split('')
let before = ''
let after = ''
let current = ''
for (let i = 0; i < chars.length; i++) {
before = chars.slice(0, i).join('')
current = chars[i]
after = chars.slice(i + 1, chars.length).join('')
let candidate = before + chars[i]
//it's a consonant that comes after a vowel
if (before.match(ends_with_vowel) && !current.match(ends_with_vowel)) {
if (after.match(starts_with_e_then_specials)) {
candidate += 'e'
after = after.replace(starts_with_e, '')
}
all.push(candidate)
return all.concat(doWord(after))
}
//unblended vowels ('noisy' vowel combinations)
if (candidate.match(ends_with_noisy_vowel_combos)) {
//'io' is noisy, not in 'ion'
all.push(before)
all.push(current)
return all.concat(doWord(after)) //recursion
}
// if candidate is followed by a CV, assume consecutive open syllables
if (candidate.match(ends_with_vowel) && after.match(starts_with_consonant_vowel)) {
all.push(candidate)
return all.concat(doWord(after))
}
}
//if still running, end last syllable
if (w.match(aiouy) || w.match(ends_with_ee)) {
//allow silent trailing e
all.push(w)
} else if (w) {
let last = all.length - 1
if (last < 0) {
last = 0
}
all[last] = (all[last] || '') + w //append it to the last one
}
return all
}
const syllables = function (str) {
let all = []
if (!str) {
return all
}
str = str.replace(/[.,?]/g, '')
str.split(all_spaces).map(s => {
all = all.concat(doWord(s))
})
// str.split(whitespace_dash).forEach(doWord)
all = postProcess(all)
//for words like 'tree' and 'free'
if (all.length === 0) {
all = [str]
}
//filter blanks
all = all.filter(s => s)
return all
}
// console.log(syllables('civilised'))
export default syllables
+7
View File
@@ -0,0 +1,7 @@
import compute from './compute/index.js'
import api from './api.js'
export default {
api,
compute
}
+15
View File
@@ -0,0 +1,15 @@
import build from '../../../builds/one/compromise-one.mjs'
import src from '../../../src/one.js'
import plgBuild from '../builds/compromise-speech.mjs'
import plg from '../src/plugin.js'
let nlp;
if (process.env.TESTENV === 'prod') {
console.warn('== production build test 🚀 ==') // eslint-disable-line
nlp = build
nlp.plugin(plgBuild)
} else {
nlp = src
nlp.plugin(plg)
}
export default nlp
+21
View File
@@ -0,0 +1,21 @@
import test from 'tape'
import nlp from './_lib.js'
test('soundsLike-tests', function (t) {
const arr = [
['phil collins', 'fil kolins'],
['Philadelphia freedom', 'filatelfia fretom'],
['Shine on me', 'shine on me'],
['peace of mind', 'pease of mint'],
['Yes I do', 'yes i to'],
['Zapped me', 'sapet me'],
['Right between the eyes', 'rit betwen the eyes'],
// ['But the times have changed', 'but the times hafe kshanjet'],
]
arr.forEach((a) => {
const doc = nlp(a[0])
const str = doc.soundsLike({})[0].join(' ')
t.equal(str, a[1], a[0])
})
t.end()
})
+73
View File
@@ -0,0 +1,73 @@
import test from 'tape'
import nlp from './_lib.js'
const words = [
'sud den ly',
'con sti pa tion',
'di a bo lic',
'fa ted',
'ge ne tic',
'hy giene',
'o ma ha',
'i mi ta ted',
'tree',
'ci vi lised',
'went',
'to ge ther',
'tog gle',
'move ment',
'mo ment',
'do nate',
'vi king',
'wat tage',
'con gre gate',
'some thing',
'sales man',
're sour ces',
'eve ry thing',
'eve ry bo dy',
'hole',
'ho ly',
'sec ret',
'cause',
'fate',
'fates',
'eu lo gy',
'e on',
'rea sons',
'and',
'hoc key',
'wild',
'fun',
'fun ny',
'hor ses',
'cal ga ry',
'tor na do',
'pen',
'stu dy',
'ja mai ca',
'tri ni dad',
'ca na da',
'i ran',
'i raq',
'sau di a ra bi a',
'clin ton',
'hub ris',
'tur key',
'cor si ca',
'dev iled',
// 'horse',
// 'chance',
// 'peo ple'
]
test('test length', function (t) {
words.forEach(sep => {
const str = sep.replace(/ /g, '')
const doc = nlp(str)
const have = doc.syllables()[0]
const want = sep.split(/ /g)
t.equal(have.length, want.length, str)
})
t.end()
})