Создание веб-приложения для преобразования речи в текст с помощью Rev AI и PHP (часть 1)

Викрам Васвани, консультант разработчиков

Это руководство было первоначально опубликовано на сайте https://docs.rev.ai/resources/tutorials/build-speech-to-text-web-application-php-1/ 17 августа 2022 года.

Введение

API автоматического распознавания речи (ASR) от Rev AI позволяют разработчикам интегрировать быстрые и точные возможности преобразования речи в текст в свои приложения. Эти API можно использовать для расшифровки как предварительно записанного, так и живого аудио, и они включают такие функции, как автоматическая пунктуация, пользовательские словари, диктофонная диаризация.

Это руководство познакомит вас с асинхронным API Rev AI для преобразования речи в текст и шаг за шагом проведет вас через процесс его интеграции в веб-приложение. В нем объясняется, как записывать аудио через веб-приложение, отправлять его в Rev AI для расшифровки, получать и сохранять расшифровку в базе данных приложения. В ходе этого процесса вы узнаете, как создать клиент API Rev AI, отправлять запросы к API Rev AI и использовать веб-крючки для получения и обработки ответов API.

Обзор приложения

Пример приложения в этом учебнике задуман как инструмент для людей, которые постоянно находятся в движении, таких как медицинские работники, журналисты или продавцы. Оно позволяет этим пользователям записывать краткие голосовые заметки в качестве напоминаний для себя или других — например, заметки о пациентах, идеи для интервью, пункты дел и так далее. Затем он транскрибирует эти голосовые заметки и делает полученный текстовый контент доступным для просмотра и поиска через веб-браузер.

Внутри приложения, каждый раз, когда записывается новая голосовая заметка, оно передает звук в Rev AI Asynchronous Speech-to-Text API для расшифровки и сохраняет полученную текстовую расшифровку в базе данных MongoDB. Приложение реализовано на PHP, при этом PHP-клиент Guzzle обеспечивает взаимодействие приложения с API на сервере, а Bootstrap и RecordRTC занимаются пользовательским интерфейсом на стороне клиента и аудиозаписью соответственно.

ПРИМЕЧАНИЕ: Полный исходный код примера приложения доступен на GitHub, так что вы можете скачать и попробовать его сразу же.

Предположения

В этом руководстве предполагается, что:

  • У вас есть учетная запись Rev AI и токен доступа. Если нет, зарегистрируйте бесплатный аккаунт и сгенерируйте токен доступа.
  • У вас установлен Docker. Если нет, скачайте и установите Docker для вашей операционной системы.
  • У вас установлен Docker Compose. Если нет, установите Docker Compose для вашей операционной системы.
  • Вы развернете приложение на общедоступном URL-адресе. Если нет, или если вы предпочитаете разрабатывать и тестировать локально, скачайте и установите ngrok и получите токен аутентификации ngrok. Он понадобится для создания временного публичного URL для веб-крючка.

Любое приложение, использующее API Rev AI, должно также соблюдать ограничения API Rev AI и условия обслуживания. Прежде чем продолжить, пожалуйста, ознакомьтесь с этими документами и убедитесь, что вы согласны с ними.

Шаг 1: Создайте среду разработки

ПРИМЕЧАНИЕ: В этом руководстве используется среда разработки Apache/PHP/MongoDB на базе Docker. Если у вас уже есть правильно настроенная среда разработки с Apache 2.x, PHP 8.1.x с расширением MongoDB и Composer, вы можете использовать ее. Возможно, вам придется заменить некоторые команды Docker их эквивалентами.

В каталоге проекта создайте следующий Dockerfile и сохраните его как Dockerfile:

FROM php:8.1.8-apache
RUN set -eux; 
  apt-get update; 
  apt-get install -y libcurl4-openssl-dev pkg-config libssl-dev zlib1g-dev zip git; 
  pecl install mongodb; 
  docker-php-ext-enable mongodb;
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www
RUN set -eux; 
  a2enmod rewrite; 
  sed -i 's!/var/www/html!/var/www/public!g' /etc/apache2/sites-available/000-default.conf
