Взаимодействие с предложениями | Создайте собственного голосового помощника Raspberry Pi со Snowboy

искусственный интеллект программист малиновый пирог

Автор: Лян Хаоран

Основатель Xanthous Tech, бывший инженер полного стека Amazon. В 2016 году он вернулся в Китай, чтобы начать бизнес, создал команду для предоставления консалтинговых услуг и услуг по разработке чат-ботов для крупных компаний по всему миру, применил диалоговую систему RASA и глубоко интегрировал чат-бота и мини-программу на основе WeChat.

представить

Чат-бот должен понимать естественный язык и реагировать соответствующим образом. Модуль чат-бота можно разобрать на следующие части:

image

В мире разработчиков уже существует множество инструментов с открытым исходным кодом, которые могут создавать модули чат-ботов, и различные облачные сервисы поддерживаются на основных облачных платформах, подключаясь к чат-платформам на рынке. На работе я часто имею дело с роботами в Slack, и использую роботов для различных напоминаний и автоматизации в процессах разработки, эксплуатации и обслуживания.

Сейчас вокруг нас тоже стали появляться всевозможные голосовые помощники, вроде Xiaodu и Xiaoai, вроде Siri, и такие устройства, как Alexa и Google Home. Я до сих пор помню первый Amazon Echo, который я купил.Я пытался говорить ему разные вещи, чтобы посмотреть, как я отреагирую.Мои друзья часто шалили и приходили ко мне домой и давали мне различные заказы на Amazon через заказ Echo. Hey Siri и OK Google на телефоне тоже очень удобно, даже если только поставить будильник или сделать какие-то функции.

Как разработчик и поклонник фильмов Marvel, я часто задаюсь вопросом, есть ли способ сделать собственного голосового помощника, такого как Джарвис и Пятница в фильмах «Железный человек». Для меня голосовой чат-бот можно разбить на следующие части:

image

Похоже, мне просто нужно подключить каждую часть, поставить ее на машину и запустить! Но подумав над этим, я подумал о другой проблеме: этого голосового помощника нужно будить, как и устройства на рынке. Если шаг пробуждения отсутствует, а мониторинг выполняется постоянно, потребность в ресурсах хранения и сетевом подключении очень велика. После некоторых поисков я нашелSnowboy.

Снежокkitt.aiСоздал Библиотеку обнаружения горячих слов (Hotwords Detection Library). После обучения горячим словам его можно запустить в автономном режиме, инизкое энергопотребление, который может работать на таких устройствах, как Raspberry Pi. В код можно интегрировать официальные обертки для Python, Golang, NodeJS, iOS и Android.

упражняться

Так что я достал запылившийся Raspberry Pi, подключил микрофон и динамики и начал размышлять, смогу ли я сделать простенького маленького Джарвиса, который бы меня понимал. Я также недавно купил iPad Pro, поэтому я собираюсь подключить Raspberry Pi напрямую через iPad Pro, чтобы войти в программирование ssh и, кстати, попрактиковаться в vim, ха-ха.

image

Конфигурация указана ниже:

Доска:NanoPi K1 Plus- Мне особенно нравится удобный армборд, который экономичен. Плата имеет 2 ГБ оперативной памяти, Wi-Fi + Ethernet (требуется кабельный порт для подключения к iPad) и даже имеет встроенный микрофон. Подходящей ОС является UbuntuCore 16.04 LTS, и большинство зависимостей можно установить через apt.

Микрофон:Blue Snowball- Так как я в основном работаю из дома, мне часто нужны видеоконференции. Микрофон Blue подключается через USB и может использоваться непосредственно под Linux без драйвера.

Согласно приведенной выше разборке голосового чат-бота, я решил подключить следующие сервисы, чтобы протестировать весь процесс:

Hotword Detection: Snowboy

Преобразование речи в текст: iFLYTEK Голосовой диктант

Чат-бот: Тьюринг Робот

Текст в речь: iFLYTEK онлайн-синтез речи

Установить после загрузки машиныnvmИспользуйте последнюю версию NodeJS v10 LTS. Затем создайте package.json и установите оболочку Snowboy Nodejs:

npm init
npm install snowboy --save

Вам необходимо подробно прочитать документацию, чтобы установить все зависимости (TODO), необходимые для компиляции Snowboy. После установки зависимостей обратимся к Snowboy'ssampleКод:

// index.js

const record = require('node-record-lpcm16');
const Detector = require('snowboy').Detector;
const Models = require('snowboy').Models;

