Бьюсь об заклад, вы уже слышали о бессерверных архитектурах: следующем этапе развития облачных вычислений. Термин «бессерверный» фактически объединяет две области облачных вычислений: бэкэнд как услугу (BaaS) и функции как услуга (FaaS).

С помощью BaaS мы разбиваем наши приложения на более мелкие части и реализуем некоторые из них полностью с помощью внешних сервисов. Обычно это делается с помощью вызова API (или вызовов gRPC). Одним из самых популярных бэкэндов как услуги является Firebase от Google, база данных в реальном времени (с множеством других интересных функций) для мобильных и веб-приложений.

С другой стороны, «Функции как услуга» - это еще одна форма «Вычислить как услуга»: FaaS - это способ создания и развертывания серверного кода путем простого развертывания отдельных функций (отсюда и название) на платформе FaaS, предоставляемой поставщиком.

Теперь, когда мы согласовали правильное определение того, что на самом деле представляет собой бессерверная архитектура, давайте создадим полное «бессерверное приложение».

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

Я уверен, что теперь вы думаете о более интересном варианте использования. В таком случае дайте мне знать!

Давайте погрузимся в…

1- Создание чат-бота

В нашем случае мы хотели бы начать разговор с нашим агентом (он же «чат-бот») и предоставить ему что-то, содержащее некоторый текст, который нужно извлечь и позже проанализировать (может быть, страница из книги или газеты?).

a- Создание «диалогового окна» для нашего агента

Поскольку «чат-бот» не является нашей основной темой в этом посте, мы будем «делать это просто, глупо» и спроектировать быстрый разговор в DialogFlow следующим образом:

  1. Создайте намерение «прочитать».
  2. Добавьте пару пользовательских выражений, например. «Прочтите этот текст» или «извлеките текст».
  3. Добавьте действие «читать».
  4. Включите использование веб-перехватчика (см. Выполнение ниже).

б- Реализация логики агента

Давайте теперь закодируем логику для нашего агента, которая действительно сделает снимок.

Для начала нам понадобятся две служебные функции:

  1. captureImage функция, снимающая изображение с помощью камеры пользователя.
  2. uploadImage функция, которая загружает это изображение в Google Cloud Storage (GCS).

Здесь реализация функции captureImage. Эта функция использует системную утилиту imagesnap, доступную в MacOS, для фактического доступа к камере, захвата изображения и сохранения файла изображения в /tmp/google-actions-reader-${Date.now()}.png. Затем эта функция возвращает имя и содержимое файла в base64:

const fs = require('fs');
const child_process = require('child_process');
const Buffer = require('safe-buffer').Buffer;
/**
 * Capture the image from the user computer's camera.
 */
function captureImage() {
  return new Promise((res, rej) => {
    const file = `/tmp/google-actions-reader-${Date.now()}.png`;
    try {
      child_process.execSync(`imagesnap -w 1 ${file}`);
      const bitmap = fs.readFileSync(file);
      res({
        base64: new Buffer(bitmap).toString('base64'),
        file
      });
    } catch (err) { rej(err); }
  });
}

Следующая функция uploadImage просто загрузит это изображение в GCS в cloud-function-ocr-demo__image сегменте:

const child_process = require('child_process');
/**
 * Uploads the file to GCS.
 *
 * @param {object} data The GCP payload metadata.
 * @param {object} data.file The filename to read.
 */
function uploadImage(data) {
  child_process.execSync(
    `gsutil cp ${data.file} gs://cloud-function-ocr-demo__image`
  );
  return data.file.split('/').pop();
}

Обратите внимание на название ведра cloud-function-ocr-demo__image, оно нам понадобится позже.

Теперь, когда у нас есть две наши служебные функции captureImage и uploadImage, давайте воспользуемся ими внутри логики намерения чтения (помните это намерение в диалоговом окне сверху?):

/**
 * The "read" intent that will trigger the capturing and uploading
 * the image to GSC.
 *
 * @param {object} app DialogflowApp instance object.
 */