EXPOSE 80
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот Dockerfile наследует официальный образ PHP Docker и настраивает его для добавления расширения PHP MongoDB и менеджера зависимостей Composer. Он также активирует модуль Apache mod_rewrite и устанавливает корень документа веб-сервера Apache в /var/www/public.

Далее создайте следующий файл Docker Compose и сохраните его в каталоге проекта под именем docker-compose.yml:

version: '3'
services:
    app:
        image: myapp:latest
        container_name: myapp
        ports:
            - 80:80
        volumes:
            - ./:/var/www/
        working_dir: /var/www/
        depends_on:
            - db
        build:
            context: .
            dockerfile: Dockerfile
        command:
            - /bin/bash
            - -c
            - |
              composer install
              apache2-foreground
    db:
        image: mongo:latest
        container_name: mydb
        restart: always
        environment:
          MONGO_INITDB_ROOT_USERNAME: myuser
          MONGO_INITDB_ROOT_PASSWORD: mypassword
        volumes:
            - data:/data/db/
        ports:
            - 27017:27017
volumes:
    data: {}
Вход в полноэкранный режим Выход из полноэкранного режима

Этот файл Docker Compose создает два контейнера: myapp для службы приложений (PHP/Apache) и mydb для службы базы данных (MongoDB). Он монтирует текущий каталог как том в контейнере приложений в точке монтирования /var/www и устанавливает учетные данные пользователя root для службы MongoDB. Он также отменяет команду запуска по умолчанию официального образа PHP Docker для запуска composer install перед запуском Apache, чтобы проверить наличие необходимых пакетов и установить их (если требуется) при запуске контейнера myapp.

Запустите среду разработки, выполнив приведенную ниже команду:

docker-compose up -d
Войти в полноэкранный режим Выйти из полноэкранного режима

Убедитесь, что обе службы запущены:

docker ps
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы должны увидеть что-то вроде этого:

CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                           NAMES
1e360eaed7e0   myapp:latest   "docker-php-entrypoi…"   12 minutes ago   Up 12 minutes   0.0.0.0:80->80/tcp, :::80->80/tcp               myapp
3d9f343639ab   mongo:latest   "docker-entrypoint.s…"   12 minutes ago   Up 12 minutes   0.0.0.0:27017->27017/tcp, :::27017->27017/tcp   mydb
Войти в полноэкранный режим Выйти из полноэкранного режима

Шаг 2: Установите необходимые пакеты

Это приложение будет использовать следующие пакеты PHP:

  • Slim: микро-фреймворк PHP для небольших веб-приложений
  • Twig-View: механизм шаблонов для Slim, основанный на Twig
  • PHP-DI: контейнер для инъекции зависимостей PHP
  • Slim PSR7: строгая реализация PSR-7, используемая Slim
  • Guzzle: PHP HTTP клиент
  • PHP-драйвер MongoDB: PHP API для работы с базами данных MongoDB.

Установите эти пакеты с помощью следующей команды:

docker exec -it myapp composer require 
  slim/slim:4.* 
  slim/twig-view 
  php-di/php-di 
  slim/psr7 
  guzzlehttp/guzzle:7.* 
  mongodb/mongodb 
  --with-all-dependencies
Войти в полноэкранный режим Выйти из полноэкранного режима

Шаг 3: Создание и тестирование скелета приложения

Создайте и протестируйте минимальный скелет приложения, как описано ниже.

  1. Создайте следующие каталоги в проекте:
