speechready 0.0.1 → 0.1.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,83 @@
1
+ /** English number words: cardinals, ordinals, years, digit strings. */
2
+ import { divmod, rpartition } from "../util.js";
3
+ const ONES = [
4
+ "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
5
+ "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen",
6
+ ];
7
+ const TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
8
+ const SCALES = [[1e9, "billion"], [1e6, "million"], [1e3, "thousand"], [100, "hundred"]];
9
+ const ORDINAL_IRREGULAR = {
10
+ one: "first", two: "second", three: "third", five: "fifth", eight: "eighth", nine: "ninth", twelve: "twelfth",
11
+ };
12
+ export function cardinal(n) {
13
+ if (n < 0)
14
+ return "minus " + cardinal(-n);
15
+ if (n < 20)
16
+ return ONES[n];
17
+ if (n < 100) {
18
+ const [tens, ones] = divmod(n, 10);
19
+ return TENS[tens] + (ones ? "-" + ONES[ones] : "");
20
+ }
21
+ for (const [value, name] of SCALES) {
22
+ if (n >= value) {
23
+ const [head, rest] = divmod(n, value);
24
+ let words = cardinal(head) + " " + name;
25
+ if (rest)
26
+ words += " " + cardinal(rest);
27
+ return words;
28
+ }
29
+ }
30
+ return String(n);
31
+ }
32
+ export function ordinal(n) {
33
+ const words = cardinal(n);
34
+ let [head, sep, last] = words.includes(" ") ? rpartition(words, " ") : ["", "", words];
35
+ if (last.includes("-")) {
36
+ const [tens, , ones] = rpartition(last, "-");
37
+ last = tens + "-" + ordinalWord(ones);
38
+ }
39
+ else {
40
+ last = ordinalWord(last);
41
+ }
42
+ return head + sep + last;
43
+ }
44
+ function ordinalWord(word) {
45
+ if (word in ORDINAL_IRREGULAR)
46
+ return ORDINAL_IRREGULAR[word];
47
+ if (word.endsWith("y"))
48
+ return word.slice(0, -1) + "ieth";
49
+ return word + "th";
50
+ }
51
+ /** Spoken year: 1999 -> nineteen ninety-nine, 2005 -> two thousand five, 2026 -> twenty twenty-six. */
52
+ export function year(n) {
53
+ if (n < 1000 || n > 9999)
54
+ return cardinal(n);
55
+ const [high, low] = divmod(n, 100);
56
+ if (low === 0) {
57
+ if (high % 10 === 0)
58
+ return cardinal(n); // 2000, 3000
59
+ return cardinal(high) + " hundred"; // 1900, 2100
60
+ }
61
+ if (n >= 2000 && n <= 2009)
62
+ return "two thousand " + cardinal(low);
63
+ if (low < 10)
64
+ return cardinal(high) + " oh " + cardinal(low); // 1905 -> nineteen oh five
65
+ return cardinal(high) + " " + cardinal(low);
66
+ }
67
+ /** Each digit as a word; non-digits pass through as their spoken form where known. */
68
+ export function digits(s) {
69
+ const out = [];
70
+ for (const ch of s) {
71
+ if (/\d/.test(ch))
72
+ out.push(ONES[parseInt(ch, 10)]);
73
+ else if (ch === "+")
74
+ out.push("plus");
75
+ }
76
+ return out.join(" ");
77
+ }
78
+ export function decimal(whole, frac) {
79
+ let text = cardinal(parseInt(whole.replace(/,/g, ""), 10));
80
+ if (frac)
81
+ text += " point " + Array.from(frac).map((c) => ONES[parseInt(c, 10)]).join(" ");
82
+ return text;
83
+ }
@@ -0,0 +1,24 @@
1
+ /** Turkish number words and suffix helpers (vowel harmony, consonant softening). */
2
+ export declare function cardinal(n: number): string;
3
+ export declare function lastVowel(word: string): string;
4
+ /** Four-way harmony vowel (ı i u ü) for the last vowel of `word`. */
5
+ export declare function harmonyI(word: string): string;
6
+ /** Two-way harmony vowel (a e). */
7
+ export declare function harmonyA(word: string): string;
8
+ /** Final consonant softening before a vowel-initial suffix: dört -> dörd, çeyrek -> çeyreğ. */
9
+ export declare function soften(word: string): string;
10
+ /** Join a spoken word and the suffix the writer attached to the numeral or abbreviation.
11
+ *
12
+ * The writer harmonised the suffix against what they wrote (3'te -> üçte works
13
+ * as is), but a buffer consonant may be wrong for the spoken word: yarım + sı
14
+ * -> yarımı, kilometre + i -> kilometreyi.
15
+ */
16
+ export declare function attach(word: string, suffix: string): string;
17
+ /** Re-harmonise a suffix written against an abbreviation (TL'ye, 1/4'ü) for the spoken word (liraya, çeyreği). */
18
+ export declare function reharmonize(word: string, suffix: string): string;
19
+ /** üçüncü, dördüncü, yirmi birinci. */
20
+ export declare function ordinal(n: number): string;
21
+ /** üç -> üçte, dört -> dörtte, on -> onda, beş -> beşte. */
22
+ export declare function locative(word: string): string;
23
+ export declare function digits(s: string): string;
24
+ export declare function decimal(whole: string, frac: string | null | undefined): string;
@@ -0,0 +1,134 @@
1
+ /** Turkish number words and suffix helpers (vowel harmony, consonant softening). */
2
+ import { divmod, rpartition } from "../util.js";
3
+ const ONES = ["sıfır", "bir", "iki", "üç", "dört", "beş", "altı", "yedi", "sekiz", "dokuz"];
4
+ const TENS = ["", "on", "yirmi", "otuz", "kırk", "elli", "altmış", "yetmiş", "seksen", "doksan"];
5
+ const SCALES = [[1e9, "milyar"], [1e6, "milyon"], [1e3, "bin"]];
6
+ const VOWELS = "aeıioöuü";
7
+ const BACK = new Set("aıou");
8
+ const ROUND = new Set("oöuü");
9
+ const VOICELESS = new Set("çfhkpsşt");
10
+ export function cardinal(n) {
11
+ if (n < 0)
12
+ return "eksi " + cardinal(-n);
13
+ if (n === 0)
14
+ return "sıfır";
15
+ const parts = [];
16
+ let rest = n;
17
+ for (const [value, name] of SCALES) {
18
+ if (rest >= value) {
19
+ const [head, r] = divmod(rest, value);
20
+ rest = r;
21
+ parts.push(head === 1 && value === 1000 ? name : `${cardinal(head)} ${name}`);
22
+ }
23
+ }
24
+ if (rest >= 100) {
25
+ const [head, r] = divmod(rest, 100);
26
+ rest = r;
27
+ parts.push(head === 1 ? "yüz" : `${ONES[head]} yüz`);
28
+ }
29
+ if (rest >= 10) {
30
+ const [head, r] = divmod(rest, 10);
31
+ rest = r;
32
+ parts.push(TENS[head]);
33
+ }
34
+ if (rest > 0)
35
+ parts.push(ONES[rest]);
36
+ return parts.join(" ");
37
+ }
38
+ export function lastVowel(word) {
39
+ const lower = Array.from(word.toLowerCase());
40
+ for (let i = lower.length - 1; i >= 0; i--) {
41
+ if (VOWELS.includes(lower[i]))
42
+ return lower[i];
43
+ }
44
+ return "e";
45
+ }
46
+ /** Four-way harmony vowel (ı i u ü) for the last vowel of `word`. */
47
+ export function harmonyI(word) {
48
+ const v = lastVowel(word);
49
+ if (BACK.has(v))
50
+ return ROUND.has(v) ? "u" : "ı";
51
+ return ROUND.has(v) ? "ü" : "i";
52
+ }
53
+ /** Two-way harmony vowel (a e). */
54
+ export function harmonyA(word) {
55
+ return BACK.has(lastVowel(word)) ? "a" : "e";
56
+ }
57
+ function vowelCount(word) {
58
+ let n = 0;
59
+ for (const ch of word)
60
+ if (VOWELS.includes(ch))
61
+ n++;
62
+ return n;
63
+ }
64
+ /** Final consonant softening before a vowel-initial suffix: dört -> dörd, çeyrek -> çeyreğ. */
65
+ export function soften(word) {
66
+ if (word.endsWith("dört"))
67
+ return word.slice(0, -1) + "d";
68
+ if (word.endsWith("k") && vowelCount(word) >= 2)
69
+ return word.slice(0, -1) + "ğ";
70
+ return word;
71
+ }
72
+ /** Join a spoken word and the suffix the writer attached to the numeral or abbreviation.
73
+ *
74
+ * The writer harmonised the suffix against what they wrote (3'te -> üçte works
75
+ * as is), but a buffer consonant may be wrong for the spoken word: yarım + sı
76
+ * -> yarımı, kilometre + i -> kilometreyi.
77
+ */
78
+ export function attach(word, suffix) {
79
+ if (!suffix)
80
+ return word;
81
+ let w = word;
82
+ let s = suffix;
83
+ const endsInVowel = VOWELS.includes(w[w.length - 1]);
84
+ if ("sy".includes(s[0]) && s.length > 1 && VOWELS.includes(s[1]) && !endsInVowel) {
85
+ s = s.slice(1); // drop the buffer consonant after a consonant
86
+ }
87
+ else if (VOWELS.includes(s[0]) && endsInVowel) {
88
+ s = "y" + s; // insert a buffer consonant between two vowels
89
+ }
90
+ if (VOWELS.includes(s[0]))
91
+ w = soften(w);
92
+ return w + s;
93
+ }
94
+ /** Re-harmonise a suffix written against an abbreviation (TL'ye, 1/4'ü) for the spoken word (liraya, çeyreği). */
95
+ export function reharmonize(word, suffix) {
96
+ if (!suffix)
97
+ return suffix;
98
+ const two = harmonyA(word);
99
+ const four = harmonyI(word);
100
+ return Array.from(suffix).map((ch) => ("ae".includes(ch) ? two : "ıiuü".includes(ch) ? four : ch)).join("");
101
+ }
102
+ /** üçüncü, dördüncü, yirmi birinci. */
103
+ export function ordinal(n) {
104
+ const words = cardinal(n);
105
+ let [head, , last] = rpartition(words, " ");
106
+ const v = harmonyI(last);
107
+ if (VOWELS.includes(last[last.length - 1]))
108
+ last = last + "nc" + v;
109
+ else
110
+ last = soften(last) + v + "nc" + v;
111
+ return (head ? head + " " : "") + last;
112
+ }
113
+ /** üç -> üçte, dört -> dörtte, on -> onda, beş -> beşte. */
114
+ export function locative(word) {
115
+ const a = harmonyA(word);
116
+ const consonant = VOICELESS.has(word[word.length - 1]) ? "t" : "d";
117
+ return word + consonant + a;
118
+ }
119
+ export function digits(s) {
120
+ const out = [];
121
+ for (const ch of s) {
122
+ if (/\d/.test(ch))
123
+ out.push(ONES[parseInt(ch, 10)]);
124
+ else if (ch === "+")
125
+ out.push("artı");
126
+ }
127
+ return out.join(" ");
128
+ }
129
+ export function decimal(whole, frac) {
130
+ let text = cardinal(parseInt(whole.replace(/\./g, ""), 10));
131
+ if (frac)
132
+ text += " virgül " + (frac.startsWith("0") ? digits(frac) : cardinal(parseInt(frac, 10)));
133
+ return text;
134
+ }
@@ -0,0 +1,9 @@
1
+ /** Turkish verbalization rules, in the order they run.
2
+ *
3
+ * Turkish text carries case and possessive suffixes on numerals with an
4
+ * apostrophe (3'te, 1990'da, %50'si) and uses a dot for thousands and a comma
5
+ * for decimals. Foreign TTS models get all of these wrong, so they are the
6
+ * first things this module rewrites.
7
+ */
8
+ import { Rule } from "./index.js";
9
+ export declare const RULES: Rule[];
@@ -0,0 +1,204 @@
1
+ /** Turkish verbalization rules, in the order they run.
2
+ *
3
+ * Turkish text carries case and possessive suffixes on numerals with an
4
+ * apostrophe (3'te, 1990'da, %50'si) and uses a dot for thousands and a comma
5
+ * for decimals. Foreign TTS models get all of these wrong, so they are the
6
+ * first things this module rewrites.
7
+ */
8
+ import { B, W, escapeRe, findall, fullmatch, group, isDigit, partition, re } from "../util.js";
9
+ import { option, registerRules, rule } from "./index.js";
10
+ import { attach, cardinal, decimal, digits, locative, ordinal, reharmonize } from "./numbers_tr.js";
11
+ const MONTHS = ["Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"];
12
+ const APOS = "['’]";
13
+ const LOWER = "a-zçğıöşü";
14
+ const UPPER = "A-ZÇĞİÖŞÜ";
15
+ // English letter names as Turkish speakers say them, for foreign tech acronyms (npm, css, sql).
16
+ const EN_LETTERS = Object.fromEntries(Array.from("abcdefghijklmnopqrstuvwxyz").map((ch, i) => [ch, ["ey", "bi", "si", "di", "i", "ef", "ci", "eyç", "ay", "cey", "key", "el", "em", "en", "o", "pi", "kyu", "ar", "es", "ti", "yu", "vi", "dabılyu", "eks", "vay", "zi"][i]]));
17
+ const UNIT_ABBREVIATIONS = new Set(["km", "kg", "cm", "mm", "mg", "ml", "lt", "sn", "dk", "hz", "gr", "kw", "mb", "gb", "kb", "tb", "ms", "vb", "vs", "bkz", "yy", "tl", "krş", "tr", "st", "nr", "sk", "mh", "cd", "pk"]);
18
+ function foreignAcronym(m) {
19
+ const word = m[0];
20
+ if (!option("foreign_acronyms", true) || UNIT_ABBREVIATIONS.has(word))
21
+ return null;
22
+ return Array.from(word).map((ch) => EN_LETTERS[ch]).join(" ");
23
+ }
24
+ const int = (s) => parseInt(s ?? "", 10);
25
+ function dateWords(y, m, d, suffix = "") {
26
+ if (!(m >= 1 && m <= 12 && d >= 1 && d <= 31))
27
+ return null;
28
+ let yy = y;
29
+ if (yy < 100)
30
+ yy += yy < 70 ? 2000 : 1900;
31
+ return `${cardinal(d)} ${MONTHS[m - 1]} ${attach(cardinal(yy), suffix)}`;
32
+ }
33
+ const dateDmy = (m) => dateWords(int(m[3]), int(m[2]), int(m[1]), group(m, 4) ?? "");
34
+ const dateIso = (m) => dateWords(int(m[1]), int(m[2]), int(m[3]), group(m, 4) ?? "");
35
+ /** How Turkish speakers say one group of a phone number: 0532 -> sıfır beş yüz otuz iki, 00 -> sıfır sıfır, 45 -> kırk beş. */
36
+ function groupWords(groupText) {
37
+ const words = [];
38
+ let g = groupText;
39
+ if (g.startsWith("+")) {
40
+ words.push("artı");
41
+ g = g.slice(1);
42
+ }
43
+ const stripped = g.replace(/^0+/, "");
44
+ for (let i = 0; i < g.length - stripped.length; i++)
45
+ words.push("sıfır");
46
+ if (stripped)
47
+ words.push(cardinal(parseInt(stripped, 10)));
48
+ return words.join(" ");
49
+ }
50
+ function digitGroups(m) {
51
+ const raw = m[1];
52
+ const suffix = group(m, 2) ?? "";
53
+ const count = (raw.match(/\d/g) ?? []).length;
54
+ if (count < 7 || count > 16)
55
+ return null;
56
+ const groups = raw.split(/[\s.\-()]+/u).filter((g) => g);
57
+ const spoken = groups.map(groupWords);
58
+ spoken[spoken.length - 1] = attach(spoken[spoken.length - 1], suffix);
59
+ return spoken.join(", ");
60
+ }
61
+ function timeWords(hh, mm) {
62
+ const h = parseInt(hh, 10);
63
+ const minute = parseInt(mm, 10);
64
+ const hour = cardinal(h);
65
+ if (minute === 0)
66
+ return `${hour} sıfır sıfır`;
67
+ return `${hour} ${mm[0] !== "0" ? cardinal(minute) : "sıfır " + cardinal(minute)}`;
68
+ }
69
+ const time = (m) => attach(timeWords(m[1], m[2]), group(m, 3) ?? "");
70
+ const timeRange = (m) => `${timeWords(m[1], m[2])} ile ${timeWords(m[3], m[4])}`;
71
+ const CURRENCY = {
72
+ "₺": ["lira", "kuruş"], TL: ["lira", "kuruş"], TRY: ["lira", "kuruş"],
73
+ $: ["dolar", "sent"], USD: ["dolar", "sent"],
74
+ "€": ["avro", "sent"], EUR: ["avro", "sent"],
75
+ "£": ["sterlin", "peni"], GBP: ["sterlin", "peni"],
76
+ };
77
+ function currencyPostfix(m) {
78
+ const [whole, , frac] = partition(m[1], ",");
79
+ const scale = group(m, 2) ? ` ${m[2]}` : "";
80
+ const [unitWord] = CURRENCY[m[3]];
81
+ return `${decimal(whole, frac || null)}${scale} ${attach(unitWord, reharmonize(unitWord, group(m, 4) ?? ""))}`;
82
+ }
83
+ function money(symbol, whole, frac, suffix = "") {
84
+ const [unitWord, subWord] = CURRENCY[symbol];
85
+ const amount = cardinal(parseInt(whole.replace(/\./g, ""), 10));
86
+ if (frac) {
87
+ const cents = parseInt(frac.padEnd(2, "0").slice(0, 2), 10);
88
+ if (cents)
89
+ return `${amount} ${unitWord} ${cardinal(cents)} ${attach(subWord, reharmonize(subWord, suffix))}`;
90
+ }
91
+ return `${amount} ${attach(unitWord, reharmonize(unitWord, suffix))}`;
92
+ }
93
+ const currencyPrefix = (m) => money(m[1], m[2], group(m, 3), group(m, 4) ?? "");
94
+ const currencySuffix = (m) => money(m[3].toUpperCase(), m[1], group(m, 2), group(m, 4) ?? "");
95
+ function percent(m) {
96
+ const number = group(m, 1) ?? group(m, 3) ?? "";
97
+ const suffix = group(m, 2) ?? group(m, 4) ?? "";
98
+ const [whole, , frac] = partition(number, ",");
99
+ const words = decimal(whole, frac || null);
100
+ return "yüzde " + attach(words, suffix);
101
+ }
102
+ const UNITS = {
103
+ "km/sa": "kilometre", "km/s": "kilometre", km: "kilometre", cm: "santimetre", mm: "milimetre",
104
+ kg: "kilogram", mg: "miligram", GB: "gigabayt", MB: "megabayt", KB: "kilobayt", TB: "terabayt",
105
+ ms: "milisaniye", sn: "saniye", dk: "dakika", sa: "saat", "°C": "derece", "°": "derece",
106
+ };
107
+ const UNIT_ALTERNATION = Object.keys(UNITS).sort((a, b) => b.length - a.length).map(escapeRe).join("|");
108
+ function unit(m) {
109
+ const number = m[1];
110
+ const u = m[2];
111
+ const suffix = group(m, 3) ?? "";
112
+ const word = UNITS[u];
113
+ if (u === "km/sa" || u === "km/s")
114
+ return `saatte ${number} ${attach(word, reharmonize(word, suffix))}`;
115
+ return `${number} ${attach(word, reharmonize(word, suffix))}`;
116
+ }
117
+ const FRACTION_NAMES = { "1/2": "yarım", "1/4": "çeyrek" };
118
+ function fraction(m) {
119
+ const num = parseInt(m[1], 10);
120
+ const den = parseInt(m[2], 10);
121
+ const suffix = group(m, 3) ?? "";
122
+ if (num >= den)
123
+ return null;
124
+ if (option("fractions", "spoken") === "division")
125
+ return `${cardinal(num)} bölü ${attach(cardinal(den), suffix)}`;
126
+ const named = FRACTION_NAMES[`${num}/${den}`];
127
+ if (named)
128
+ return attach(named, reharmonize(named, suffix));
129
+ return `${locative(cardinal(den))} ${attach(cardinal(num), suffix)}`;
130
+ }
131
+ function range(m) {
132
+ const a = m[1];
133
+ const b = m[2];
134
+ if (a.length > 4 || b.length > 4)
135
+ return null;
136
+ return `${a} ila ${b}`;
137
+ }
138
+ const thousands = (m) => attach(decimal(m[1], group(m, 2)), group(m, 3) ?? "");
139
+ const decimalComma = (m) => attach(decimal(m[1], m[2]), group(m, 3) ?? "");
140
+ const ORDINAL_SUFFIX = re("(?:[ıiuü]nc[ıiuü]|nc[ıiuü])");
141
+ function suffixedNumber(m) {
142
+ const n = parseInt(m[1], 10);
143
+ const suffix = m[2];
144
+ if (fullmatch(ORDINAL_SUFFIX, suffix))
145
+ return ordinal(n);
146
+ return attach(cardinal(n), suffix);
147
+ }
148
+ const ordinalDot = (m) => ordinal(parseInt(m[1], 10));
149
+ const ABBREVIATIONS = {
150
+ vb: "ve benzeri", vs: "vesaire", vd: "ve diğerleri", örn: "örneğin", bkz: "bakınız",
151
+ Dr: "Doktor", Prof: "Profesör", Doç: "Doçent", Av: "Avukat", Sn: "Sayın",
152
+ No: "numara", no: "numara", Tel: "telefon", tel: "telefon", yy: "yüzyıl",
153
+ Cad: "Caddesi", Sok: "Sokak", Mah: "Mahallesi", Apt: "Apartmanı", Bul: "Bulvarı",
154
+ "T.C": "Türkiye Cumhuriyeti", Ltd: "Limited", Şti: "Şirketi", "A.Ş": "Anonim Şirketi",
155
+ Üniv: "Üniversitesi", Fak: "Fakültesi", Böl: "Bölümü", Hst: "Hastanesi", Müh: "Mühendisi",
156
+ Alb: "Albay", Yzb: "Yüzbaşı", Bşk: "Başkanı", Yrd: "Yardımcı", Uzm: "Uzman", Öğr: "Öğretim",
157
+ };
158
+ const ABBR_ALTERNATION = Object.keys(ABBREVIATIONS).sort((a, b) => b.length - a.length).map(escapeRe).join("|");
159
+ const abbreviation = (m) => ABBREVIATIONS[m[1]];
160
+ const VOWELS_UP = new Set("AEIİOÖUÜ");
161
+ const RUNS = re(`[${UPPER}]+|\\d+`);
162
+ function alnum(m) {
163
+ const token = m[0];
164
+ if (token.replace(/-/g, "").length < 4)
165
+ return null;
166
+ const runs = findall(RUNS, token);
167
+ const out = [];
168
+ for (const run of runs) {
169
+ if (isDigit(run))
170
+ out.push(runs.length === 2 && run.length <= 2 ? cardinal(parseInt(run, 10)) : digits(run));
171
+ else if (run.length >= 3 && Array.from(run).some((c) => VOWELS_UP.has(c)))
172
+ out.push(run);
173
+ else
174
+ out.push(Array.from(run).join(" "));
175
+ }
176
+ return out.join(" ");
177
+ }
178
+ const SEP = String.raw `(?:[ \-]|\.(?!\s))`;
179
+ const SUFFIX = `(?:${APOS}([${LOWER}]+))?`;
180
+ export const RULES = [
181
+ rule("verbalize.abbreviation", re(String.raw `(?<![${W}.])(${ABBR_ALTERNATION})\.(?![${LOWER}${UPPER}])`), abbreviation, "abbreviation read in full"),
182
+ rule("verbalize.date.dmy", re(String.raw `${B}(\d{1,2})[./](\d{1,2})[./](\d{4}|\d{2})${B}${SUFFIX}`), dateDmy, "date read as day, month name, year"),
183
+ rule("verbalize.date.iso", re(String.raw `${B}(\d{4})-(\d{2})-(\d{2})${B}${SUFFIX}`), dateIso, "ISO date read as day, month name, year"),
184
+ rule("verbalize.phone", re(String.raw `(?<![${W}.])((?:\+?\d{1,3}${SEP})?(?:\(\d{2,4}\)[ \-]?|\d{2,4}${SEP})(?:\d{2,4}${SEP}){1,4}\d{2,4})${SUFFIX}(?![${W}\-]|[.,]\d)`), digitGroups, "digit groups read digit by digit with a pause between groups"),
185
+ rule("verbalize.number_sign", re(String.raw `#(?=\d)`), () => "numara ", "number sign read as a word"),
186
+ rule("verbalize.acronym.foreign", re(String.raw `(?<![${W}'’.])[bcdfghjklmnpqrstvwxyz]{2,5}(?![${W}'’])`), foreignAcronym, "lowercase foreign acronym read with English letter names"),
187
+ rule("verbalize.digits.long", re(String.raw `(?<![${W}.,])\d{7,}(?![${W}]|[.,]\d)`), (m) => digits(m[0]), "long digit string read digit by digit"),
188
+ rule("verbalize.time.range", re(String.raw `${B}([01]?\d|2[0-3]):([0-5]\d)\s?[-–]\s?([01]?\d|2[0-3]):([0-5]\d)${B}`), timeRange, "time range read with 'ile'"),
189
+ rule("verbalize.time", re(String.raw `${B}([01]?\d|2[0-3]):([0-5]\d)${B}(?!:)${SUFFIX}`), time, "clock time read as hours and minutes"),
190
+ rule("verbalize.currency.postfix", re(String.raw `(?<![${W}.,])(\d+(?:,\d+)?)\s?(bin|milyon|milyar)?\s?([₺$€£])${SUFFIX}(?![${W}])`), currencyPostfix, "currency symbol after the amount read as words"),
191
+ rule("verbalize.currency", re(String.raw `([₺$€£])\s?(\d{1,3}(?:\.\d{3})*|\d+)(?:,(\d{1,2}))?${SUFFIX}(?![${W}]|[.,]\d)`), currencyPrefix, "currency amount read as words"),
192
+ rule("verbalize.currency.code", re(String.raw `(?<![${W}.,])(\d{1,3}(?:\.\d{3})*|\d+)(?:,(\d{1,2}))?\s?(TL|TRY|USD|EUR|GBP)${B}${SUFFIX}`), currencySuffix, "currency code read as words"),
193
+ rule("verbalize.percent", re(String.raw `%\s?(\d+(?:,\d+)?)${SUFFIX}|(?<![${W}.])(\d+(?:,\d+)?)\s?%${SUFFIX}`), percent, "percent read as 'yüzde'"),
194
+ rule("verbalize.unit", re(String.raw `(?<![${W}.,])(\d+(?:,\d+)?)\s?(${UNIT_ALTERNATION})${SUFFIX}(?![${W}/])`), unit, "unit abbreviation read as a word"),
195
+ rule("verbalize.fraction", re(String.raw `(?<![\d/])([1-9])/([2-9]|10)${SUFFIX}(?![\d/])`), fraction, "fraction read as words"),
196
+ rule("verbalize.range", re(String.raw `(?<![${W}.,\-/:])(\d+)\s?[-–]\s?(\d+)(?![${W}\-/:]|[.,]\d)`), range, "number range read with 'ila'"),
197
+ rule("verbalize.thousands", re(String.raw `(?<![${W}.,])(\d{1,3}(?:\.\d{3})+)(?:,(\d+))?${SUFFIX}(?![${W}]|[.,]\d)`), thousands, "dotted thousands read as a number, not a decimal"),
198
+ rule("verbalize.decimal", re(String.raw `(?<![${W}.,])(\d+),(\d+)${SUFFIX}(?![${W}]|[.,]\d)`), decimalComma, "decimal comma read as 'virgül'"),
199
+ rule("verbalize.suffix", re(String.raw `${B}(\d+)${APOS}([${LOWER}]+)${B}`), suffixedNumber, "numeral with a suffix read as one word"),
200
+ rule("verbalize.ordinal", re(String.raw `${B}(\d{1,3})\.(?=\s+[${LOWER}])`), ordinalDot, "numeral with a dot before a lowercase word read as an ordinal"),
201
+ rule("verbalize.minus", re(String.raw `(?<![${W}.)\-–])[-–](?=\d)`), () => "eksi ", "leading minus read as a word"),
202
+ rule("verbalize.alnum", re(String.raw `${B}(?=[${UPPER}0-9-]*\d)(?=[${UPPER}0-9-]*[${UPPER}])[${UPPER}0-9]+(?:-[${UPPER}0-9]+)*${B}`), alnum, "identifier spelled out"),
203
+ ];
204
+ registerRules("tr", RULES);
@@ -0,0 +1 @@
1
+ export declare const version = "0.1.0";
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/build-profiles.mjs from package.json. Do not edit.
2
+ export const version = "0.1.0";
package/package.json CHANGED
@@ -1,11 +1,31 @@
1
1
  {
2
2
  "name": "speechready",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Prepare text for text-to-speech: clean LLM output, verbalize ambiguous tokens, compile pauses to each provider's syntax, chunk under model limits.",
5
- "main": "index.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
6
14
  "license": "MIT",
7
15
  "author": "Berker Batur",
8
- "repository": { "type": "git", "url": "https://github.com/berkerbatur/speechready" },
16
+ "repository": { "type": "git", "url": "https://github.com/berkerbatur/speechready", "directory": "js" },
17
+ "homepage": "https://github.com/berkerbatur/speechready",
9
18
  "keywords": ["tts", "text-to-speech", "text-normalization", "ssml", "voice-agents"],
10
- "files": ["index.js", "README.md", "LICENSE"]
19
+ "files": ["dist", "README.md", "LICENSE"],
20
+ "engines": { "node": ">=20" },
21
+ "scripts": {
22
+ "build": "node scripts/build-profiles.mjs && tsc -p tsconfig.json",
23
+ "test": "npm run build && node --test test/spec.test.mjs test/package.test.mjs",
24
+ "prepack": "npm run build"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.17.0",
28
+ "typescript": "^5.6.3",
29
+ "yaml": "^2.6.1"
30
+ }
11
31
  }
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // speechready: prepare text for text-to-speech. Placeholder release; the first working version follows.
2
- module.exports = { version: "0.0.1" };