Only getting the audio of the last request when doing multiple requests at once
See original GitHub issueWhen doing multiple requests at once, using Promise.all
, I seem to only get the audioContent
of the last resolving request. It seems like the underlying code the library uses cannot handle concurrent requests?
I’m synthesizing large text’s and need to split it up using the API’s character limit. I had this working before, so I know it should work, but stopped working recently.
Am I doing something wrong here? Or is this a bug?
I’m doing the exact same with Amazon’s Polly, and there it works. It’s exactly the same code, but with a different client and different request options.
So that made me think maybe it’s a library thing? Or a Google service issue?
Environment details
- OS: Mac
- Node.js version: 10.15.0
- npm version: 6.8.0
@google-cloud/text-to-speech
version: 0.5.1
Steps to reproduce
export const googleSsmlToSpeech = async (
index: number,
ssmlPart: string,
type: SynthesizerType,
identifier: string,
synthesizerOptions: GoogleSynthesizerOptions,
storageUploadPath: string
) => {
let extension = 'mp3';
if (synthesizerOptions.audioConfig.audioEncoding === 'OGG_OPUS') {
extension = 'opus';
}
if (synthesizerOptions.audioConfig.audioEncoding === 'LINEAR16') {
extension = 'wav';
}
synthesizerOptions.input.ssml = ssmlPart;
const tempLocalAudiofilePath = `${appRootPath}/temp/${storageUploadPath}-${index}.${extension}`;
try {
// Make sure the path exists, if not, we create it
await fsExtra.ensureFile(tempLocalAudiofilePath);
// Performs the Text-to-Speech request
const [response] = await client.synthesizeSpeech(synthesizerOptions);
// Write the binary audio content to a local file
await fsExtra.writeFile(tempLocalAudiofilePath, response.audioContent, 'binary');
return tempLocalAudiofilePath;
} catch (err) {
throw err;
}
};
/**
* Synthesizes the SSML parts into seperate audiofiles
*/
export const googleSsmlPartsToSpeech = async (
ssmlParts: string[],
type: SynthesizerType,
identifier: string,
synthesizerOptions: GoogleSynthesizerOptions,
storageUploadPath: string
) => {
const promises: Promise<string>[] = [];
// Overwrite for demo purposes
ssmlParts = [
'<speak><p>this</p></speak>',
'<speak><p>is a</p></speak>',
'<speak><p>bit of a longer test</p></speak>'
];
ssmlParts.forEach((ssmlPart: string, index: number) => {
promises.push(googleSsmlToSpeech(index, ssmlPart, type, identifier, synthesizerOptions, storageUploadPath));
});
const tempAudioFiles = await Promise.all(promises);
tempAudioFiles.sort((a: any, b: any) => b - a); // Sort: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 etc...
return tempAudioFiles;
};
The above code creates multiple files with the correct naming and index number, however, they all contain the same audio. That is; the audio response of the last request (<speak><p>bit of a longer test</p></speak>
).
824163ed-b4d9-4830-99da-6e6f985727e2-0.mp3
824163ed-b4d9-4830-99da-6e6f985727e2-1.mp3
824163ed-b4d9-4830-99da-6e6f985727e2-2.mp3
Replacing the Promise.all
with a simple for
loop, makes it work. But this takes longer as it waits for every request to resolve. I know a Promise.all
can work, because I had it working before.
const tempAudioFiles = [];
for (var i = 0; i < ssmlParts.length; i++) {
tempAudioFiles[i] = await googleSsmlToSpeech(i, ssmlParts[i], type, identifier, synthesizerOptions, storageUploadPath);
}
edit: updated the title and first paragraph with new findings. It seems to always return just the last one, even if it contains more text/audio then the other requests.
Issue Analytics
- State:
- Created 4 years ago
- Comments:8 (4 by maintainers)
So what happens is that the API method you’re calling (
client.synthesizeSpeech
) is asynchronous inside (makes several network calls and uses promises). It’s likely that it yields the control flow before the actual request is sent to the server over gRPC, and this yielding is enough for the outerforEach
loop to reassign the request parameter. You should not expect that your request parameter will be sent to the server right away without a context switch. Using unique object for each request is the best option.Thanks!
To give you even more details, in Node the resolution of the promise is always asynchronous: in
promise.then(func)
,func
will be always executed afterthen()
returns, on the next tick. To demonstrate:It will print
next tick happened
before it printspromise resolved
. Since we do have a promise and.then()
in the method implenentation, this is where we yield the execution. We have a plan to change that.