- `public/`: for public assets
- `config/`: for application configuration
- `views/`: for page templates
Войти в полноэкранный режим Выход из полноэкранного режима
```bash
mkdir public config views
```
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Создайте следующий файл для хранения конфигурации приложения. Сохраните его как config/settings.php и замените место <REVAI_ACCESS_TOKEN> на ваш токен доступа Rev AI.

    <?php
    return [
        'rev' => [
            'token' => '<REVAI_ACCESS_TOKEN>',
        ],
    ];
    
  2. Подготовьте простой базовый шаблон Bootstrap. Сохраните его под именем views/layout.twig.

    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
      </head>
      <body>
        <nav class="navbar navbar-light bg-light border-bottom">
          <div class="container-fluid">
            <div class="navbar-nav">
              <a class="btn btn-primary" href="{{ url_for('index') }}" role="button">Home</a>
            </div>
          </div>
        </nav>
        <div id="content">
          {% block content %}
          {% endblock %}
        </div>
      </body>
    </html>
    
  3. Создайте временный шаблон индексной страницы, наследующий от базового шаблона. Сохраните его как views/index.twig.

    {% extends "layout.twig" %}
    
    {% block content %}
      <header class="d-flex justify-content-center py-3">
      <h1>PAGE UNDER CONSTRUCTION</h1>
      </header>
    {% endblock %}
    
  4. Настройте правила перезаписи URL для Apache и ограничения загрузки файлов PHP, создав файл public/.htaccess со следующим содержимым:

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^ index.php [QSA,L]
    php_value upload_max_filesize 256M
    php_value post_max_size 256M
    
  5. Создайте фронт-контроллер приложения со следующим PHP-кодом. Сохраните его как public/index.php.

    <?php
    use PsrHttpMessageResponseInterface as Response;
    use PsrHttpMessageServerRequestInterface as Request;
    use SlimFactoryAppFactory;
    use SlimViewsTwig;
    use SlimViewsTwigMiddleware;
    use SlimRoutingRouteContext;
    use DIContainerBuilder;
    use GuzzleHttpClient;
    use GuzzleHttpPsr7;
    use MongoDBBSONObjectID;
    
    // load dependencies
    require __DIR__ . '/../vendor/autoload.php';
    
    // create DI container
    $containerBuilder = new ContainerBuilder();
    
    // define services
    $containerBuilder->addDefinitions(
        [
            'settings' => function () {
                return include __DIR__ . '/../config/settings.php';
            },
            'view'     => function () {
                return Twig::create(__DIR__ . '/../views');
            },
        ]
    );
    
    $container = $containerBuilder->build();
    
    AppFactory::setContainer($container);
    
    // create application with DI container
    $app = AppFactory::create();
    
    // add Twig middleware
    $app->add(TwigMiddleware::createFromContainer($app));
    
    // add error handling middleware
    $app->addErrorMiddleware(true, true, true);
    
    // GET request handler for index page
    $app->get(
        '/[index[/]]',
        function (Request $request, Response $response, $args) {
            return $this->get('view')->render(
                $response,
                'index.twig',
                []
            );
        }
    )->setName('index');
    
    $app->run();
    

Этот скрипт фронт-контроллера устанавливает минимальное Slim-приложение.

  • Он начинает с загрузки всех необходимых классов с помощью автозагрузки Composer.
  • Он создает контейнер PHP Dependency Injection (DI) с записями для двух сервисов: settings для конфигурации приложения и view для шаблонизатора Twig.
  • Он создает новое Slim-приложение $app и связывает его с DI-контейнером.
  • Он добавляет в приложение промежуточное ПО Twig-View и промежуточное ПО обработки ошибок.
  • Он определяет обработчик маршрутов с именем index для запросов GET / и GET /index. Этот обработчик возвращает временный шаблон views/index.twig, созданный ранее.
  • Он запускает приложение с помощью $app->run().

Протестируйте это скелетное приложение, перейдя по адресу http://<DOCKER_HOST> и убедившись, что вы видите следующий результат:

Шаг 4: Захват и загрузка аудио в клиенте

Когда основной скелет приложения определен, настало время перейти к захвату аудио. Для веб-приложения это делается в клиенте с помощью WebRTC. Существует ряд библиотек JavaScript для упрощения этой интеграции; в данном руководстве используется библиотека RecordRTC, лицензированная MIT, которая поставляется с обширной документацией и может быть доставлена на клиент через CDN.

Начните с создания шаблона страницы с кнопкой начала/остановки записи, как показано ниже. Сохраните его как views/add.twig.

{% extends "layout.twig" %}

