«volute» Raspberry Pi + Node.js для создания голосового помощника с душой

Node.js малиновый пирог
«volute» Raspberry Pi + Node.js для создания голосового помощника с душой

Что такое волюта?

volute — голосовой помощник, созданный с помощью Raspberry Pi+Node.js.

Что такое малиновый пи?

raspberry-pi

raspberry-pi-4

Raspberry Pi (англ. Raspberry Pi) — это однокристальный компьютер на базе Linux, разработанный Raspberry Pi Foundation в Великобритании для продвижения базового компьютерного образования в школах с недорогим оборудованием и бесплатным программным обеспечением.

Каждое поколение Raspberry Pi использует процессор архитектуры ARM производства Broadcom.Сегодняшние модели имеют память от 2 ГБ до 8 ГБ, в основном используют SD-карты или TF-карты в качестве носителя и оснащены интерфейсами USB и видеовыходом HDMI.(Поддержка вывода звука ) и выход терминала RCA, встроенный сетевой канал Ethernet/WLAN/Bluetooth (в зависимости от модели) и могут использовать различные операционные системы. Модели продуктовой линейки делятся на вычислительные карты A-type, B-type, Zero-type и ComputeModule.

Проще говоря, это компьютер, который помещается в вашем кармане!

Что такое Node.js?

node-js

Первоначально Javascript мог выполняться только в среде браузера. Рождение Node.js позволяет нам использовать Javascript на стороне сервера. Node.js — это среда, которая может выполнять Javascript, управляемый событиями ввод-вывод на стороне сервера. Среда Javascript, основанная на движке Google V8.

Что такое диалоговая система человек-машина?

chatbot

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

Диалоговую систему можно условно разделить на 5 основных модулей: распознавание речи (ASR), понимание естественной речи (NLU), управление диалогами (DM), генерация естественного языка (NLG) и синтез речи (TTS).

  • Распознавание речи (ASR): полное преобразование речи в текст, преобразование голоса пользователя в речь.
  • Понимание естественного языка (NLU): полный семантический анализ текста, извлечение ключевой информации и выполнение распознавания намерений и сущностей.
  • Управление диалогом (DM): отвечает за поддержание состояния диалога, запросы к базе данных, управление контекстом и т. д.
  • Генерация естественного языка (NLG): Генерация соответствующего текста на естественном языке.
  • Синтез речи (TTS): преобразует сгенерированный текст в речь.

Подготовка материала

  • Материнская плата Raspberry Pi 4B
  • Интерфейс Raspberry Pi 5V3A TYPE C
  • Микрофон USB
  • мини-динамик
  • 16G TF-карта
  • кард-ридер Chuanyu
  • Провод Dupont, корпус, радиатор...

material

Установка системы Raspberry Pi и базовая настройка

Новый Raspberry Pi не готов к использованию, как Macbook, который вы купили.

Сжечь операционную систему

Raspberry Pi не имеет структуры жесткого диска и имеет только слот для карты micro SD для хранения, поэтому операционная система должна быть установлена ​​на карту micro SD.

Raspberry Pi поддерживает множество операционных систем, вот официально рекомендуемая Raspbian, специализированная система Raspberry Pi на основе Debian Linux, подходящая для всех моделей Raspberry Pi.

Для системы установки я использовал инструмент Raspberry Pi Imager, чтобы записать образ системы для Raspberry Pi.

imager

Базовая конфигурация

Чтобы настроить Raspberry Pi, вы должны сначала запустить систему. Вы можете подключить Raspberry Pi к монитору, клавиатуре и мыши, чтобы увидеть рабочий стол системы. Я использую другой метод:

  • Используйте инструмент IP Scanner для сканирования IP-адреса Raspberry Pi.

ip-scanner

  • После сканирования IP-адреса используйте инструмент VNC Viewer для подключения к системе.

vnc-viewer

  • Вы также можете напрямую подключиться к ssh и настроить его с помощью команды raspi-config.

ssh-pi

  • Настройте такие параметры, как сеть/разрешение/язык/вход и выходной звук

asound

идеи реализации улитки

volute

сервис планирования задач