function readIntent(app) {
  captureImage()
    .then(uploadImage)
    .then(content => {
      app.tell(`I sent you an SMS with your content.`);
    })
    .catch(e => app.ask(`[ERROR] ${e}`) );
}

Этот readIntent в основном захватит, а затем загрузит изображение в GCS.

Теперь, когда у нас реализована вся логика агента, давайте создадим основную облачную функцию, которая будет обрабатывать запросы DialogFlow:

const aog = require('actions-on-google');
const DialogflowApp = aog.DialogflowApp;
/**
 * Handles the agent (chatbot) logic. Triggered from an HTTP call.
 *
 * @param {object} request Express.js request object.
 * @param {object} response Express.js response object.
 */
module.exports.assistant = (request, response) => {
  const app = new DialogflowApp({ request, response });
  const actions = new Map();
  actions.set('read', readIntent);
  app.handleRequest(actions);
};

assistant Облачная функция будет запущена при HTTP-вызове. Этот вызов будет выполнен DialogFlow, если пользователь скажет, например, «прочтите этот текст» (как упомянуто выше), что является выражением, определенным в намерении чтения.

c- Развертывание облачной функции помощника

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

Чтобы развернуть облачную функцию, мы можем использовать команду gcloud со следующими аргументами:

gcloud beta functions 
  deploy <function-label> 
  <trigger-type> 
  --source <source-code> 
  --entry-point <function-name>
  1. <function-label> - это метка функции, она может быть такой же или отличной от <function-name>.
  2. <trigger-type> - как будет запускаться ваша функция (тема, http, хранилище… и т. Д.).
  3. <source-code> - это облачный репозиторий Google, в котором размещен исходный код функции. Это не может быть какой-либо другой общедоступный репозиторий Git!
  4. <function-name> - это фактическое имя экспортируемой функции (в вашем коде).

Вы также можете использовать корзину Google Cloud Storage для размещения исходного кода вашей функции. Но мы не будем здесь останавливаться на этом.

О, и кстати…

Размещение исходного кода в репозитории Google Cloud (репозиторий Git) - хорошая идея, если у вас есть стратегия непрерывной доставки в вашей организации.

В нашем случае это полная команда:

gcloud beta functions 
  deploy ocr-assistant 
  --source https://source.developers.google.com/projects/...
  --trigger-http
  --entry-point assistant

Если вам интересно, исходный код Google Cloud Repository имеет следующий формат:

https://source.developers.google.com/projects/<project-id>/repos/<repo-id>/moveable-aliases/<branch-name>

После развертывания ваша функция должна быть готова к запуску:

Вам также будет предоставлен общедоступный URL-адрес, который выглядит следующим образом:

https://us-central1-<project-id>.cloudfunctions.net/ocr-assistant

Это URL-адрес, который мы будем использовать в нашем проекте DialogFlow.

Попался!!

Если вы внимательно следили, то, возможно, заметили, что функции captureImage требуется… ну, доступ к камере! Это означает, что мы не сможем развернуть эту конкретную функцию в Google Cloud Platform. Скорее, мы разместим его на нашем конкретном оборудовании, скажем, на Raspberry PI (для упрощения), и будем использовать другой URL-адрес (очевидно).

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

г- Добавление URL-адреса выполнения

Затем давайте добавим URL выполнения, который указывает на assistant Облачную функцию, которая будет обрабатывать запросы агента:

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

2- Обработка изображения

До сих пор мы говорили только об облачных функциях - FaaS. Давайте перейдем к части Backends as a Service (или BaaS).

Мы хотим иметь возможность извлекать некоторый контент из изображения, в нашем случае текст. Для этого у нас есть множество библиотек с открытым исходным кодом - OpenCV или Tensorflow, и это лишь некоторые из них. К сожалению, эти библиотеки требуют от нас некоторого опыта в машинном обучении и обработке изображений (или звука). Нанять этих специалистов непросто! Кроме того, в идеале мы не хотим поддерживать этот код и хотим, чтобы наше решение могло автоматически масштабироваться в случае, если наше приложение станет популярным. Проще говоря, мы не хотим управлять этой функцией. К счастью, облачная платформа Google покрыла нас:

  1. Google Vision API позволяет нам извлекать контент.
  2. Использование Google Translation API позволяет нам… ну, переводить содержание.