const models = new Models();

models.add({
  file: 'resources/models/snowboy.umdl',
  sensitivity: '0.5',
  hotwords : 'snowboy'
});

const detector = new Detector({
  resource: "resources/common.res",
  models: models,
  audioGain: 2.0,
  applyFrontend: true
});

detector.on('silence', function () {
  console.log('silence');
});

detector.on('sound', function (buffer) {
  // <buffer> contains the last chunk of the audio that triggers the "sound"
  // event. It could be written to a wav stream.
  console.log('sound');
});

detector.on('error', function () {
  console.log('error');
});

detector.on('hotword', function (index, hotword, buffer) {
  // <buffer> contains the last chunk of the audio that triggers the "hotword"
  // event. It could be written to a wav stream. You will have to use it
  // together with the <buffer> in the "sound" event if you want to get audio
  // data after the hotword.
  console.log(buffer);
  console.log('hotword', index, hotword);
});

const mic = record.start({
  threshold: 0,
  verbose: true
});

mic.pipe(detector);

Поскольку в этом образце не указан номер версии node-record-lpcm16, после некоторой отладки я обнаружил, что в новой версии 1.x изменился API, поэтому я перевернул документ здесь и нашел изменения API:

// index.js

const { record } = require('node-record-lpcm16');

const mic = record({
  sampleRate: 16000,
  threshold: 0.5,
  recorder: 'rec',
  device: 'plughw:CARD=Snowball',
}).stream();

Здесь добавлены некоторые новые параметры.Во-первых, это указание аппаратного идентификатора Snowball, который можно узнать с помощью команды arecord -L. Кроме того, установлена ​​частота дискретизации 16 000, поскольку модель Snowboy обучается на звуке с частотой дискретизации 16 000, а частота дискретизации непостоянна и не может быть распознана. Кроме того, порог немного повышен, чтобы блокировать некоторые шумы.

в соответствии сДокументацияИзмените модель, чтобы использовать Jarvis, и настройте параметры чувствительности:

// index.js

models.add({
  file: 'snowboy/resources/models/jarvis.umdl',
  sensitivity: '0.8,0.80',
  hotwords : ['jarvis', 'jarvis'],
});

После использования теста модели Джарвиса обнаружено, что горячее слово Джарвиса может быть распознано, и запускается обратный вызов горячего слова. Я тут подумал, мне нужно сохранить аудиопоток, а потом отправить его в iFLYTEK на диктовку, чтобы получить текст. Поэтому, когда запускается событие hotword, поток микрофона необходимо передать в fsWriteStream для записи аудиофайла. В Snowboy's Detector также есть обратные вызовы для звука и тишины, поэтому я реализовал запись голоса с помощью простого флага и передал его в API диктовки iFLYTEK в конце выступления.

// index.js

const { xunfeiTranscriber } = require('./xunfei_stt');

let audios = 0;
let duplex;
let silenceCount;
let speaking;

const init = () => {
  const filename = `audio${audios}.wav`;
  duplex = fs.createWriteStream(filename, { binary: true });
  silenceCount = 0;
  speaking = false;
  console.log(`initialized audio write stream to ${filename}`);
};

const transcribe = () => {
  console.log('transcribing');
  const filename = `audio${audios}.wav`;
  xunfeiTranscriber.push(filename);
};

detector.on('silence', function () {
  if (speaking) {
    if (++silenceCount > MAX_SILENCE_COUNT) {
      mic.unpipe(duplex);
      duplex.destroy();
      transcribe();
      audios++;
      init();
    }
  }
  console.log('silence', speaking, silenceCount);
});

detector.on('sound', function (buffer) {
  if (speaking) {
    silenceCount = 0;
  }

  console.log('sound');
});

detector.on('hotword', function (index, hotword, buffer) {
  if (!speaking) {
    silenceCount = 0;
    speaking = true;
    mic.pipe(duplex);
  }

  console.log('hotword', index, hotword);
});

mic.pipe(detector);
init();

XunfeiTranscriber в приведенном выше коде — это наш модуль диктовки Xunfei. Поскольку теперь есть аудиофайл, удобнее всего, если API сразу передает весь звук, а затем получает текст. Но, к сожалению, iFLYTEK отказался от REST API и перешел на API потоковой диктовки на основе WebSocket, поэтому я могу только честно использовать клиент. Здесь я использую EventEmitter для обмена сообщениями, чтобы он мог быстрее обмениваться информацией с основной программой.