const fs = require("fs");
const path = require("path");
const Speaker = require("speaker");
const { record } = require("node-record-lpcm16");
const XunFeiIAT = require("./services/xunfeiiat.service");
const XunFeiTTS = require("./services/xunfeitts.service");
const initSnowboy = require("./services/snowboy.service");
const TulingBotService = require("./services/tulingbot.service");
// 任务调度服务
const taskScheduling = {
  // 麦克风
  mic: null,
  speaker: null,
  detector: null,
  // 音频输入流
  inputStream: null,
  // 音頻輸出流
  outputStream: null,
  init() {
    // 初始化snowboy
    this.detector = initSnowboy({
      record: this.recordSound.bind(this),
      stopRecord: this.stopRecord.bind(this),
    });
    // 管道流,将麦克风接收到的流传递给snowboy
    this.mic.pipe(this.detector);
  },
  start() {
    // 监听麦克风输入流
    this.mic = record({
      sampleRate: 16000, // 采样率
      threshold: 0.5,
      verbose: true,
      recordProgram: "arecord",
    }).stream();
    this.init();
  },
  // 记录音频输入
  recordSound() {
    // 每次记录前,先停止上次未播放完成的输出流
    this.stopSpeak();
    console.log("start record");
    // 创建可写流
    this.inputStream = fs.createWriteStream(
      path.resolve(__dirname, "./assets/input.wav"),
      {
        encoding: "binary",
      }
    );
    // 管道流,将麦克风接受到的输入流 传递给 创建的可写流
    this.mic.pipe(this.inputStream);
  },
  // 停止音频输入
  stopRecord() {
    if (this.inputStream) {
      console.log("stop record");
      // 解绑this.mac绑定的管道流
      this.mic.unpipe(this.inputStream);
      this.mic.unpipe(this.detector);
      process.nextTick(() => {
        // 销毁输入流
        this.inputStream.destroy();
        this.inputStream = null;
        // 重新初始化
        this.init();
        // 调用语音听写服务
        this.speech2Text();
      });
    }
  },
  // speech to text
  speech2Text() {
    // 实例化 语音听写服务
    const iatService = new XunFeiIAT({
      onReply: (msg) => {
        console.log("msg", msg);
        // 回调,调用聊天功能
        this.onChat(msg);
      },
    });
    iatService.init();
  },
  // 聊天->图灵机器人
  onChat(text) {
    // 实例化聊天机器人
    TulingBotService.start(text).then((res) => {
      console.log(res);
      // 接收到聊天消息,调用语音合成服务
      this.text2Speech(res);
    });
  },
  // text to speech
  text2Speech(text) {
    // 实例化 语音合成服务
    const ttsService = new XunFeiTTS({
      text,
      onDone: () => {
        console.log("onDone");
        this.onSpeak();
      },
    });
    ttsService.init();
  },
  // 播放,音频输出
  onSpeak() {
    // 实例化speaker,用于播放语音
    this.speaker = new Speaker({
      channels: 1,
      bitDepth: 16,
      sampleRate: 16000,
    });
    // 创建可读流
    this.outputStream = fs.createReadStream(
      path.resolve(__dirname, "./assets/output.wav")
    );
    // this is just to activate the speaker, 2s delay
    this.speaker.write(Buffer.alloc(32000, 10));
    // 管道流,将输出流传递给speaker进行播放
    this.outputStream.pipe(this.speaker);
    this.outputStream.on("end", () => {
      this.outputStream = null;
      this.speaker = null;
    });
  },
  // 停止播放
  stopSpeak() {
    this.outputStream && this.outputStream.unpipe(this.speaker);
  },
};
taskScheduling.start();

Горячие слова будят Снежка

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

Snowboy — это настраиваемый механизм обнаружения слов пробуждения (библиотека обнаружения горячих слов), который можно использовать во встроенных системах реального времени.После обучения горячих слов он может работать в автономном режиме с низким энергопотреблением. В настоящее время он работает на системах Raspberry Pi, (Ubuntu) Linux и Mac OS X.

snowboy

const path = require("path");
const snowboy = require("snowboy");
const models = new snowboy.Models();

// 添加训练模型
models.add({
  file: path.resolve(__dirname, "../configs/volute.pmdl"),
  sensitivity: "0.5",
  hotwords: "volute",
});

// 初始化 Detector 对象
const detector = new snowboy.Detector({
  resource: path.resolve(__dirname, "../configs/common.res"),
  models: models,
  audioGain: 1.0,
  applyFrontend: false,
});

/**
 * 初始化 initSnowboy
 * 实现思路:
 * 1. 监听到热词,进行唤醒,开始录音
 * 2. 录音期间,有声音时,重置silenceCount参数
 * 3. 录音期间,未接受到声音时,对silenceCount进行累加,当累加值大于3时,停止录音
 */