{% block content %}
  <header class="d-flex justify-content-center py-3">
    <h1>Add Note</h1>
  </header>

  <div class="d-flex justify-content-center btn-group-lg mb-5">
    <button type="button" class="btn btn-success btn-control">Start recording</button>
  </div>
  <h4 class="justify-content-center timer" style="text-align:center"></h4>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/RecordRTC/5.5.6/RecordRTC.js"></script>

  <script>
  const controlButton = document.querySelector('.btn-control');
  const timer = document.querySelector('.timer');

  navigator.mediaDevices.getUserMedia({
    video: false,
    audio: true
  }).then(async function(stream) {

    const recorder = RecordRTC(stream, {
      type: 'audio',
      mimeType: 'audio/wav',
      recorderType: StereoAudioRecorder
    });

    controlButton.onclick = function() {
      if (controlButton.textContent === 'Start recording') {
        startRecording();
      } else {
        stopAndProcessRecording();
      }
    }

    startRecording = function() {
      controlButton.textContent = 'Stop recording';
      controlButton.classList.toggle('btn-danger');
      recorder.startRecording();
    }

    stopAndProcessRecording = function() {
      controlButton.disabled = true;
      recorder.stopRecording(function() {
        const blob = recorder.getBlob();
        const formData = new FormData();
        formData.append('file', blob);
        fetch('', {
          method: 'POST',
          body: formData
        })
        .then(response => {
          if (response.ok === false) {
            window.location.href = '{{ url_for('index', {}, {'status':'error'}) }}';
          } else {
            window.location.href = '{{ url_for('index', {}, {'status':'submitted'}) }}';
          }
        })
      });
    }
  }).catch(function(error) {
    console.error(error);
  });

  </script>
{% endblock %}
Вход в полноэкранный режим Выход из полноэкранного режима

При загрузке страницы скрипт использует метод getUserMedia() API Media Streams, чтобы запросить у пользователя разрешение на запись звука в браузере. Если разрешение получено, создается объект RecordRTC audio recorder и настраивается на аудиопоток WAV.

Страница также определяет элемент HTML-кнопки, который позволяет пользователю управлять аудиозаписью. Когда пользователь нажимает на эту кнопку управления, обработчик события onclick кнопки заботится о переключении состояния диктофона с помощью двух функций: startRecording() и stopAndProcessRecording().

  • Функция startRecording() просто изменяет метку и цвет кнопки и вызывает функцию startRecording() диктофона.
  • Функция stopAndProcessRecording() является более сложной:
    • При вызове она сначала отключает кнопку (чтобы избежать повторной записи), а затем вызывает метод stopRecording() диктофона с функцией обратного вызова.
    • Эта функция обратного вызова получает записанный звук в виде двоичного объекта (getBlob()) и загружает его на сервер с помощью HTTP-запроса multipart/form-data POST (fetch()).
    • Он использует флаг response.ok, чтобы проверить, был ли запрос успешным или нет.
    • Наконец, он перенаправляет клиента обратно на индексную страницу, передавая флаг успеха или ошибки в качестве параметра запроса URL.

ПРИМЕЧАНИЕ: RecordRTC записывает все свои операции в консоль, поэтому если вам интересно, как он работает, посмотрите консоль браузера при взаимодействии с вышеуказанной страницей.

Шаг 5: Передача аудио с сервера на Rev AI

Теперь фронт-контроллер приложения должен быть обновлен, чтобы принять файл, загруженный на шаге 4, и, в свою очередь, вызвать запрос на задание транскрипции в конечную точку Rev AI Asynchronous Speech-to-Text API по адресу https://api.rev.ai/speechtotext/v1/jobs. Этот запрос также представляет собой запрос multipart/form-data, содержащий данные двоичного файла и параметры задания; он также должен включать заголовок Authorization, содержащий маркер доступа Rev AI.

Ниже приведен пример отправки аудиофайла в API для расшифровки:

curl -X POST "https://api.rev.ai/speechtotext/v1/jobs" 
     -H "Authorization: Bearer <REVAI_ACCESS_TOKEN>" 
     -H "Content-Type: multipart/form-data" 
     -F "media=@/<FILEPATH>" 
     -F "options={"filter_profanity":"true"}"
Войти в полноэкранный режим Выйти из полноэкранного режима

Вместе с запросом на выполнение задания можно отправить ряд опций для управления процессом расшифровки. После отправки задания в Rev AI API, API вернет уникальный идентификатор задания.

Вот пример ответа от API:

{
  "id":"AxYUC5GSuXZD",
  "created_on":"2022-04-25T10:41:27.535Z",
  "name":"FTC_Sample_1.mp3",
  "filter_profanity":true,
  "status":"in_progress",
  "type":"async",
  "language":"en"
}
Войти в полноэкранный режим Выход из полноэкранного режима

