Три года назад я внедрил IBM Cloud Speech-to-Text в свое веб-приложение LanguageTwo. Это была худшая часть моего приложения. Потоковое соединение WebSockets было в лучшем случае ненадежным. Я потратил три недели на то, чтобы починить это.

В моем проекте используются AngularJS и Firebase.

Мой первый план состоял в том, чтобы прекратить потоковую передачу WebSockets и вместо этого записывать каждый аудиофайл, сохранять его в базе данных, отправлять файл в IBM Cloud для обработки, а затем отправлять ответ в браузер для отображения пользователю.

Я потратил неделю на то, чтобы выяснить, как настроить браузер для записи с микрофона. Оказывается, есть старый способ, использующий Navigator.getUserMedia(), и новый способ, использующий MediaDevices.getUserMedia(). Есть тонны библиотек, которые плохо используют старый способ. Мне пришлось в них покопаться, прежде чем я нашел новый путь. Новый способ прост. Сначала в шаблоне я создал две кнопки (Bootstrap) для запуска и остановки записи и место для результатов.

<div class="col-sm-2 col-md-2 col-lg-2" ng-show="nativeLanguage === 'en-US'">
    <button type="button" class="btn btn-block btn-default" ng-click="startWatsonSpeechToText()" uib-tooltip="Wait for 'Listening'">Start pronunciation</button>
  </div>
  <div class="col-sm-2 col-md-2 col-lg-2" ng-show="nativeLanguage === 'en-US'">
    <button type="button" class="btn btn-block btn-default" ng-click="stopWatsonSpeechToText()">Stop pronunciation</button>
  </div>
<div class="col-sm-6 col-md-6 col-lg-6" ng-class="speechToTextResults">
    <h3>{{normalizedText}}&nbsp;&nbsp;&nbsp;{{confidence}}</h3>
  </div>