Вот подархитектура этой функции:

a- Извлечение содержимого из изображения

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

  1. processImage Облачная функция, которая запускается всякий раз, когда новое изображение загружается в GCS в сегменте cloud-function-ocr-demo__image.
  2. detectText функция, которая фактически извлекает текст из изображения с помощью Google Vision API.

Вот реализация processImage:

/**
 * Cloud Function triggered by GCS when a file is uploaded.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data A Google Cloud Storage File object.
 */
exports.processImage = function processImage(event) {
  let file = event.data;

  return Promise.resolve()
    .then(() => {
      if (file.resourceState === 'not_exists') {
        // This was a deletion event, we don't want to process this
        return;
      }

      return detectText(file.bucket, file.name);
    })
    .then(() => {
      console.log(`File ${file.name} processed.`);
    });
};

Реализация функции detectText проста (позже мы ее улучшим):

const vision = require('@google-cloud/vision')();
/**
 * Detects the text in an image using the Google Vision API.
 *
 * @param {string} bucketName Cloud Storage bucket name.
 * @param {string} filename Cloud Storage file name.
 */
function detectText(bucketName, filename) {
  let text;

  return vision
    .textDetection({ 
      source: { 
         imageUri: `gs://${bucketName}/${filename}` 
      }
    })
    .then(([detections]) => {
      const annotation = detections.textAnnotations[0];
      text = annotation ? annotation.description : '';
      return Promise.resole(text);
    });
}

Теперь нам нужно развернуть processImage Облачную функцию, и мы хотим, чтобы она запускалась всякий раз, когда новый образ загружается в GCS в cloud-function-ocr-demo__image ведре:

gcloud beta functions 
   deploy ocr-extract
   --source https://source.developers.google.com/projects/...
   --trigger-bucket cloud-function-ocr-demo__image 
   --entry-point processImage

А теперь добавим переводы…

б- Перевод текста

Перевод извлеченного текста будет инициирован определенной темой TRANSLATE_TOPIC Google Cloud Pub / Sub и будет состоять из двух операций:

  1. Определите язык извлеченного контента. Мы сделаем это в нашей предыдущей processImage функции. Мы могли бы создать для этого еще одну облачную функцию, но не будем чрезмерно усложнять нашу архитектуру!
  2. translateText: Перевести этот контент на заданный язык.

Давайте улучшим нашу существующую processImage облачную функцию с помощью функции определения языка:

const vision = require('@google-cloud/vision')();
const translate = require('@google-cloud/translate')();
const config = require('./config.json');
/**
 * Detects the text in an image using the Google Vision API.
 *
 * @param {string} bucketName Cloud Storage bucket name.
 * @param {string} filename Cloud Storage file name.
 * @returns {Promise}
 */
function detectText(bucketName, filename) {
  let text;

  return vision
    .textDetection({ 
      source: { 
         imageUri: `gs://${bucketName}/${filename}` 
      }
    })
    .then(([detections]) => {
      const annotation = detections.textAnnotations[0];
      text = annotation ? annotation.description : '';
      return translate.detect(text);
    })
    .then(([detection]) => {
      if (Array.isArray(detection)) {
        detection = detection[0];
      }

      // Submit a message to the bus for each language 
      // we're going to translate to
      const tasks = config.TO_LANG.map(lang => {
        let topicName = config.TRANSLATE_TOPIC;
        if (detection.language === lang) {
          topicName = config.RESULT_TOPIC;
        }
        const messageData = {
          text: text,
          filename: filename,
          lang: lang,
          from: detection.language
        };

        return publishResult(topicName, messageData);
      });

      return Promise.all(tasks);
    });
}

