Как стать автором
Обновить
48.48
Just AI
Разработка и внедрение технологий разговорного AI

Как встроить голосового помощника в любое мобильное приложение. Разбираем на примере Habitica

Время на прочтение7 мин
Количество просмотров8.2K
Вам не кажется, что многие мобильные приложения стали бы куда удобнее, будь в них голосовое управление? Нет, речь не о том, чтобы вести беседы с банковским ассистентом в чате техподдержки. В основном было бы достаточно голосовой навигации по приложению или form-filling в режиме диалога.

На примере Habitica (опенсорсный app для закрепления привычек и достижения целей, написан на Kotlin) Виталя Горбачёв, архитектор решений в Just AI, показывает, как быстро и бесшовно встроить голосовой интерфейс в функционал любого приложения.


Но для начала давайте обсудим, почему голосовое управление мобильным приложением — это удобно? Начнем с очевидных вещей.

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

Кейсы и так прозрачные, но на самом деле всё еще проще: в некоторых случаях набор голосом просто быстрее! Представьте — заказ авиабилета одной фразой «Купи мне билет на завтра на двоих в Самару» вместо долгого заполнения формы. При этом с возможностью задавать пользователю уточняющие вопросы: вечером или днем? с багажом или без?
Голос полезен при прохождении нами сценария «form-filling» и удобен для заполнения почти любых длинных форм, требующих от пользователя определенного объема информации. И такие формы присутствуют в большинстве мобильных приложений.



Слева направо: приложение РЖД «Пригород», дневник питания FatSecret (пользователям приходится заполнять форму несколько раз в день, выбирая из сотен продуктов), приложение пекарни «Коржов».

Из-за того, что сегодня голосовых ассистентов часто внедряют в чат поддержки и развиваются они именно оттуда, большинство компаний пытается запихнуть функционал приложения в чат. Пополнить баланс, узнать что-то о товаре или услуге… Это далеко не всегда удобно реализовано, а в случае с голосовым вводом и вовсе контрпродуктивно, хотя бы потому, что рапознавание речи часто работает совсем не идеально.
Правильный подход — встраивать ассистента бесшовно в уже существующий функционал приложения, в интерфейсе которого будет происходить заполнение формы, чтобы человек мог просто проверить, что он все правильно сказал, и нажать ОК.
Мы решили показать, как это можно сделать, на примере Habitica — это опенсорсное приложение, написаное почти на чистом Котлине. «Хабитика» отлично подходит под кейс с голосовым ассистентом — тут тоже для того, чтобы завести новую задачу, требуется заполнить довольно объемную форму. Попробуем заменить этот муторный процесс одной фразой с наводящими вопросами?

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

Что нужно для работы


SDK. Мы взяли Aimybox как SDK для построения диалоговых интерфейсов. Из коробки Aimybox дает SDK ассистента и лаконичный и кастомизируемый UI (который при желании можно и вовсе переделать). При этом в качестве движков распознавания, синтеза и NLP можно выбрать из уже имеющихся или создать свой модуль.

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

Инструмент для создания сценария. Сценарий будем писать на JAICF (это опенсорсный и совершенно бесплатный фреймворк для разработки голосовых приложений от Just AI), а интенты распознавать с помощью Caila (NLU-сервис) в JAICP (Just AI Conversational Platform). Про них подробнее расскажу в следующей части туториала — когда дойдем до их использования.

Смартфон. Для тестов нам понадобится смартфон на Android, на котором мы будем запускать и тестить «Хабитику».

Порядок действий


Для начала форкаем «Хабитику» (ветку Release) и ищем самые важные для нас файлы. Я пользовался IDE Android Studio:

Находим MainActivity.kt — туда мы будем встраивать логику.

HabiticaBaseApplication.kt — там будем инициализировать Aimybox.

Activity_main.xml — туда встроим элемент интерфейса.

AndroidManifest.xml — там хранится вся структура приложения и его разрешения.

Согласно инструкции в репе «Хабитики» переименовываем habitica.properties.example и habitica.resources.example, убирая из них example, заводим проект в firebase под приложение и копируем в корень файл google-services.json.

Запускаем приложение, чтобы проверить, что сборка рабочая. Вуаля!

image

Для начала добавим зависимости Aimybox.

implementation 'com.justai.aimybox:core:0.11.0'
    implementation("com.justai.aimybox:components:0.1.8")

в dependencies и

    maven { url 'https://dl.bintray.com/aimybox/aimybox-android-sdk/' }
    maven { url "https://dl.bintray.com/aimybox/aimybox-android-assistant/" }
 

в repositories.

И добавим сразу после compileOptions следующую строку, чтобы все работало корректно

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

Теперь разрешения.

Убираем флаги с разрешений RECORD_AUDIO и MODIFY_AUDIO_SETTINGS в AndroidManifest.xml, чтобы опции выглядели следующим образом.


    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.android.vending.BILLING" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

Теперь инициализурем Aimybox в BaseApplication.

Добавляем AimyboxProvider при инициализации класса.

image

И делаем собственно инициализацию.

