Как мы научили мессенджер ТамТам распознавать адреса в тексте

    Привет! Меня зовут Юра Дорофеев, я работаю над Android-версией мессенджера ТамТам. Представьте, что вы договариваетесь о встрече с другом, и он отправляет вам адрес. Но не отдельным сообщением, а посреди другого текста:


    Как этот адрес быстро вставить в навигатор или карту? Приходится копировать всё сообщение, потом вырезать лишнее. А если вы за рулем, то проще переписать адрес с нуля или вообще озвучить его голосом. Ну и моё самое любимое — поздравления с днем рождения. В текущих реалиях это номер банковской карты среди текста сообщения:


    И тоже сидишь, копируешь, вырезаешь или просишь автора скинуть отдельным сообщением, чтобы весь чат не мучился так же, как и ты.

    Выбор Google


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

    Все библиотеки из пакета ML Kit работают с уже обученными моделями. Никакие данные не уходят на серверы Google, вся обработка происходит офлайн и локально на устройстве. А самое главное, это бесплатно! Google готовился к запуску новой библиотеки Entity Extraction, которая умеет находить сущности в тексте и классифицировать их. Вот пример:



    Всего библиотека умеет находить 11 типов сущностей на 15 языках:



    Принцип работы


    Нахождение сущностей устроено следующим образом: вначале текст разбивается на слова. Дальше все слова объединятся во всевозможные последовательности с максимальной длиной 15 слов. И для каждой из этих последовательностей производится оценка, насколько этот набор слов похож на какую-либо сущность. Чем больше похож, тем ближе оценка к единице.


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


    Интересно, что не для всех типов сущностей используются нейронные сети. Например, для распознавания времени, даты, номеров авиарейсов используются регулярные выражения.

    Пробуем


    Звучит очень круто, а самое главное — должно помочь нам с решением проблемы копирования номера банковской карты из текста сообщения. Мы согласились поучаствовать в программе раннего доступа. Google выслал нам библиотеку с документацией. Было опасение, что библиотека окажется монструозная и использовать её будет очень сложно. Давайте проверим.

    Чтобы начать работу, нам нужно инициализировать EntityExtractor. Это та самая штука, которая будет находить сущности в тексте. Инициализируется она очень просто: нужно передать опции с минимальным количеством аргументов — язык, на котором написан исходный текст, и Executor. По умолчанию библиотека использует свой Executor, но можно перевести выполнение на любой другой.

    private val entityExtractor: Lazy<EntityExtractor> = lazy {
           EntityExtraction.getClient(
               EntityExtractorOptions.Builder(buildModelLocale())
                   .setExecutor(executor)
                   .build()
           )
       }
    

    Зачем указывать язык?


    Под каждый язык обучена своя модель, и библиотеке нужно знать, какую модель использовать. Казалось бы, можно же автоматически определить язык? Да, для этого есть отдельная библиотека из набора ML Kit и можно её подключить. Она тоже довольно легкая и имеет простой интерфейс.

    Как использовать EntityExtractor?


    Всё просто. Вначале собираем параметры для работы экстрактора. Устанавливаем preferredLocale. Это не язык, на котором будет вестись распознавание, а языковой стандарт для форматирования различных сущностей. Например: 1.10.2021 в русском языке — первое октября, а в английском — десятое января, здесь разный порядок месяца и даты.

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

    private fun entityAnnotationsSingle(text: String): Single<List<EntityAnnotation>> {
           return Single.create { emitter: SingleEmitter<List<EntityAnnotation>> ->
               // .........
               val params = EntityExtractionParams.Builder(text)
                   .setPreferredLocale(userLocale)
                   .setEntityTypesFilter(SUPPORTED_TYPES)
                   .build()
               entityExtractor
                   .downloadModelIfNeeded()
                   .onSuccessTask { entityExtractor.annotate(params) }
                   .addOnFailureListener(executor, onFailureListener)
                   .addOnSuccessListener(executor, onSuccessListener)
           }
       }
    

    Скачивание моделей


    В начале статьи я сказал, что библиотека работает полностью офлайн, а парой строчек выше я говорю, что нужно качать какие-то модели. Нестыковка. В самой библиотеке нет моделей, они скачиваются под каждый язык. Но это единственное, что вам нужно будет скачать, далее библиотека будет работать офлайн. Сами модели небольшие, примерно 600-700 Кб. Модель скачивается в папку files в директории приложения. Странно, конечно, что нельзя задать свой путь. Поэтому, если у вас в приложении есть какая-то очистка кэша, не забудьте настроить исключение на эту папку:



    P.S. Уже во время использования выяснилось, что библиотека может падать на вызове downloadModelIfNeeded, не забудьте завернуть в try-catch.

    Используем сущности


    Окей, мы скачали модели, задействовали EntityExtractor, что дальше? А дальше просто выставляйте Span в ваш текст, настраивайте цвет отображения, действия по клику и всё, что вашей душе угодно:

    fun addMlEntities(text: CharSequence): Maybe<CharSequence> {
           return entityAnnotationsSingle(text.toString())
               .onErrorReturnItem(emptyList())
               .flatMapMaybe { entityAnnotations: List<EntityAnnotation> ->
                   if (entityAnnotations.isEmpty()) {
                       return@flatMapMaybe Maybe.empty()
                   }
                   val spannable = text.spannable()
                   for (annotation in entityAnnotations) {
                       if (annotation.entities.isNotEmpty()) {
                           val span = MlSpan(annotation.entities[0], annotation.annotatedText, color)
                           spannable.setSpan(span, annotation.start, annotation.end, SPAN_EXCLUSIVE_EXCLUSIVE)
                       }
                   }
                   return@flatMapMaybe Maybe.just(spannable)
               }
       }
    

    Всё просто и понятно. Так? Нет, не так. Скорость обработки одного сообщения колеблется от 8 до 100 мс. Это не так уж и быстро. Сообщения в чатах у нас грузятся чанками по 40 сообщений. В худшем случае обработка займет 4000 мс или 4 с. То есть потенциально можно задержать открытие чата на 4 с.


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

    class MlSpan(...) : ClickableSpan() {
       // ....
       override fun updateDrawState(ds: TextPaint) {
           if (!this::colorAnimator.isInitialized) {
               colorAnimator = ValueAnimator.ofObject(argbEvaluator, ds.color, color)
               colorAnimator.duration = 200
               colorAnimator.addUpdateListener {
                   currentColor = it.animatedValue as Int
                   animationListener?.onAnimationUpdate()
               }
               colorAnimator.start()
           }
           ds.color = currentColor
       }
    }
    

    Выглядит неплохо, и не пришлось задерживать открытие чата:



    Заключение


    Сейчас в Google Play опубликована версия мессенджера ТамТам, которая включает все решения, описанные выше. Мы умеем находить в тексте:

    • адреса;
    • e-mail;
    • номера телефонов;
    • почтовые номера отслеживания;
    • номера банковских карт.

    Кроме того, поскольку мы одними из первых приложений в мире успешно внедрили библиотеку, о ТамТам написал Google в своем девелоперском блоге:

    Mail.ru Group
    Building the Internet

    Comments 41

      +16

      Не взлетит. Карма плохая.

        +7
        Да, но решение гугла и адаптация тамтамовцев (a.k.a костыль) – кайф. У меня прям боль от примеров с карточкой и адресом.

        В случае с там-там, правда, не очень понятно, почему бы не сделать тоже самое на сервере ;-)
          +2
          В случае с там-там, правда, не очень понятно, почему бы не сделать тоже самое на сервере ;-)

          Хабр торт.

            0
            Решение с ML не исключает фичу выделения любого текста пальцем в будущем =)

            На сервере не сделали, ибо преимуществ у данной библиотеки сильно больше чем в реализации своего решения. Начиная от того, что у Google просто больше данных, заканчивая разнообразием языков
              –2
              >>На сервере не сделали, ибо преимуществ у данной библиотеки сильно больше

              Ахаха. То есть мяу, товарищ майор.
                0

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

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

                    Тогда лучше делать анализ у отправителя и сразу размечать сообщение.
                    Вместо множества проходов на множестве клиентов будет всего один.

                      0
                      Тогда нужно и так и этак. Чтобы можно было разметить и сообщения в истории до введения фичи или, например, когда добавят новые сущности. А это уже не так рационально с точки зрения поддержки кода как минимум.
                    0
                    ответ в том, что сообщения не должны храниться на сервере в незашифрованном виде.
              +9
              Все данные сразу заносятся в личное дело?)
                +15

                Все время было интересно почему чаты держать пользователя за дебила, и дают копировать только сообщение целиком? Просто дайте ему возможность выделить текст самому, как в браузере, и не надо никакого хипста ML и прочих перделок

                  +4
                  Копировать -> Заметки -> Вставить -> Выделить -> Копировать -> Вставить

                  Или свайпать влево-вправо и через голову копировать.

                  Ад.
                    +2

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

                      +5
                      Один известный мессенджер так умеет. Хотя не сказать, что очень давно научился (всего-то год) по меркам его существования.
                        +2

                        Вчера как раз в этом мессенджере на андроиде копировал адрес-по итогу просто запомнил и ввел в приложение такси заново этот адрес, скопировать не смог.

                          +1
                          К сожалению, не использовал на андроиде, но на iOS работает идеально.
                            +1
                            И на андроиде работает
                          0

                          Хороший человек еще на отправляющей стороне расставит кавычки, чтобы копировалось кликом.

                            0
                            на ios не работает у меня, чяднт :(
                              0
                              Я тоже не сразу понял, как это работает.
                              Долго нажимаете на сообщение, пока не появится меню с «Поделиться», «Копировать» итд, а потом еще раз нажимаете на сообщение. И — вуаля, появляется курсор и можно выделять нужный фрагмент.
                              Не совсем очевидный путь, конечно.
                              –1

                              Этим мессенждером был Альберт Эйнштейн.

                              +1
                              Да уж, бесит это в СМС и Whatsapp. А вот Telegram текст кусками копировать даёт.
                              +18
                              Привет, Юра!

                              Мы очень рады что вы смогли использовать библиотеку в вашем продукте и написали такую прекрасную и содержательную статью.
                              Надеемся, что рано или поздно вы перейдете в проект, который кому-то нужен. Удачи!
                              –2

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

                              • UFO just landed and posted this here
                                • UFO just landed and posted this here
                                  +10
                                  Ну смотрите, сама статья по себе не очень содержательна. Мы захотели фичу — нашли библиотеку — подключили — фича работает. В общем-то и все. Можно было как-то дополнить, в стиле «сначала думали сделать вот так — но получилось плохо — руками писать долго/сложно потому-то, вот классное решение — если хотите сделать так же то вот анонс релиза.»

                                  А получается что статья высосана из пальца, потому что (скорее всего) у кого-то KPI по статьям в корп блог на хабре.

                                  P.S. Контекст имеет значение. Как мне кажется, есть разница между «пилю пет проект для себя и бабушки» и «пилю никому не нужный мессенджер который государство будет форсить, а если все остальные заблочат то и вы в нем будете сидеть».
                                    +4

                                    Ваши аргументы услышал, но не могу согласиться, сорри. Если честно, меня с вашего предыдущего поста и триггернуло, вы там ничего не говорите про неинтересную статью или ненужную фичу, а просто типа "чувак, твой продукт никому не нужен". Ну так и пет проект для бабушки никому не нужен. Про личную ответственность разраба за степень токсичности компании — тоже не могу согласиться. Это все суперскбьективно, при желании можно половину компаний захейтить, от Касперского до Гугла с фб, список зависит только от белизны собственного плаща (и не лично на вас наезд, но мой опыт показывает, что в большинстве случаев плащик спокойно вешается в шкаф, когда приходит хороший оффер не от каких то совсем чертей, но от таких, по которым в другой день можно и высказать свою высокоморальную позицию в бложике).
                                    Я вообще понимаю, откуда подобные сражения в интернетах берутся — народ фрустрирован властью в целом (совершенно обоснованно), и выплескивает свое раздражение на все, что так или иначе с этой властью ассоциируется. Но блин, тут просто ложная цель, на которую все сагрились под влиянием этих чувств. Ребята, разраб, пилящий мобильный мессенджер, не виноват в том, что в России спайка власти и крупного бизнеса выглядит так омерзительно. Не нужно сбиваться в стаю и мочить лёгкую цель, это контрпродуктивно.

                                    +8
                                    Ну да, ведь продукты корпорации, которая сливает все твои переписки и прочие данные кому надо, появилась из ничего, а не эти разработчики которые работают там пилили.
                                      +2

                                      Ну да, разработчик ведь понятия не имеет, на кого работает. Или его в плену держат и на хедхантере забанили.

                                      +5
                                      Дайте людям возможность произвольно выделять и копировать текст из сообщений, и не надо никаких ML фичей. Человек все равно лучше вас и лучше нейросетей знает, что конкретно ему требуется выделить.
                                        +4
                                        Отличный пример компромисса для UX, когда программные средства по другому не позволяют.

                                        Также очень приятно видеть использование RxJava и Maybe там, где он подходит :)
                                        Правда, можно было полегче сделать — вместо flatMapMaybe лучше в данном случае все же filter { it.isNotEmpty() } — он как раз Single в Maybe превращает.

                                          +11
                                          Я думал, бар «Цветочки» на Рубинштейна закрылся, а оказывается, переехал на Некрасова. Спасибо, полезный пост!
                                            +2
                                            Хах) Рад, что полезно. В следующей статье добавлю гороскоп и курс валют
                                            +1

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


                                            Кому обычно присылают адреса не в тамтам, рекомендую включить функцию гугл ассистента "Объекты на экране"


                                              +3

                                              Из этого поста я узнал, что ТамТам всё ещё существует.

                                                0
                                                Товарищ майор не хочет сам их находить?
                                                  0

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

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

                                                    Интересно, что не для всех типов сущностей используются нейронные сети. Например, для распознавания времени, даты, номеров авиарейсов используются регулярные выражения

                                                  Only users with full accounts can post comments. Log in, please.