function initSnowboy({ record, stopRecord }) {
  const MAX_SILENCE_COUNT = 3;
  let silenceCount = 0,
    speaking = false;
  /**
   * silence事件回调,没声音时触发
   */
  const onSilence = () => {
    console.log("silence");
    if (speaking && ++silenceCount > MAX_SILENCE_COUNT) {
      speaking = false;
      stopRecord && stopRecord();
      detector.off("silence", onSilence);
      detector.off("sound", onSound);
      detector.off("hotword", onHotword);
    }
  };
  /**
   * sound事件回调,有声音时触发
   */
  const onSound = () => {
    console.log("sound");
    if (speaking) {
      silenceCount = 0;
    }
  };
  /**
   * hotword事件回调,监听到热词时触发
   */
  const onHotword = (index, hotword, buffer) => {
    if (!speaking) {
      silenceCount = 0;
      speaking = true;
      record && record();
    }
  };
  detector.on("silence", onSilence);
  detector.on("sound", onSound);
  detector.on("hotword", onHotword);
  return detector;
}

module.exports = initSnowboy;

Голосовая диктовка iFLYTEK API

Преобразование речи в текст использует службу голосовой диктовки открытой платформы iFLYTEK. Он может точно распознавать короткие аудио (≤60 секунд) в текст. Помимо китайского мандарина и английского языка, он поддерживает 25 диалектов и 12 языков и возвращает результаты в реальном времени. время.Для достижения эффекта разговора и возвращения.

require("dotenv").config();
const fs = require("fs");
const WebSocket = require("ws");
const { resolve } = require("path");
const { createAuthParams } = require("../utils/auth");

class XunFeiIAT {
  constructor({ onReply }) {
    super();
    // websocket 连接
    this.ws = null;
    // 返回结果,解析后的消息文字
    this.message = "";
    this.onReply = onReply;
    // 需要进行转换的输入流 语音文件
    this.inputFile = resolve(__dirname, "../assets/input.wav");
    // 接口 入参
    this.params = {
      host: "iat-api.xfyun.cn",
      path: "/v2/iat",
      apiKey: process.env.XUNFEI_API_KEY,
      secret: process.env.XUNFEI_SECRET,
    };
  }
  // 生成websocket连接
  generateWsUrl() {
    const { host, path } = this.params;
    // 接口鉴权,参数加密
    const params = createAuthParams(this.params);
    return `ws://${host}${path}?${params}`;
  }
  // 初始化
  init() {
    const reqUrl = this.generateWsUrl();
    this.ws = new WebSocket(reqUrl);
    this.initWsEvent();
  }
  // 初始化websocket事件
  initWsEvent() {
    this.ws.on("open", this.onOpen.bind(this));
    this.ws.on("error", this.onError);
    this.ws.on("close", this.onClose);
    this.ws.on("message", this.onMessage.bind(this));
  }
  /**
   *  websocket open事件,触发表示已成功建立连接
   */
  onOpen() {
    console.log("open");
    this.onPush(this.inputFile);
  }
  onPush(file) {
    this.pushAudioFile(file);
  }
  // websocket 消息接收 回调
  onMessage(data) {
    const payload = JSON.parse(data);
    if (payload.data && payload.data.result) {
      // 拼接消息结果
      this.message += payload.data.result.ws.reduce(
        (acc, item) => acc + item.cw.map((cw) => cw.w),
        ""
      );
      // status 2表示结束
      if (payload.data.status === 2) {
        this.onReply(this.message);
      }
    }
  }
  // websocket 关闭事件
  onClose() {
    console.log("close");
  }
  // websocket 错误事件
  onError(error) {
    console.log(error);
  }
  /**
   * 解析语音文件,将语音以二进制流的形式传送给后端
   */
  pushAudioFile(audioFile) {
    this.message = "";
    // 发送需要的载体参数
    const audioPayload = (statusCode, audioBase64) => ({
      common:
        statusCode === 0
          ? {
              app_id: "5f6cab72",
            }
          : 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;
    // 创建buffer,用于存储二进制数据
    const buffer = Buffer.alloc(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;
          }
          // nread表示文件流已读完,发送传输结束标识(status=2)
          if (nread === 0) {
            this.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");
          const payload = audioPayload(i >= 1 ? 1 : 0, audioBase64);
          this.ws.send(JSON.stringify(payload));
          i++;
          readNextChunk.call(this);
        });
      }

      readNextChunk.call(this);
    });
  }
}

module.exports = XunFeiIAT;

Чат-бот Turing Bot API

Turing Robot API V2.0 — это онлайн-сервис и интерфейс разработки, предоставляемый разработчикам и предприятиям на основе основных технологий, таких как семантическое понимание и глубокое изучение платформы Turing Robot.

В настоящее время интерфейс API может вызывать корпус из трех модулей: чат-диалог, корпус и навыки:

Диалог в чате относится к почти 1 миллиарду материалов для публичного диалога, бесплатно предоставляемых платформой для удовлетворения потребностей пользователей в диалоге и развлечениях;

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

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

require("dotenv").config();
const axios = require("axios");

// 太简单了..懒得解释 🐶

const TulingBotService = {
  requestUrl: "http://openapi.tuling123.com/openapi/api/v2",
  start(text) {
    return new Promise((resolve) => {
      axios
        .post(this.requestUrl, {
          reqType: 0,
          perception: {
            inputText: {
              text,
            },
          },
          userInfo: {
            apiKey: process.env.TULING_BOT_API_KEY,
            userId: process.env.TULING_BOT_USER_ID,
          },
        })
        .then((res) => {
          // console.log(JSON.stringify(res.data, null, 2));
          resolve(res.data.results[0].values.text);
        });
    });
  },
};

module.exports = TulingBotService;

Синтез речи iFLYTEK API

Потоковый интерфейс синтеза речи преобразует текстовую информацию в звуковую и предоставляет вам на выбор множество различных динамиков (звуковых библиотек).

Эта голосовая возможность предоставляет разработчикам общий интерфейс через Websocket API. Websocket API имеет возможность потоковой передачи и подходит для сценариев службы ИИ, требующих потоковой передачи данных. По сравнению с SDK этот API является легковесным и кросс-языковым; по сравнению с HTTP API протокол Websocket API имеет преимущество изначальной поддержки междоменного взаимодействия.

require("dotenv").config();
const fs = require("fs");
const WebSocket = require("ws");
const { resolve } = require("path");
const { createAuthParams } = require("../utils/auth");

class XunFeiTTS {
  constructor({ text, onDone }) {
    super();
    this.ws = null;
    // 要转换的文字
    this.text = text;
    this.onDone = onDone;
    // 转换后的语音文件
    this.outputFile = resolve(__dirname, "../assets/output.wav");
    // 接口入参
    this.params = {
      host: "tts-api.xfyun.cn",
      path: "/v2/tts",
      appid: process.env.XUNFEI_APP_ID,
      apiKey: process.env.XUNFEI_API_KEY,
      secret: process.env.XUNFEI_SECRET,
    };
  }
  // 生成websocket连接
  generateWsUrl() {
    const { host, path } = this.params;
    const params = createAuthParams(this.params);
    return `ws://${host}${path}?${params}`;
  }
  // 初始化
  init() {
    const reqUrl = this.generateWsUrl();
    console.log(reqUrl);
    this.ws = new WebSocket(reqUrl);
    this.initWsEvent();
  }
  // 初始化websocket事件
  initWsEvent() {
    this.ws.on("open", this.onOpen.bind(this));
    this.ws.on("error", this.onError);
    this.ws.on("close", this.onClose);
    this.ws.on("message", this.onMessage.bind(this));
  }
  /**
   *  websocket open事件,触发表示已成功建立连接
   */
  onOpen() {
    console.log("open");
    this.onSend();
    if (fs.existsSync(this.outputFile)) {
      fs.unlinkSync(this.outputFile);
    }
  }
  // 发送要转换的参数信息
  onSend() {
    const frame = {
      // 填充common
      common: {
        app_id: this.params.appid,
      },
      // 填充business
      business: {
        aue: "raw",
        auf: "audio/L16;rate=16000",
        vcn: "xiaoyan",
        tte: "UTF8",
      },
      // 填充data
      data: {
        text: Buffer.from(this.text).toString("base64"),
        status: 2,
      },
    };
    this.ws.send(JSON.stringify(frame));
  }
  // 保存转换后的语音结果
  onSave(data) {
    fs.writeFileSync(this.outputFile, data, { flag: "a" });
  }
  // websocket 消息接收 回调
  onMessage(data, err) {
    if (err) return;
    const res = JSON.parse(data);
    if (res.code !== 0) {
      this.ws.close();
      return;
    }
    // 接收消息结果并进行保存
    const audio = res.data.audio;
    const audioBuf = Buffer.from(audio, "base64");
    this.onSave(audioBuf);
    if (res.code == 0 && res.data.status == 2) {
      this.ws.close();
      this.onDone();
    }
  }
  onClose() {
    console.log("close");
  }
  onError(error) {
    console.log(error);
  }
}

module.exports = XunFeiTTS;

Демонстрация эффекта

Yuque - Вы можете увидеть эффект внизу статьи

Адрес источника

Адрес источника на гитхабеЕсли это поможет вам, пожалуйста, оставьте звезду~