Давайте объясним новый дополнительный код, который мы добавили:

Сначала мы добавили вызов API перевода Google, чтобы определить основной язык извлеченного текста translate.detect(text);. Затем, в следующем блоке, мы в основном перебираем массив config.TO_LANG, который есть в файле конфигурации, и публикуем TRANSLATE_TOPIC с определенной полезной нагрузкой, содержащей текстовый контент (text), исходный язык (from) и целевой язык, который мы хотим перевести на (lang). Если исходный язык совпадает с целевым, мы просто публикуем RESULT_TOPIC.

Боковое примечание о Google Cloud Pub / Sub

Для удобства мы также добавили новую служебную функцию publishResult, которая отвечает за публикацию темы Pub / Sub. По сути, он использует API Google Cloud Pub / Sub для создания (при необходимости) и публикации данной темы:

const pubsub = require('@google-cloud/pubsub')();
/**
 * Publishes the result to the given pub-sub topic.
 *
 * @param {string} topicName Name of the topic on which to publish.
 * @param {object} data The message data to publish.
 */
function publishResult(topicName, data) {
  return pubsub
    .topic(topicName)
    .get({ autoCreate: true })
    .then(([topic]) => topic.publish(data));
}

Затем давайте создадим translateText Cloud Function, который будет переводить извлеченный текст:

const translate = require('@google-cloud/translate')();
const Buffer = require('safe-buffer').Buffer;
const config = require('./config.json');
/**
 * Translates text using the Google Translate API. 
 * Triggered from a message on a Pub/Sub topic.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data The Cloud Pub/Sub Message object.
 * @param {string} event.data.data The "data" property of 
 *    the Cloud Pub/Sub Message. 
 *    This property will be a base64-encoded string that 
 *    you must decode.
 */
exports.translateText = function translateText(event) {
  const pubsubMessage = event.data;
  const jsonString = Buffer.from(
                       pubsubMessage.data, 'base64'
                     ).toString();
  const payload = JSON.parse(jsonString);

  return Promise.resolve()
    .then(() => {

      const options = {
        from: payload.from,
        to: payload.lang
      };

      return translate.translate(payload.text, options);
    })
    .then(([translation]) => {
      const messageData = {
        text: translation,
        filename: payload.filename,
        lang: payload.lang
      };

      return publishResult(config.RESULT_TOPIC, messageData);
    });
};

Реализация этой функции не требует пояснений: в основном мы вызываем translation.translate(payload.text, options);, и как только мы получаем результат, мы публикуем RESULT_TOPIC с переведенным контентом.

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

gcloud beta functions 
   deploy ocr-translate
   --source https://source.developers.google.com/projects/...
   --trigger-topic TRANSLATE_TOPIC
   --entry-point translateText

c- Сохранить переведенный текст

Пока все хорошо, теперь нам удалось захватить изображение, загрузить его в GCS, обработать и извлечь текст, а затем перевести его. Последним шагом будет сохранение переведенного текста обратно в GCS.

Вот реализация такой функции:

const storage = require('@google-cloud/storage')();
const Buffer = require('safe-buffer').Buffer;
const config = require('./config.json');
/**
 * Saves the data packet to a file in GCS. 
 * Triggered from a message on a Pub/Sub topic.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data The Cloud Pub/Sub Message object.
 * @param {string} event.data.data The "data" property of 
 *    the Cloud Pub/Sub Message. 
 *    This property will be a base64-encoded string that 
 *    you must decode.
 */
exports.saveResult = function saveResult(event) {
  const pubsubMessage = event.data;
  const jsonString = Buffer.from(
                       pubsubMessage.data, 'base64'
                     ).toString();
  const payload = JSON.parse(jsonString);

  return Promise.resolve()
    .then(() => {
      const bucketName = config.RESULT_BUCKET;
     // Appends a .txt suffix to the image name. 
     const filename = renameFile(payload.filename, payload.lang);
      
      const file = storage.bucket(bucketName).file(filename);

      return file.save(payload.text)
        .then(_ => publishResult(config.READ_TOPIC, payload));
    });
};

