1

I am trying to upload the audio returned by Google's Text-to-Speech API in a Firebase Function and having trouble writing the audio file to the Node.js server's temp directory. I receive the following error in my functions log:

Write ERROR: { Error: ENOENT: no such file or directory, open '/tmp/synthesized/output.mp3' at Error (native) errno: -2, code: 'ENOENT', syscall: 'open', path: '/tmp/synthesized/output.mp3' }

Here's my imports:

    // Cloud Storage
    import * as Storage from '@google-cloud/storage';
    const gcs = new Storage();

    import { tmpdir } from 'os';
    import { join, dirname } from 'path';
    import * as fs from 'fs';
    import * as fse from 'fs-extra';

    // Cloud Text to Speech
    import * as textToSpeech from '@google-cloud/text-to-speech';
    const client = new textToSpeech.TextToSpeechClient();

...and the part of my function I'm having trouble with:

    // Construct the text-to-speech request
    const request = {
        input: { text: text },
        voice: { languageCode: 'en-US', ssmlGender: 'NEUTRAL' },
        audioConfig: { audioEncoding: 'MP3' },
    };

    // Creat temp directory
    const workingDir = join(tmpdir(), 'synthesized');
    const tmpFilePath = join(workingDir, 'output.mp3');

    // Ensure temp directory exists
    await fse.ensureDir(workingDir);

    // Performs the Text-to-Speech request
    client.synthesizeSpeech(request)
        .then(responses => {
            const response = responses[0];
            // Write the binary audio content to a local file in temp directory
            fs.writeFile(tmpFilePath, response.audioContent, 'binary', writeErr => {
                if (writeErr) {
                    console.error('Write ERROR:', writeErr);
                    return;
                }
                // Upload audio to Firebase Storage
                gcs.bucket(fileBucket).upload(tmpFilePath, {
                    destination: join(bucketDir, pageName)
                })
                    .then(() => { console.log('audio uploaded successfully') })
                    .catch((error) => { console.log(error) });
            });
        })
        .catch(err => {
            console.error('Synthesize ERROR:', err);
        });

What is wrong with my temp directory creation or fs.writeFile() function?

1 Answers1

4

(Answer edited in response to question edit...)

In your original question, you invoked

client.synthesizeSpeech(request, (err, response) => {...})

following Node's http callback pattern, in which the callback function may initiate before the response is complete. Your subsequent code calls methods that assume response content; if the response is still empty, fs.writeFile() writes nothing initially, and subsequent methods cannot find the non-existent file. (Because fs.writeFile() follows the same callback pattern, you might even discover that output.mp3 file after the program exits, because fs will stream the input. But I bet your Firebase methods aren't waiting.)

The solution is to use Promises or async/await. Looking at the Google TextToSpeechClient class docs, it looks like the synthesizeSpeech method supports this:

Returns: Promise -> Array. The first element of the array is an object representing SynthesizeSpeechResponse.

Example:

client.synthesizeSpeech(request)
  .then(responses => {
      var response = responses[0];
      // doThingsWith(response)
  })
  .catch(err => {
      console.error(err);
  });

That should solve the problem with client.synthesizeSpeech, but unfortunately fs.writeFile is still synchronous. If you were using Node >10 you could use a native fsPromise.writeFile method, and if you were using Node >8 you could use util.promisify() to convert fs.writeFile to promises. But you've indicated in comments that you are using Node 6, so we'll have to do things manually. Thieving from this reference:

const writeFilePromise = (file, data, option) => {
    return new Promise((resolve, reject) => {
        fs.writeFile(file, data, option, error => {
            if (error) reject(error);
            resolve("File created! Time for the next step!");
        });
    });
};

client.synthesizeSpeech(request)
    .then(responses => {
        const response = responses[0];
        return writeFilePromise(tmpFilePath, response.audioContent, 'binary');
    })
    .then(() => {
        return gcs.bucket(fileBucket).upload(tmpFilePath, {
            destination: join(bucketDir, pageName)
        });
    })
    .then(() => {
        console.log('audio uploaded successfully');
        return null;
    })
    .catch((error) => { console.log(error) });

I've written all of this using .then constructs, but naturally, you could also use async/await if you would rather do that. I hope this fixes things--it will force your Firebase code to wait until fs.writeFile has completed its job. I have also, unfortunately, smooshed all of the error checking into one final .catch block. And made things a bit verbose for clarity. I'm sure you can do better.

Andy Taton
  • 491
  • 4
  • 8
  • Thank you, Andy! I've updated `synthesizeSpeech` to return a Promise. I still have the same error, and am slowly chewing over things to implement your other advice. Two challenges I'm up against: Firebase Functions uses Node.js 6 so fsPromises is not available, and I'm still wrapping my head around Promises and async-await. But I'm getting there, so was excited to hear that my solution lay in implementing those techniques! – Jacob Bowdoin Sep 28 '18 at 13:28
  • @JacobBowdoin, could you update your question with your new code? I understand what you are saying about still getting the error, and I can think of a number of (Promise-related) reasons why. I can also help you manually promisify for Node 6--just because we can't use fsPromise doesn't mean we can't implement them on our own. – Andy Taton Sep 28 '18 at 15:57
  • Sorry for the delay - I've updated the question with my current code. Thank you for pursuing getting to the bottom of this! – Jacob Bowdoin Oct 02 '18 at 17:15
  • Excellent, thanks. I can see that the nested `gcs.bucket().upload()` function is still not async with respect to `fs.writeFile()`; it's not waiting for writeFile to finish (or even start, really) its job before it fires. We need to manually promisify that method. I'll update my answer with a replacement. – Andy Taton Oct 03 '18 at 03:16
  • Updated answer. Alternately, I noticed that Firebase Functions allows for use of Node 8 in beta; this would let you use `util.promisify`. – Andy Taton Oct 03 '18 at 04:19
  • Thank you! It works and I took that last .then a step further to grab the audio's downloadURL path (see https://stackoverflow.com/questions/42956250/get-download-url-from-file-uploaded-with-cloud-functions-for-firebase) .then(() => { console.log('audio uploaded successfully'); const updloadedAudioFile = gcs.bucket(fileBucket).file(audioName); return updloadedAudioFile.getSignedUrl({ action: 'read', expires: '03-09-2491' }).then(signedUrls => { audioPath = signedUrls[0]; }); }) – Jacob Bowdoin Oct 03 '18 at 17:24
  • [Documentation](https://developers.google.com/api-client-library/javascript/features/promises) on how to convert from **Callbacks** to **Promises**. I'm new to Js as an Android Developer so this post was useful. Thanks! – AdamHurwitz Dec 09 '18 at 01:41