// xunfei_stt.js

const EventEmitter = require('events');
const WebSocket = require('ws');

let ws;
let transcriptionBuffer = '';

class XunfeiTranscriber extends EventEmitter {
  constructor() {
    super();
    this.ready = false;
    this.on('ready', () => {
      console.log('transcriber ready');
      this.ready = true;
    });
    this.on('error', (err) => {
      console.log(err);
    });
    this.on('result', () => {
      cleanupWs();
      this.ready = false;
      init();
    });
  }

  push(audioFile) {
    if (!this.ready) {
      console.log('transcriber not ready');
      return;
    }

    this.emit('push', audioFile);
  }
}

function init() {
  const host = 'iat-api.xfyun.cn';
  const path = '/v2/iat';

  const xunfeiUrl = () => {
    return `ws://${host}${path}?host=${host}&date=${encodeURIComponent(dateString)}&authorization=${authorization}`;
  };

  const url = xunfeiUrl();

  console.log(url);

  ws = new WebSocket(url);

  ws.on('open', () => {
    console.log('transcriber connection established');
    xunfeiTranscriber.emit('ready');
  });

  ws.on('message', (data) => {
    console.log('incoming xunfei transcription result');

    const payload = JSON.parse(data);

    if (payload.code !== 0) {
      cleanupWs();
      init();
      xunfeiTranscriber.emit('error', payload);
      return;
    }

    if (payload.data) {
      transcriptionBuffer += payload.data.result.ws.reduce((acc, item) => {
        return acc + item.cw.map(cw => cw.w);
      }, '');

      if (payload.data.status === 2) {
        xunfeiTranscriber.emit('result', transcriptionBuffer);
      }
    }
  });

  ws.on('error', (error) => {
    console.log(error);
    cleanupWs();
  });

  ws.on('close', () => {
    console.log('closed');
    init();
  });
}

const xunfeiTranscriber = new XunfeiTranscriber();

init();

module.exports = {
  xunfeiTranscriber,
};

Обработка push-события сложна.После тестирования было обнаружено, что API диктовки iFLYTEK поддерживает отправку только 13 КБ аудиоинформации на каждое сообщение через веб-сокет. Аудиоинформация кодируется base64, поэтому каждый может отправить не более 9 КБ байт. Здесь вам нужно отправлять пакетами в соответствии с документацией API iFLYTEK, и вы должны отправить конечный кадр в конце, иначе API истечет время ожидания и закроет его. Возвращаемый текст также сегментируется, поэтому для его хранения необходим буфер, и после того, как весь текст возвращен, вывод склеивается.

// xunfei_stt.js

const fs = require('fs');

xunfeiTranscriber.on('push', function pushAudioFile(audioFile) {
  transcriptionBuffer = '';

  const audioPayload = (statusCode, audioBase64) => ({
    common: statusCode === 0 ? {
      app_id: process.env.XUNFEI_APPID,
    } : undefined,
    business: statusCode === 0 ? {
      language: 'zh_cn',
      domain: 'iat',
      ptt: 0,
    } : undefined,
    data: {
      status: statusCode,
      format: 'audio/L16;rate=16000',
      encoding: 'raw',
      audio: audioBase64,
    },
  });

  const chunkSize = 9000;
  const buffer = new Buffer(chunkSize);

  fs.open(audioFile, 'r', (err, fd) => {
    if (err) {
      throw err;
    }

    let i = 0;

    function readNextChunk() {
      fs.read(fd, buffer, 0, chunkSize, null, (errr, nread) => {
        if (errr) {
          throw errr;
        }

        if (nread === 0) {
          console.log('sending end frame');

          ws.send(JSON.stringify({
            data: { status: 2 },
          }));

          return fs.close(fd, (err) => {
            if (err) {
              throw err;
            }
          });
        }

        let data;
        if (nread < chunkSize) {
          data = buffer.slice(0, nread);
        } else {
          data = buffer;
        }

        const audioBase64 = data.toString('base64');
        console.log('chunk', i, 'size', audioBase64.length);
        const payload = audioPayload(i >= 1 ? 1 : 0, audioBase64);

        ws.send(JSON.stringify(payload));
        i++;

        readNextChunk();
      });
    }

    readNextChunk();
  });
});

