diff --git a/packages/api/src/textToSpeech.d.ts b/packages/api/src/textToSpeech.d.ts index 194db1fbd..5e438e05e 100644 --- a/packages/api/src/textToSpeech.d.ts +++ b/packages/api/src/textToSpeech.d.ts @@ -1,5 +1,5 @@ declare module '@omnivore/text-to-speech-handler' { - function htmlToSsml(html: string, options: SSMLOptions): SSMLItem[] + function htmlToSsml(html: string, options: SSMLOptions): string[] interface SSMLOptions { primaryVoice: string @@ -8,11 +8,5 @@ declare module '@omnivore/text-to-speech-handler' { language: string } - interface SSMLItem { - open: string - close: string - textItems: string[] - } - export { htmlToSsml } } diff --git a/packages/text-to-speech/src/htmlToSsml.ts b/packages/text-to-speech/src/htmlToSsml.ts index ed125114d..e65755cfc 100644 --- a/packages/text-to-speech/src/htmlToSsml.ts +++ b/packages/text-to-speech/src/htmlToSsml.ts @@ -179,7 +179,10 @@ export const ssmlItemText = (item: SSMLItem): string => { return [item.open, ...item.textItems, item.close].join('') } -export const htmlToSsml = (html: string, options: SSMLOptions): SSMLItem[] => { +export const htmlToSsmlItems = ( + html: string, + options: SSMLOptions +): SSMLItem[] => { console.log('creating ssml with options', options) const dom = parseHTML(html) @@ -218,3 +221,7 @@ export const htmlToSsml = (html: string, options: SSMLOptions): SSMLItem[] => { return items } + +export const htmlToSsml = (html: string, options: SSMLOptions): string[] => { + return htmlToSsmlItems(html, options).map(ssmlItemText) +} diff --git a/packages/text-to-speech/src/index.ts b/packages/text-to-speech/src/index.ts index 55a9eabc6..5006a2666 100644 --- a/packages/text-to-speech/src/index.ts +++ b/packages/text-to-speech/src/index.ts @@ -10,7 +10,7 @@ import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-d import { synthesizeTextToSpeech, TextToSpeechInput } from './textToSpeech' import { File, Storage } from '@google-cloud/storage' import { PassThrough } from 'stream' -import { htmlToSsml, SSMLItem } from './htmlToSsml' +import { htmlToSsml } from './htmlToSsml' dotenv.config() Sentry.GCPFunction.init({ @@ -141,7 +141,10 @@ export const textToSpeechStreamingHandler = Sentry.GCPFunction.wrapHttpFunction( try { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const ssmlItems = req.body.ssmlItems as SSMLItem[] + const ssmlItems = req.body.ssmlItems as string[] + if (!ssmlItems || ssmlItems.length === 0) { + return res.status(200).send({ errorCode: 'INVALID_DATA' }) + } const audioStream = new PassThrough() const input: TextToSpeechInput = { text: '', diff --git a/packages/text-to-speech/src/textToSpeech.ts b/packages/text-to-speech/src/textToSpeech.ts index ed7799e2b..0fdfc43ed 100644 --- a/packages/text-to-speech/src/textToSpeech.ts +++ b/packages/text-to-speech/src/textToSpeech.ts @@ -7,7 +7,7 @@ import { SpeechSynthesisResult, SpeechSynthesizer, } from 'microsoft-cognitiveservices-speech-sdk' -import { htmlToSsml, SSMLItem, ssmlItemText } from './htmlToSsml' +import { htmlToSsmlItems, ssmlItemText } from './htmlToSsml' export interface TextToSpeechInput { id?: string @@ -20,7 +20,7 @@ export interface TextToSpeechInput { complimentaryVoice?: string bucket?: string audioStream: NodeJS.ReadWriteStream - ssmlItems?: SSMLItem[] + ssmlItems?: string[] } export interface TextToSpeechOutput { @@ -41,6 +41,7 @@ export const synthesizeTextToSpeech = async ( if (!process.env.AZURE_SPEECH_KEY || !process.env.AZURE_SPEECH_REGION) { throw new Error('Azure Speech Key or Region not set') } + const textType = input.textType || 'html' const writeStream = input.audioStream const speechConfig = SpeechConfig.fromSubscription( process.env.AZURE_SPEECH_KEY, @@ -131,24 +132,22 @@ export const synthesizeTextToSpeech = async ( } try { - const ssmlItems = - input.textType === 'ssml' - ? input.ssmlItems - : htmlToSsml(input.text, { - primaryVoice: input.voice || 'en-US-JennyNeural', - secondaryVoice: input.complimentaryVoice || 'en-US-GuyNeural', - language: input.languageCode || 'en-US', - rate: '1', - }) - if (!ssmlItems || ssmlItems.length === 0) { - throw new Error('No SSML items found') - } - - for (const ssmlItem of Array.from(ssmlItems)) { - const ssml = ssmlItemText(ssmlItem) - console.debug('start synthesizing', ssml) - const result = await speakSsmlAsyncPromise(ssml) - timeOffset = timeOffset + result.audioDuration + if (textType === 'html') { + const ssmlItems = htmlToSsmlItems(input.text, { + primaryVoice: input.voice || 'en-US-JennyNeural', + secondaryVoice: input.complimentaryVoice || 'en-US-GuyNeural', + language: input.languageCode || 'en-US', + rate: '1', + }) + for (const ssmlItem of ssmlItems) { + const ssml = ssmlItemText(ssmlItem) + const result = await speakSsmlAsyncPromise(ssml) + timeOffset = timeOffset + result.audioDuration + } + } else { + for (const ssmlItem of input.ssmlItems || []) { + await speakSsmlAsyncPromise(ssmlItem) + } } } catch (error) { console.error('synthesis error', error) diff --git a/packages/text-to-speech/test/htmlToSsml.test.ts b/packages/text-to-speech/test/htmlToSsml.test.ts index 46bd0243c..80fd3fbd3 100644 --- a/packages/text-to-speech/test/htmlToSsml.test.ts +++ b/packages/text-to-speech/test/htmlToSsml.test.ts @@ -1,8 +1,8 @@ import 'mocha' import { expect } from 'chai' -import { htmlToSsml } from '../src/htmlToSsml' +import { htmlToSsmlItems } from '../src/htmlToSsml' -describe('htmlToSsml', () => { +describe('htmlToSsmlItems', () => { const TEST_OPTIONS = { primaryVoice: 'test-primary', secondaryVoice: 'test-secondary', @@ -12,7 +12,7 @@ describe('htmlToSsml', () => { describe('a simple html file', () => { xit('should convert Html to SSML', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -28,7 +28,7 @@ describe('htmlToSsml', () => { }) describe('a file with nested elements', () => { xit('should collapse spans into the parent paragraph', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -57,7 +57,7 @@ describe('htmlToSsml', () => { ) }) xit('should extract child paragraphs to the top level', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -77,7 +77,7 @@ describe('htmlToSsml', () => { ) }) xit('should hoist paragraphs in spans to the top level', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -95,7 +95,7 @@ describe('htmlToSsml', () => { expect(text).to.equal(`TBD`.trim()) }) xit('should hoist lists to the top level', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -113,7 +113,7 @@ describe('htmlToSsml', () => { expect(text).to.equal(`TBD`.trim()) }) xit('should hoist headers to the top level', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -131,7 +131,7 @@ describe('htmlToSsml', () => { expect(text).to.equal(`TBD`.trim()) }) xit('should hoist blockquotes to the top level', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -151,7 +151,7 @@ describe('htmlToSsml', () => { }) describe('a file with blockquotes', () => { xit('should convert Html to SSML with complimentary voices', () => { - const ssml = htmlToSsml( + const ssml = htmlToSsmlItems( `
@@ -193,7 +193,7 @@ describe('htmlToSsml', () => { // continue // } // const html = fs.readFileSync(readablePath, { encoding: 'utf-8' }) - // const ssmlItems = htmlToSsml(html, TEST_OPTIONS) + // const ssmlItems = htmlToSsmlItems(html, TEST_OPTIONS) // console.log('SSML ITEMS', ssmlItems) // } // })