private fun createAimybox (context: Context): Aimybox {
        
        val unitId = UUID.randomUUID().toString()

        val textToSpeech = GooglePlatformTextToSpeech(context, Locale("Ru"))
        val speechToText = GooglePlatformSpeechToText(context, Locale("Ru"))
        val dialogApi = AimyboxDialogApi(
                "YOUR KEY", unitId)
        
        return Aimybox(Config.create(speechToText, textToSpeech, dialogApi))
    }

Вместо YOUR_KEY впоследствии будет ваш код от Aimybox Console.

Теперь встраиваем фрагмент в mainActivity.kt. Предварительно вставляем ФреймЛэйаут в activity_main.xml, прямо под фреймлэйаутом с id bottom_navigation

<FrameLayout
                                android:id="@+id/assistant_container"
                                android:layout_width="match_parent"
                                android:layout_height="match_parent"/>

В сам MainActivity сначала добавляем эксплицитный запрос разрешений в OnCreate
        ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.RECORD_AUDIO), 1)

И при их получении добавляем фрагмент в указанный выше фрейм.

    @SuppressLint("MissingPermission")
    override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
    ) {
        val fragmentManager = supportFragmentManager
        val fragmentTransaction = fragmentManager.beginTransaction()

        fragmentTransaction.add(R.id.assistant_container, AimyboxAssistantFragment())
        fragmentTransaction.commit()
    }

Не забываем добавить в OnBackPressed возможности выйти из ассистента после захода в него.

        val assistantFragment = (supportFragmentManager.findFragmentById(R.id.assistant_container)
                as? AimyboxAssistantFragment)
        if (assistantFragment?.onBackPressed() != true) {
            return
        }

Кроме этого, добавим в стили (styles.xml) в AppTheme

<item name="aimybox_assistantButtonTheme">@style/CustomAssistantButtonTheme</item>
        <item name="aimybox_recognitionTheme">@style/CustomRecognitionWidgetTheme</item>
        <item name="aimybox_responseTheme">@style/CustomResponseWidgetTheme</item>
        <item name="aimybox_imageReplyTheme">@style/CustomImageReplyWidgetTheme</item>
        <item name="aimybox_buttonReplyTheme">@style/CustomButtonReplyWidgetTheme</item>

И отдельные стили чуть ниже:

 <style name="CustomAssistantButtonTheme" parent="DefaultAssistantTheme.AssistantButton">
    </style>

    <style name="CustomRecognitionWidgetTheme" parent="DefaultAssistantTheme.Widget.Recognition">
    </style>

    <style name="CustomResponseWidgetTheme" parent="DefaultAssistantTheme.Widget.Response">
    </style>

    <style name="CustomButtonReplyWidgetTheme" parent="DefaultAssistantTheme.Widget.ButtonReply">
    </style>

    <style name="CustomImageReplyWidgetTheme" parent="DefaultAssistantTheme.Widget.ImageReply">
    </style>

Давайте проверим, добавился ли микрофончик. Запускаем приложение.

У нас посыпалась куча ошибок о неправильном синтаксисе. Исправляем все, как советует IDE.

Работает!

image

Но микрофончик наползает на нижнюю навигацию. Давайте чуть поднимем. Добавим в стили выше в CustomAssistantButtonTheme:

        <item name="aimybox_buttonMarginBottom">72dp</item>

Лучше!

image

Теперь подключим туда асисстента и проверим, нормально ли он отвечает. Для этого нам понадобится консоль Aimybox.

Начнем с того, что зайдем в app.aimybox.com под нашим акком гитхаба, сделаем новый проект, подключим пару навыков (я подключил DateTime для теста) и попробуем задать соответсвующие вопросы в асисстенте. Здесь же в настройках, в правом верхнем углу, берем apiKey, который вставляем в createAimybox вместо YOUR KEY.

private fun createAimybox (context: Context): Aimybox {
        
        val unitId = UUID.randomUUID().toString()

        val textToSpeech = GooglePlatformTextToSpeech(context)
        val speechToText = GooglePlatformSpeechToText(context)
        val dialogApi = AimyboxDialogApi(
                "YOUR KEY", unitId)
        
        return Aimybox(Config.create(speechToText, textToSpeech, dialogApi))
    }

Работает!

image

Только надпись на английском, давайте поменяем приветственное сообщение в strings.constants.xml.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--  Prefs -->
    <string name="SP_userID" translatable="false">UserID</string>
    <string name="SP_APIToken" translatable="false">APIToken</string>
    <string name="base_url" translatable="false">https://habitica.com</string>
    <string name="initial_phrase">"Привет! Чем могу помочь?</string>

Ура!

image

Вот ссылка на репозиторий с кодом.

В следующей статье про ассистента для «Хабитики» расскажу, как с помощью голоса не только узнавать погоду, а управлять непосредственно приложением — переходить по страничкам и добавлять привычки и задания.
Теги:
Хабы:
+2
Комментарии7

Публикации

Информация

Сайт
just-ai.com
Дата регистрации
Дата основания
2011
Численность
101–200 человек
Местоположение
Россия