ПРИМЕЧАНИЕ: Файлы, загруженные в API асинхронного преобразования речи в текст с помощью multipart/form-data, должны иметь размер менее 2 ГБ. Медиафайлы длительностью более 17 часов не поддерживаются для английской транскрипции. Подробнее об ограничениях API.

Начните реализовывать эту функциональность, как показано ниже:

A. Обновите файл config/settings.php с дополнительным ключом конфигурации для учетных данных базы данных MongoDB. Если вы используете файл docker-compose.yml из Шага 1, замените место <MONGODB_URI> на строку учетных данных mongodb://myuser:mypassword@db.

<?php
    return [
        'rev' => [
            'token' => '<REVAI_ACCESS_TOKEN>',
        ],
        'mongo' => [
            'uri' => '<MONGODB_URI>'
        ]
    ];
Вход в полноэкранный режим Выход из полноэкранного режима

B. Обновите фронт-контроллер по адресу publicindex.php со следующими изменениями:

  • Измените контейнер DI и инициализируйте клиенты Guzzle и MongoDB, которые будут использоваться для связи с API Rev AI и базой данных приложения соответственно:

      <?php
      // ...
    
      // define services
      $containerBuilder->addDefinitions(
          [
              'settings' => function () {
                  return include __DIR__ . '/../config/settings.php';
              },
              'view'     => function () {
                  return Twig::create(__DIR__ . '/../views');
              },
              'mongo'    => function ($c) {
                  return new MongoDBClient($c->get('settings')['mongo']['uri']);
              },
              'guzzle'   => function ($c) {
                  $token = $c->get('settings')['rev']['token'];
                  return new Client(
                      [
                          'base_uri' => 'https://api.rev.ai/speechtotext/v1/jobs',
                          'headers'  => ['Authorization' => "Bearer $token"],
                      ]
                  );
              },
          ]
      );
    
      // ...
    
  • Создайте обработчик маршрута GET для новой конечной точки URL /add, которая возвращает интерфейс записи в шаблоне страницы add.twig.

      <?php
      // ...
    
      // GET request handler for /add page
      $app->get(
        '/add',
        function (Request $request, Response $response, $args) {
            return $this->get('view')->render(
                $response,
                'add.twig',
                []
            );
        }
      )->setName('add');
    
      // ...
    
  • Создайте обработчик маршрута POST для конечной точки URL /add, который принимает и обрабатывает загруженный аудиофайл, как описано ранее.

      <?php
      // ...
    
      // POST request handler for /add page
      $app->post(
        '/add',
        function (Request $request, Response $response) {
            // get MongoDB service
            // insert a record in the database for the audio upload
            // get MongoDB document ID
            $mongoClient = $this->get('mongo');
            try {
                $insertResult = $mongoClient->mydb->notes->insertOne(
                    [
                        'status' => 'JOB_RECORDED',
                        'ts'     => time(),
                        'jid'    => false,
                        'error'  => false,
                        'data'   => false,
                    ]
                );
                $id           = (string) $insertResult->getInsertedId();
    
                // get uploaded file
                // if no upload errors, change status in database record
                $uploadedFiles = $request->getUploadedFiles();
                $uploadedFile = $uploadedFiles['file'];
    
                if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => ['status' => 'JOB_UPLOADED'],
                        ]
                    );
    
                    // get Rev AI API client
                    // submit audio to API as POST request
                    $revClient   = $this->get('guzzle');
                    $revResponse = $revClient->request(
                        'POST',
                        'jobs',
                        [
                            'multipart' => [
                                [
                                    'name'     => 'media',
                                    'contents' => fopen($uploadedFile->getFilePath(), 'r'),
                                ],
                                [
                                    'name'     => 'options',
                                    'contents' => json_encode(
                                        [
                                            'metadata'         => $id,
                                            'skip_diarization' => 'true',
                                        ]
                                    ),
                                ],
                            ],
                        ]
                    )->getBody()->getContents();
    
                    // get API response
                    // if no API error, update status in database record
                    // send 200 response code to client
                    $json        = json_decode($revResponse);
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => [
                                'status' => 'JOB_TRANSCRIPTION_IN_PROGRESS',
                                'jid'    => $json->id,
                            ],
                        ]
                    );
                    $response->getBody()->write(json_encode(['success' => true]));
                    return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
                }
            } catch (GuzzleHttpExceptionRequestException $e) {
                // in case of API error
                // update status in database record
                // send error code to client with error message as payload
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => [
                            'status' => 'JOB_TRANSCRIPTION_FAILURE',
                            'error'  => $e->getMessage(),
                        ],
                    ]
                );
                $response->getBody()->write(json_encode(['success' => false]));
                return $response->withHeader('Content-Type', 'application/json')->withStatus($e->getResponse()->getStatusCode());
            }
        }
      );
    
      // ...
    

    Этот обработчик маршрута содержит много кода, поэтому давайте пройдемся по нему:

    • Когда эта конечная точка вызывается HTTP POST запросом, содержащим загрузку файла multipart/form-data, обработчик сначала вставляет новый документ в базу данных MongoDB с помощью метода insertOne() клиента MongoDB. Этот документ пуст, за исключением метки времени и поля status, которое в данный момент установлено в JOB_RECORDED. Обработчик также хранит уникальный идентификатор документа, возвращенный методом getInsertedId() клиента MongoDB.
    • Обработчик получает загруженный файл через метод getUploadedFiles() объекта запроса и проверяет наличие ошибок через метод getError(). Если файл был загружен успешно, документ status обновляется до JOB_UPLOADED с помощью метода updateOne() клиента MongoDB.
    • Обработчик использует клиент Guzzle Rev AI API для подготовки и отправки HTTP POST запроса на https://api.rev.ai/speechtotext/v1/jobs. Как описано ранее, это запрос multipart/form-data, содержащий загруженный аудиофайл и объект options, содержащий два ключа: ключ metadata, содержащий идентификатор документа MongoDB для последующих перекрестных ссылок, и флаг skip_diarization для пропуска диаризации диктора.
    • Если запрос API прошел успешно, обработчик декодирует тело ответа JSON, извлекает идентификатор задания Rev AI и добавляет его в запись базы данных. Он также обновляет документ status до JOB_TRANSCRIPTION_IN_PROGRESS. Он возвращает клиенту JSON-документ, указывающий на успех, с кодом ответа 200.
    • Если запрос API по какой-либо причине не удался — например, из-за недействительного маркера доступа или проблемы с аудиофайлом — клиент Guzzle получит от API код ошибки и выбросит исключение. Обработчик исключений перехватывает это исключение и обновляет статус документа до JOB_TRANSCRIPTION_FAILURE. Он возвращает клиенту JSON-документ, свидетельствующий о неудаче, с кодом ошибки сервера Rev AI.ПРИМЕЧАНИЕ: Узнайте больше о подаче задания на асинхронную транскрипцию и получении транскрипта.
  • Обновите обработчик маршрута GET для конечной точки /index, чтобы учесть новый параметр состояния, переданный в URL с помощью функции stopAndProcessRecording() на стороне клиента в шаге 4. Этот параметр передается в шаблон индексной страницы как переменная шаблона Twig.

      <?php
      // ...
    
      // GET request handler for index page
      $app->get(
          '/[index[/]]',
          function (Request $request, Response $response, $args) {
              $params = $request->getQueryParams();
              return $this->get('view')->render(
                  $response,
                  'index.twig',
                  [
                      'status' => !empty($params['status']) ? $params['status'] : null,
                  ]
              );
          }
      )->setName('index');
    
      // ...
    

    Для справки, вот полный сценарий фронт-контроллера, включающий все изменения, рассмотренные выше. Замените файл publicindex.php на эту версию.

      <?php
      use PsrHttpMessageResponseInterface as Response;
      use PsrHttpMessageServerRequestInterface as Request;
      use SlimFactoryAppFactory;
      use SlimViewsTwig;
      use SlimViewsTwigMiddleware;
      use SlimRoutingRouteContext;
      use DIContainerBuilder;
      use GuzzleHttpClient;
      use GuzzleHttpPsr7;
      use MongoDBBSONObjectID;
    
      // load dependencies
      require __DIR__ . '/../vendor/autoload.php';
    
      // create DI container
      $containerBuilder = new ContainerBuilder();
    
      // define services
      $containerBuilder->addDefinitions(
          [
              'settings' => function () {
                  return include __DIR__ . '/../config/settings.php';
              },
              'view'     => function () {
                  return Twig::create(__DIR__ . '/../views');
              },
              'mongo'    => function ($c) {
                  return new MongoDBClient($c->get('settings')['mongo']['uri']);
              },
              'guzzle'   => function ($c) {
                  $token = $c->get('settings')['rev']['token'];
                  return new Client(
                      [
                          'base_uri' => 'https://api.rev.ai/speechtotext/v1/jobs',
                          'headers'  => ['Authorization' => "Bearer $token"],
                      ]
                  );
              },
          ]
      );
    
      $container = $containerBuilder->build();
    
      AppFactory::setContainer($container);
    
      // create application with DI container
      $app = AppFactory::create();
    
      // add Twig middleware
      $app->add(TwigMiddleware::createFromContainer($app));
    
      // add error handling middleware
      $app->addErrorMiddleware(true, true, true);
    
      // GET request handler for index page
      $app->get(
          '/[index[/]]',
          function (Request $request, Response $response, $args) {
              $params = $request->getQueryParams();
              return $this->get('view')->render(
                  $response,
                  'index.twig',
                  [
                      'status' => !empty($params['status']) ? $params['status'] : null,
                  ]
              );
          }
      )->setName('index');
    
      // GET request handler for /add page
      $app->get(
        '/add',
        function (Request $request, Response $response, $args) {
            return $this->get('view')->render(
                $response,
                'add.twig',
                []
            );
        }
      )->setName('add');
    
      // POST request handler for /add page
      $app->post(
        '/add',
        function (Request $request, Response $response) {
            // get MongoDB service
            // insert a record in the database for the audio upload
            // get MongoDB document ID
            $mongoClient = $this->get('mongo');
            try {
                $insertResult = $mongoClient->mydb->notes->insertOne(
                    [
                        'status' => 'JOB_RECORDED',
                        'ts'     => time(),
                        'jid'    => false,
                        'error'  => false,
                        'data'   => false,
                    ]
                );
                $id           = (string) $insertResult->getInsertedId();
    
                // get uploaded file
                // if no upload errors, change status in database record
                $uploadedFiles = $request->getUploadedFiles();
                $uploadedFile = $uploadedFiles['file'];
    
                if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => ['status' => 'JOB_UPLOADED'],
                        ]
                    );
    
                    // get Rev AI API client
                    // submit audio to API as POST request
                    $revClient   = $this->get('guzzle');
                    $revResponse = $revClient->request(
                        'POST',
                        'jobs',
                        [
                            'multipart' => [
                                [
                                    'name'     => 'media',
                                    'contents' => fopen($uploadedFile->getFilePath(), 'r'),
                                ],
                                [
                                    'name'     => 'options',
                                    'contents' => json_encode(
                                        [
                                            'metadata'         => $id,
                                            'skip_diarization' => 'true',
                                        ]
                                    ),
                                ],
                            ],
                        ]
                    )->getBody()->getContents();
    
                    // get API response
                    // if no API error, update status in database record
                    // send 200 response code to client
                    $json        = json_decode($revResponse);
                    $mongoClient->mydb->notes->updateOne(
                        [
                            '_id' => new ObjectID($id),
                        ],
                        [
                            '$set' => [
                                'status' => 'JOB_TRANSCRIPTION_IN_PROGRESS',
                                'jid'    => $json->id,
                            ],
                        ]
                    );
                    $response->getBody()->write(json_encode(['success' => true]));
                    return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
                }
            } catch (GuzzleHttpExceptionRequestException $e) {
                // in case of API error
                // update status in database record
                // send error code to client with error message as payload
                $mongoClient->mydb->notes->updateOne(
                    [
                        '_id' => new ObjectID($id),
                    ],
                    [
                        '$set' => [
                            'status' => 'JOB_TRANSCRIPTION_FAILURE',
                            'error'  => $e->getMessage(),
                        ],
                    ]
                );
                $response->getBody()->write(json_encode(['success' => false]));
                return $response->withHeader('Content-Type', 'application/json')->withStatus($e->getResponse()->getStatusCode());
            }
        }
      );
    
      $app->run();
    