```

Вот функция-обработчик контроллера:

$scope.getMicrophone = function() {
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
   .then(stream => {
   console.error("Error");
}
var options = {
   audioBitsPerSecond: 16000,
   mimeType: 'audio/webm;codecs=opus'
};
const mediaRecorder = new MediaRecorder(stream, options);
   mediaRecorder.start();
const audioChunks = [];
mediaRecorder.addEventListener("dataavailable", event => {
   audioChunks.push(event.data);
});
mediaRecorder.addEventListener("stop", () => {
   const audioBlob = new Blob(audioChunks);
// upload to Firebase Storage
firebase.storage().ref('Users/' + $scope.user.uid + '/Pronunciation_Test').put(audioBlob) 
      .then(function(snapshot) {
firebase.storage().ref(snapshot.ref.location.path).getDownloadURL()   
          .then(function(url) { // get downloadURL
            firebase.firestore().collection('Users').doc($scope.user.uid).collection("Pronunciation_Test").doc('downloadURL').set({
              downloadURL: url,
              keywords: keywordArray,
              model: watsonSpeechModel,
            })
            .then(function() {
              console.log("Document successfully written!");
            })
            .catch(function(error) {
              console.error("Error writing document: ", error);
            });
          })
          .catch(error => console.error(error));
        })
        .catch(error => console.error(error));
// play back the audio blob
     const audioUrl = URL.createObjectURL(audioBlob);
     const audio = new Audio(audioUrl);
     audio.play();
});
// click to stop
     $scope.stopMicrophone = function() {
     $scope.normalizedText = "Waiting for IBM Cloud Speech-to-Text"; // displays this text
     $scope.confidence = "0.00"; // displays this confidence level
        mediaRecorder.stop();
     }; // end $scope.stopMicrophone
   })
   .catch(function(error) {
      console.log(error.name + ": " + error.message);
});
};

Первая строка - это функция-обработчик AngularJS для кнопки start.

Первый блок кода получает микрофон и создает поток с микрофона. Эта реализация требует освоения потоковой передачи. Код «старой школы» - это алгоритмы и структуры данных; собеседования при приеме на работу иногда включали решение шахматной задачи. Пришло время писать код: обрабатывать потоки данных и создавать цепочки асинхронных вызовов API.

Следующий блок кода устанавливает объект `options`. Это устанавливает три параметра в двух свойствах: биты в секунду, тип мультимедиа и формат кодирования. В Chrome есть много типов мультимедиа для видео и форматов кодирования, но только один тип мультимедиа - webm - и один формат кодирования - opus. Вы можете выбрать бит в секунду. Читая документацию по преобразованию речи в текст IBM Cloud, я увидел, что она имеет два режима: широкополосный и узкополосный. Первый субдискретизирует все до 16 000 бит в секунду; последний субдискретизирует все до 8000 бит / с. Нет причин записывать более 16 000 бит / с.

Следующий блок кода берет stream и options и запускаетmediaRecorder. mediaRecorder является частью пакета getUserMedia.

Затем мы создаем массив для audioChunks и помещаем в него потоковые данные.

Когда пользователь нажимает кнопку stop, audioChunks объединяются в blob файл.

blob загружается в базу данных Firebase Storage. Это база данных для больших цифровых файлов, таких как фотографии, видео и аудио. Затем база данных возвращает downloadURL.

downloadURL записывается в базу данных Firebase Firestore. Это база данных NoSQL, которая обрабатывает документы и коллекции (объекты и массивы). Мы также пишем массив ключевых слов и модель, например, en-US_BroadbandModel.

Затем есть блок кода, который воспроизводит запись пользователю.

Наконец, у меня есть функция-обработчик AngularJS для кнопки stop. Обратите внимание, что я вложил две функции-обработчики AngularJS, start и stop.

Теперь у нас есть аудиофайл, хранящийся в облачной базе данных. Я использовал функцию Firebase Cloud, также известную как функция Google Cloud, для отправки аудиофайла в IBM Cloud Speech-to-Text:

exports.IBM_Speech_to_Text = functions.firestore.document('Users/{userID}/Pronunciation_Test/downloadURL').onUpdate((change, context) => {
    const axios = require('axios'); // library for HTTP requests
    const SpeechToTextV1 = require('ibm-watson/speech-to-text/v1');
    const { IamAuthenticator } = require('ibm-watson/auth');
const speechToText = new SpeechToTextV1({
      authenticator: new IamAuthenticator({
        apikey: 's00pers3cret',
      }),
      url: 'https://api.us-south.speech-to-text.watson.cloud.ibm.com/instances/01010101',
    });
const downloadURL = change.after.data().downloadURL.toString();
const keywords = change.after.data().keywords;
const model = change.after.data().model;
const userID = context.params.userID;
return axios({ 
      method: 'get',
      url: downloadURL,
      responseType: 'stream', // createReadStream is included in axios
    })
    .then(function (response) {
      var params = {
        audio: response.data, // response is already a stream, no need for fs.createReadStream
        contentType: 'application/octet-stream', // media file type/encoding; can be omitted, with 'application/octet-stream' the service automatically detect the format of the audio; consider using 'audio/webm;codecs=opus'
        wordAlternativesThreshold: 0.9,
        max_alternatives: 3,
        keywords: keywords, // pass in from browser
        keywordsThreshold: 0.5,
        model: model, // pass in from browser
        wordConfidence: true,
      };
      speechToText.recognize(params)
      .then(results => {
        console.log(JSON.stringify(results, null, 2));
        console.log(results.result.results[0].alternatives[0].transcript);
        console.log(results.result.results[0].alternatives[0].confidence);
admin.firestore().collection('Users').doc(userID).collection('Pronunciation_Test').doc('downloadURL').update( {
          response: {
            transcript: results.result.results[0].alternatives[0].transcript,
            confidence: results.result.results[0].alternatives[0].confidence
          } // make an object so that one listener can get both fields
        })
        .then(function() {
          console.log("Document updated.");
        })
        .catch(error => console.error(error));
      })
      .catch(error => console.error(error));
    })
    .catch(error => console.error(error));
  });

Первая строка - это триггер облачной функции. Когда новый URL-адрес загрузки записывается в расположение базы данных, срабатывает облачная функция.

Далее я втягиваю три библиотеки. axios - это современный пакет Node для обработки HTTP-запросов. Мы также получаем SDK IBM Watson Speech-to-Text и IBM Cloud auth SDK.

Я отправляю свои api-key и url в SDK преобразования речи в текст. Я получил их на панели инструментов IBM Cloud.

Я беру четыре константы из триггера: downloadURL, массив ключевых слов, модель и userID.

Теперь я готов отправить http-запрос в Firebase Storage, чтобы получить аудиофайл. Параметр responseType: 'stream', создает узел createReadStream, другими словами, аудиофайл передается в потоковом режиме. Причина, по которой мы хотим передавать данные в потоковом режиме, заключается в том, что для потоковой передачи используется меньше памяти. Если мы прочитаем аудиофайл в память, для аудиофайла размером 100 КБ потребуется 100 КБ памяти. Для потоковой передачи файла нам потребуется всего 5 или 10 КБ памяти.

Затем жду ответа от Firebase Storage. Первая строчка params забросила меня на пару дней. Это response.data, а не response. response - это объект JSON с заголовками и т. Д. Все, что нам сейчас нужно, это поток данных. Мне потребовалось бы пять минут, чтобы решить эту проблему с помощью Postman, но Postman не обрабатывает потоки, поэтому я не мог видеть, что возвращалось из Firebase Storage.

Вторая строка params устанавливает contentType в application/octet-stream. Подождите, разве мы уже не установили это как webm с opus кодировкой? Это тип мультимедиа, который использует Firebase Storage, если вы не укажете тип мультимедиа. Mozilla говорит оapplication/octet-stream

Это значение по умолчанию для двоичных файлов. Поскольку это означает неизвестный двоичный файл, браузеры обычно не запускают его или даже не спрашивают, нужно ли его запускать.

Другими словами, тип носителя не изменился, он остался webm/opus. Мы просто не указали тип носителя при загрузке в Firebase Storage.

Google Voice не поддерживает webm/opus тип мультимедиа. Чтобы отправить файл в Google Voice, потребуется его обработка (перекодирование) с использованием ffmpeg.

Остальные params сообщают IBM Cloud Speech-to-Text, что я хочу.

Теперь я вызываю SDK преобразования речи в текст с speechToText.recognize(params). Я жду results, регистрирую их и записываю в базу данных Firestore.

В моем AngularJS HomeController.js есть слушатель, который отслеживает изменения в этом местоположении базы данных:

firebase.firestore().collection('Users').doc($scope.user.uid).collection("Pronunciation_Test").doc('downloadURL')
  .onSnapshot(function(doc) {
    // console.log("Response: ", doc.data().response);
    $scope.normalizedText = doc.data().response.transcript; // displays this text
    $scope.confidence = doc.data().response.confidence; // displays this confidence level
    $scope.$apply();
  });

И пользователь видит ответ в браузере.

Работает отлично! За исключением одного. Время отклика составляет от одной до двух минут для трехсекундной записи. Ни один пользователь не хочет ждать так долго. Я заархивировал код и начал все сначала, используя SDK для потоковой передачи.

Потоковая передача

Вы можете посмотреть демонстрацию Транскрибировать с микрофона с уверенностью в словах.

Первый шаг - установить модуль узла watson-speech SDK с npm install watson-speech. Установите его в каталог своего проекта, чтобы упростить связывание. Теперь ссылка на файл в index.html:

<script src="node_modules/watson-speech/dist/watson-speech.min.js"></script>

Обновляйте watson-speech не реже одного раза в месяц. Я обновляю все свои модули Node еженедельно.

watson-speech - это как преобразование речи в текст, так и преобразование текста в речь. Может быть способ вызвать только watson-speech/speech-to-text и сэкономить время загрузки.

Далее вам понадобится жетон на предъявителя. Вы должны сделать запрос токена со своего сервера, а не из браузера, потому что ваш запрос будет включать ваш API-ключ IAM.

(Вы можете получить токен от старой службы преобразования речи в текст Cloud Foundry с вашим именем пользователя и паролем, но я обнаружил, что выставление счетов Cloud Foundry стало кошмаром. Каждые несколько месяцев я получал электронное письмо от кого-то из Мексики, в котором говорилось позвонить им по моей кредитной карте, чтобы оплатить счет в размере 0,38 доллара, иначе они закроют мою учетную запись в IBM Cloud. Они не смогли показать мне счет или информацию о том, как настроить кредитную карту в Cloud Foundry. Это выглядело как фишинговая экспедиция, но Именно так IBM Cloud Foundry обрабатывает выставление счетов. Новые сервисы IAM используют IBM Wallet, который упрощает просмотр счетов и внесение в вашу кредитную карту для выставления счетов.)

Мой проект бессерверный. Я использую Firebase Cloud Functions, что в большей или меньшей степени является функциями Google Cloud. Я написал функцию Node.js для получения токена на предъявителя:

exports.IBM_IAM_Bearer_Token = functions.firestore.document('Users/{userID}/IBM_Token/Token_Request').onWrite((change, context) => {
    const axios = require('axios');
    const querystring = require('querystring');
return axios.post('https://iam.cloud.ibm.com/identity/token', querystring.stringify({
      grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
      apikey: 's00pers3cret'
    }))
    .then(function(response) { 
      console.log(response.data.access_token);
      admin.firestore().collection('Users').doc(context.params.userID).collection('IBM_Token').doc('Token_Value').set({ 'token': response.data })
      .then(function() {
        console.log('Token written to database.');
      })
      .catch(error => console.error(error));
    })
    .catch(error => console.error(error));
  });

Первая строка - это триггер. Мой контроллер AngularJS начинается с:

firebase.firestore().collection('Users').doc($scope.user.uid).collection('IBM_Token').doc('Token_Value').onSnapshot(function(doc) {
    $scope.token = doc.data().token;
    // $scope.token = doc.data().token.access_token;
  });
firebase.firestore().collection('Users').doc($scope.user.uid).collection('IBM_Token').doc('Token_Request').set({ request: Math.random() });
$scope.normalizedText = undefined;
$scope.confidence = undefined;

Когда страница загружается, второй блок кода записывает случайное число в расположение базы данных Firebase Firestore, которое запускает облачную функцию Firebase. Первый блок кода - это слушатель, который ожидает нового токена. Слушатель должен начать прослушивание до того, как будет запрошен новый токен. Последние две строки очищают отображаемый текст.

Пользователь загружает страницу, в базу данных записывается случайное число, и срабатывает облачная функция. Следующие две строки загружают два модуля Node: axios и его помощник querystring. Эти модули Node помогают с HTTP-запросами.

Затем облачная функция выполняет HTTP-запрос, отправляя ключ API в URL-адрес. Ответ возвращается, и response.data записывается в базу данных.

IBM Cloud возвращает объект токена JSON:

{
access_token: "eyJraWQiOiIyMD..."
expiration: 1585780896
expires_in: 3600
refresh_token: "OKDoBLmGnqDoLX4bVO..."
scope: "ibm openid"
token_type: "Bearer"
}

access_token - это то, что вы отправляете в IBM Cloud для запуска службы преобразования речи в текст. expiration - дата в часе. Therefresh_token позволяет обновить токен.

Мой слушатель AngularJS помещает объект токена в $scope.

Я использую тот же шаблон с двумя кнопками для запуска и запуска функции обработчика.

Документация для IBM Cloud Speech-to-Text SDK находится здесь.

Приступим к функции-обработчику. Первые три строки:

$scope.startWatsonSpeechToText = function() {
    $scope.normalizedText = "Wait...;
    $scope.confidence = undefined;

Это срабатывает, когда пользователь нажимает кнопку Start. Пользователю предлагается подождать, пока SDK подключится к IBM Cloud.

Следующий,

console.log (WatsonSpeech.version);

Это регистрирует подключенную версию SDK. Я усердно обновлял модуль Node в течение трех лет, не понимая, что мой код связан со старым SDK, установленным с bower, который устарел на три года. Неудивительно, что я думал, что IBM Cloud Speech-to-Text работает нестабильно!

Следующий,

var expiry = new Date($scope.token.expiration * 1000);
    var now = Date.now();
    var duration = -(now - expiry);
function msToTime(duration) {
      var milliseconds = parseInt((duration % 1000) / 100),
      seconds = Math.floor((duration / 1000) % 60),
      minutes = Math.floor((duration / (1000 * 60)) % 60),
      hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
      minutes = (minutes < 10) ? "0" + minutes : minutes;
      seconds = (seconds < 10) ? "0" + seconds : seconds;
return "Token expires in " + minutes + ":" + seconds + " minutes:seconds";
    }
    console.log(msToTime(duration))

Это регистрирует минуты и секунды, оставшиеся до истечения срока действия вашего токена. Поверьте, однажды вы будете ломать голову над тем, почему ваш auth вышел из строя, и это потому, что прошло больше часа с тех пор, как вы получили жетон.

Далее мы настраиваем параметры:

const options = {
      accessToken: $scope.token.access_token,
      objectMode: true,
      format: false,
      wordConfidence: false,
      model: "en-US_BroadbandModel",
      interimResults: true,
    };

Если вы используете старое имя пользователя / пароль для аутентификации Cloud Foundry для получения токена, то первым свойством будет token или access_token. С аутентификацией IAM это accessToken. Это была одна из тех ловушек, на выяснение которых ушла пара дней!

Обратите внимание, что мы предоставляем только access_token, а не весь объект токена JSON.

Документация по параметрам опции находится здесь. Помните, что SDK версии 37.0 и более поздних использует имена свойств camelCase, а не under_score.

Я не знаю, что делает objectMode, но без него вы не добьетесь результатов.

Теперь мы готовы позвонить WatsonSpeech.

var stream = WatsonSpeech.SpeechToText.recognizeMicrophone(options);

SDK потоковой передачи речи в текст IBM Cloud обрабатывает потоковую передачу WebSockets за вас.

Далее ловите ошибки.

stream.on('error', function(error) {
      console.log(error);
      $scope.normalizedText = error;
      $scope.speechToTextResults = 'red';
      $scope.$apply();
    });

Сообщите пользователю, когда открывается соединение.

stream.on('open', function(error) {
      // emitted once the WebSocket connection has been established
      $scope.normalizedText = "Open";
      $scope.confidence = " ";
      $scope.$apply();
    });

Скажите пользователю, чтобы он начал говорить.

stream.on('listening', function(error) {
      // prompt user to start talking
      $scope.normalizedText = "Listening";
      $scope.confidence = " ";
      $scope.$apply();
    });

Регистрируйте или отображайте данные, когда они поступают.

stream.on('data', function(msg) {
      if (msg.results) {
        msg.results.forEach(function(result) {
          if (result.final) {
            $scope.normalizedText = result.alternatives[0].transcript;
            $scope.confidence = result.alternatives[0].confidence
            // console.log(result.alternatives[0].word_confidence);
            $scope.$apply();
          } else {
            // for interim results
            $scope.normalizedText = result.alternatives[0].transcript;
            // confidence doesn't come back until final results
            $scope.$apply();
          }
        });
      }
    });

Только окончательные результаты включают меру достоверности. confidence - для высказывания; word_confidence показывает, в каких словах Уотсон не был уверен. Чтобы отобразить word_confidence, установите wordConfidence = true и посмотрите код для Расшифровывать с микрофона с достоверностью слов.

Остановите сеанс и закройте функцию-обработчик.

   $scope.stopWatsonSpeechToText = function() {
      stream.stop();
   };
}; // close handler function

Пользователь нажимает Start, начинает говорить, и через секунду или две начинают появляться слова, почти в реальном времени. Потоковая передача выполняется намного быстрее и удобнее для пользователей, а вам нужно писать меньше кода.