Внимательные студенты должны заметить, что в этом коде есть некоторая логика перезапуска, потому что во время теста было обнаружено, что API iFLYTEK поддерживает отправку только одного сообщения на одно соединение, и API необходимо переподключить, чтобы принять новый аудиопоток. . . Таким образом, мы должны активно закрывать соединение WebSocket после отправки каждого сообщения.

Следующим шагом является интеграция робота Тьюринга для получения ответа.xunfeiTranscriber предоставляет событие результата, поэтому здесь, прослушивая событие результата, сообщение передается роботу Тьюринга после его получения.

// index.js

const { tulingBot } = require('./tuling_bot');

xunfeiTranscriber.on('result', async (data) => {
  console.log('transcriber result:', data);
  const response = await tulingBot(data);
  console.log(response);
});
// tuling_bot.js

const axios = require('axios');

const url = 'http://openapi.tuling123.com/openapi/api/v2';

async function tulingBot(text) {
  const response = await axios.post(url, {
    reqType: 0,
    perception: {
      inputText: {
        text,
      },
    },
    userInfo: {
      apiKey: process.env.TULING_API_KEY,
      userId: 'myUser',
    },
  });

  console.log(JSON.stringify(response.data, null, 2));
  return response.data;
}

module.exports = {
  tulingBot,
};

После стыковки робота Тьюринга нам нужно выполнить синтез речи на тексте, возвращенном роботом Тьюринга. Здесь WebAPI синтеза речи Xunfei по-прежнему основан на REST, и кто-то уже сделал соответствующую реализацию с открытым исходным кодом, так что это относительно просто.

// index.js

const { xunfeiTTS } = require('./xunfei_tts');

xunfeiTranscriber.on('result', async (data) => {
  console.log('transcriber result:', data);
  const response = await tulingBot(data);

  const playVoice = (filename) => {
    return new Promise((resolve, reject) => {
      const speaker = new Speaker({
        channels: 1,
        bitDepth: 16,
        sampleRate: 16000,
      });
      const outStream = fs.createReadStream(filename);
      // this is just to activate the speaker, 2s delay
      speaker.write(Buffer.alloc(32000, 10));
      outStream.pipe(speaker);
      outStream.on('end', resolve);
    });
  };

  for (let i = 0; i < response.results.length; i++) {
    const result = response.results[i];
    if (result.values && result.values.text) {
      const outputFilename = await xunfeiTTS(result.values.text, `${audios-1}-${i}`);
      if (outputFilename) {
        await playVoice(outputFilename);
      }
    }
  }
});
// xunfei_tts.js
const fs = require('fs');
const xunfei = require('xunfeisdk');
const { promisify } = require('util');

const writeFileAsync = promisify(fs.writeFile);

const client = new xunfei.Client(process.env.XUNFEI_APPID);
client.TTSAppKey = process.env.XUNFEI_TTS_KEY;

async function xunfeiTTS(text, audios) {
  console.log('turning following text into speech:', text);

  try {
    const result = await client.TTS(
      text,
      xunfei.TTSAufType.L16_16K,
      xunfei.TTSAueType.RAW,
      xunfei.TTSVoiceName.XiaoYan,
    );

    console.log(result);

    const filename = `response${audios}.wav`;

    await writeFileAsync(filename, result.audio);

    console.log(`response written to ${filename}`);

    return filename;
  } catch (err) {
    console.log(err.response.status);
    console.log(err.response.headers);
    console.log(err.response.data);

    return null;
  }
}

module.exports = {
  xunfeiTTS,
};

Наконец-то этот робот понимает, что я говорю!

Прилагается нижеполный код

постскриптум

Я думаю, что общая производительность по-прежнему хороша и легко настраивается. Я надеюсь позже протестировать голосовые API других производителей и подключиться к Rasa и Wechaty, чтобы я мог поговорить с роботом дома и получить некоторую графическую и текстовую информацию в WeChat. Интеграция API iFLYTEK неожиданно сложна, и есть фатальная проблема, которая, я думаю, заключается в том, что задержка соединения WebAPI iFLYTEK особенно серьезна.Я обнаружил, что скорость отклика API Turing очень высока, но API Xunfei требует долгое время для подключения, поэтому текущий модуль STT нуждается в прогреве и может говорить только тогда, когда соединение готово. Позже я хочу переключиться на API другого производителя, чтобы посмотреть, смогу ли я улучшить работу.

Я надеюсь, что эта демонстрация поможет привлечь все больше и больше крутых голосовых помощников и роботов в будущем.

Ссылка на сайт

Original