C. Обновите шаблон страницы views/index.twig, чтобы использовать переменную шаблона status Twig и вывести пользователю сообщение об успехе или неудаче.

{% extends "layout.twig" %}

    {% block content %}
      <header class="d-flex justify-content-center py-3">
      <h1>My Notes</h1>
      </header>

      {% if status == 'submitted' %}
      <div class="alert alert-success text-center" role="alert">Audio sent for transcription.</div>
      {% endif %}

      {% if status == 'error' %}
      <div class="alert alert-danger text-center" role="alert">Audio transcription failed.</div>
      {% endif %}
{% endblock %}
Вход в полноэкранный режим Выход из полноэкранного режима

D. Наконец, обновите базовый шаблон publiclayout.twig, чтобы включить дополнительную кнопку в правом верхнем углу для новой страницы «Добавить заметку»:

<!doctype html>
<html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
      </head>
      <body>
        <nav class="navbar navbar-light bg-light border-bottom">
          <div class="container-fluid">
            <div class="navbar-nav">
              <a class="btn btn-primary" href="{{ url_for('index') }}" role="button">Home</a>
            </div>
            <div class="navbar-nav justify-content-end">
              <a class="btn btn-primary" href="{{ url_for('add') }}" role="button">Add</a>
            </div>
          </div>
        </nav>
        <div id="content">
          {% block content %}
          {% endblock %}
        </div>
      </body>
</html>
Войти в полноэкранный режим Выход из полноэкранного режима

Шаг 6: Протестируйте пример приложения

Протестируйте пример приложения, перейдя по адресу http://<DOCKER_HOST> и нажав на новую кнопку «Добавить» в правом верхнем углу. Вы должны увидеть страницу ниже.

Браузер запросит разрешение на доступ к системному микрофону. Предоставьте это разрешение, затем нажмите кнопку «Начать запись». Говорите и нажмите кнопку «Остановить запись» после завершения. Аудиозапись будет загружена, и вы будете перенаправлены обратно на индексную страницу.

Сделайте запрос к базе данных MongoDB, чтобы проверить состояние записи, как показано ниже. Замените место MONGODB_URI на те же учетные данные, которые использовались в предыдущем шаге. Вы увидите новый документ MongoDB с идентификатором задания Rev AI.

docker exec -it mydb mongosh <MONGODB_URI>
admin> use mydb
switched to db mydb
mydb> db.notes.find()
[
  {
    _id: ObjectId("62667cae5fac4d5871017cb2"),
    status: 'JOB_TRANSCRIPTION_IN_PROGRESS',
    ts: 1650883758,
    jid: 'JDmCeVLfjlFO',
    error: false,
    data: false
  }
]
Вход в полноэкранный режим Выйдите из полноэкранного режима

Проверьте приборную панель своей учетной записи Rev AI по адресу https://www.rev.ai/jobs/speech-to-text. Вы сможете увидеть статус задания, а также загрузить окончательный транскрипт.

Следующие шаги

На этом первая часть примера веб-приложения «речь в текст» завершена: аудио принимается и записывается через браузер и передается в Rev AI для расшифровки. Во второй части этой статьи вы узнаете, как получить окончательную расшифровку от Rev AI с помощью API и отобразить ее в веб-приложении. Вы также узнаете, как удалять и искать транскрипты в интерфейсе веб-приложения.

Подробнее о разработке рече-текстовых приложений с Rev AI и PHP вы можете узнать, перейдя по следующим ссылкам:

  • Документация: Асинхронная передача заданий API Speech-To-Text
  • Примеры кода: Asynchronous Speech-To-Text API
  • Учебное пособие: Начало работы с распознаванием речи в PHP
  • Учебное пособие: Лучшие практики использования асинхронного API распознавания речи и текста
  • Документация: Slim framework
  • Документация: PHP HTTP-клиент Guzzle
  • Документация: PHP-драйвер MongoDB

Оцените статью
devanswers.ru
Добавить комментарий