saveResult запускается RESULT_TOPIC, который является темой, содержащей переведенный текст. Мы просто используем эту полезную нагрузку и вызываем API Google Cloud Storage для сохранения содержимого в корзине с именем config.RESULT_BUCKET (то есть cloud-functions-orc-demo). Как только это будет сделано, мы публикуем тему READ_TOPIC, которая запустит следующую облачную функцию (см. Следующий раздел).

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

gcloud beta functions 
   deploy ocr-save
   --source https://source.developers.google.com/projects/...
   --trigger-topic RESULT_TOPIC
   --entry-point saveResult

3- Отправка SMS-уведомлений

Наконец, теперь мы готовы прочитать переведенный текст из GCS и отправить его по SMS на телефон пользователя.

а- Чтение переведенного текста из GCS

Чтение файла из GCS, опять же, несложная операция:

const Buffer = require('safe-buffer').Buffer;
/**
 * Reads the data packet from a file in GCS. 
 * Triggered from a message on a Pub/Sub topic.
 *
 * @param {object} event The Cloud Functions event.
 * @param {object} event.data The Cloud Pub/Sub Message object.
 * @param {string} event.data.data The "data" property of 
 *    the Cloud Pub/Sub Message. 
 *    This property will be a base64-encoded string that 
 *    you must decode.
 */
exports.readResult = function readResult(event) {
  const pubsubMessage = event.data;
  const jsonString = Buffer.from(
                       pubsubMessage.data, 'base64'
                     ).toString();
  const payload = JSON.parse(jsonString);
  return Promise.resolve()
    .then(() => readFromBucket(payload))
    .then(content => sendSMS(content).then(_ => call(content)));
};

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

const storage = require('@google-cloud/storage')();
const config = require('./config.json');
/**
 * Reads the data packet from a file in GCS. 
 * Triggered from a message on a Pub/Sub topic.
 *
 * @param {object} payload The GCS payload metadata.
 * @param {object} payload.filename The filename to read.
 */
function readFromBucket(payload) {
  // Appends a .txt suffix to the image name.
  const filename = renameFile(payload.filename, payload.lang);
  const bucketName = config.RESULT_BUCKET;
  const file = storage.bucket(bucketName).file(filename);
  const chunks = [];

  return new Promise((res, rej) => {
    file
      .createReadStream()
      .on('data', chunck => {
        chunks.push(chunck);
      })
      .on('error', err => {
        rej(err);
      })
      .on('response', response => {
        // Server connected and responded with 
        // the specified status and headers.
      })
      .on('end', () => {
        // The file is fully downloaded.
        res(chunks.join(''));
      });
  });
}

Это просто так. Теперь давайте развернем облачную функцию readResult и сделаем ее триггерами из темы READ_TOPIC:

gcloud beta functions 
   deploy ocr-read
   --source https://source.developers.google.com/projects/...
   --trigger-topic READ_TOPIC
   --entry-point readResult

б- Отправка SMS-уведомлений

Когда дело доходит до отправки SMS на телефон пользователя, мы используем отличный сервис Twilio, который… просто работает!

Для использования сервисов Twilio необходимо создать учетную запись разработчика.

const Twilio = require('twilio');
const TwilioClient = new Twilio(
   config.TWILIO.accountSid,
   config.TWILIO.authToken
);
/**
 * Sends an SMS using Twilio's service.
 *
 * @param {string} body The content to send via SMS.
 */
function sendSMS(body) {
  return TwilioClient.messages
    .create({
      to: '+33000000000',
      from: '+33000000000',
      body: body || 'MESSAGE NOT FOUND'
    });
}

c- Телефонные звонки (БОНУС)

Отправить переведенный контент по телефону обратно пользователю немного сложно, так как вам нужно предоставить две функции:

  1. call, который выполняет телефонный звонок: он действительно звонит пользователю!
  2. twilioCalls - конечная точка HTTP, которая будет обрабатывать входящие вызовы, сделанные функцией call.

