mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Archive audio file and speech marks file in GCS
This commit is contained in:
parent
751699ab53
commit
b9c5f02fe3
2 changed files with 55 additions and 28 deletions
|
|
@ -279,16 +279,50 @@ export const textToSpeechStreamingHandler = Sentry.GCPFunction.wrapHttpFunction(
|
|||
return
|
||||
}
|
||||
console.log('Cache miss')
|
||||
// synthesize text to speech if cache miss
|
||||
|
||||
const bucket = process.env.GCS_UPLOAD_BUCKET
|
||||
if (!bucket) {
|
||||
throw new Error('GCS_UPLOAD_BUCKET not set')
|
||||
}
|
||||
|
||||
// audio file to be saved in GCS
|
||||
const audioFileName = `speech/${cacheKey}.mp3`
|
||||
const speechMarksFileName = `speech/${cacheKey}.json`
|
||||
const audioFile = createGCSFile(bucket, audioFileName)
|
||||
const speechMarksFile = createGCSFile(bucket, speechMarksFileName)
|
||||
// check if audio file already exists
|
||||
const [exists] = await audioFile.exists()
|
||||
if (exists) {
|
||||
console.debug('Audio file already exists')
|
||||
const [audioData] = await audioFile.download()
|
||||
const [speechMarksExists] = await speechMarksFile.exists()
|
||||
|
||||
return {
|
||||
audioData,
|
||||
speechMarks: speechMarksExists
|
||||
? JSON.parse((await speechMarksFile.download()).toString())
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
const input: TextToSpeechInput = {
|
||||
...utteranceInput,
|
||||
textType: 'ssml',
|
||||
key: cacheKey,
|
||||
}
|
||||
// synthesize text to speech if cache miss
|
||||
const { audioData, speechMarks } = await synthesizeTextToSpeech(input)
|
||||
if (!audioData) {
|
||||
return res.status(500).send({ errorCode: 'SYNTHESIZER_ERROR' })
|
||||
}
|
||||
|
||||
// upload audio data to GCS
|
||||
await audioFile.save(audioData)
|
||||
// upload speech marks to GCS
|
||||
if (speechMarks.length > 0) {
|
||||
await speechMarksFile.save(JSON.stringify(speechMarks))
|
||||
}
|
||||
|
||||
const audioDataString = audioData.toString('hex')
|
||||
// save audio data to cache for 24 hours for mainly the newsletters
|
||||
await redisClient.set(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import axios from 'axios'
|
|||
import ffmpegPath from '@ffmpeg-installer/ffmpeg'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
import { PassThrough } from 'stream'
|
||||
import { createGCSFile } from './index'
|
||||
|
||||
ffmpeg.setFfmpegPath(ffmpegPath.path)
|
||||
|
||||
|
|
@ -46,28 +45,6 @@ export class RealisticTextToSpeech implements TextToSpeech {
|
|||
throw new Error('PlayHT API credentials not set')
|
||||
}
|
||||
|
||||
const bucket = process.env.GCS_UPLOAD_BUCKET
|
||||
if (!bucket) {
|
||||
throw new Error('GCS_UPLOAD_BUCKET not set')
|
||||
}
|
||||
|
||||
// audio file to be saved in GCS
|
||||
const audioFileName = `speech/${input.key}.mp3`
|
||||
const audioFile = createGCSFile(bucket, audioFileName)
|
||||
// check if audio file already exists
|
||||
const [exists] = await audioFile.exists()
|
||||
if (exists) {
|
||||
console.debug('Audio file already exists')
|
||||
const [audioData] = await audioFile.download()
|
||||
return {
|
||||
audioData,
|
||||
speechMarks: [],
|
||||
}
|
||||
}
|
||||
|
||||
const outputStream = audioFile.createWriteStream({
|
||||
resumable: true,
|
||||
}) as PassThrough
|
||||
const inputStream = new PassThrough()
|
||||
|
||||
const HEADERS = {
|
||||
|
|
@ -100,8 +77,8 @@ export class RealisticTextToSpeech implements TextToSpeech {
|
|||
// timeout after 1 hour
|
||||
const timeout = 60 * 60 * 1000
|
||||
const startTime = Date.now()
|
||||
let audioData: Buffer | undefined
|
||||
while (!audioData) {
|
||||
let isReady = false
|
||||
while (!isReady) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error('Timeout when polling the download url')
|
||||
}
|
||||
|
|
@ -117,17 +94,33 @@ export class RealisticTextToSpeech implements TextToSpeech {
|
|||
|
||||
// write the audio file to the input stream
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
audioData = Buffer.from(downloadResponse.data, 'binary')
|
||||
inputStream.end(audioData)
|
||||
inputStream.end(Buffer.from(downloadResponse.data, 'binary'))
|
||||
isReady = true
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
console.debug('checking status of audio file', downloadUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const outputStream = new PassThrough()
|
||||
// transcode the audio file to mp3
|
||||
await convertWavToMp3AndUpload(inputStream, outputStream)
|
||||
|
||||
// convert the buffer stream to a buffer
|
||||
const audioData = await new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = []
|
||||
outputStream.on('data', (chunk) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
chunks.push(chunk)
|
||||
})
|
||||
outputStream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks))
|
||||
})
|
||||
outputStream.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
audioData,
|
||||
speechMarks: [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue