Wrap a callback-based JS library with Promises

Ste Griffiths - Jul 25 '23 - - Dev Community

I’ve been integrating the Microsoft Cognitive Services Text-to-speech thingy, to make a little web API that can speak Vietnamese.

Take a look at this sample code for using the microsoft-cognitiveservices-speech-sdk npm package (I’ve snipped some stuff for brevity):

var sdk = require("microsoft-cognitiveservices-speech-sdk");

// This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
const speechConfig = sdk.SpeechConfig.fromSubscription(process.env.SPEECH_KEY, process.env.SPEECH_REGION);
const audioConfig = sdk.AudioConfig.fromAudioFileOutput("audio.wav");

// The language of the voice that speaks.
speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural"; 

// Create the speech synthesizer.
var synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);
var text = "You have selected Microsoft Sam as the Computer's default voice.";

// Start the synthesizer and wait for a result.
synthesizer.speakTextAsync(text,
  function (result) {
    if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
      console.log("Synthesis finished.");
    } else {
      console.error("Speech synthesis canceled, " + result.errorDetails);
    }
    synthesizer.close();
    synthesizer = null;
  },
  function (err) {
    console.trace("err - " + err);
    synthesizer.close();
    synthesizer = null;
  }
);

console.log("Now synthesizing to: " + audioFile);

Enter fullscreen mode Exit fullscreen mode

The problem

The example synthesizer object only produces values in its callbacks.

If you wrap this code sample in a speak(text) function, then it will return before the values you want are “ready”. You can’t directly return the outcome; you’d have to either match its old-school vibes and pass a cb callback argument, or if you were hoisting it into express, perhaps you’d pass in the express response object so that your speak function can directly return a web response.

Rubbish solutions!!

ObviouslY we want to wrap it in a Promise so that we can provide a dead simple awaitable interface for an async route handler to use:

app.get("/create/:text", async (req, res) => {
  const result = await speak(req.params.text);
  return res.json(result);
});

Enter fullscreen mode Exit fullscreen mode

Trading callbacks for a Promise

But how do we get a Promise out of this mess?

  1. Wrap the insides of the new function in return new Promise((resolve, reject) => { ... }
  2. Anywhere you want to return a success, invoke resolve(someResponseObject)
  3. Anywhere you want to handle an error, invoke reject(someErrorObject)

That’s it! Your new method is then-able and async!

export const speak = (text) => {
  return new Promise((resolve, reject) => {
    const audioFileName = "audio.wav";

    if (!process.env.SPEECH_KEY || !process.env.SPEECH_REGION)
    {
      const resultModel = {
        "Status" : "Error",
        "Error" : "Unable to connect to speech server"
      }
      reject(resultModel);
    }

    try {
      // Connect SDK
      const speechConfig = sdk.SpeechConfig.fromSubscription(
        process.env.SPEECH_KEY,
        process.env.SPEECH_REGION
      );
      const audioConfig = sdk.AudioConfig.fromAudioFileOutput(audioFileName);

      speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural"; 

      // Create the speech synthesizer.
      var synthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);

      synthesizer.speakTextAsync(
        text,
        function (result) {
          synthesizer.close();
          synthesizer = null;
          const resultModel = {
            "Status" : "OK",
            "Text" : text,
            "Audio" : audioFileName,
            "ResultID" : result.resultId
          }
          resolve(resultModel);
        },
        function (err) {
          synthesizer.close();
          synthesizer = null;
          const resultModel = {
            "Status" : "Error",
            "Error" : err
          }
          reject(resultModel);
        }
      );
    } catch (error) {
      const resultModel = {
        "Status" : "Error",
        "Error" : error
      }
      reject(resultModel);
    }
  });
};

Enter fullscreen mode Exit fullscreen mode

Isn’t that cool?

As usual, I basically got this all from the great Promise examples on MDN.

I wish you growth and harmony this summertime 🌼

. . . . . . . . . . . . . . . . . . . . . . .