diff --git a/packages/text-to-speech/package.json b/packages/text-to-speech/package.json index 2c4e8591b..6c40d7faf 100644 --- a/packages/text-to-speech/package.json +++ b/packages/text-to-speech/package.json @@ -42,7 +42,7 @@ "jsonwebtoken": "^8.5.1", "linkedom": "^0.14.12", "microsoft-cognitiveservices-speech-sdk": "^1.22.0", - "natural": "^5.2.3", + "natural": "^6.2.0", "redis": "^4.3.1", "underscore": "^1.13.4" } diff --git a/packages/text-to-speech/src/htmlToSsml.ts b/packages/text-to-speech/src/htmlToSsml.ts index e6c1207d6..41e780011 100644 --- a/packages/text-to-speech/src/htmlToSsml.ts +++ b/packages/text-to-speech/src/htmlToSsml.ts @@ -1,6 +1,10 @@ -import { parseHTML } from 'linkedom' -import { SentenceTokenizer, WordPunctTokenizer } from 'natural' import { htmlToText } from 'html-to-text' +import { parseHTML } from 'linkedom' +import { + SentenceTokenizer, + SentenceTokenizerNew, + WordPunctTokenizer, +} from 'natural' // this code needs to be kept in sync with the // frontend code in: useReadingProgressAnchor @@ -262,14 +266,14 @@ export const stripEmojis = (text: string): string => { } const textToUtterances = ({ - tokenizer, + wordTokenizer, idx, textItems, wordOffset, voice, isHtml = true, }: { - tokenizer: WordPunctTokenizer + wordTokenizer: WordPunctTokenizer idx: string textItems: string[] wordOffset: number @@ -284,7 +288,7 @@ const textToUtterances = ({ idx, text, wordOffset, - wordCount: tokenizer.tokenize(text).length, + wordCount: wordTokenizer.tokenize(text).length, voice, }, ] @@ -303,10 +307,18 @@ const textToUtterances = ({ text = parseHTML(text).document.documentElement.textContent ?? text console.info('Converted HTML to text:', text) } - const MAX_CHARS = 256 - const sentenceTokenizer = new SentenceTokenizer() - const sentences = sentenceTokenizer.tokenize(text) + let sentences: string[] = [] + try { + // use new sentence tokenizer + const sentenceTokenizer = new SentenceTokenizerNew() + sentences = sentenceTokenizer.tokenize(text) + } catch (err) { + console.debug('Unable to tokenize sentences, text:', text, ', error:', err) + // fallback to old sentence tokenizer + const sentenceTokenizer = new SentenceTokenizer() + sentences = sentenceTokenizer.tokenize(text) + } let currentText = '' // split text to max 256 chars per utterance and // use nlp lib to detect sentences and @@ -319,7 +331,7 @@ const textToUtterances = ({ const nextText = currentText + sentence if (nextText.length > MAX_CHARS) { if (currentText.length > 0) { - const wordCount = tokenizer.tokenize(currentText).length + const wordCount = wordTokenizer.tokenize(currentText).length utterances.push({ idx, text: currentText, @@ -330,7 +342,7 @@ const textToUtterances = ({ wordOffset += wordCount currentText = sentence } else { - const wordCount = tokenizer.tokenize(sentence).length + const wordCount = wordTokenizer.tokenize(sentence).length utterances.push({ idx, text: sentence, @@ -348,7 +360,7 @@ const textToUtterances = ({ idx, text: currentText, wordOffset, - wordCount: tokenizer.tokenize(currentText).length, + wordCount: wordTokenizer.tokenize(currentText).length, voice, }) } @@ -386,13 +398,13 @@ export const htmlToSpeechFile = (htmlInput: HtmlInput): SpeechFile => { } } - const tokenizer = new WordPunctTokenizer() + const wordTokenizer = new WordPunctTokenizer() const utterances: Utterance[] = [] let wordOffset = 0 if (title) { // first utterances is the title const titleUtterance = textToUtterances({ - tokenizer, + wordTokenizer, idx: '', textItems: [stripEmojis(title)], // title could have emoji wordOffset, @@ -413,7 +425,7 @@ export const htmlToSpeechFile = (htmlInput: HtmlInput): SpeechFile => { const idx = i.toString() i = emitElement(textItems, node, true) const newUtterances = textToUtterances({ - tokenizer, + wordTokenizer, idx, textItems, wordOffset, diff --git a/packages/text-to-speech/test/htmlToSsml.test.ts b/packages/text-to-speech/test/htmlToSsml.test.ts index a2673f465..61f1da66d 100644 --- a/packages/text-to-speech/test/htmlToSsml.test.ts +++ b/packages/text-to-speech/test/htmlToSsml.test.ts @@ -1,12 +1,12 @@ -import 'mocha' import { expect } from 'chai' +import * as fs from 'fs' +import 'mocha' +import path from 'path' import { htmlToSpeechFile, htmlToSsmlItems, stripEmojis, } from '../src/htmlToSsml' -import * as fs from 'fs' -import path from 'path' const TEST_OPTIONS = { primaryVoice: 'test-primary', @@ -309,4 +309,32 @@ describe('convert HTML to Speech file', () => { 'I feel like I’m working on reading, listening, and speaking all at once, sometimes I feel like I’m just getting surface understanding. ' ) }) + + it('splits sentences in German correctly', () => { + const html = `
Q: „Die kürzliche Razzia in den BBC-Büros in Delhi sind ein weiterer Versuch der Regierung, kritische Medien-Kommentare zu unterdrücken. Man hat des Gefühl, Herr Modi hat Angst, in den Spiegel zu schauen!?“
+这是一段中文,我想看看它是怎么分句的。如果买二手房有中介参与,要找相对大的、知名的中介。中介的收费、服务情况要先问清。还要和中介谈好,中介费的付款时间,一般来说是签完合同付一部分,过户后付一部分,省的太早付完钱,中介就不管事了。付完记得要发票。中介如果提供贷款服务,让他玩去。贷款之类的问题,别怕麻烦,自己去找银行。
+