Чтобы продемонстрировать, как будет работать этот процесс, давайте сначала взглянем на реализацию twilioCalls:

const Twilio = require('twilio');
const VoiceResponse = Twilio.twiml.VoiceResponse;
/**
 * Handles the incoming Twilio call request. 
 * Triggered from an HTTP call.
 *
 * @param {object} request Express.js request object.
 * @param {object} response Express.js response object.
 */
module.exports.twilioCall = function(request, response) {
  return readFromBucket({
    filename: 'twilio_user_33000000000.txt'
  }).then(content => {
    const twiml = new VoiceResponse();
    twiml.say(`
    <Say voice="woman">Hi, this is your extracted text:</Say>
    <Pause length="1"></Pause>
    <Say voice="woman">${content}</Say>
    `);
    res.writeHead(200, { 'Content-Type': 'text/xml' });
    res.end(twiml.toString());
  });
};

Функция twilioCall отвечает за чтение файла из корзины и отправку обратно XML-ответа, созданного благодаря языку разметки Twilio (TwilioML).

Затем вам нужно будет развернуть эту облачную функцию, чтобы получить общедоступный URL-адрес, необходимый для функции call:

gcloud beta functions 
   deploy ocr-twilio-call
   --source https://source.developers.google.com/projects/...
   --trigger-http
   --entry-point twilioCall

После развертывания вы получите общедоступный URL-адрес, подобный этому:

https://us-central1-<projet-id>.cloudfunctions.net/ocr-twilio-call

Затем мы будем использовать этот URL в функции call:

/**
 * Triggers a call using Twilio's service.
 */
function call() {
  return TwilioClient.api.calls
    .create({
      url: 'https://the-url-from-above/ocr-twilio-call',
      to: '+33000000000',
      from: '+33000000000'
    });
}

Выполнено! Теперь ваша конечная точка HTTP Twilio готова к входящим звонкам.

Подведение итогов!

В этом руководстве мы реализовали набор облачных функций, которые выполняли разные задачи:

  1. assistant обрабатывает запрос агента, поступающий от DialogFlow.
  2. processImage извлекает текст из загруженного изображения.
  3. translateText переводит извлеченный текст на разные языки.
  4. saveResult сохраняет переведенный текст в GCS.
  5. readResult читает переведенный текст из файлов, хранящихся в GCS.
  6. twilioCall обрабатывает запросы на входящие звонки.

Вот краткий обзор всех развернутых облачных функций:

И снова полная архитектура:

Попробуй это

Чтобы протестировать приложение, нам сначала нужно развернуть агент DialogFlow. Мы решили развернуть его в Google Assistant, поскольку наша assistant облачная функция предназначена для обработки запросов Google Assistant. Если вы хотите выполнить развертывание в других сервисах (Slack, Facebook, Twitter и т. Д.), Вам просто нужно предоставить и развернуть другие облачные функции.

На вкладке интеграции выберите Google Assistant и нажмите кнопку ТЕСТ:

Это откроет симулятор Actions on Google, позволяющий протестировать своего агента прямо в баузере. Кроме того, вы также можете на своем телефоне или устройствах Google Home:

Обратите внимание, что мы дали нашему агенту имя: Шекспир. Мы сделали это из того же симулятора на обзорной панели.

В качестве образца текста мы будем использовать следующую цитату (Зиад К. Абдельнур):

И… вот SMS, отправленное нашей readResult функцией:

⚠️ Важные примечания ⚠️

  1. В моем примере кода я не обработал никаких ошибок. Следует!
  2. В моем примере кода я ничего не регистрировал. Следует!
  3. В моем примере кода я не писал модульные тесты. Вы должны!

Вот полный исходный код:



Поздравляю! Вы только что создали свое первое «бессерверное» приложение! И, с Новым 2018 годом

Подпишитесь на меня в Twitter @manekinekko, чтобы узнать больше о веб-платформах и облачных платформах.