diff --git a/packages/api/package.json b/packages/api/package.json index 28ba0df6c..7b275b385 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -19,6 +19,7 @@ "@google-cloud/storage": "^5.18.1", "@google-cloud/tasks": "^2.3.0", "@omnivore/readability": "1.0.0", + "@omnivore/text-to-speech-handler": "1.0.0", "@opentelemetry/api": "^1.0.1", "@opentelemetry/core": "^1.3.1", "@opentelemetry/exporter-jaeger": "^1.0.1", diff --git a/packages/api/src/routers/article_router.ts b/packages/api/src/routers/article_router.ts index e0f625aad..3302cdf11 100644 --- a/packages/api/src/routers/article_router.ts +++ b/packages/api/src/routers/article_router.ts @@ -21,6 +21,7 @@ import { getPageById, updatePage } from '../elastic/pages' import { generateDownloadSignedUrl } from '../utils/uploads' import { enqueueTextToSpeech } from '../utils/createTask' import { createPubSubClient } from '../datalayer/pubsub' +import { htmlToSsml } from '@omnivore/text-to-speech-handler' const logger = buildLogger('app.dispatch') @@ -81,7 +82,7 @@ export function articleRouter() { const priority = req.params.priority if ( !articleId || - !['mp3', 'speech-marks'].includes(outputFormat) || + !['mp3', 'speech-marks', 'ssml'].includes(outputFormat) || !['low', 'high'].includes(priority) ) { return res.status(400).send('Invalid data') @@ -91,7 +92,6 @@ export function articleRouter() { return res.status(401).send({ errorCode: 'UNAUTHORIZED' }) } const { uid } = jwt.decode(token) as Claims - logger.info(`Get article speech in ${outputFormat} format`, { params: req.params, labels: { @@ -100,6 +100,21 @@ export function articleRouter() { }, }) + if (outputFormat === 'ssml') { + const page = await getPageById(articleId) + if (!page) { + return res.status(404).send('Page not found') + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const ssmlItems = htmlToSsml(page.content, { + primaryVoice: voice, + secondaryVoice: 'en-US-GuyNeural', + rate: '1', + language: page.language || 'en-US', + }) + return res.send({ ssmlItems }) + } + const existingSpeech = await getRepository(Speech).findOne({ where: { elasticPageId: articleId, diff --git a/packages/api/src/textToSpeech.d.ts b/packages/api/src/textToSpeech.d.ts new file mode 100644 index 000000000..194db1fbd --- /dev/null +++ b/packages/api/src/textToSpeech.d.ts @@ -0,0 +1,18 @@ +declare module '@omnivore/text-to-speech-handler' { + function htmlToSsml(html: string, options: SSMLOptions): SSMLItem[] + + interface SSMLOptions { + primaryVoice: string + secondaryVoice: string + rate: string + language: string + } + + interface SSMLItem { + open: string + close: string + textItems: string[] + } + + export { htmlToSsml } +} diff --git a/packages/text-to-speech/.npmignore b/packages/text-to-speech/.npmignore new file mode 100644 index 000000000..193378602 --- /dev/null +++ b/packages/text-to-speech/.npmignore @@ -0,0 +1 @@ +/test/