diff --git a/packages/api/package.json b/packages/api/package.json index 688c2ef87..6a1fef691 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -68,6 +68,7 @@ "knex-stringcase": "^1.4.2", "linkedom": "^0.14.9", "luxon": "^2.3.1", + "microsoft-cognitiveservices-speech-sdk": "^1.22.0", "nanoid": "^3.1.25", "nodemailer": "^6.7.3", "normalize-url": "^6.1.0", diff --git a/packages/api/src/entity/speech.ts b/packages/api/src/entity/speech.ts index 27e189d6b..0f61d7dd7 100644 --- a/packages/api/src/entity/speech.ts +++ b/packages/api/src/entity/speech.ts @@ -28,7 +28,7 @@ export class Speech { speechMarks!: string @Column('text') - voiceId!: string + voice!: string @CreateDateColumn({ default: () => 'CURRENT_TIMESTAMP' }) createdAt!: Date diff --git a/packages/api/src/entity/user.ts b/packages/api/src/entity/user.ts index 58c4a6975..b03c36e11 100644 --- a/packages/api/src/entity/user.ts +++ b/packages/api/src/entity/user.ts @@ -12,6 +12,7 @@ import { NewsletterEmail } from './newsletter_email' import { Profile } from './profile' import { Label } from './label' import { Subscription } from './subscription' +import { UserPersonalization } from './user_personalization' @Entity() export class User { @@ -53,4 +54,10 @@ export class User { @Column({ type: 'enum', enum: StatusType }) status!: StatusType + + @OneToOne( + () => UserPersonalization, + (userPersonalization) => userPersonalization.user + ) + userPersonalization!: UserPersonalization } diff --git a/packages/api/src/routers/svc/textToSpeech.ts b/packages/api/src/routers/svc/textToSpeech.ts new file mode 100644 index 000000000..42ec982e4 --- /dev/null +++ b/packages/api/src/routers/svc/textToSpeech.ts @@ -0,0 +1,57 @@ +import express from 'express' +import cors from 'cors' +import { corsConfig } from '../../utils/corsConfig' +import { getRepository } from '../../entity/utils' +import { User } from '../../entity/user' +import { getPageById } from '../../elastic/pages' +import { htmlToSsml, synthesizeTextToSpeech } from '../../utils/textToSpeech' +import { Speech } from '../../entity/speech' + +export function textToSpeechServiceRouter() { + const router = express.Router() + + router.options('/', cors({ ...corsConfig, maxAge: 600 })) + router.post('/', async (req, res) => { + const { userId, pageId } = req.body as { + userId: string + pageId: string + } + + if (!userId || !pageId) { + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } + + const user = await getRepository(User).findOne({ + where: { id: userId }, + relations: ['user_personalization'], + }) + if (!user) { + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } + + const page = await getPageById(pageId) + if (!page) { + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } + + const html = page.content + const language = page.language + const voice = user.userPersonalization.speechVoice || 'en-US_AllisonVoice' + const rate = user.userPersonalization.speechRate || 100 + const volume = user.userPersonalization.speechVolume || 100 + const ssml = htmlToSsml(html, language, voice, rate, volume) + + const audioAndSpeechMarks = await synthesizeTextToSpeech({ + id: pageId, + text: ssml, + }) + + await getRepository(Speech).save({ + elasticPageId: pageId, + audioUrl: audioAndSpeechMarks.audioUrl, + speechMarks: JSON.stringify(audioAndSpeechMarks.speechMarks), + id: pageId, + user, + }) + }) +} diff --git a/packages/api/src/util.ts b/packages/api/src/util.ts index 3cd8f7edb..1ca344d02 100755 --- a/packages/api/src/util.ts +++ b/packages/api/src/util.ts @@ -89,6 +89,10 @@ interface BackendEnv { readwise: { apiUrl: string } + azure: { + speechKey: string + speechRegion: string + } } /*** @@ -140,6 +144,8 @@ const nullableEnvVars = [ 'READWISE_API_URL', 'INTEGRATION_TASK_HANDLER_URL', 'TEXT_TO_SPEECH_TASK_HANDLER_URL', + 'AZURE_SPEECH_KEY', + 'AZURE_SPEECH_REGION', ] // Allow some vars to be null/empty /* If not in GAE and Prod/QA/Demo env (f.e. on localhost/dev env), allow following env vars to be null */ @@ -259,6 +265,11 @@ export function getEnv(): BackendEnv { apiUrl: parse('READWISE_API_URL'), } + const azure = { + speechKey: parse('AZURE_SPEECH_KEY'), + speechRegion: parse('AZURE_SPEECH_REGION'), + } + return { pg, client, @@ -277,6 +288,7 @@ export function getEnv(): BackendEnv { sender, sendgrid, readwise, + azure, } } diff --git a/packages/api/src/utils/textToSpeech.ts b/packages/api/src/utils/textToSpeech.ts index 7f855cab7..9a1a3ad1a 100644 --- a/packages/api/src/utils/textToSpeech.ts +++ b/packages/api/src/utils/textToSpeech.ts @@ -1,7 +1,7 @@ -import * as AWS from 'aws-sdk' import { buildLogger } from './logger' -import { getFilePublicUrl, uploadToBucket } from './uploads' -import { SynthesizeSpeechInput } from 'aws-sdk/clients/polly' +import { createGCSFile, getFilePublicUrl } from './uploads' +import * as sdk from 'microsoft-cognitiveservices-speech-sdk' +import { env } from '../env' export interface TextToSpeechInput { id: string @@ -14,82 +14,197 @@ export interface TextToSpeechInput { export interface TextToSpeechOutput { audioUrl: string - speechMarks: string + speechMarks: SpeechMark[] +} + +export interface SpeechMark { + time: number + start: number + length: number + word: string } const logger = buildLogger('app.dispatch') -// create a new AWS Polly client -const client = new AWS.Polly() +// // create a new AWS Polly client +// const client = new AWS.Polly() -export const createAudio = async ( - input: TextToSpeechInput -): Promise => { - const { text, voice, textType, engine, languageCode } = input - const params: SynthesizeSpeechInput = { - OutputFormat: 'ogg_vorbis', - Text: text, - TextType: textType || 'text', - VoiceId: voice || 'Joanna', - Engine: engine || 'neural', - LanguageCode: languageCode || 'en-US', - } - try { - const data = await client.synthesizeSpeech(params).promise() - return data.AudioStream as Buffer - } catch (error) { - logger.error('Unable to create audio file', { error }) - throw error - } -} - -export const createSpeechMarks = async ( - input: TextToSpeechInput -): Promise => { - const { text, voice, textType, engine, languageCode } = input - const params: SynthesizeSpeechInput = { - OutputFormat: 'json', - Text: text, - TextType: textType || 'text', - VoiceId: voice || 'Joanna', - Engine: engine || 'neural', - SpeechMarkTypes: ['word'], - LanguageCode: languageCode || 'en-US', - } - try { - const data = await client.synthesizeSpeech(params).promise() - return (data.AudioStream as Buffer).toString() - } catch (error) { - logger.error('Unable to create speech marks', { error }) - throw error - } -} - -export const createAudioWithSpeechMarks = async ( +export const synthesizeTextToSpeech = async ( input: TextToSpeechInput ): Promise => { - try { - const audio = await createAudio(input) - // upload audio to google cloud storage - const filePath = `speech/${input.id}.ogg` + const audioFile = `speech/${input.id}.mp3` + const gcsFile = createGCSFile(audioFile) + const writeStream = gcsFile.createWriteStream({ + public: true, + resumable: true, + }) + const speechConfig = sdk.SpeechConfig.fromSubscription( + env.azure.speechKey, + env.azure.speechRegion + ) + speechConfig.speechSynthesisLanguage = input.languageCode || 'en-US' + speechConfig.speechSynthesisVoiceName = input.voice || 'en-US-JennyNeural' + speechConfig.speechSynthesisOutputFormat = + sdk.SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3 - logger.info('start uploading...', { filePath }) - await uploadToBucket(filePath, audio, { - contentType: 'audio/ogg', - public: true, - }) + // Create the speech synthesizer. + const synthesizer = new sdk.SpeechSynthesizer(speechConfig) + const speechMarks: SpeechMark[] = [] - // get public url for audio file - const publicUrl = getFilePublicUrl(filePath) - logger.info('upload complete', { publicUrl }) + synthesizer.synthesizing = function (s, e) { + logger.debug(`synthesizing ${e.result.audioData.byteLength} bytes`) + // convert arrayBuffer to stream and write to gcs file + writeStream.write(Buffer.from(e.result.audioData)) + } - const speechMarks = await createSpeechMarks(input) - return { - audioUrl: publicUrl, - speechMarks, + // The event synthesis completed signals that the synthesis is completed. + synthesizer.synthesisCompleted = function (s, e) { + logger.info( + '(synthesized) Reason: ' + + sdk.ResultReason[e.result.reason] + + ' Audio length: ' + + e.result.audioData.byteLength + ) + } + + // The synthesis started event signals that the synthesis is started. + synthesizer.synthesisStarted = function (s, e) { + logger.info('(synthesis started)') + } + + // The event signals that the service has stopped processing speech. + // This can happen when an error is encountered. + synthesizer.SynthesisCanceled = function (s, e) { + const cancellationDetails = sdk.CancellationDetails.fromResult(e.result) + let str = + '(cancel) Reason: ' + sdk.CancellationReason[cancellationDetails.reason] + if (cancellationDetails.reason === sdk.CancellationReason.Error) { + str += ': ' + e.result.errorDetails } - } catch (error) { - logger.error('Unable to create audio with speech marks', error) - throw error + logger.info(str) + } + + synthesizer.wordBoundary = function (s, e) { + speechMarks.push({ + word: e.text, + time: e.audioOffset, + start: e.textOffset, + length: e.wordLength, + }) + } + + const speakTextAsyncPromise = ( + text: string + ): Promise => { + return new Promise((resolve, reject) => { + synthesizer.speakTextAsync( + text, + (result) => { + resolve(result) + }, + (error) => { + synthesizer.close() + reject(error) + } + ) + }) + } + // slice the text into chunks of 1,000 characters + const textChunks = input.text.match(/.{1,1000}/g) || [] + for (const textChunk of textChunks) { + console.debug(`synthesizing ${textChunk}`) + await speakTextAsyncPromise(textChunk) + } + writeStream.end() + synthesizer.close() + + logger.debug(`audio file: ${audioFile}`) + logger.debug(`speechMarks: ${speechMarks}`) + + return { + audioUrl: getFilePublicUrl(audioFile), + speechMarks, } } + +// export const createAudio = async ( +// input: TextToSpeechInput +// ): Promise => { +// const { text, voice, textType, engine, languageCode } = input +// const params: SynthesizeSpeechInput = { +// OutputFormat: 'ogg_vorbis', +// Text: text, +// TextType: textType || 'text', +// VoiceId: voice || 'Joanna', +// Engine: engine || 'neural', +// LanguageCode: languageCode || 'en-US', +// } +// try { +// const data = await client.synthesizeSpeech(params).promise() +// return data.AudioStream as Buffer +// } catch (error) { +// logger.error('Unable to create audio file', { error }) +// throw error +// } +// } + +// export const createSpeechMarks = async ( +// input: TextToSpeechInput +// ): Promise => { +// const { text, voice, textType, engine, languageCode } = input +// const params: SynthesizeSpeechInput = { +// OutputFormat: 'json', +// Text: text, +// TextType: textType || 'text', +// VoiceId: voice || 'Joanna', +// Engine: engine || 'neural', +// SpeechMarkTypes: ['word'], +// LanguageCode: languageCode || 'en-US', +// } +// try { +// const data = await client.synthesizeSpeech(params).promise() +// return (data.AudioStream as Buffer).toString() +// } catch (error) { +// logger.error('Unable to create speech marks', { error }) +// throw error +// } +// } +// +// export const createAudioWithSpeechMarks = async ( +// input: TextToSpeechInput +// ): Promise => { +// try { +// const audio = await createAudio(input) +// // upload audio to google cloud storage +// const filePath = `speech/${input.id}.ogg` +// +// logger.info('start uploading...', { filePath }) +// await uploadToBucket(filePath, audio, { +// contentType: 'audio/ogg', +// public: true, +// }) +// +// // get public url for audio file +// const publicUrl = getFilePublicUrl(filePath) +// logger.info('upload complete', { publicUrl }) +// +// const speechMarks = await createSpeechMarks(input) +// return { +// audioUrl: publicUrl, +// speechMarks, +// } +// } catch (error) { +// logger.error('Unable to create audio with speech marks', error) +// throw error +// } +// } + +export const htmlToSsml = ( + html: string, + language = 'en-US', + voice = 'en-US-JennyNeural', + rate = 100, + volume = 100 +): string => { + return `${html}` +} diff --git a/packages/api/src/utils/uploads.ts b/packages/api/src/utils/uploads.ts index 351799a34..8e631f3e1 100644 --- a/packages/api/src/utils/uploads.ts +++ b/packages/api/src/utils/uploads.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { env } from '../env' -import { GetSignedUrlConfig, Storage } from '@google-cloud/storage' +import { File, GetSignedUrlConfig, Storage } from '@google-cloud/storage' /* On GAE/Prod, we shall rely on default app engine service account credentials. * Two changes needed: 1) add default service account to our uploads GCS Bucket @@ -112,3 +112,7 @@ export const uploadToBucket = async ( .file(filePath) .save(data, options) } + +export const createGCSFile = (filename: string): File => { + return storage.bucket(bucketName).file(filename) +} diff --git a/packages/api/test/utils/textToSpeech.test.ts b/packages/api/test/utils/textToSpeech.test.ts index 0dc698c33..4909192a5 100644 --- a/packages/api/test/utils/textToSpeech.test.ts +++ b/packages/api/test/utils/textToSpeech.test.ts @@ -1,6 +1,6 @@ import 'mocha' import { - createAudioWithSpeechMarks, + synthesizeTextToSpeech, TextToSpeechInput, } from '../../src/utils/textToSpeech' import { expect } from 'chai' @@ -11,13 +11,76 @@ describe('textToSpeech', () => { it('should create an audio file with speech marks', async () => { const input: TextToSpeechInput = { id: generateFakeUuid(), - text: 'The rumor mill suggests that Google may be looking to kill off its game streaming platform, Stadia, for good before the end of the year.', - engine: 'standard', - textType: 'ssml', + text: + 'MIT spinout Quaise Energy is working to create geothermal wells made from the deepest holes in the world.\n' + + '\n' + + 'Publication Date:\n' + + '\n' + + 'June 28, 2022\n' + + '\n' + + "A graphic depicting the heat at the earth's core\n" + + 'Caption:\n' + + '\n' + + 'Quaise Energy wants to repurpose coal and gas plants into deep geothermal wells by using X-rays to melt rock.\n' + + '\n' + + 'Credits:\n' + + '\n' + + 'Image: Collage by MIT News with images courtesy of Quaise Energy\n' + + '\n' + + 'There’s an abandoned coal power plant in upstate New York that most people regard as a useless relic. But MIT’s Paul Woskov sees things differently.\n' + + '\n' + + 'Woskov, a research engineer in MIT’s Plasma Science and Fusion Center, notes the plant’s power turbine is still intact and the transmission lines still run to the grid. Using an approach he’s been working on for the last 14 years, he’s hoping it will be back online, completely carbon-free, within the decade.\n' + + 'In fact, Quaise Energy, the company commercializing Woskov’s work, believes if it can retrofit one power plant, the same process will work on virtually every coal and gas power plant in the world.\n' + + '\n' + + 'Quaise is hoping to accomplish those lofty goals by tapping into the energy source below our feet. The company plans to vaporize enough rock to create the world’s deepest holes and harvest geothermal energy at a scale that could satisfy human energy consumption for millions of years. They haven’t yet solved all the related engineering challenges, but Quaise’s founders have set an ambitious timeline to begin harvesting energy from a pilot well by 2026.\n' + + '\n' + + 'The plan would be easier to dismiss as unrealistic if it were based on a new and unproven technology. But Quaise’s drilling systems center around a microwave-emitting device called a gyrotron that has been used in research and manufacturing for decades.\n' + + '\n' + + '“This will happen quickly once we solve the immediate engineering problems of transmitting a clean beam and having it operate at a high energy density without breakdown,” explains Woskov, who is not formally affiliated with Quaise but serves as an advisor. “It’ll go fast because the underlying technology, gyrotrons, are commercially available. You could place an order with a company and have a system delivered right now — granted, these beam sources have never been used 24/7, but they are engineered to be operational for long time periods. In five or six years, I think we’ll have a plant running if we solve these engineering problems. I’m very optimistic.”\n' + + 'Woskov and many other researchers have been using gyrotrons to heat material in nuclear fusion experiments for decades. It wasn’t until 2008, however, after the MIT Energy Initiative (MITEI) published a request for proposals on new geothermal drilling technologies, that Woskov thought of using gyrotrons for a new application.\n' + + '\n' + + '“[Gyrotrons] haven’t been well-publicized in the general science community, but those of us in fusion research understood they were very powerful beam sources — like lasers, but in a different frequency range,” Woskov says. “I thought, why not direct these high-power beams, instead of into fusion plasma, down into rock and vaporize the hole?”\n' + + '\n' + + 'As power from other renewable energy sources has exploded in recent decades, geothermal energy has plateaued, mainly because geothermal plants only exist in places where natural conditions allow for energy extraction at relatively shallow depths of up to 400 feet beneath the Earth’s surface. At a certain point, conventional drilling becomes impractical because deeper crust is both hotter and harder, which wears down mechanical drill bits.\n' + + '\n' + + 'Woskov’s idea to use gyrotron beams to vaporize rock sent him on a research journey that has never really stopped. With some funding from MITEI, he began running tests, quickly filling his office with small rock formations he’d blasted with millimeter waves from a small gyrotron in MIT’s Plasma Science and Fusion Center.\n' + + '\n' + + 'Paul Woskov with blasted rock samples\n' + + 'Woskov displaying samples in his lab in 2016.\n' + + '\n' + + 'Photo: Paul Rivenberg\n' + + '\n' + + 'Around 2018, Woskov’s rocks got the attention of Carlos Araque ’01, SM ’02, who had spent his career in the oil and gas industry and was the technical director of MIT’s investment fund The Engine at the time.\n' + + '\n' + + 'That year, Araque and Matt Houde, who’d been working with geothermal company AltaRock Energy, founded Quaise. Quaise was soon given a grant by the Department of Energy to scale up Woskov’s experiments using a larger gyrotron.\n' + + '\n' + + 'With the larger machine, the team hopes to vaporize a hole 10 times the depth of Woskov’s lab experiments. That is expected to be accomplished by the end of this year. After that, the team will vaporize a hole 10 times the depth of the previous one — what Houde calls a 100-to-1 hole.\n' + + '“That’s something [the DOE] is particularly interested in, because they want to address the challenges posed by material removal over those greater lengths — in other words, can we show we’re fully flushing out the rock vapors?” Houde explains. “We believe the 100-to-1 test also gives us the confidence to go out and mobilize a prototype gyrotron drilling rig in the field for the first field demonstrations.”\n' + + '\n' + + 'Tests on the 100-to-1 hole are expected to be completed sometime next year. Quaise is also hoping to begin vaporizing rock in field tests late next year. The short timeline reflects the progress Woskov has already made in his lab.\n' + + '\n' + + "Although more engineering research is needed, ultimately, the team expects to be able to drill and operate these geothermal wells safely. “We believe, because of Paul’s work at MIT over the past decade, that most if not all of the core physics questions have been answered and addressed,” Houde says. “It’s really engineering challenges we have to answer, which doesn’t mean they’re easy to solve, but we’re not working against the laws of physics, to which there is no answer. It's more a matter of overcoming some of the more technical and cost considerations to making this work at a large scale.”\n" + + '\n' + + 'The company plans to begin harvesting energy from pilot geothermal wells that reach rock temperatures at up to 500 C by 2026. From there, the team hopes to begin repurposing coal and natural gas plants using its system.\n' + + '\n' + + '“We believe, if we can drill down to 20 kilometers, we can access these super-hot temperatures in greater than 90 percent of locations across the globe,” Houde says.\n' + + '\n' + + 'Quaise’s work with the DOE is addressing what it sees as the biggest remaining questions about drilling holes of unprecedented depth and pressure, such as material removal and determining the best casing to keep the hole stable and open. For the latter problem of well stability, Houde believes additional computer modeling is needed and expects to complete that modeling by the end of 2024.\n' + + '\n' + + 'By drilling the holes at existing power plants, Quaise will be able to move faster than if it had to get permits to build new plants and transmission lines. And by making their millimeter-wave drilling equipment compatible with the existing global fleet of drilling rigs, it will also allow the company to tap into the oil and gas industry’s global workforce.\n' + + '\n' + + '“At these high temperatures [we’re accessing], we’re producing steam very close to, if not exceeding, the temperature that today’s coal and gas-fired power plants operate at,” Houde says. “So, we can go to existing power plants and say, ‘We can replace 95 to 100 percent of your coal use by developing a geothermal field and producing steam from the Earth, at the same temperature you’re burning coal to run your turbine, directly replacing carbon emissions.”\n' + + '\n' + + 'Transforming the world’s energy systems in such a short timeframe is something the founders see as critical to help avoid the most catastrophic global warming scenarios.\n' + + '\n' + + '“There have been tremendous gains in renewables over the last decade, but the big picture today is we’re not going nearly fast enough to hit the milestones we need for limiting the worst impacts of climate change,” Houde says. “[Deep geothermal] is a power resource that can scale anywhere and has the ability to tap into a large workforce in the energy industry to readily repackage their skills for a totally carbon free energy source.”\n' + + '\n' + + 'Related Topics\n' + + 'Related Articles', } - const output = await createAudioWithSpeechMarks(input) + const output = await synthesizeTextToSpeech(input) expect(output.audioUrl).to.be.a('string') - expect(output.speechMarks).to.be.a('string') + expect(output.speechMarks).to.be.a('array') }) }) }) diff --git a/yarn.lock b/yarn.lock index e706dd28c..c73f437a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8739,6 +8739,11 @@ addressparser@^1.0.1: resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746" integrity sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg== +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + agent-base@6: version "6.0.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" @@ -8746,7 +8751,7 @@ agent-base@6: dependencies: debug "4" -agent-base@^6.0.2: +agent-base@^6.0.1, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -9360,7 +9365,21 @@ asap@^2.0.0, asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -asn1.js@^5.2.0: +asn1.js-rfc2560@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/asn1.js-rfc2560/-/asn1.js-rfc2560-5.0.1.tgz#cff99b903e714756b29503ad49de01c72f131e60" + integrity sha512-1PrVg6kuBziDN3PGFmRk3QrjpKvP9h/Hv5yMrFZvC1kpzP6dQRzf5BpKstANqHBkaOUmTpakJWhicTATOA/SbA== + dependencies: + asn1.js-rfc5280 "^3.0.0" + +asn1.js-rfc5280@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/asn1.js-rfc5280/-/asn1.js-rfc5280-3.0.0.tgz#94e60498d5d4984b842d1a825485837574ccc902" + integrity sha512-Y2LZPOWeZ6qehv698ZgOGGCZXBQShObWnGthTrIFlIQjuV1gg2B8QOhWFRExq/MR1VnPpIIe7P9vX2vElxv+Pg== + dependencies: + asn1.js "^5.0.0" + +asn1.js@^5.0.0, asn1.js@^5.2.0: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== @@ -9422,6 +9441,19 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-disk-cache@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-2.1.0.tgz#e0f37b187ed8c41a5991518a9556d206ae2843a2" + integrity sha512-iH+boep2xivfD9wMaZWkywYIURSmsL96d6MoqrC94BnGSvXE4Quf8hnJiHGFYhw/nLeIa1XyRaf4vvcvkwAefg== + dependencies: + debug "^4.1.1" + heimdalljs "^0.2.3" + istextorbinary "^2.5.1" + mkdirp "^0.5.0" + rimraf "^3.0.0" + rsvp "^4.8.5" + username-sync "^1.0.2" + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -9845,6 +9877,15 @@ benchmark@^2.1.4: lodash "^4.17.4" platform "^1.3.3" +bent@^7.3.12: + version "7.3.12" + resolved "https://registry.yarnpkg.com/bent/-/bent-7.3.12.tgz#e0a2775d4425e7674c64b78b242af4f49da6b035" + integrity sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w== + dependencies: + bytesish "^0.4.1" + caseless "~0.12.0" + is-stream "^2.0.0" + better-opn@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-2.1.1.tgz#94a55b4695dc79288f31d7d0e5f658320759f7c6" @@ -9872,6 +9913,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== +binaryextensions@^2.1.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" + integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -10218,6 +10264,11 @@ bytes@3.1.1: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== +bytesish@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/bytesish/-/bytesish-0.4.4.tgz#f3b535a0f1153747427aee27256748cff92347e6" + integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== + c8@^7.6.0: version "7.11.0" resolved "https://registry.yarnpkg.com/c8/-/c8-7.11.0.tgz#b3ab4e9e03295a102c47ce11d4ef6d735d9a9ac9" @@ -12488,6 +12539,14 @@ ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: dependencies: safe-buffer "^5.0.1" +editions@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698" + integrity sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA== + dependencies: + errlop "^2.0.0" + semver "^6.3.0" + editorconfig@^0.15.3: version "0.15.3" resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" @@ -12664,6 +12723,11 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== +errlop@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b" + integrity sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -14961,6 +15025,13 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" +heimdalljs@^0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.6.tgz#b0eebabc412813aeb9542f9cc622cb58dbdcd9fe" + integrity sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA== + dependencies: + rsvp "~3.2.1" + hexer@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653" @@ -15309,6 +15380,14 @@ https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -16335,6 +16414,15 @@ istanbul-reports@^3.0.2, istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +istextorbinary@^2.5.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.6.0.tgz#60776315fb0fa3999add276c02c69557b9ca28ab" + integrity sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA== + dependencies: + binaryextensions "^2.1.2" + editions "^2.2.0" + textextensions "^2.5.0" + iterall@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" @@ -18255,6 +18343,21 @@ micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" +microsoft-cognitiveservices-speech-sdk@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.22.0.tgz#4c6f82147cbb364c5fa7478c7de691af781d6594" + integrity sha512-C1YV5jui3SD02DlmAlN+i7BKdBevETIbGxmkpFy/19yefja14Y7zOR/Hh0qb+ixuU49tPXWmTf2cL+FZE4YD6Q== + dependencies: + agent-base "^6.0.1" + asn1.js-rfc2560 "^5.0.1" + asn1.js-rfc5280 "^3.0.0" + async-disk-cache "^2.1.0" + bent "^7.3.12" + https-proxy-agent "^4.0.0" + simple-lru-cache "0.0.2" + uuid "^8.3.0" + ws "^7.5.6" + microtime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.0.0.tgz#d140914bde88aa89b4f9fd2a18620b435af0f39b" @@ -21994,11 +22097,16 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rsvp@^4.8.4: +rsvp@^4.8.4, rsvp@^4.8.5: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rsvp@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" + integrity sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg== + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -22423,6 +22531,11 @@ signedsource@^1.0.0: resolved "https://registry.yarnpkg.com/signedsource/-/signedsource-1.0.0.tgz#1ddace4981798f93bd833973803d80d52e93ad6a" integrity sha1-HdrOSYF5j5O9gzlzgD2A1S6TrWo= +simple-lru-cache@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz#d59cc3a193c1a5d0320f84ee732f6e4713e511dd" + integrity sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw== + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -23561,6 +23674,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +textextensions@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" + integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -24513,6 +24631,11 @@ user-home@^1.1.1: resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA= +username-sync@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/username-sync/-/username-sync-1.0.3.tgz#ae41c5c8a4c8c2ecc1443a7d0742742bd7e36732" + integrity sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -24584,7 +24707,7 @@ uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.0.0, uuid@^8.3.2: +uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.1, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -25339,6 +25462,11 @@ ws@8.8.1, ws@^8.2.3, ws@^8.3.0, ws@^8.4.2: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== +ws@^7.5.6: + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"