/src/espeak-ng/src/libespeak-ng/translate.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2005 to 2014 by Jonathan Duddington |
3 | | * email: jonsd@users.sourceforge.net |
4 | | * Copyright (C) 2015-2017 Reece H. Dunn |
5 | | * |
6 | | * This program is free software; you can redistribute it and/or modify |
7 | | * it under the terms of the GNU General Public License as published by |
8 | | * the Free Software Foundation; either version 3 of the License, or |
9 | | * (at your option) any later version. |
10 | | * |
11 | | * This program is distributed in the hope that it will be useful, |
12 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | | * GNU General Public License for more details. |
15 | | * |
16 | | * You should have received a copy of the GNU General Public License |
17 | | * along with this program; if not, see: <http://www.gnu.org/licenses/>. |
18 | | */ |
19 | | |
20 | | #include "config.h" |
21 | | |
22 | | #include <ctype.h> |
23 | | #include <stdbool.h> |
24 | | #include <stdint.h> |
25 | | #include <stdio.h> |
26 | | #include <stdlib.h> |
27 | | #include <string.h> |
28 | | #include <wchar.h> |
29 | | #include <wctype.h> |
30 | | |
31 | | #include <espeak-ng/espeak_ng.h> |
32 | | #include <espeak-ng/speak_lib.h> |
33 | | #include <espeak-ng/encoding.h> |
34 | | |
35 | | #include "translate.h" |
36 | | #include "common.h" |
37 | | #include "dictionary.h" // for TranslateRules, LookupDictList, Cha... |
38 | | #include "phoneme.h" // for phonSWITCH, PHONEME_TAB, phonPAUSE_... |
39 | | #include "phonemelist.h" // for MakePhonemeList |
40 | | #include "readclause.h" // for towlower2, Eof, ReadClause, is_str_... |
41 | | #include "synthdata.h" // for SelectPhonemeTable, LookupPhonemeTable |
42 | | #include "synthesize.h" // for PHONEME_LIST2, N_PHONEME_LIST, PHON... |
43 | | #include "ucd/ucd.h" // for ucd_toupper |
44 | | #include "voice.h" // for voice, voice_t |
45 | | #include "speech.h" // for MAKE_MEM_UNDEFINED |
46 | | #include "translateword.h" |
47 | | |
48 | | static int CalcWordLength(int source_index, int charix_top, short int *charix, WORD_TAB *words, int word_count); |
49 | | static void CombineFlag(Translator *tr, WORD_TAB *wtab, int wtab_remaining, char *word, int *flags, unsigned char *p, char *word_phonemes); |
50 | | static void SwitchLanguage(char *word, char *word_phonemes); |
51 | | static int TranslateWordWithBounds(Translator *tr, char *word_start, WORD_TAB *wtab, int wtab_remaining, char *word_out, int depth); |
52 | | |
53 | | Translator *translator = NULL; // the main translator |
54 | | Translator *translator2 = NULL; // secondary translator for certain words |
55 | | static char translator2_language[20] = { 0 }; |
56 | | Translator *translator3 = NULL; // tertiary translator for certain words |
57 | | static char translator3_language[20] = { 0 }; |
58 | | |
59 | | FILE *f_trans = NULL; // phoneme output text |
60 | | int option_tone_flags = 0; // bit 8=emphasize allcaps, bit 9=emphasize penultimate stress |
61 | | int option_phonemes = 0; |
62 | | int option_phoneme_events = 0; |
63 | | int option_endpause = 0; // suppress pause after end of text |
64 | | int option_capitals = 0; |
65 | | int option_punctuation = 0; |
66 | | int option_sayas = 0; |
67 | | static int option_sayas2 = 0; // used in translate_clause() |
68 | | static int option_emphasis = 0; // 0=normal, 1=normal, 2=weak, 3=moderate, 4=strong |
69 | | int option_ssml = 0; |
70 | | int option_phoneme_input = 0; // allow [[phonemes]] in input |
71 | | int option_wordgap = 0; |
72 | | |
73 | | static int count_sayas_digits; |
74 | | int skip_sentences; |
75 | | int skip_words; |
76 | | int skip_characters; |
77 | | char skip_marker[N_MARKER_LENGTH]; |
78 | | bool skipping_text; // waiting until word count, sentence count, or named marker is reached |
79 | | int end_character_position; |
80 | | int count_sentences; |
81 | | static int count_words; |
82 | | int clause_start_char; |
83 | | int clause_start_word; |
84 | | static bool new_sentence; |
85 | | static int word_emphasis = 0; // set if emphasis level 3 or 4 |
86 | | static int embedded_flag = 0; // there are embedded commands to be applied to the next phoneme, used in TranslateWord2() |
87 | | |
88 | | static int max_clause_pause = 0; |
89 | | static bool any_stressed_words; |
90 | | int pre_pause; |
91 | | static ALPHABET *current_alphabet; |
92 | | |
93 | | char word_phonemes[N_WORD_PHONEMES]; // a word translated into phoneme codes |
94 | | int n_ph_list2; |
95 | | PHONEME_LIST2 ph_list2[N_PHONEME_LIST]; // first stage of text->phonemes |
96 | | |
97 | | wchar_t option_punctlist[N_PUNCTLIST] = { 0 }; |
98 | | |
99 | | // these are overridden by defaults set in the "speak" file |
100 | | int option_linelength = 0; |
101 | | |
102 | 67.3k | #define N_EMBEDDED_LIST 250 |
103 | | static int embedded_ix; |
104 | | static int embedded_read; |
105 | | unsigned int embedded_list[N_EMBEDDED_LIST]; |
106 | | |
107 | | // the source text of a single clause (UTF8 bytes) |
108 | | static char source[N_TR_SOURCE+40]; // extra space for embedded command & voice change info at end |
109 | | |
110 | | int n_replace_phonemes; |
111 | | REPLACE_PHONEMES replace_phonemes[N_REPLACE_PHONEMES]; |
112 | | |
113 | | // other characters which break a word, but don't produce a pause |
114 | | static const unsigned short breaks[] = { '_', 0 }; |
115 | | |
116 | | void DeleteTranslator(Translator *tr) |
117 | 34.6k | { |
118 | 34.6k | if (!tr) return; |
119 | | |
120 | 34.6k | if (tr->data_dictlist != NULL) |
121 | 34.6k | free(tr->data_dictlist); |
122 | 34.6k | free(tr); |
123 | 34.6k | } |
124 | | |
125 | | int lookupwchar(const unsigned short *list, int c) |
126 | 7.81M | { |
127 | | // Is the character c in the list ? |
128 | 7.81M | int ix; |
129 | | |
130 | 70.2M | for (ix = 0; list[ix] != 0; ix++) { |
131 | 63.8M | if (list[ix] == c) |
132 | 1.41M | return ix+1; |
133 | 63.8M | } |
134 | 6.40M | return 0; |
135 | 7.81M | } |
136 | | |
137 | | char *strchr_w(const char *s, int c) |
138 | 67.1k | { |
139 | | // return NULL for any non-ascii character |
140 | 67.1k | if (c >= 0x80) |
141 | 8.15k | return NULL; |
142 | 58.9k | return strchr((char *)s, c); // (char *) is needed for Borland compiler |
143 | 67.1k | } |
144 | | |
145 | | static bool ShouldSplitIdeographs(Translator *tr, int c, int prev) |
146 | 709k | { |
147 | 709k | if (tr->langopts.ideographs && ((c > 0x3040) || (prev > 0x3040))) |
148 | 424 | return true; |
149 | | |
150 | | // A non-ideographic base language may nominate an ideographic alternate |
151 | | // translator (Arabic uses Mandarin for Han characters). Split that run in |
152 | | // the same way as the alternate translator would, so every Han character |
153 | | // reaches its dictionary entry instead of one long foreign-script token. |
154 | 709k | if (tr->langopts.alt_alphabet != 0) { |
155 | 1.47k | const ALPHABET *alpha_c = AlphabetFromChar(c); |
156 | 1.47k | const ALPHABET *alpha_prev = AlphabetFromChar(prev); |
157 | 1.47k | if (((alpha_c != NULL) && (alpha_c->offset == tr->langopts.alt_alphabet) |
158 | 21 | && (alpha_c->flags & AL_IDEOGRAPHS)) |
159 | 1.47k | || ((alpha_prev != NULL) && (alpha_prev->offset == tr->langopts.alt_alphabet) |
160 | 32 | && (alpha_prev->flags & AL_IDEOGRAPHS))) |
161 | 0 | return true; |
162 | 1.47k | } |
163 | 709k | return false; |
164 | 709k | } |
165 | | |
166 | | static void SegmentReplacement(Translator *tr, const char *text, char *out, int out_size) |
167 | 16.4k | { |
168 | | // Apply the word-splitting rules of the clause tokenizer (TranslateClause) |
169 | | // to dictionary replacement text, so that a $textmode replacement is |
170 | | // re-translated in the same word units as if it had been normal input: |
171 | | // for languages with langopts.ideographs each CJK character is a separate |
172 | | // word (so that hanzi words can match their multi-word *_list entries), |
173 | | // and a non-alpha mark such as a Thai tone mark terminates a word instead |
174 | | // of derailing the letter-to-phoneme rules mid-word. A hyphen between two |
175 | | // letters also ends a word, as it does in normal input; without this a |
176 | | // hyphenated replacement name ("Burkina-Faso", "États-Unis") is handed to |
177 | | // the rules as one token, which stops at the hyphen and drops the rest. |
178 | | // Other ASCII characters are kept in-word, so that Latin-script |
179 | | // replacement text such as "cai3hong2" stays a single word. |
180 | 16.4k | int ix = 0, out_ix = 0; |
181 | 16.4k | int c = 0; |
182 | | |
183 | 262k | while (text[ix] != 0) { |
184 | 245k | int prev = c; |
185 | 245k | int nbytes = utf8_in(&c, &text[ix]); |
186 | 245k | bool insert_space = false; |
187 | | |
188 | 245k | if (IsAlpha(prev)) { |
189 | 206k | if (IsAlpha(c)) { |
190 | 167k | if (ShouldSplitIdeographs(tr, c, prev)) |
191 | 194 | insert_space = true; // each ideograph is a separate word |
192 | 167k | } else if (c == '-') { |
193 | 166 | int next; |
194 | | |
195 | 166 | utf8_in(&next, &text[ix + nbytes]); |
196 | 166 | if (IsAlpha(next)) { |
197 | | // hyphen between two letters: the clause tokenizer turns it |
198 | | // into a word break, so do the same and drop the hyphen |
199 | 163 | if (out_ix + 2 > out_size) |
200 | 0 | break; |
201 | 163 | out[out_ix++] = ' '; |
202 | 163 | ix += nbytes; |
203 | 163 | c = ' '; |
204 | 163 | continue; |
205 | 163 | } |
206 | 38.4k | } else if (!IsSpace(c) && (c >= 0x80) && (wcschr(tr->punct_within_word, c) == 0)) |
207 | 849 | insert_space = true; // eg. a Thai tone mark ends the word, as in the clause tokenizer |
208 | 206k | } else if ((prev >= 0x80) && !IsSpace(prev) && IsAlpha(c) && (wcschr(tr->punct_within_word, prev) == 0)) |
209 | 611 | insert_space = true; // a letter after such a mark starts a new word |
210 | | |
211 | 245k | if (out_ix + nbytes + 2 > out_size) |
212 | 0 | break; |
213 | 245k | if (insert_space) |
214 | 1.65k | out[out_ix++] = ' '; |
215 | 245k | memcpy(&out[out_ix], &text[ix], nbytes); |
216 | 245k | out_ix += nbytes; |
217 | 245k | ix += nbytes; |
218 | 245k | } |
219 | 16.4k | out[out_ix] = 0; |
220 | 16.4k | } |
221 | | |
222 | | static int TranslateWordWithBounds(Translator *tr, char *word_start, WORD_TAB *wtab, int wtab_remaining, char *word_out, int depth) |
223 | 1.07M | { |
224 | 1.07M | char words_phonemes[N_WORD_PHONEMES]; // a word translated into phoneme codes |
225 | 1.07M | char *phonemes = words_phonemes; |
226 | | |
227 | | |
228 | 1.07M | int flags = TranslateWord3(tr, word_start, wtab, wtab_remaining, word_out, &any_stressed_words, current_alphabet, word_phonemes, sizeof(word_phonemes)); |
229 | 1.07M | if (flags & FLAG_TEXTMODE && word_out) { |
230 | | // Ensure that start of word rules match with the replaced text, |
231 | | // so that emoji and other characters are pronounced correctly. |
232 | | // The buffer allows for a space inserted after every character of |
233 | | // the replacement by SegmentReplacement(). |
234 | 16.4k | char word[2+N_WORD_BYTES*2]; |
235 | 16.4k | word[0] = 0; |
236 | 16.4k | word[1] = ' '; |
237 | 16.4k | SegmentReplacement(tr, word_out, word+2, sizeof(word)-2); |
238 | 16.4k | word_out = word+2; |
239 | | |
240 | 16.4k | bool first_word = true; |
241 | 16.4k | int available = N_WORD_PHONEMES; |
242 | 56.0k | while (*word_out && available > 1) { |
243 | 39.6k | int c; |
244 | 39.6k | int nbytes = utf8_in(&c, word_out); |
245 | 39.6k | if (iswupper(c)) { |
246 | 798 | char lower_buf[8]; |
247 | 798 | int n_lower; |
248 | | |
249 | 798 | wtab->flags |= FLAG_FIRST_UPPER; |
250 | | // towlower2(), not tolower(): the latter only knows ASCII, and |
251 | | // on a non-ASCII capital (Казахстан, Καζακστάν) it returned a |
252 | | // value whose UTF-8 form is a different length, which corrupted |
253 | | // the first letter of the word. Leave the letter alone if its |
254 | | // lower case form does not fit the same number of bytes. |
255 | 798 | n_lower = utf8_out(towlower2(c, tr), lower_buf); |
256 | 798 | if (n_lower == nbytes) |
257 | 798 | memcpy(word_out, lower_buf, nbytes); |
258 | 38.8k | } else { |
259 | 38.8k | wtab->flags &= ~FLAG_FIRST_UPPER; |
260 | 38.8k | } |
261 | | |
262 | | // dictionary_skipwords is a global variable and TranslateWord3 will reset it to 0 at the beginning. |
263 | | // However, dictionary_skipwords value is still needed outside this scope. |
264 | | // So we backup and restore it at the end of this scope. |
265 | 39.6k | int skipwords = dictionary_skipwords; |
266 | 39.6k | if (depth < 3) { |
267 | | // translate through TranslateWordWithBounds so that a replacement |
268 | | // word which itself has a $textmode entry is expanded too: eg. an |
269 | | // emoji entry replaces the emoji by hanzi, and the hanzi words map |
270 | | // to pinyin through *_list. The depth limit guards against an |
271 | | // entry cycle in the dictionary (a $textmode b, b $textmode a). |
272 | 39.6k | char word_replacement2[N_WORD_BYTES+1]; |
273 | 39.6k | word_replacement2[0] = 0; |
274 | 39.6k | TranslateWordWithBounds(tr, word_out, wtab, wtab_remaining, word_replacement2, depth+1); |
275 | 39.6k | } else |
276 | 0 | TranslateWord3(tr, word_out, wtab, wtab_remaining, NULL, &any_stressed_words, current_alphabet, word_phonemes, sizeof(word_phonemes)); |
277 | | |
278 | 39.6k | int n; |
279 | 39.6k | if (first_word) { |
280 | 16.4k | n = snprintf(phonemes, available, "%s", word_phonemes); |
281 | 16.4k | first_word = false; |
282 | 23.1k | } else { |
283 | 23.1k | n = snprintf(phonemes, available, "%c%s", phonEND_WORD, word_phonemes); |
284 | 23.1k | } |
285 | | |
286 | 39.6k | available -= n; |
287 | 39.6k | phonemes += n; |
288 | | |
289 | | // skip to the next word in a multi-word replacement. Always skip at least one word. |
290 | 79.2k | for (dictionary_skipwords++; dictionary_skipwords > 0; dictionary_skipwords--) { |
291 | 317k | while (!isspace(*word_out)) ++word_out; |
292 | 39.6k | while (isspace(*word_out)) ++word_out; |
293 | 39.6k | } |
294 | 39.6k | dictionary_skipwords = skipwords; |
295 | 39.6k | } |
296 | | |
297 | | // If the list file contains a text replacement to another |
298 | | // entry in the list file, e.g.: |
299 | | // ripost riposte $text |
300 | | // riposte rI#p0st |
301 | | // calling it from a prefix or suffix rule such as 'riposted' |
302 | | // causes word_out[0] to be NULL, as TranslateWord3 has the |
303 | | // information needed to perform the mapping. In this case, |
304 | | // no phonemes have been written in this loop and the phonemes |
305 | | // have been calculated, so don't override them. |
306 | 16.4k | if (phonemes != words_phonemes) { |
307 | 16.4k | snprintf(word_phonemes, sizeof(word_phonemes), "%s", words_phonemes); |
308 | 16.4k | } |
309 | 16.4k | } |
310 | 1.07M | return flags; |
311 | 1.07M | } |
312 | | |
313 | | int TranslateWord(Translator *tr, char *word_start, WORD_TAB *wtab, char *word_out) |
314 | 18.1k | { |
315 | 18.1k | return TranslateWordWithBounds(tr, word_start, wtab, 0, word_out, 0); |
316 | 18.1k | } |
317 | | |
318 | | static void SetPlist2(PHONEME_LIST2 *p, unsigned char phcode) |
319 | 335k | { |
320 | 335k | p->phcode = phcode; |
321 | 335k | p->stresslevel = 0; |
322 | 335k | p->tone_ph = 0; |
323 | 335k | p->synthflags = embedded_flag; |
324 | 335k | p->sourceix = 0; |
325 | 335k | embedded_flag = 0; |
326 | 335k | } |
327 | | |
328 | | static int CountSyllables(unsigned char *phonemes) |
329 | 11 | { |
330 | 11 | int count = 0; |
331 | 11 | int phon; |
332 | 110 | while ((phon = *phonemes++) != 0) { |
333 | 99 | if (phoneme_tab[phon]->type == phVOWEL) |
334 | 48 | count++; |
335 | 99 | } |
336 | 11 | return count; |
337 | 11 | } |
338 | | |
339 | | static void Word_EmbeddedCmd(void) |
340 | 19.2k | { |
341 | | // Process embedded commands for emphasis, sayas, and break |
342 | 19.2k | int embedded_cmd; |
343 | 34.3k | do { |
344 | 34.3k | embedded_cmd = embedded_list[embedded_read++]; |
345 | 34.3k | int value = embedded_cmd >> 8; |
346 | | |
347 | 34.3k | switch (embedded_cmd & 0x1f) |
348 | 34.3k | { |
349 | 2.55k | case EMBED_Y: |
350 | 2.55k | option_sayas = value; |
351 | 2.55k | break; |
352 | | |
353 | 2.21k | case EMBED_F: |
354 | 2.21k | option_emphasis = value; |
355 | 2.21k | break; |
356 | | |
357 | 2.92k | case EMBED_B: |
358 | | // break command |
359 | 2.92k | if (value == 0) |
360 | 414 | pre_pause = 0; // break=none |
361 | 2.51k | else |
362 | 2.51k | pre_pause += value; |
363 | 2.92k | break; |
364 | 34.3k | } |
365 | 34.3k | } while (((embedded_cmd & 0x80) == 0) && (embedded_read < embedded_ix)); |
366 | 19.2k | } |
367 | | |
368 | | static int SetAlternateTranslator(const char *new_language, Translator **translator, char translator_language[20]) |
369 | 748k | { |
370 | | // Set alternate translator to a second language |
371 | 748k | int new_phoneme_tab; |
372 | | |
373 | 748k | if ((new_phoneme_tab = SelectPhonemeTableName(new_language)) >= 0) { |
374 | 748k | if ((*translator != NULL) && (strcmp(new_language, translator_language) != 0)) { |
375 | | // we already have an alternative translator, but not for the required language, delete it |
376 | 25.5k | DeleteTranslator(*translator); |
377 | 25.5k | *translator = NULL; |
378 | 25.5k | } |
379 | | |
380 | 748k | if (*translator == NULL) { |
381 | 25.5k | *translator = SelectTranslator(new_language); |
382 | 25.5k | strcpy(translator_language, new_language); |
383 | | |
384 | 25.5k | if (LoadDictionary(*translator, (*translator)->dictionary_name, 0) != 0) { |
385 | 0 | SelectPhonemeTable(voice->phoneme_tab_ix); // revert to original phoneme table |
386 | 0 | new_phoneme_tab = -1; |
387 | 0 | translator_language[0] = 0; |
388 | 0 | } |
389 | 25.5k | (*translator)->phoneme_tab_ix = new_phoneme_tab; |
390 | 25.5k | } |
391 | 748k | } |
392 | 748k | if (*translator != NULL) |
393 | 748k | (*translator)->phonemes_repeat[0] = 0; |
394 | 748k | return new_phoneme_tab; |
395 | 748k | } |
396 | | |
397 | | int SetTranslator2(const char *new_language) |
398 | 163k | { |
399 | 163k | return SetAlternateTranslator(new_language, &translator2, translator2_language); |
400 | 163k | } |
401 | | |
402 | | int SetTranslator3(const char *new_language) |
403 | 584k | { |
404 | 584k | return SetAlternateTranslator(new_language, &translator3, translator3_language); |
405 | 584k | } |
406 | | |
407 | | static int TranslateWord2(Translator *tr, char *word, WORD_TAB *wtab, int wtab_remaining, int pre_pause) |
408 | 986k | { |
409 | 986k | int flags = 0; |
410 | 986k | int stress; |
411 | 986k | int next_stress; |
412 | 986k | int next_tone = 0; |
413 | 986k | unsigned char *p; |
414 | 986k | int srcix; |
415 | 986k | int found_dict_flag; |
416 | 986k | unsigned char ph_code; |
417 | 986k | PHONEME_LIST2 *plist2; |
418 | 986k | PHONEME_TAB *ph; |
419 | 986k | int max_stress; |
420 | 986k | int max_stress_ix = 0; |
421 | 986k | int prev_vowel = -1; |
422 | 986k | int pitch_raised = 0; |
423 | 986k | int switch_phonemes = -1; |
424 | 986k | bool first_phoneme = true; |
425 | 986k | int source_ix; |
426 | 986k | int len; |
427 | 986k | int bad_phoneme; |
428 | 986k | int word_flags; |
429 | 986k | char word_copy[N_WORD_BYTES+1]; |
430 | 986k | char word_replaced[N_WORD_BYTES+1]; |
431 | 986k | char old_dictionary_name[40]; |
432 | | |
433 | 986k | len = wtab->length; |
434 | 986k | if (len > 31) len = 31; |
435 | 986k | source_ix = (wtab->sourceix & 0x7ff) | (len << 11); // bits 0-10 sourceix, bits 11-15 word length |
436 | | |
437 | 986k | word_flags = wtab[0].flags; |
438 | 986k | if (word_flags & FLAG_EMBEDDED) { |
439 | 17.8k | wtab[0].flags &= ~FLAG_EMBEDDED; // clear it in case we call TranslateWord2() again for the same word |
440 | 17.8k | embedded_flag = SFLAG_EMBEDDED; |
441 | | |
442 | 17.8k | Word_EmbeddedCmd(); |
443 | 17.8k | } |
444 | | |
445 | 986k | if (n_ph_list2 >= N_PHONEME_LIST-2) { |
446 | | // No room, can't translate anything |
447 | 0 | return 0; |
448 | 0 | } |
449 | | |
450 | 986k | if ((word[0] == 0) || (word_flags & FLAG_DELETE_WORD)) { |
451 | | // nothing to translate. Add a dummy phoneme to carry any embedded commands |
452 | 8.07k | if (embedded_flag) { |
453 | 785 | SetPlist2(&ph_list2[n_ph_list2], phonEND_WORD); |
454 | 785 | ph_list2[n_ph_list2].wordstress = 0; |
455 | 785 | n_ph_list2++; |
456 | 785 | embedded_flag = 0; |
457 | 785 | } |
458 | 8.07k | word_phonemes[0] = 0; |
459 | 8.07k | return 0; |
460 | 8.07k | } |
461 | | |
462 | 977k | if (n_ph_list2 >= N_PHONEME_LIST-7-2) { |
463 | | // We may require up to 7 phonemes, plus the 2 phonemes from the caller, can't translate safely |
464 | 3.35k | return 0; |
465 | 3.35k | } |
466 | | |
467 | | // after a $pause word attribute, ignore a $pause attribute on the next two words |
468 | 974k | if (tr->prepause_timeout > 0) |
469 | 4.73k | tr->prepause_timeout--; |
470 | | |
471 | 974k | if ((option_sayas & 0xf0) == 0x10) { |
472 | 3.95k | if (!(word_flags & FLAG_FIRST_WORD)) { |
473 | | // SAYAS_CHARS, SAYAS_GLYPHS, or SAYAS_SINGLECHARS. Pause between each word. |
474 | 3.47k | pre_pause += 4; |
475 | 3.47k | } |
476 | 3.95k | } |
477 | | |
478 | 974k | if (word_flags & FLAG_FIRST_UPPER) { |
479 | 116k | if ((option_capitals > 2) && (embedded_ix < N_EMBEDDED_LIST-6)) { |
480 | | // indicate capital letter by raising pitch |
481 | 0 | if (embedded_flag) |
482 | 0 | embedded_list[embedded_ix-1] &= ~0x80; // already embedded command before this word, remove terminator |
483 | 0 | if ((pitch_raised = option_capitals) == 3) |
484 | 0 | pitch_raised = 20; // default pitch raise for capitals |
485 | 0 | embedded_list[embedded_ix++] = EMBED_P+0x40+0x80 + (pitch_raised << 8); // raise pitch |
486 | 0 | embedded_flag = SFLAG_EMBEDDED; |
487 | 0 | } |
488 | 116k | } |
489 | | |
490 | 974k | p = (unsigned char *)word_phonemes; |
491 | 974k | if (word_flags & FLAG_PHONEMES) { |
492 | | // The input is in phoneme mnemonics, not language text |
493 | | |
494 | 105k | if (memcmp(word, "_^_", 3) == 0) { |
495 | 5.51k | SwitchLanguage(word, word_phonemes); |
496 | 100k | } else { |
497 | 100k | EncodePhonemes(word, word_phonemes, &bad_phoneme); |
498 | 100k | } |
499 | | |
500 | 105k | flags = FLAG_FOUND; |
501 | 869k | } else { |
502 | 869k | int c2; |
503 | 869k | int ix = 0; |
504 | 869k | int word_copy_len; |
505 | 3.21M | while (((c2 = word_copy[ix] = word[ix]) != ' ') && (c2 != 0) && (ix < N_WORD_BYTES)) ix++; |
506 | 869k | word_copy_len = ix; |
507 | | |
508 | 869k | word_replaced[2] = 0; |
509 | 869k | flags = TranslateWordWithBounds(translator, word, wtab, wtab_remaining, &word_replaced[2], 0); |
510 | | |
511 | 869k | if (flags & FLAG_SPELLWORD) { |
512 | | // re-translate the word as individual letters, separated by spaces |
513 | 12.6k | memcpy(word, word_copy, word_copy_len); |
514 | 12.6k | return flags; |
515 | 12.6k | } |
516 | | |
517 | 856k | if ((flags & FLAG_COMBINE) && (wtab_remaining > 1) && !(wtab[1].flags & FLAG_PHONEMES)) { |
518 | 111 | CombineFlag(tr, wtab, wtab_remaining, word, &flags, p, word_phonemes); |
519 | 111 | } |
520 | | |
521 | 856k | if (p[0] == phonSWITCH) { |
522 | 130k | int switch_attempt; |
523 | 130k | strcpy(old_dictionary_name, dictionary_name); |
524 | 156k | for (switch_attempt = 0; switch_attempt < 2; switch_attempt++) { |
525 | | // this word uses a different language |
526 | 143k | memcpy(word, word_copy, word_copy_len); |
527 | | |
528 | 143k | const char *new_language; |
529 | 143k | new_language = (char *)(&p[1]); |
530 | 143k | if (new_language[0] == 0) |
531 | 28.3k | new_language = ESPEAKNG_DEFAULT_VOICE; |
532 | | |
533 | 143k | switch_phonemes = SetTranslator2(new_language); |
534 | | |
535 | 143k | if (switch_phonemes >= 0) { |
536 | | // re-translate the word using the new translator |
537 | 143k | wtab[0].flags |= FLAG_TRANSLATOR2; |
538 | 143k | if (word_replaced[2] != 0) { |
539 | 0 | word_replaced[0] = 0; // byte before the start of the word |
540 | 0 | word_replaced[1] = ' '; |
541 | 0 | flags = TranslateWordWithBounds(translator2, &word_replaced[1], wtab, wtab_remaining, NULL, 0); |
542 | 0 | } else |
543 | 143k | flags = TranslateWordWithBounds(translator2, word, wtab, wtab_remaining, &word_replaced[2], 0); |
544 | 143k | } |
545 | | |
546 | 143k | if (p[0] != phonSWITCH) |
547 | 116k | break; |
548 | 143k | } |
549 | | |
550 | 130k | if (p[0] == phonSWITCH) |
551 | 13.3k | return FLAG_SPELLWORD; |
552 | | |
553 | 116k | if (switch_phonemes < 0) { |
554 | | // language code is not recognised or 2nd translator won't translate it |
555 | 0 | p[0] = phonSCHWA; // just say something |
556 | 0 | p[1] = phonSCHWA; |
557 | 0 | p[2] = 0; |
558 | 0 | } |
559 | | |
560 | 116k | if (switch_phonemes == -1) { |
561 | 0 | strcpy(dictionary_name, old_dictionary_name); |
562 | 0 | SelectPhonemeTable(voice->phoneme_tab_ix); |
563 | | |
564 | | // leave switch_phonemes set, but use the original phoneme table number. |
565 | | // This will suppress LOPT_REGRESSIVE_VOICING |
566 | 0 | switch_phonemes = voice->phoneme_tab_ix; // original phoneme table |
567 | 0 | } |
568 | 116k | } |
569 | | |
570 | 842k | if (!(word_flags & FLAG_HYPHEN)) { |
571 | 834k | if (flags & FLAG_PAUSE1) { |
572 | 33.0k | if (pre_pause < 1) |
573 | 29.2k | pre_pause = 1; |
574 | 33.0k | } |
575 | 834k | if ((flags & FLAG_PREPAUSE) && !(word_flags & (FLAG_LAST_WORD | FLAG_FIRST_WORD)) && !(wtab[-1].flags & FLAG_FIRST_WORD) && (tr->prepause_timeout == 0)) { |
576 | | // the word is marked in the dictionary list with $pause |
577 | 1.65k | if (pre_pause < 4) pre_pause = 4; |
578 | 1.65k | tr->prepause_timeout = 3; |
579 | 1.65k | } |
580 | 834k | } |
581 | | |
582 | 842k | if ((option_emphasis >= 3) && (pre_pause < 1)) |
583 | 6.87k | pre_pause = 1; |
584 | 842k | } |
585 | | |
586 | 948k | stress = 0; |
587 | 948k | next_stress = 1; |
588 | 948k | srcix = 0; |
589 | 948k | max_stress = -1; |
590 | | |
591 | 948k | found_dict_flag = 0; |
592 | 948k | if ((flags & FLAG_FOUND) && !(flags & FLAG_TEXTMODE)) |
593 | 327k | found_dict_flag = SFLAG_DICTIONARY; |
594 | | |
595 | | // Each iteration may require up to 1 phoneme |
596 | | // and after this loop we may require up to 7 phonemes |
597 | | // and our caller requires 2 phonemes |
598 | 1.12M | while ((pre_pause > 0) && (n_ph_list2 < N_PHONEME_LIST-7-2)) { |
599 | | // add pause phonemes here. Either because of punctuation (brackets or quotes) in the |
600 | | // text, or because the word is marked in the dictionary lookup as a conjunction |
601 | 173k | if (pre_pause > 1) { |
602 | 137k | SetPlist2(&ph_list2[n_ph_list2++], phonPAUSE); |
603 | 137k | pre_pause -= 2; |
604 | 137k | } else { |
605 | 36.1k | SetPlist2(&ph_list2[n_ph_list2++], phonPAUSE_NOLINK); |
606 | 36.1k | pre_pause--; |
607 | 36.1k | } |
608 | 173k | tr->end_stressed_vowel = 0; // forget about the previous word |
609 | 173k | tr->prev_dict_flags[0] = 0; |
610 | 173k | tr->prev_dict_flags[1] = 0; |
611 | 173k | } |
612 | 948k | plist2 = &ph_list2[n_ph_list2]; |
613 | | // From here we may require up to 4+1+3 phonemes |
614 | | |
615 | | // This may require up to 4 phonemes |
616 | 948k | if ((option_capitals == 1) && (word_flags & FLAG_FIRST_UPPER)) { |
617 | 0 | SetPlist2(&ph_list2[n_ph_list2++], phonPAUSE_SHORT); |
618 | 0 | SetPlist2(&ph_list2[n_ph_list2++], phonCAPITAL); |
619 | 0 | if ((word_flags & FLAG_ALL_UPPER) && IsAlpha(word[1])) { |
620 | | // word > 1 letter and all capitals |
621 | 0 | SetPlist2(&ph_list2[n_ph_list2++], phonPAUSE_SHORT); |
622 | 0 | SetPlist2(&ph_list2[n_ph_list2++], phonCAPITAL); |
623 | 0 | } |
624 | 0 | } |
625 | | |
626 | | // This may require up to 1 phoneme |
627 | 948k | if (switch_phonemes >= 0) { |
628 | 116k | if ((p[0] == phonPAUSE) && (p[1] == phonSWITCH)) { |
629 | | // the new word starts with a phoneme table switch, so there's no need to switch before it. |
630 | 12.1k | if (ph_list2[n_ph_list2-1].phcode == phonSWITCH) { |
631 | | // previous phoneme is also a phonSWITCH, delete it |
632 | 5.98k | n_ph_list2--; |
633 | 5.98k | } |
634 | 104k | } else { |
635 | | // this word uses a different phoneme table |
636 | 104k | if (ph_list2[n_ph_list2-1].phcode == phonSWITCH) { |
637 | | // previous phoneme is also a phonSWITCH, just change its phoneme table number |
638 | 60.6k | n_ph_list2--; |
639 | 60.6k | } else |
640 | 43.8k | SetPlist2(&ph_list2[n_ph_list2], phonSWITCH); |
641 | 104k | ph_list2[n_ph_list2++].tone_ph = switch_phonemes; // temporary phoneme table number |
642 | 104k | } |
643 | 116k | } |
644 | | |
645 | | // remove initial pause from a word if it follows a hyphen |
646 | 948k | if ((word_flags & FLAG_HYPHEN) && (phoneme_tab[*p]->type == phPAUSE)) |
647 | 2.44k | p++; |
648 | | |
649 | 948k | if ((p[0] == 0) && (embedded_flag)) { |
650 | | // no phonemes. Insert a very short pause to carry an embedded command |
651 | 5.47k | p[0] = phonPAUSE_VSHORT; |
652 | 5.47k | p[1] = 0; |
653 | 5.47k | } |
654 | | |
655 | | // Each iteration may require up to 1 phoneme |
656 | | // and after this loop we may require up to 3 phonemes |
657 | | // and our caller requires 2 phonemes |
658 | 7.65M | while (((ph_code = *p++) != 0) && (n_ph_list2 < N_PHONEME_LIST-3-2)) { |
659 | 6.70M | if (ph_code == 255) |
660 | 0 | continue; // unknown phoneme |
661 | | |
662 | | // Add the phonemes to the first stage phoneme list (ph_list2) |
663 | 6.70M | ph = phoneme_tab[ph_code]; |
664 | 6.70M | if (ph == NULL) { |
665 | 6.71k | printf("Invalid phoneme code %d\n", ph_code); |
666 | 6.71k | continue; |
667 | 6.71k | } |
668 | | |
669 | 6.70M | if (ph_code == phonSWITCH) { |
670 | 99.5k | ph_list2[n_ph_list2].phcode = ph_code; |
671 | 99.5k | ph_list2[n_ph_list2].stresslevel = 0; |
672 | 99.5k | ph_list2[n_ph_list2].sourceix = 0; |
673 | 99.5k | ph_list2[n_ph_list2].synthflags = 0; |
674 | 99.5k | ph_list2[n_ph_list2++].tone_ph = *p; |
675 | 99.5k | SelectPhonemeTable(*p); |
676 | 99.5k | p++; |
677 | 6.60M | } else if (ph->type == phSTRESS) { |
678 | | // don't add stress phonemes codes to the list, but give their stress |
679 | | // value to the next vowel phoneme |
680 | | // std_length is used to hold stress number or (if >10) a tone number for a tone language |
681 | 1.46M | if (ph->program == 0) |
682 | 1.42M | next_stress = ph->std_length; |
683 | 34.8k | else { |
684 | | // for tone languages, the tone number for a syllable follows the vowel |
685 | 34.8k | if (prev_vowel >= 0) |
686 | 34.1k | ph_list2[prev_vowel].tone_ph = ph_code; |
687 | 682 | else |
688 | 682 | next_tone = ph_code; // no previous vowel, apply to the next vowel |
689 | 34.8k | } |
690 | 5.13M | } else if (ph_code == phonSYLLABIC) { |
691 | | // mark the previous phoneme as a syllabic consonant |
692 | 1.22k | prev_vowel = n_ph_list2-1; |
693 | 1.22k | ph_list2[prev_vowel].synthflags |= SFLAG_SYLLABLE; |
694 | 1.22k | ph_list2[prev_vowel].stresslevel = next_stress; |
695 | 5.13M | } else if (ph_code == phonLENGTHEN) |
696 | 67.5k | ph_list2[n_ph_list2-1].synthflags |= SFLAG_LENGTHEN; |
697 | 5.06M | else if (ph_code == phonEND_WORD) { |
698 | | // a || symbol in a phoneme string was used to indicate a word boundary |
699 | | // Don't add this phoneme to the list, but make sure the next phoneme has |
700 | | // a newword indication |
701 | 256k | srcix = source_ix+1; |
702 | 4.81M | } else if (ph_code == phonX1) { |
703 | | // a language specific action |
704 | 4 | flags |= FLAG_DOUBLING; |
705 | 4.81M | } else { |
706 | 4.81M | ph_list2[n_ph_list2].phcode = ph_code; |
707 | 4.81M | ph_list2[n_ph_list2].tone_ph = 0; |
708 | 4.81M | ph_list2[n_ph_list2].synthflags = embedded_flag | found_dict_flag; |
709 | 4.81M | embedded_flag = 0; |
710 | 4.81M | ph_list2[n_ph_list2].sourceix = srcix; |
711 | 4.81M | srcix = 0; |
712 | | |
713 | 4.81M | if (ph->type == phVOWEL) { |
714 | 1.89M | stress = next_stress; |
715 | 1.89M | next_stress = 1; // default is 'unstressed' |
716 | | |
717 | 1.89M | if (stress >= 4) |
718 | 856k | any_stressed_words = true; |
719 | | |
720 | 1.89M | if ((prev_vowel >= 0) && (n_ph_list2-1) != prev_vowel) |
721 | 1.13M | ph_list2[n_ph_list2-1].stresslevel = stress; // set stress for previous consonant |
722 | | |
723 | 1.89M | ph_list2[n_ph_list2].synthflags |= SFLAG_SYLLABLE; |
724 | 1.89M | prev_vowel = n_ph_list2; |
725 | | |
726 | 1.89M | if (stress > max_stress) { |
727 | 695k | max_stress = stress; |
728 | 695k | max_stress_ix = n_ph_list2; |
729 | 695k | } |
730 | 1.89M | if (next_tone != 0) { |
731 | 557 | ph_list2[n_ph_list2].tone_ph = next_tone; |
732 | 557 | next_tone = 0; |
733 | 557 | } |
734 | 2.91M | } else { |
735 | 2.91M | if (first_phoneme && tr->prev_dict_flags[0] & FLAG_DOUBLING) { |
736 | | // double the initial consonant if the previous word is marked with a flag |
737 | 2 | ph_list2[n_ph_list2].synthflags |= SFLAG_LENGTHEN; |
738 | 2 | } |
739 | 2.91M | } |
740 | | |
741 | 4.81M | ph_list2[n_ph_list2].stresslevel = stress; |
742 | 4.81M | n_ph_list2++; |
743 | 4.81M | first_phoneme = false; |
744 | 4.81M | } |
745 | 6.70M | } |
746 | | // From here, we may require up to 3 phonemes |
747 | | |
748 | | // This may require up to 1 phoneme |
749 | 948k | if (word_flags & FLAG_COMMA_AFTER) |
750 | 125 | SetPlist2(&ph_list2[n_ph_list2++], phonPAUSE_CLAUSE); |
751 | | |
752 | | // don't set new-word if there is a hyphen before it |
753 | 948k | if ((word_flags & FLAG_HYPHEN) == 0) |
754 | 940k | plist2->sourceix = source_ix; |
755 | | |
756 | 948k | tr->end_stressed_vowel = 0; |
757 | 948k | if ((stress >= 4) && (phoneme_tab[ph_list2[n_ph_list2-1].phcode]->type == phVOWEL)) |
758 | 136k | tr->end_stressed_vowel = 1; // word ends with a stressed vowel |
759 | | |
760 | | // This may require up to 1 phoneme |
761 | 948k | if (switch_phonemes >= 0) { |
762 | | // this word uses a different phoneme table, now switch back |
763 | 116k | strcpy(dictionary_name, old_dictionary_name); |
764 | 116k | SelectPhonemeTable(voice->phoneme_tab_ix); |
765 | 116k | SetPlist2(&ph_list2[n_ph_list2], phonSWITCH); |
766 | 116k | ph_list2[n_ph_list2++].tone_ph = voice->phoneme_tab_ix; // original phoneme table number |
767 | 116k | } |
768 | | |
769 | | |
770 | | // This may require up to 1 phoneme |
771 | 948k | if (pitch_raised > 0) { |
772 | 0 | embedded_list[embedded_ix++] = EMBED_P+0x60+0x80 + (pitch_raised << 8); // lower pitch |
773 | 0 | SetPlist2(&ph_list2[n_ph_list2], phonPAUSE_SHORT); |
774 | 0 | ph_list2[n_ph_list2++].synthflags = SFLAG_EMBEDDED; |
775 | 0 | } |
776 | | |
777 | 948k | if (flags & FLAG_STRESS_END2) { |
778 | | // this's word's stress could be increased later |
779 | 2.21k | ph_list2[max_stress_ix].synthflags |= SFLAG_PROMOTE_STRESS; |
780 | 2.21k | } |
781 | | |
782 | 948k | tr->prev_dict_flags[0] = flags; |
783 | 948k | return flags; |
784 | 974k | } |
785 | | |
786 | | static int EmbeddedCommand(unsigned int *source_index_out) |
787 | 67.3k | { |
788 | | // An embedded command to change the pitch, volume, etc. |
789 | | // returns number of commands added to embedded_list |
790 | | |
791 | | // pitch,speed,amplitude,expression,reverb,tone,voice,sayas |
792 | 67.3k | const char *commands = "PSARHTIVYMUBF"; |
793 | 67.3k | int value = -1; |
794 | 67.3k | int sign = 0; |
795 | 67.3k | unsigned char c; |
796 | 67.3k | char *p; |
797 | 67.3k | int cmd; |
798 | 67.3k | int source_index = *source_index_out; |
799 | | |
800 | 67.3k | c = source[source_index]; |
801 | 67.3k | if (c == '+') { |
802 | 1.98k | sign = 0x40; |
803 | 1.98k | source_index++; |
804 | 65.3k | } else if (c == '-') { |
805 | 4.11k | sign = 0x60; |
806 | 4.11k | source_index++; |
807 | 4.11k | } |
808 | | |
809 | 67.3k | if (IsDigit09(source[source_index])) { |
810 | 15.6k | value = atoi(&source[source_index]); |
811 | 55.3k | while (IsDigit09(source[source_index])) |
812 | 39.6k | source_index++; |
813 | 15.6k | } |
814 | | |
815 | 67.3k | c = source[source_index++]; |
816 | 67.3k | if (embedded_ix >= (N_EMBEDDED_LIST - 2)) |
817 | 218 | return 0; // list is full |
818 | | |
819 | 67.1k | if ((p = strchr_w(commands, c)) == NULL) |
820 | 30.2k | return 0; |
821 | 36.8k | cmd = (p - commands)+1; |
822 | 36.8k | if (value == -1) { |
823 | 24.0k | value = embedded_default[cmd]; |
824 | 24.0k | sign = 0; |
825 | 24.0k | } |
826 | | |
827 | 36.8k | if (cmd == EMBED_Y) { |
828 | 3.53k | option_sayas2 = value; |
829 | 3.53k | count_sayas_digits = 0; |
830 | 3.53k | } |
831 | 36.8k | if (cmd == EMBED_F) { |
832 | 2.25k | if (value >= 3) |
833 | 412 | word_emphasis = FLAG_EMPHASIZED; |
834 | 1.84k | else |
835 | 1.84k | word_emphasis = 0; |
836 | 2.25k | } |
837 | | |
838 | 36.8k | embedded_list[embedded_ix++] = cmd + sign + (value << 8); |
839 | 36.8k | *source_index_out = source_index; |
840 | 36.8k | return 1; |
841 | 67.1k | } |
842 | | |
843 | | static const char *FindReplacementChars(Translator *tr, const char **pfrom, unsigned int c, const char *next, int *ignore_next_n) |
844 | 1.95M | { |
845 | 1.95M | const char *from = *pfrom; |
846 | 50.6M | while ( !is_str_totally_null(from, 4) ) { |
847 | 48.6M | unsigned int fc = 0; // from character |
848 | 48.6M | unsigned int nc = c; // next character |
849 | 48.6M | const char *match_next = next; |
850 | | |
851 | 48.6M | *pfrom = from; |
852 | | |
853 | 48.6M | from += utf8_in((int *)&fc, from); |
854 | 48.6M | if (nc == fc) { |
855 | 66.7k | if (*from == 0) return from + 1; |
856 | | |
857 | 66.7k | bool matched = true; |
858 | 45.2k | int nmatched = 0; |
859 | 90.5k | while (*from != 0) { |
860 | 45.2k | from += utf8_in((int *)&fc, from); |
861 | | |
862 | 45.2k | match_next += utf8_in((int *)&nc, match_next); |
863 | 45.2k | nc = towlower2(nc, tr); |
864 | | |
865 | 45.2k | if (nc != fc) |
866 | 44.8k | matched = false; |
867 | 419 | else |
868 | 419 | nmatched++; |
869 | 45.2k | } |
870 | | |
871 | 45.2k | if (matched) { |
872 | 419 | *ignore_next_n = nmatched; |
873 | 419 | return from + 1; |
874 | 419 | } |
875 | 45.2k | } |
876 | | |
877 | | // replacement 'from' string (skip the remaining part, if any) |
878 | 120M | while (*from != '\0') from++; |
879 | 48.6M | from++; |
880 | | |
881 | | // replacement 'to' string |
882 | 163M | while (*from != '\0') from++; |
883 | 48.6M | from++; |
884 | 48.6M | } |
885 | 1.92M | return NULL; |
886 | 1.95M | } |
887 | | |
888 | | // handle .replace rule in xx_rules file |
889 | | static int SubstituteChar(Translator *tr, unsigned int c, unsigned int next_in, const char *next, int *insert, int *wordflags) |
890 | 4.03M | { |
891 | 4.03M | unsigned int new_c, c2 = ' ', c_lower; |
892 | 4.03M | int upper_case = 0; |
893 | | |
894 | 4.03M | static int ignore_next_n = 0; |
895 | 4.03M | if (ignore_next_n > 0) { |
896 | 419 | ignore_next_n--; |
897 | 419 | return 8; |
898 | 419 | } |
899 | | |
900 | 4.03M | if (c == 0) return 0; |
901 | | |
902 | 4.03M | const char *from = (const char *)tr->langopts.replace_chars; |
903 | 4.03M | if (from == NULL) |
904 | 2.08M | return c; |
905 | | |
906 | | // there is a list of character codes to be substituted with alternative codes |
907 | | |
908 | 1.95M | if (iswupper(c_lower = c)) { |
909 | 161k | c_lower = towlower2(c, tr); |
910 | 161k | upper_case = 1; |
911 | 161k | } |
912 | | |
913 | 1.95M | const char *to = FindReplacementChars(tr, &from, c_lower, next, &ignore_next_n); |
914 | 1.95M | if (to == NULL) |
915 | 1.92M | return c; // no substitution |
916 | | |
917 | 21.8k | if (option_phonemes & espeakPHONEMES_TRACE) |
918 | 0 | fprintf(f_trans, "Replace: %s > %s\n", from, to); |
919 | | |
920 | 21.8k | to += utf8_in((int *)&new_c, to); |
921 | 21.8k | if (*to != 0) { |
922 | | // there is a second character to be inserted |
923 | | // don't convert the case of the second character unless the next letter is also upper case |
924 | 352 | to += utf8_in((int *)&c2, to); |
925 | 352 | if (upper_case && iswupper(next_in)) |
926 | 5 | c2 = ucd_toupper(c2); |
927 | 352 | *insert = c2; |
928 | 352 | } |
929 | | |
930 | 21.8k | if (upper_case) |
931 | 5.55k | new_c = ucd_toupper(new_c); |
932 | | |
933 | 21.8k | *wordflags |= FLAG_CHAR_REPLACED; |
934 | 21.8k | return new_c; |
935 | 1.95M | } |
936 | | |
937 | | static int TranslateChar(Translator *tr, char *ptr, int prev_in, unsigned int c, unsigned int next_in, int *insert, int *wordflags) |
938 | 4.04M | { |
939 | | // To allow language specific examination and replacement of characters |
940 | | |
941 | 4.04M | int code; |
942 | 4.04M | int next2; |
943 | | |
944 | 4.04M | static const unsigned char hangul_compatibility[0x34] = { |
945 | 4.04M | 0, 0x00, 0x01, 0xaa, 0x02, 0xac, 0xad, 0x03, |
946 | 4.04M | 0x04, 0x05, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb4, |
947 | 4.04M | 0xb6, 0x06, 0x07, 0x08, 0xb9, 0x09, 0x0a, 0xbc, |
948 | 4.04M | 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x61, |
949 | 4.04M | 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, |
950 | 4.04M | 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, |
951 | 4.04M | 0x72, 0x73, 0x74, 0x75 |
952 | 4.04M | }; |
953 | | |
954 | | // check for Korean Hangul letters |
955 | 4.04M | if (((code = c - 0xac00) >= 0) && (c <= 0xd7af)) { |
956 | | // break a syllable hangul into 2 or 3 individual jamo |
957 | 8.56k | int initial = (code/28)/21; |
958 | 8.56k | int medial = (code/28) % 21; |
959 | 8.56k | int final = code % 28; |
960 | | |
961 | 8.56k | if (initial == 11) { |
962 | | // null initial |
963 | 4.35k | c = medial + 0x1161; |
964 | 4.35k | if (final > 0) |
965 | 4.13k | *insert = final + 0x11a7; |
966 | 4.35k | } else { |
967 | | // extract the initial and insert the remainder with a null initial |
968 | 4.21k | c = initial + 0x1100; |
969 | 4.21k | *insert = (11*28*21) + (medial*28) + final + 0xac00; |
970 | 4.21k | } |
971 | 8.56k | return c; |
972 | 4.03M | } else if (((code = c - 0x3130) >= 0) && (code < 0x34)) { |
973 | | // Hangul compatibility jamo |
974 | 453 | return hangul_compatibility[code] + 0x1100; |
975 | 453 | } |
976 | | |
977 | 4.03M | switch (tr->translator_name) |
978 | 4.03M | { |
979 | 695 | case L('a', 'f'): |
980 | 49.1k | case L('n', 'l'): |
981 | | // look for 'n and replace by a special character (unicode: schwa) |
982 | | |
983 | 49.1k | if ((c == '\'') && !iswalpha(prev_in)) { |
984 | 409 | utf8_in(&next2, &ptr[1]); |
985 | | |
986 | 409 | if (IsSpace(next2)) { |
987 | 236 | if ((next_in == 'n') && (tr->translator_name == L('a', 'f'))) { |
988 | | // n preceded by either apostrophe or U2019 "right single quotation mark" |
989 | 0 | ptr[0] = ' '; // delete the n |
990 | 0 | return 0x0259; // replace ' by unicode schwa character |
991 | 0 | } |
992 | 236 | if ((next_in == 'n') || (next_in == 't')) { |
993 | | // Dutch, [@n] and [@t] |
994 | 0 | return 0x0259; // replace ' by unicode schwa character |
995 | 0 | } |
996 | 236 | } |
997 | 409 | } |
998 | 49.1k | break; |
999 | 4.03M | } |
1000 | | // handle .replace rule in xx_rules file |
1001 | 4.03M | return SubstituteChar(tr, c, next_in, ptr, insert, wordflags); |
1002 | 4.03M | } |
1003 | | |
1004 | | static const char *const UCase_ga[] = { "bp", "bhf", "dt", "gc", "hA", "mb", "nd", "ng", "ts", "tA", "nA", NULL }; |
1005 | | |
1006 | | static int UpperCaseInWord(Translator *tr, char *word, int c) |
1007 | 16.2k | { |
1008 | 16.2k | if (tr->translator_name == L('g', 'a')) { |
1009 | 863 | int ix; |
1010 | 863 | const char *p; |
1011 | | |
1012 | 10.1k | for (ix = 0;; ix++) { |
1013 | 10.1k | int len; |
1014 | 10.1k | if ((p = UCase_ga[ix]) == NULL) |
1015 | 750 | break; |
1016 | | |
1017 | 9.38k | len = strlen(p); |
1018 | 9.38k | if ((word[-len] == ' ') && (memcmp(&word[-len+1], p, len-1) == 0)) { |
1019 | 801 | if ((c == p[len-1]) || ((p[len-1] == 'A') && IsVowel(tr, c))) |
1020 | 113 | return 1; |
1021 | 801 | } |
1022 | 9.38k | } |
1023 | 863 | } |
1024 | 16.0k | return 0; |
1025 | 16.2k | } |
1026 | | |
1027 | | // Same as TranslateClause except we also get the clause terminator used (full stop, comma, etc.). |
1028 | | // Used by espeak_TextToPhonemesWithTerminator. |
1029 | | void TranslateClauseWithTerminator(Translator *tr, int *tone_out, char **voice_change, int *terminator_out) |
1030 | 72.9k | { |
1031 | 72.9k | int ix; |
1032 | 72.9k | int c; |
1033 | 72.9k | int cc = 0; |
1034 | 72.9k | unsigned int source_index = 0; |
1035 | 72.9k | int source_index_word = 0; |
1036 | 72.9k | int prev_in; |
1037 | 72.9k | int prev_out = ' '; |
1038 | 72.9k | int prev_in_save = 0; |
1039 | 72.9k | int next_in; |
1040 | 72.9k | int next_in_nbytes; |
1041 | 72.9k | int char_inserted = 0; |
1042 | 72.9k | int clause_pause; |
1043 | 72.9k | int pre_pause_add = 0; |
1044 | 72.9k | int all_upper_case = FLAG_ALL_UPPER; |
1045 | 72.9k | int alpha_count = 0; |
1046 | 72.9k | bool finished = false; |
1047 | 72.9k | bool single_quoted = false; |
1048 | 72.9k | bool phoneme_mode = false; |
1049 | 72.9k | int dict_flags = 0; // returned from dictionary lookup |
1050 | 72.9k | int word_flags; // set here |
1051 | 72.9k | int next_word_flags; |
1052 | 72.9k | bool new_sentence2; |
1053 | 72.9k | int embedded_count = 0; |
1054 | 72.9k | int letter_count = 0; |
1055 | 72.9k | bool space_inserted = false; |
1056 | 72.9k | bool syllable_marked = false; |
1057 | 72.9k | bool decimal_sep_count = false; |
1058 | 72.9k | char *word; |
1059 | 72.9k | char *p; |
1060 | 72.9k | int j, k; |
1061 | 72.9k | int n_digits; |
1062 | 72.9k | int charix_top = 0; |
1063 | | |
1064 | 72.9k | short charix[N_TR_SOURCE+4]; |
1065 | 72.9k | WORD_TAB words[N_CLAUSE_WORDS]; |
1066 | 72.9k | static char voice_change_name[40]; |
1067 | 72.9k | int word_count = 0; // index into words |
1068 | | |
1069 | 72.9k | char sbuf[N_TR_SOURCE]; |
1070 | | |
1071 | 72.9k | int terminator; |
1072 | 72.9k | int tone; |
1073 | | |
1074 | 72.9k | if (tr == NULL) |
1075 | 0 | return; |
1076 | | |
1077 | 72.9k | MAKE_MEM_UNDEFINED(&voice_change_name, sizeof(voice_change_name)); |
1078 | | |
1079 | 72.9k | embedded_ix = 0; |
1080 | 72.9k | embedded_read = 0; |
1081 | 72.9k | pre_pause = 0; |
1082 | 72.9k | any_stressed_words = false; |
1083 | | |
1084 | 72.9k | if ((clause_start_char = count_characters) < 0) |
1085 | 2.37k | clause_start_char = 0; |
1086 | 72.9k | clause_start_word = count_words + 1; |
1087 | | |
1088 | 58.4M | for (ix = 0; ix < N_TR_SOURCE; ix++) |
1089 | 58.3M | charix[ix] = 0; |
1090 | 72.9k | MAKE_MEM_UNDEFINED(&source, sizeof(source)); |
1091 | 72.9k | terminator = ReadClause(tr, source, charix, &charix_top, N_TR_SOURCE, &tone, voice_change_name); |
1092 | | |
1093 | 72.9k | if (terminator_out != NULL) { |
1094 | 0 | *terminator_out = terminator; |
1095 | 0 | } |
1096 | | |
1097 | 72.9k | if (tone_out != NULL) { |
1098 | 72.9k | if (tone == 0) |
1099 | 71.2k | *tone_out = (terminator & CLAUSE_INTONATION_TYPE) >> 12; // tone type not overridden in ReadClause, use default |
1100 | 1.62k | else |
1101 | 1.62k | *tone_out = tone; // override tone type |
1102 | 72.9k | } |
1103 | | |
1104 | 72.9k | charix[charix_top+1] = 0; |
1105 | 72.9k | charix[charix_top+2] = 0x7fff; |
1106 | 72.9k | charix[charix_top+3] = 0; |
1107 | | |
1108 | 72.9k | clause_pause = (terminator & CLAUSE_PAUSE) * 10; // mS |
1109 | 72.9k | if (terminator & CLAUSE_PAUSE_LONG) |
1110 | 0 | clause_pause = clause_pause * 32; // pause value is *320mS not *10mS |
1111 | | |
1112 | 316k | for (p = source; *p != 0; p++) { |
1113 | 303k | if (!isspace2(*p)) |
1114 | 59.3k | break; |
1115 | 303k | } |
1116 | 72.9k | if (*p == 0) { |
1117 | | // No characters except spaces. This is not a sentence. |
1118 | | // Don't add this pause, just make up the previous pause to this value; |
1119 | 13.5k | clause_pause -= max_clause_pause; |
1120 | 13.5k | if (clause_pause < 0) |
1121 | 2.13k | clause_pause = 0; |
1122 | | |
1123 | 13.5k | if (new_sentence) |
1124 | 6.94k | terminator |= CLAUSE_TYPE_SENTENCE; // carry forward an end-of-sentence indicator |
1125 | 13.5k | max_clause_pause += clause_pause; |
1126 | 13.5k | new_sentence2 = false; |
1127 | 59.3k | } else { |
1128 | 59.3k | max_clause_pause = clause_pause; |
1129 | 59.3k | new_sentence2 = new_sentence; |
1130 | 59.3k | } |
1131 | 72.9k | tr->clause_terminator = terminator; |
1132 | | |
1133 | 72.9k | if (new_sentence2) { |
1134 | 40.8k | count_sentences++; |
1135 | 40.8k | if (skip_sentences > 0) { |
1136 | 0 | skip_sentences--; |
1137 | 0 | if (skip_sentences == 0) |
1138 | 0 | skipping_text = false; |
1139 | 0 | } |
1140 | 40.8k | } |
1141 | | |
1142 | 72.9k | MAKE_MEM_UNDEFINED(&ph_list2, sizeof(ph_list2)); |
1143 | 72.9k | memset(&ph_list2[0], 0, sizeof(ph_list2[0])); |
1144 | 72.9k | ph_list2[0].phcode = phonPAUSE_SHORT; |
1145 | | |
1146 | 72.9k | n_ph_list2 = 1; |
1147 | 72.9k | tr->prev_last_stress = 0; |
1148 | 72.9k | tr->prepause_timeout = 0; |
1149 | 72.9k | tr->expect_verb = 0; |
1150 | 72.9k | tr->expect_noun = 0; |
1151 | 72.9k | tr->expect_past = 0; |
1152 | 72.9k | tr->expect_verb_s = 0; |
1153 | 72.9k | tr->phonemes_repeat_count = 0; |
1154 | 72.9k | tr->end_stressed_vowel = 0; |
1155 | 72.9k | tr->prev_dict_flags[0] = 0; |
1156 | 72.9k | tr->prev_dict_flags[1] = 0; |
1157 | | |
1158 | 72.9k | word_count = 0; |
1159 | 72.9k | word_flags = 0; |
1160 | 72.9k | next_word_flags = 0; |
1161 | | |
1162 | 72.9k | sbuf[0] = 0; |
1163 | 72.9k | sbuf[1] = ' '; |
1164 | 72.9k | sbuf[2] = ' '; |
1165 | 72.9k | ix = 3; |
1166 | 72.9k | prev_in = ' '; |
1167 | | |
1168 | 72.9k | words[0].start = ix; |
1169 | 72.9k | words[0].flags = 0; |
1170 | | |
1171 | 72.9k | words[0].length = CalcWordLength(source_index, charix_top, charix, words, 0); |
1172 | | |
1173 | 72.9k | int prev_out2; |
1174 | 5.90M | while (!finished && (ix < (int)sizeof(sbuf) - 1)) { |
1175 | 5.83M | prev_out2 = prev_out; |
1176 | 5.83M | utf8_in2(&prev_out, &sbuf[ix-1], 1); |
1177 | | |
1178 | 5.83M | if (tr->langopts.tone_numbers && IsDigit09(prev_out) && IsAlpha(prev_out2)) { |
1179 | | // tone numbers can be part of a word, consider them as alphabetic |
1180 | 424 | prev_out = 'a'; |
1181 | 424 | } |
1182 | | |
1183 | 5.83M | if (prev_in_save != 0) { |
1184 | 114k | prev_in = prev_in_save; |
1185 | 114k | prev_in_save = 0; |
1186 | 5.71M | } else if (source_index > 0) |
1187 | 5.64M | utf8_in2(&prev_in, &source[source_index-1], 1); |
1188 | | |
1189 | 5.83M | unsigned int prev_source_index = source_index; |
1190 | | |
1191 | 5.83M | if (char_inserted) { |
1192 | 7.69k | c = char_inserted; |
1193 | 7.69k | char_inserted = 0; |
1194 | 5.82M | } else { |
1195 | 5.82M | source_index += utf8_in(&cc, &source[source_index]); |
1196 | 5.82M | c = cc; |
1197 | 5.82M | } |
1198 | | |
1199 | 5.83M | if (c == 0) { |
1200 | 72.9k | finished = true; |
1201 | 72.9k | c = ' '; |
1202 | 72.9k | next_in = ' '; |
1203 | 72.9k | next_in_nbytes = 0; |
1204 | 72.9k | } |
1205 | 5.75M | else |
1206 | 5.75M | next_in_nbytes = utf8_in(&next_in, &source[source_index]); |
1207 | | |
1208 | 5.83M | if (c == CTRL_EMBEDDED) { |
1209 | | // start of embedded command in the text |
1210 | 96.0k | int srcix = source_index-1; |
1211 | | |
1212 | 96.0k | if (prev_in != ' ') { |
1213 | 28.7k | c = ' '; |
1214 | 28.7k | prev_in_save = c; |
1215 | 28.7k | source_index--; |
1216 | 67.3k | } else { |
1217 | 67.3k | embedded_count += EmbeddedCommand(&source_index); |
1218 | 67.3k | prev_in_save = prev_in; |
1219 | | // replace the embedded command by spaces |
1220 | 67.3k | memset(&source[srcix], ' ', source_index-srcix); |
1221 | 67.3k | source_index = srcix; |
1222 | 67.3k | continue; |
1223 | 67.3k | } |
1224 | 96.0k | } |
1225 | | |
1226 | 5.76M | if ((option_sayas2 == SAYAS_KEY) && (c != ' ')) { |
1227 | 7.57k | if ((prev_in == ' ') && (next_in == ' ')) |
1228 | 434 | option_sayas2 = SAYAS_SINGLE_CHARS; // single character, speak its name |
1229 | 7.57k | c = towlower2(c, tr); |
1230 | 7.57k | } |
1231 | | |
1232 | | |
1233 | 5.76M | if (phoneme_mode) { |
1234 | 1.45M | all_upper_case = FLAG_PHONEMES; |
1235 | | |
1236 | 1.45M | if ((c == ']') && (next_in == ']')) { |
1237 | 87.2k | phoneme_mode = false; |
1238 | 87.2k | source_index++; |
1239 | 87.2k | c = ' '; |
1240 | 87.2k | } |
1241 | 4.30M | } else if ((option_sayas2 & 0xf0) == SAYAS_DIGITS) { |
1242 | 132k | if (iswdigit(c)) { |
1243 | 10.9k | count_sayas_digits++; |
1244 | 10.9k | if (count_sayas_digits > (option_sayas2 & 0xf)) { |
1245 | | // break after the specified number of digits |
1246 | 174 | c = ' '; |
1247 | 174 | space_inserted = true; |
1248 | 174 | count_sayas_digits = 0; |
1249 | 174 | } |
1250 | 121k | } else { |
1251 | 121k | count_sayas_digits = 0; |
1252 | 121k | if (iswdigit(prev_out)) { |
1253 | 9.48k | c = ' '; |
1254 | 9.48k | space_inserted = true; |
1255 | 9.48k | } |
1256 | 121k | } |
1257 | 4.17M | } else if ((option_sayas2 & 0x10) == 0) { |
1258 | | // speak as words |
1259 | | |
1260 | 4.04M | if ((c == 0x92) || (c == 0xb4) || (c == 0x2019) || (c == 0x2032)) |
1261 | 133 | c = '\''; // 'microsoft' quote or sexed closing single quote, or prime - possibly used as apostrophe |
1262 | | |
1263 | 4.04M | if (((c == 0x2018) || (c == '?')) && IsAlpha(prev_out) && IsAlpha(next_in)) { |
1264 | | // ? between two letters may be a smart-quote replaced by ? |
1265 | 1.11k | c = '\''; |
1266 | 1.11k | } |
1267 | | |
1268 | 4.04M | if (c == CHAR_EMPHASIS) { |
1269 | | // this character is a marker that the previous word is the focus of the clause |
1270 | 3.07k | c = ' '; |
1271 | 3.07k | word_flags |= FLAG_FOCUS; |
1272 | 3.07k | } |
1273 | | |
1274 | 4.04M | if (c == CHAR_COMMA_BREAK) { |
1275 | 191 | c = ' '; |
1276 | 191 | word_flags |= FLAG_COMMA_AFTER; |
1277 | 191 | } |
1278 | | // language specific character translations |
1279 | 4.04M | c = TranslateChar(tr, &source[source_index], prev_in, c, next_in, &char_inserted, &word_flags); |
1280 | 4.04M | if (c == 8) |
1281 | 3.53k | continue; // ignore this character |
1282 | | |
1283 | 4.04M | if ((c == 0xfe0e) || (c == 0xfe0f)) |
1284 | 27 | continue; // variation selector (text/emoji presentation style), not spoken; |
1285 | | // emoji dictionary keys are stripped of these at compile time to match |
1286 | | |
1287 | 4.04M | if (char_inserted) |
1288 | 8.70k | next_in = char_inserted; |
1289 | | |
1290 | | // allow certain punctuation within a word (usually only apostrophe) |
1291 | 4.04M | if (!IsAlpha(c) && !IsSpace(c) && (wcschr(tr->punct_within_word, c) == 0)) { |
1292 | 1.36M | if (IsAlpha(prev_out)) { |
1293 | 111k | if (tr->langopts.tone_numbers && IsDigit09(c) && !IsDigit09(next_in)) { |
1294 | | // allow a tone number as part of the word |
1295 | 111k | } else if ((c == 0x200d) && IsEmoji(prev_out) && IsEmoji(next_in)) { |
1296 | | // keep U+200D ZERO WIDTH JOINER between two emoji in the word, |
1297 | | // so the sequence can match a multi-codepoint emoji dictionary |
1298 | | // entry (e.g. woman + ZWJ + microscope = woman scientist) |
1299 | 111k | } else if (IsEmojiTag(c) && IsEmoji(prev_out)) { |
1300 | | // keep the tag characters of an emoji tag sequence in the |
1301 | | // word, so that it can match its dictionary entry (black |
1302 | | // flag + the tag letters "gbsct" + cancel tag = flag of |
1303 | | // Scotland). The following tag characters get here with a |
1304 | | // tag character as prev_out, which is not IsAlpha, so they |
1305 | | // are kept without needing a case of their own. |
1306 | 111k | } else { |
1307 | 111k | c = ' '; // ensure we have an end-of-word terminator |
1308 | 111k | space_inserted = true; |
1309 | 111k | } |
1310 | 111k | } |
1311 | 1.36M | } |
1312 | | |
1313 | 4.04M | if (iswdigit(prev_out)) { |
1314 | 292k | if (!iswdigit(c) && (c != '.') && (c != ',') && (c != ' ')) { |
1315 | 59.8k | c = ' '; // terminate digit string with a space |
1316 | 59.8k | space_inserted = true; |
1317 | 59.8k | } |
1318 | 3.74M | } else { // Prev output is not digit |
1319 | 3.74M | if (prev_in == ',') { |
1320 | | // Workaround for several consecutive commas — |
1321 | | // replace current character with space |
1322 | 19.2k | if (c == ',') |
1323 | 1 | c = ' '; |
1324 | 3.72M | } else { |
1325 | 3.72M | decimal_sep_count = false; |
1326 | 3.72M | } |
1327 | 3.74M | } |
1328 | | |
1329 | 4.04M | if (c == '[') { |
1330 | 101k | if ((next_in == '\002') || ((next_in == '[') && option_phoneme_input)) { |
1331 | | // "[\002" is used internally to start phoneme mode |
1332 | 89.2k | phoneme_mode = true; |
1333 | 89.2k | source_index++; |
1334 | 89.2k | continue; |
1335 | 89.2k | } |
1336 | 101k | } |
1337 | | |
1338 | 3.95M | if (IsAlpha(c)) { |
1339 | 955k | bool emoji_join = false; |
1340 | 955k | bool emoji_break = false; |
1341 | | |
1342 | 955k | alpha_count++; |
1343 | 955k | if ((prev_out == 0x200d) && IsEmoji(c)) { |
1344 | | // U+200D ZERO WIDTH JOINER: continue the emoji sequence only if |
1345 | | // the current word began with an emoji; a word-initial ZWJ (stray |
1346 | | // joiner in the input) must not absorb the following emoji, which |
1347 | | // would prevent its dictionary lookup |
1348 | 2 | int wc_first; |
1349 | 2 | utf8_in(&wc_first, &sbuf[words[word_count].start]); |
1350 | 2 | emoji_join = IsEmoji(wc_first); |
1351 | 955k | } else if (IsEmoji(c) && IsEmoji(prev_out)) { |
1352 | | // Two emoji written next to each other. They only belong to the |
1353 | | // same token when they form one of the sequences that the emoji |
1354 | | // dictionary has a key for; otherwise each emoji must become its |
1355 | | // own word, or the whole run fails its dictionary lookup and is |
1356 | | // spelled out codepoint by codepoint. |
1357 | 984 | if (IsEmojiModifier(c)) { |
1358 | | // skin tone modifier, part of the preceding emoji |
1359 | 950 | } else if (IsRegionalIndicator(c) && IsRegionalIndicator(prev_out)) { |
1360 | | // A flag is exactly two regional indicators (e.g. 🇰🇿), so |
1361 | | // count how many of them the word already holds: an odd |
1362 | | // count means this one completes a flag, an even count |
1363 | | // means it starts the next one. |
1364 | 24 | int n_ri = 0; |
1365 | 24 | int j = ix; |
1366 | | |
1367 | 54 | while (j > (int)words[word_count].start) { |
1368 | 30 | int wc_ri; |
1369 | 30 | utf8_in2(&wc_ri, &sbuf[j-1], 1); |
1370 | 30 | if (!IsRegionalIndicator(wc_ri)) |
1371 | 0 | break; |
1372 | 30 | n_ri++; |
1373 | 30 | j -= 4; // a regional indicator is 4 bytes in UTF-8 |
1374 | 30 | } |
1375 | 24 | emoji_break = ((n_ri & 1) == 0); |
1376 | 24 | } else |
1377 | 926 | emoji_break = true; |
1378 | 984 | } |
1379 | | // Start a new word where an adjacent letter belongs to a script |
1380 | | // that is foreign to the base language (e.g. Latin glued to |
1381 | | // Malayalam, as in "BJPയുമായുള്ള"), so each run is translated in |
1382 | | // its own language rather than the whole token being spelled out |
1383 | | // letter by letter. Comparing foreign-ness to the base language |
1384 | | // (rather than raw alphabet identity) keeps the language's own |
1385 | | // script joined, including ranges outside the alphabet table such |
1386 | | // as Greek Extended. |
1387 | 955k | const ALPHABET *alpha_c = AlphabetFromChar(c); |
1388 | 955k | const ALPHABET *alpha_prev = AlphabetFromChar(prev_out); |
1389 | 955k | bool c_foreign = (alpha_c != NULL) && (alpha_c->offset != tr->letter_bits_offset); |
1390 | 955k | bool prev_foreign = (alpha_prev != NULL) && (alpha_prev->offset != tr->letter_bits_offset); |
1391 | 955k | if (emoji_join) { |
1392 | | // continuation of a ZWJ emoji sequence: not a word boundary |
1393 | 955k | } else if (emoji_break || !IsAlpha(prev_out) || ShouldSplitIdeographs(tr, c, prev_out) || (IsEmoji(c) != IsEmoji(prev_out)) || (c_foreign != prev_foreign)) { |
1394 | 425k | if (wcschr(tr->punct_within_word, prev_out) == 0) |
1395 | 418k | letter_count = 0; // don't reset count for an apostrophy within a word |
1396 | | |
1397 | 425k | if ((prev_out != ' ') && (wcschr(tr->punct_within_word, prev_out) == 0)) { |
1398 | | // start of word, insert space if not one there already |
1399 | 105k | c = ' '; |
1400 | 105k | space_inserted = true; |
1401 | | |
1402 | 105k | if (!IsBracket(prev_out)) // ?? perhaps only set FLAG_NOSPACE for . - / (hyphenated words, URLs, etc) |
1403 | 92.5k | next_word_flags |= FLAG_NOSPACE; |
1404 | 320k | } else { |
1405 | 320k | if (iswupper(c)) |
1406 | 118k | word_flags |= FLAG_FIRST_UPPER; |
1407 | | |
1408 | 320k | if ((prev_out == ' ') && iswdigit(sbuf[ix-2]) && !iswdigit(prev_in)) { |
1409 | | // word, following a number, but with a space between |
1410 | | // Add an extra space, to distinguish "2 a" from "2a" |
1411 | 12.2k | sbuf[ix++] = ' '; |
1412 | 12.2k | words[word_count].start++; |
1413 | 12.2k | } |
1414 | 320k | } |
1415 | 425k | } |
1416 | | |
1417 | 955k | if (c != ' ') { |
1418 | 849k | letter_count++; |
1419 | | |
1420 | 849k | if (tr->letter_bits_offset > 0) { |
1421 | 330k | if (((c < 0x250) && (prev_out >= tr->letter_bits_offset)) || |
1422 | 329k | ((c >= tr->letter_bits_offset) && (letter_count > 1) && (prev_out < 0x250))) { |
1423 | | // Don't mix native and Latin characters in the same word |
1424 | | // Break into separate words |
1425 | 1.89k | if (IsAlpha(prev_out)) { |
1426 | 1.30k | c = ' '; |
1427 | 1.30k | space_inserted = true; |
1428 | 1.30k | word_flags |= FLAG_HYPHEN_AFTER; |
1429 | 1.30k | next_word_flags |= FLAG_HYPHEN; |
1430 | 1.30k | } |
1431 | 1.89k | } |
1432 | 330k | } |
1433 | 849k | } |
1434 | | |
1435 | 955k | if (iswupper(c)) { |
1436 | 318k | c = towlower2(c, tr); |
1437 | | |
1438 | 318k | if (tr->langopts.param[LOPT_CAPS_IN_WORD]) { |
1439 | 0 | if (syllable_marked == false) { |
1440 | 0 | char_inserted = c; |
1441 | 0 | c = 0x2c8; // stress marker |
1442 | 0 | syllable_marked = true; |
1443 | 0 | } |
1444 | 318k | } else { |
1445 | 318k | if (iswlower(prev_in)) { |
1446 | | // lower case followed by upper case, possibly CamelCase |
1447 | 17.3k | if ((prev_out != ' ') && UpperCaseInWord(tr, &sbuf[ix], c) == 0) |
1448 | 16.0k | { // start a new word |
1449 | 16.0k | c = ' '; |
1450 | 16.0k | space_inserted = true; |
1451 | 16.0k | prev_in_save = c; |
1452 | 16.0k | } |
1453 | 301k | } else if ((c != ' ') && iswupper(prev_in) && iswlower(next_in)) { |
1454 | 3.52k | int next2_in; |
1455 | 3.52k | utf8_in(&next2_in, &source[source_index + next_in_nbytes]); |
1456 | | |
1457 | 3.52k | if ((tr->translator_name == L('n', 'l')) && (letter_count == 2) && (c == 'j') && (prev_in == 'I')) { |
1458 | | // Dutch words may capitalise initial IJ, don't split |
1459 | 3.52k | } else if ((prev_out != ' ') && IsAlpha(next2_in)) { |
1460 | | // changing from upper to lower case, start new word at the last uppercase, if 3 or more letters |
1461 | 2.08k | c = ' '; |
1462 | 2.08k | space_inserted = true; |
1463 | 2.08k | prev_in_save = c; |
1464 | 2.08k | next_word_flags |= FLAG_NOSPACE; |
1465 | 2.08k | } |
1466 | 3.52k | } |
1467 | 318k | } |
1468 | 636k | } else { |
1469 | 636k | if ((all_upper_case) && (letter_count > 2)) { |
1470 | | // Flag as plural only English |
1471 | 2.96k | if (tr->translator_name == L('e', 'n') && (c == 's') && (next_in == ' ')) { |
1472 | 28 | c = ' '; |
1473 | 28 | all_upper_case |= FLAG_HAS_PLURAL; |
1474 | | |
1475 | 28 | if (sbuf[ix-1] == '\'') |
1476 | 20 | sbuf[ix-1] = ' '; |
1477 | 28 | } else |
1478 | 2.93k | all_upper_case = 0; // current word contains lower case letters, not "'s" |
1479 | 2.96k | } else |
1480 | 633k | all_upper_case = 0; |
1481 | 636k | } |
1482 | 2.99M | } else if (c == '-') { |
1483 | 25.6k | if (!IsSpace(prev_in) && IsAlpha(next_in)) { |
1484 | 7.92k | if (prev_out != ' ') { |
1485 | | // previous 'word' not yet ended (not alpha or numeric), start new word now. |
1486 | 671 | c = ' '; |
1487 | 671 | space_inserted = true; |
1488 | 7.25k | } else { |
1489 | | // '-' between two letters is a hyphen, treat as a space |
1490 | 7.25k | word_flags |= FLAG_HYPHEN; |
1491 | 7.25k | if (word_count > 0) |
1492 | 7.05k | words[word_count-1].flags |= FLAG_HYPHEN_AFTER; |
1493 | 7.25k | c = ' '; |
1494 | 7.25k | } |
1495 | 17.7k | } else if ((prev_in == ' ') && (next_in == ' ')) { |
1496 | | // ' - ' dash between two spaces, treat as pause |
1497 | 956 | c = ' '; |
1498 | 956 | pre_pause_add = 4; |
1499 | 16.8k | } else if (next_in == '-') { |
1500 | | // double hyphen, treat as pause |
1501 | 5.14k | source_index++; |
1502 | 5.14k | c = ' '; |
1503 | 5.14k | pre_pause_add = 4; |
1504 | 11.6k | } else if ((prev_out == ' ') && IsAlpha(prev_out2) && !IsAlpha(prev_in)) { |
1505 | | // insert extra space between a word + space + hyphen, to distinguish 'a -2' from 'a-2' |
1506 | 1.00k | sbuf[ix++] = ' '; |
1507 | 1.00k | words[word_count].start++; |
1508 | 1.00k | } |
1509 | 2.97M | } else if (c == '.') { |
1510 | 43.5k | if (prev_out == '.') { |
1511 | | // multiple dots, separate by spaces. Note >3 dots has been replaced by elipsis |
1512 | 1.41k | c = ' '; |
1513 | 1.41k | space_inserted = true; |
1514 | 42.1k | } else if ((word_count > 0) && !(words[word_count-1].flags & FLAG_NOSPACE) && IsAlpha(prev_in)) { |
1515 | | // dot after a word, with space following, probably an abbreviation |
1516 | 9.85k | words[word_count-1].flags |= FLAG_HAS_DOT; |
1517 | | |
1518 | 9.85k | if (IsSpace(next_in) || (next_in == '-')) |
1519 | 4.34k | c = ' '; // remove the dot if it's followed by a space or hyphen, so that it's not pronounced |
1520 | 9.85k | } |
1521 | 2.92M | } else if (c == '\'') { |
1522 | 16.0k | if (((prev_in == '.' && next_in == 's') || iswalnum(prev_in)) && IsAlpha(next_in)) { |
1523 | | // between two letters, or in an abbreviation (eg. u.s.a.'s). Consider the apostrophe as part of the word |
1524 | 7.00k | single_quoted = false; |
1525 | 9.00k | } else if ((tr->langopts.param[LOPT_APOSTROPHE] & 1) && IsAlpha(next_in)) |
1526 | 0 | single_quoted = false; // apostrophe at start of word is part of the word |
1527 | 9.00k | else if ((tr->langopts.param[LOPT_APOSTROPHE] & 2) && IsAlpha(prev_in)) |
1528 | 32 | single_quoted = false; // apostrophe at end of word is part of the word |
1529 | 8.96k | else if ((wcschr(tr->char_plus_apostrophe, prev_in) != 0) && (prev_out2 == ' ')) { |
1530 | | // consider single character plus apostrophe as a word |
1531 | 0 | single_quoted = false; |
1532 | 0 | if (next_in == ' ') |
1533 | 0 | source_index++; // skip following space |
1534 | 8.96k | } else { |
1535 | 8.96k | if ((prev_out == 's') && (single_quoted == false)) { |
1536 | | // looks like apostrophe after an 's' |
1537 | 82 | c = ' '; |
1538 | 8.88k | } else { |
1539 | 8.88k | if (IsSpace(prev_out)) |
1540 | 5.89k | single_quoted = true; |
1541 | 2.99k | else |
1542 | 2.99k | single_quoted = false; |
1543 | | |
1544 | 8.88k | pre_pause_add = 4; // single quote |
1545 | 8.88k | c = ' '; |
1546 | 8.88k | } |
1547 | 8.96k | } |
1548 | 2.91M | } else if (lookupwchar(breaks, c) != 0) |
1549 | 9.83k | c = ' '; // various characters to treat as space |
1550 | 2.90M | else if (iswdigit(c)) { |
1551 | 326k | if (tr->langopts.tone_numbers && IsAlpha(prev_out) && !IsDigit(next_in)) { |
1552 | 326k | } else if ((prev_out != ' ') && !iswdigit(prev_out)) { |
1553 | 37.8k | if ((prev_out != tr->langopts.decimal_sep) || ((decimal_sep_count == true) && (tr->langopts.decimal_sep == ','))) { |
1554 | 32.9k | c = ' '; |
1555 | 32.9k | space_inserted = true; |
1556 | 32.9k | } else |
1557 | 4.85k | decimal_sep_count = true; |
1558 | 288k | } else if ((prev_out == ' ') && IsAlpha(prev_out2) && !IsAlpha(prev_in)) { |
1559 | | // insert extra space between a word and a number, to distinguish 'a 2' from 'a2' |
1560 | 4.14k | sbuf[ix++] = ' '; |
1561 | 4.14k | words[word_count].start++; |
1562 | 4.14k | } |
1563 | 326k | } |
1564 | 3.95M | } |
1565 | | |
1566 | 5.67M | if (IsSpace(c)) { |
1567 | 2.24M | if (prev_out == ' ') { |
1568 | 1.35M | word_flags |= FLAG_MULTIPLE_SPACES; |
1569 | 1.35M | continue; // multiple spaces |
1570 | 1.35M | } |
1571 | | |
1572 | 894k | if ((cc == 0x09) || (cc == 0x0a)) |
1573 | 8.36k | next_word_flags |= FLAG_MULTIPLE_SPACES; // tab or newline, not a simple space |
1574 | | |
1575 | 894k | if (space_inserted) { |
1576 | | // count the number of characters since the start of the word |
1577 | 340k | j = 0; |
1578 | 340k | k = source_index - 1; |
1579 | 1.44M | while ((k >= source_index_word) && (charix[k] != 0)) { |
1580 | 1.10M | if (charix[k] > 0) // don't count initial bytes of multi-byte character |
1581 | 801k | j++; |
1582 | 1.10M | k--; |
1583 | 1.10M | } |
1584 | 340k | words[word_count].length = j; |
1585 | 340k | } |
1586 | | |
1587 | 894k | source_index_word = source_index; |
1588 | | |
1589 | | // end of 'word' |
1590 | 894k | sbuf[ix++] = ' '; |
1591 | | |
1592 | 894k | if ((word_count < N_CLAUSE_WORDS-1) && (ix > words[word_count].start)) { |
1593 | 892k | if (embedded_count > 0) { |
1594 | | // there are embedded commands before this word |
1595 | 19.6k | embedded_list[embedded_ix-1] |= 0x80; // terminate list of commands for this word |
1596 | 19.6k | words[word_count].flags |= FLAG_EMBEDDED; |
1597 | 19.6k | embedded_count = 0; |
1598 | 19.6k | } |
1599 | 892k | if (alpha_count == 0) { |
1600 | 493k | all_upper_case &= ~FLAG_ALL_UPPER; |
1601 | 493k | } |
1602 | 892k | words[word_count].pre_pause = pre_pause; |
1603 | 892k | words[word_count].flags |= (all_upper_case | word_flags | word_emphasis); |
1604 | | |
1605 | 892k | if (pre_pause > 0) { |
1606 | | // insert an extra space before the word, to prevent influence from previous word across the pause |
1607 | 53.6k | for (j = ix; j > words[word_count].start; j--) |
1608 | 42.7k | sbuf[j] = sbuf[j-1]; |
1609 | 10.8k | sbuf[j] = ' '; |
1610 | 10.8k | words[word_count].start++; |
1611 | 10.8k | ix++; |
1612 | 10.8k | } |
1613 | | |
1614 | 892k | word_count++; |
1615 | 892k | words[word_count].start = ix; |
1616 | 892k | words[word_count].flags = 0; |
1617 | | |
1618 | 892k | words[word_count].length = CalcWordLength(source_index, charix_top, charix, words, word_count); |
1619 | | |
1620 | 892k | word_flags = next_word_flags; |
1621 | 892k | next_word_flags = 0; |
1622 | 892k | pre_pause = 0; |
1623 | 892k | all_upper_case = FLAG_ALL_UPPER; |
1624 | 892k | alpha_count = 0; |
1625 | 892k | syllable_marked = false; |
1626 | 892k | } |
1627 | | |
1628 | 894k | if (space_inserted) { |
1629 | 340k | source_index = prev_source_index; // rewind to the previous character |
1630 | 340k | char_inserted = 0; |
1631 | 340k | space_inserted = false; |
1632 | 340k | } |
1633 | 3.42M | } else { |
1634 | 3.42M | if ((ix < (N_TR_SOURCE - 4))) |
1635 | 3.41M | ix += utf8_out(c, &sbuf[ix]); |
1636 | 3.42M | } |
1637 | 4.32M | if (pre_pause_add > pre_pause) |
1638 | 10.6k | pre_pause = pre_pause_add; |
1639 | 4.32M | pre_pause_add = 0; |
1640 | 4.32M | } |
1641 | | |
1642 | 72.9k | if ((word_count == 0) && (embedded_count > 0)) { |
1643 | | // add a null 'word' to carry the embedded command flag |
1644 | 765 | embedded_list[embedded_ix-1] |= 0x80; |
1645 | 765 | words[word_count].flags |= FLAG_EMBEDDED; |
1646 | 765 | word_count = 1; |
1647 | 765 | } |
1648 | | |
1649 | 72.9k | tr->clause_end = &sbuf[ix-1]; |
1650 | 72.9k | sbuf[ix] = 0; |
1651 | 72.9k | words[0].pre_pause = 0; // don't add extra pause at beginning of clause |
1652 | 72.9k | words[word_count].pre_pause = 8; |
1653 | 72.9k | if (word_count > 0) { |
1654 | 61.6k | ix = word_count-1; |
1655 | 74.1k | while ((ix > 0) && (IsBracket(sbuf[words[ix].start]))) |
1656 | 12.5k | ix--; // the last word is a bracket, mark the previous word as last |
1657 | 61.6k | words[ix].flags |= FLAG_LAST_WORD; |
1658 | | |
1659 | | // FLAG_NOSPACE check to avoid recognizing .mr -mr |
1660 | 61.6k | if ((terminator & CLAUSE_DOT_AFTER_LAST_WORD) && !(words[word_count-1].flags & FLAG_NOSPACE)) |
1661 | 4.75k | words[word_count-1].flags |= FLAG_HAS_DOT; |
1662 | 61.6k | } |
1663 | 72.9k | words[0].flags |= FLAG_FIRST_WORD; |
1664 | | |
1665 | | // Each TranslateWord2 may require up to 7 phonemes |
1666 | | // and after this loop we require 2 phonemes |
1667 | 916k | for (ix = 0; ix < word_count && (n_ph_list2 < N_PHONEME_LIST-7-2); ix++) { |
1668 | 843k | int nx; |
1669 | 843k | int c_temp; |
1670 | 843k | char *pn; |
1671 | 843k | char *pw; |
1672 | 843k | char number_buf[150]; |
1673 | 843k | WORD_TAB num_wtab[N_CLAUSE_WORDS]; // copy of 'words', when splitting numbers into parts |
1674 | | |
1675 | | // start speaking at a specified word position in the text? |
1676 | 843k | count_words++; |
1677 | 843k | if (skip_words > 0) { |
1678 | 0 | skip_words--; |
1679 | 0 | if (skip_words == 0) |
1680 | 0 | skipping_text = false; |
1681 | 0 | } |
1682 | 843k | if (skipping_text) |
1683 | 0 | continue; |
1684 | | |
1685 | 843k | current_alphabet = NULL; |
1686 | | |
1687 | | // digits should have been converted to Latin alphabet ('0' to '9') |
1688 | 843k | word = pw = &sbuf[words[ix].start]; |
1689 | | |
1690 | 843k | if (iswdigit(word[0]) && (tr->langopts.break_numbers != BREAK_THOUSANDS)) { |
1691 | | // Languages with 100000 numbers. Remove thousands separators so that we can insert them again later |
1692 | 7.20k | pn = number_buf; |
1693 | 31.5k | while (pn < &number_buf[sizeof(number_buf)-20]) { |
1694 | 31.5k | if (iswdigit(*pw)) |
1695 | 24.0k | *pn++ = *pw++; |
1696 | 7.45k | else if ((*pw == tr->langopts.thousands_sep) && (pw[1] == ' ') |
1697 | 7.45k | && iswdigit(pw[2]) && (pw[3] != ' ') && (pw[4] != ' ')) { // don't allow only 1 or 2 digits in the final part |
1698 | 260 | pw += 2; |
1699 | 260 | ix++; // skip "word" |
1700 | 7.19k | } else { |
1701 | 7.19k | nx = pw - word; |
1702 | 7.19k | memset(word, ' ', nx); |
1703 | 7.19k | nx = pn - number_buf; |
1704 | 7.19k | memcpy(word, number_buf, nx); |
1705 | 7.19k | break; |
1706 | 7.19k | } |
1707 | 31.5k | } |
1708 | 7.20k | pw = word; |
1709 | 7.20k | } |
1710 | | |
1711 | 1.10M | for (n_digits = 0; iswdigit(word[n_digits]); n_digits++) // count consecutive digits |
1712 | 256k | ; |
1713 | | |
1714 | 843k | if (n_digits > 4 && n_digits <= 32) { |
1715 | | // word is entirely digits, insert commas and break into 3 digit "words" |
1716 | 10.2k | int nw = 0; |
1717 | 10.2k | int num_wtab_count; |
1718 | | |
1719 | 10.2k | number_buf[0] = ' '; |
1720 | 10.2k | number_buf[1] = ' '; |
1721 | 10.2k | number_buf[2] = ' '; |
1722 | 10.2k | pn = &number_buf[3]; |
1723 | 10.2k | nx = n_digits; |
1724 | | |
1725 | 10.2k | if ((n_digits > tr->langopts.max_digits) || (word[0] == '0')) |
1726 | 1.68k | words[ix].flags |= FLAG_INDIVIDUAL_DIGITS; |
1727 | | |
1728 | 107k | while (pn < &number_buf[sizeof(number_buf)-20] && nw < N_CLAUSE_WORDS-2) { |
1729 | 107k | if (!IsDigit09(c = *pw++) && (c != tr->langopts.decimal_sep)) |
1730 | 10.2k | break; |
1731 | | |
1732 | 97.1k | *pn++ = c; |
1733 | 97.1k | nx--; |
1734 | 97.1k | if ((nx > 0) && (tr->langopts.break_numbers & (1U << nx))) { |
1735 | 25.1k | memcpy(&num_wtab[nw++], &words[ix], sizeof(WORD_TAB)); // copy the 'words' entry for each word of numbers |
1736 | | |
1737 | 25.1k | if (tr->langopts.thousands_sep != ' ') |
1738 | 22.8k | *pn++ = tr->langopts.thousands_sep; |
1739 | 25.1k | *pn++ = ' '; |
1740 | | |
1741 | 25.1k | if ((words[ix].flags & FLAG_INDIVIDUAL_DIGITS) == 0) { |
1742 | 17.9k | if (tr->langopts.break_numbers & (1 << (nx-1))) { |
1743 | | // the next group only has 1 digits, make it three |
1744 | 4 | *pn++ = '0'; |
1745 | 4 | *pn++ = '0'; |
1746 | 4 | } |
1747 | 17.9k | if (tr->langopts.break_numbers & (1 << (nx-2))) { |
1748 | | // the next group only has 2 digits (eg. Indian languages), make it three |
1749 | 1.30k | *pn++ = '0'; |
1750 | 1.30k | } |
1751 | 17.9k | } |
1752 | 25.1k | } |
1753 | 97.1k | } |
1754 | 10.2k | pw--; |
1755 | 10.2k | memcpy(&num_wtab[nw], &words[ix], sizeof(WORD_TAB)*2); // the original number word, and the word after it |
1756 | 10.2k | num_wtab_count = nw + 2; |
1757 | | |
1758 | 35.3k | for (j = 1; j <= nw; j++) |
1759 | 25.1k | num_wtab[j].flags &= ~(FLAG_MULTIPLE_SPACES | FLAG_EMBEDDED); // don't use these flags for subsequent parts when splitting a number |
1760 | | |
1761 | | // include the next few characters, in case there are an ordinal indicator or other suffix |
1762 | 10.2k | strncpy(pn, pw, 16); |
1763 | 10.2k | pn[16] = 0; |
1764 | 10.2k | nw = 0; |
1765 | | |
1766 | 45.5k | for (pw = &number_buf[3]; pw < pn && nw < N_CLAUSE_WORDS;) { |
1767 | | // keep wflags for each part, for FLAG_HYPHEN_AFTER |
1768 | 35.3k | dict_flags = TranslateWord2(tr, pw, &num_wtab[nw], num_wtab_count - nw, words[ix].pre_pause); |
1769 | 35.3k | nw++; |
1770 | 156k | while (pw < pn && *pw++ != ' ') |
1771 | 121k | ; |
1772 | 35.3k | words[ix].pre_pause = 0; |
1773 | 35.3k | } |
1774 | 833k | } else { |
1775 | 833k | pre_pause = 0; |
1776 | | |
1777 | 833k | dict_flags = TranslateWord2(tr, word, &words[ix], word_count - ix, words[ix].pre_pause); |
1778 | | |
1779 | 833k | if (pre_pause > words[ix+1].pre_pause) { |
1780 | 60.2k | words[ix+1].pre_pause = pre_pause; |
1781 | 60.2k | pre_pause = 0; |
1782 | 60.2k | } |
1783 | | |
1784 | 833k | if (dict_flags & FLAG_SPELLWORD) { |
1785 | | // redo the word, speaking single letters |
1786 | 140k | for (pw = word; *pw != ' ';) { |
1787 | 116k | memset(number_buf, 0, sizeof(number_buf)); |
1788 | 116k | memset(number_buf+1, ' ', 9); |
1789 | 116k | nx = utf8_in(&c_temp, pw); |
1790 | 116k | memcpy(&number_buf[3], pw, nx); |
1791 | 116k | TranslateWord2(tr, &number_buf[3], &words[ix], word_count - ix, 0); |
1792 | 116k | pw += nx; |
1793 | 116k | } |
1794 | 23.2k | } |
1795 | | |
1796 | 833k | if ((dict_flags & (FLAG_ALLOW_DOT | FLAG_NEEDS_DOT)) && (ix == word_count - 1 - dictionary_skipwords) && (terminator & CLAUSE_DOT_AFTER_LAST_WORD)) { |
1797 | | // probably an abbreviation such as Mr. or B. rather than end of sentence |
1798 | 202 | clause_pause = 10; |
1799 | 202 | if (tone_out != NULL) |
1800 | 202 | *tone_out = 4; |
1801 | 202 | } |
1802 | 833k | } |
1803 | | |
1804 | 843k | if (dict_flags & FLAG_SKIPWORDS) { |
1805 | | // dictionary indicates skip next word(s) |
1806 | 10.2k | while (dictionary_skipwords > 0) { |
1807 | 7.34k | if (dictionary_skipwords >= word_count - ix) { |
1808 | 15 | dictionary_skipwords = 0; |
1809 | 15 | break; |
1810 | 15 | } |
1811 | 7.33k | words[ix+dictionary_skipwords].flags |= FLAG_DELETE_WORD; |
1812 | 7.33k | dictionary_skipwords--; |
1813 | 7.33k | } |
1814 | 2.88k | } |
1815 | 843k | } |
1816 | | |
1817 | 72.9k | if (embedded_read < embedded_ix) { |
1818 | | // any embedded commands not yet processed? |
1819 | 1.35k | Word_EmbeddedCmd(); |
1820 | 1.35k | } |
1821 | | |
1822 | 218k | for (ix = 0; ix < 2; ix++) { |
1823 | | // terminate the clause with 2 PAUSE phonemes |
1824 | 145k | PHONEME_LIST2 *p2; |
1825 | 145k | p2 = &ph_list2[n_ph_list2 + ix]; |
1826 | 145k | p2->phcode = phonPAUSE; |
1827 | 145k | p2->stresslevel = 0; |
1828 | 145k | p2->sourceix = source_index; |
1829 | 145k | p2->synthflags = 0; |
1830 | 145k | } |
1831 | 72.9k | n_ph_list2 += 2; |
1832 | | |
1833 | 72.9k | if (Eof() && ((word_count == 0) || (option_endpause == 0))) |
1834 | 2.32k | clause_pause = 10; |
1835 | | |
1836 | 72.9k | MakePhonemeList(tr, clause_pause, new_sentence2); |
1837 | 72.9k | phoneme_list[N_PHONEME_LIST].ph = NULL; // recognize end of phoneme_list array, in Generate() |
1838 | 72.9k | phoneme_list[N_PHONEME_LIST].sourceix = 1; |
1839 | | |
1840 | 72.9k | if (embedded_count) { // ???? is this needed |
1841 | 1.72k | phoneme_list[n_phoneme_list-2].synthflags = SFLAG_EMBEDDED; |
1842 | 1.72k | embedded_list[embedded_ix-1] |= 0x80; |
1843 | 1.72k | embedded_list[embedded_ix] = 0x80; |
1844 | 1.72k | } |
1845 | | |
1846 | 72.9k | new_sentence = false; |
1847 | 72.9k | if (terminator & CLAUSE_TYPE_SENTENCE) |
1848 | 47.5k | new_sentence = true; // next clause is a new sentence |
1849 | | |
1850 | 72.9k | if (voice_change != NULL) { |
1851 | | // return new voice name if an embedded voice change command terminated the clause |
1852 | 72.9k | if (terminator & CLAUSE_TYPE_VOICE_CHANGE) |
1853 | 16.7k | *voice_change = voice_change_name; |
1854 | 56.2k | else |
1855 | 56.2k | *voice_change = NULL; |
1856 | 72.9k | } |
1857 | 72.9k | } |
1858 | | |
1859 | | void TranslateClause(Translator *tr, int *tone_out, char **voice_change) |
1860 | 72.9k | { |
1861 | 72.9k | TranslateClauseWithTerminator(tr, tone_out, voice_change, NULL); |
1862 | 72.9k | } |
1863 | | |
1864 | 965k | static int CalcWordLength(int source_index, int charix_top, short int *charix, WORD_TAB *words, int word_count) { |
1865 | 965k | int j; |
1866 | 965k | int k; |
1867 | | |
1868 | 5.77M | for (j = source_index; j < charix_top && charix[j] <= 0; j++); // skip blanks |
1869 | 965k | words[word_count].sourceix = charix[j]; |
1870 | 965k | k = 0; |
1871 | 72.4M | while (charix[j] != 0) { |
1872 | | // count the number of characters (excluding multibyte continuation bytes) |
1873 | 71.4M | if (charix[j++] != -1) |
1874 | 40.6M | k++; |
1875 | 71.4M | } |
1876 | 965k | return k; |
1877 | 965k | } |
1878 | | |
1879 | 111 | static void CombineFlag(Translator *tr, WORD_TAB *wtab, int wtab_remaining, char *word, int *flags, unsigned char *p, char *word_phonemes) { |
1880 | | // combine a preposition with the following word |
1881 | | |
1882 | | |
1883 | 111 | int sylimit; // max. number of syllables in a word to be combined with a preceding preposition |
1884 | 111 | sylimit = tr->langopts.param[LOPT_COMBINE_WORDS]; |
1885 | | |
1886 | | |
1887 | 111 | char *p2; |
1888 | 111 | p2 = word; |
1889 | 223 | while (*p2 != ' ') p2++; |
1890 | | |
1891 | 111 | bool ok = true; |
1892 | 111 | int c_word2; |
1893 | | |
1894 | 111 | if (wtab_remaining <= 1) |
1895 | 0 | ok = false; |
1896 | | |
1897 | 111 | utf8_in(&c_word2, p2+1); // first character of the next word; |
1898 | | |
1899 | 111 | if (!iswalpha(c_word2)) |
1900 | 42 | ok = false; |
1901 | | |
1902 | 111 | int flags2[2]; |
1903 | 111 | flags2[0] = 0; |
1904 | | |
1905 | | |
1906 | 111 | if (ok) { |
1907 | 69 | char ph_buf[N_WORD_PHONEMES]; |
1908 | 69 | strcpy(ph_buf, word_phonemes); |
1909 | | |
1910 | 69 | flags2[0] = TranslateWordWithBounds(tr, p2+1, wtab+1, wtab_remaining-1, NULL, 0); |
1911 | 69 | if ((flags2[0] & FLAG_WAS_UNPRONOUNCABLE) || (word_phonemes[0] == phonSWITCH)) |
1912 | 29 | ok = false; |
1913 | | |
1914 | 69 | if ((sylimit & 0x100) && ((flags2[0] & FLAG_ALT_TRANS) == 0)) { |
1915 | | // only if the second word has $alt attribute |
1916 | 0 | ok = false; |
1917 | 0 | } |
1918 | | |
1919 | 69 | if ((sylimit & 0x200) && (wtab_remaining > 1) && ((wtab+1)->flags & FLAG_LAST_WORD)) { |
1920 | | // not if the next word is end-of-sentence |
1921 | 0 | ok = false; |
1922 | 0 | } |
1923 | | |
1924 | 69 | if (ok == false) |
1925 | 29 | strcpy(word_phonemes, ph_buf); |
1926 | 69 | } |
1927 | | |
1928 | 111 | if (ok) { |
1929 | 40 | *p2 = '-'; // replace next space by hyphen |
1930 | 40 | wtab[0].flags &= ~FLAG_ALL_UPPER; // prevent it being considered an abbreviation |
1931 | 40 | *flags = TranslateWordWithBounds(translator, word, wtab, wtab_remaining, NULL, 0); // translate the combined word |
1932 | 40 | if ((sylimit > 0) && (CountSyllables(p) > (sylimit & 0x1f))) { |
1933 | | // revert to separate words |
1934 | 6 | *p2 = ' '; |
1935 | 6 | *flags = TranslateWordWithBounds(translator, word, wtab, wtab_remaining, NULL, 0); |
1936 | 34 | } else { |
1937 | 34 | if (*flags == 0) |
1938 | 30 | *flags = flags2[0]; // no flags for the combined word, so use flags from the second word eg. lang-hu "nem december 7-e" |
1939 | 34 | *flags |= FLAG_SKIPWORDS; |
1940 | 34 | dictionary_skipwords = 1; |
1941 | 34 | } |
1942 | 40 | } |
1943 | 111 | } |
1944 | | |
1945 | 5.51k | static void SwitchLanguage(char *word, char *word_phonemes) { |
1946 | 5.51k | char lang_name[12]; |
1947 | 5.51k | int ix; |
1948 | | |
1949 | 5.51k | word += 3; |
1950 | | |
1951 | 16.6k | for (ix = 0;;) { |
1952 | 16.6k | int c1; |
1953 | 16.6k | c1 = *word++; |
1954 | 16.6k | if ((c1 == ' ') || (c1 == 0)) |
1955 | 5.51k | break; |
1956 | 11.1k | lang_name[ix++] = tolower(c1); |
1957 | 11.1k | } |
1958 | 5.51k | lang_name[ix] = 0; |
1959 | | |
1960 | 5.51k | if ((ix = LookupPhonemeTable(lang_name)) > 0) { |
1961 | 5.07k | SelectPhonemeTable(ix); |
1962 | 5.07k | word_phonemes[0] = phonSWITCH; |
1963 | 5.07k | word_phonemes[1] = ix; |
1964 | 5.07k | word_phonemes[2] = 0; |
1965 | 5.07k | } |
1966 | 5.51k | } |
1967 | | |
1968 | | void InitText(int control) |
1969 | 2.37k | { |
1970 | 2.37k | count_sentences = 0; |
1971 | 2.37k | count_words = 0; |
1972 | 2.37k | end_character_position = 0; |
1973 | 2.37k | skip_sentences = 0; |
1974 | 2.37k | skip_marker[0] = 0; |
1975 | 2.37k | skip_words = 0; |
1976 | 2.37k | skip_characters = 0; |
1977 | 2.37k | skipping_text = false; |
1978 | 2.37k | new_sentence = true; |
1979 | | |
1980 | 2.37k | option_sayas = 0; |
1981 | 2.37k | option_sayas2 = 0; |
1982 | 2.37k | option_emphasis = 0; |
1983 | 2.37k | word_emphasis = 0; |
1984 | 2.37k | embedded_flag = 0; |
1985 | | |
1986 | 2.37k | InitText2(); |
1987 | | |
1988 | 2.37k | if ((control & espeakKEEP_NAMEDATA) == 0) |
1989 | 2.37k | InitNamedata(); |
1990 | 2.37k | } |