Приложение слушает разговоры продавца с клиентами и превращает их в текст. Человек на той стороне трубки не подписывался на то, чтобы его голос уехал на чей‑то сервер, и это решило архитектуру раньше, чем я написал первую строчку: наружу не уходит ничего.
Дальше идёт счёт, который за это пришёл, и он вполне конкретный: шесть с половиной минут работы на десятиминутный разговор и половина возможностей, которые в приложении с сервером есть по умолчанию.
Рамка: что решаем и чем платим
Задача простая и старая. Продавец живёт в звонках и переписке, и через две недели он не помнит, кому что обещал и когда перезванивать. Нужна карточка клиента, где сама собирается история общения, и напоминание «пора связаться».
Ограничение, которое я себе поставил: ни сервера, ни аккаунта, ни регистрации. Всё, что приложение знает, лежит на телефоне владельца телефона.
И сразу то, чем за это платят, чтобы дальше читать честно:
нет синхронизации между устройствами;
нет автоматического восстановления, если телефон утонул;
нет поиска и разбора за пределами того, что влезло в аппарат;
нет никакой серверной аналитики, то есть я не вижу, как приложением пользуются.
Каждый пункт решался бы бэкендом за неделю. Бэкенд был бы дешевле. Но тогда записи чужих разговоров лежали бы у меня, а я этого не хочу.
Что «без сервера» значит на практике
База это SQLite на телефоне, интерфейс на Compose, никакой учётной записи. Приложение не спрашивает почту, не просит войти и не имеет понятия, кто им пользуется. Стереть всё можно одним движением в настройках системы, и после этого от данных не остаётся ничего нигде.
Звучит просто ровно до того момента, когда в приложении появляется распознавание речи. Вот на нём вся экономия на сервере и заканчивается.
Распознавание на телефоне: замеры, а не обещания
Транскрибация это единственная тяжёлая вещь во всём приложении. На сервере она стоила бы копейки. На телефоне за неё платит телефон.
Первое решение было про железо: тестовый аппарат я взял не средний, а самый слабый из тех, что могут оказаться у пользователя. Samsung A17: два сильных ядра и шесть слабых. Логика такая: то, что не проявится на середине рынка, обязательно проявится здесь, а мне нужен худший случай, а не красивый.
Прогон whisper.cpp на живом аппарате, минута русской речи:
Модель | 2 потока | 4 потока |
|---|---|---|
tiny‑q5_1 | 14 с (0.24 от реального времени) | 14 с (0.24) |
base‑q5_1 | 38 с (0.65) | 42 с (0.71) |
small‑q5_1 | 103 с (1.72) | 101 с (1.68) |
Первое, что видно: четыре потока не дают ничего, а у base даже чуть хуже. Причина в том же железе — сильных ядер два, остальные тормозят общий барьер, и попытка занять их всех делает только хуже.
Второе: разница между моделями решает продуктовый вопрос. Десятиминутный разговор base расшифрует за шесть с половиной минут, small — за семнадцать. Быстрее реального времени против медленнее реального времени: это не «немного лучше», это разница между «к концу дня всё разобрано» и «очередь растёт быстрее, чем разгребается». Поэтому по умолчанию стоит base, а small остался переключателем в настройках для тех, кому качество важнее скорости.
Третье, про память. Пик — 699 мегабайт, причём в куче Java живёт всего 23, остальное занимает нативная память модели. Цифра выглядит пугающе, а на деле означает одно: тестовый телефон должен быть с восемью гигабайтами, потому что на четырёх система начинает убивать фоновый процесс. Держится этот пик только пока идёт расшифровка.
А вот чего я зря опасался — так это места на диске. У первой пользовательницы на телефоне лежат уже сотни записей разговоров, и они не мешают ни системе, ни приложению: аудио занимает мало, тяжёлая часть тут только расшифровка, да и та на время работы.
Про честность замеров
Повтор того же прогона на свежесброшенном телефоне, где нет ни одного мессенджера, дал base 0.50 вместо 0.65. Разброс между прогонами оказался заметно больше, чем я ожидал.
В таблицу пошла худшая цифра. Не из скромности: у живого пользователя телефон не стерильный, на нём висят почта, мессенджеры и десяток фоновых сервисов, и он ближе к 0.65, чем к 0.50. Публиковать лабораторный результат, зная, что в жизни будет хуже, значит соврать, не сказав ни одного неверного слова.
Ловушка первая: система сама отправляет ваши файлы в облако
Дальше начались вещи, которые не гуглятся, пока в них не вляпаешься. Все три стоили по дню и ни одна не связана с криптографией или моделями.
В манифесте Android есть флаг allowBackup, и по умолчанию он включён. Означает это буквально следующее: операционная система периодически забирает приватные файлы приложения и кладёт их в облачный бэкап пользователя.
Для приложения‑заметочника это ничего страшного. Для приложения, которое хранит записи телефонных разговоров, это отмена всей архитектуры одной строкой, которую я не писал. Я строю локальность, никуда ничего не отправляю, а система заботливо синхронизирует записи чужих голосов в облако.
android:allowBackup="false"
Одна строка. Сейчас она стоит в манифесте. Большинство приложений, подозреваю, её не трогают вовсе.
Ловушка вторая: диагностика, которая уверенно врёт
Приложение умеет подхватывать входящие сообщения мессенджеров через службу уведомлений. Значит, надо уметь объяснить пользователю, почему сообщения вдруг перестали приходить: у мессенджера выключены уведомления, у нас отобрали доступ, телефон усыпил службу.
Тут выяснилась подлость. Android возвращает одинаковый ответ на два совершенно разных случая: «у этого приложения уведомления запрещены» и «этот пакет тебе не виден». Видимость чужих пакетов с некоторых пор надо объявлять в манифесте отдельно, и если ты этого не сделал, система не говорит «не вижу» — она отвечает так же, как при запрете.
Результат: диагностика уверенно сообщает пользователю, что у него выключены уведомления в мессенджере. Пользователь идёт проверять, там всё включено, и он справедливо считает, что приложение врёт. Так и есть.
Лечится порядком проверок: сначала убеждаемся, что пакет вообще виден и установлен, и только потом трактуем разрешение. Проверено с обоих концов — на приложении с заведомо выключенными уведомлениями и на двух установленных мессенджерах.
Ловушка третья: запись звонков, которой нет
Тут пришлось сдаться и переделать замысел.
Записывать телефонный разговор из стороннего приложения на современном Android нельзя. Источник звука VOICE_CALL отдают только системным приложениям, обходной путь через специальные возможности запрещён политикой магазина с мая 2022-го, а трюк «микрофон плюс громкая связь» даёт качество, ради которого не стоит и начинать.
Решение получилось скучное и рабочее: звонок остаётся обычным звонком, пишет его встроенный рекордер телефона, а приложение импортирует готовый файл и привязывает к карточке клиента. Не так красиво, зато честно и работает.
А потом случился сюжет, ради которого я это всё рассказываю. Тестовый телефон я покупал специально под этот сценарий — Samsung, у которого рекордер в прошивке есть. Приехал аппарат, в котором записи звонков нет вообще.
Оказалось, это параллельный импорт с малайзийской прошивкой. Подсистема записи на месте, приложение‑компаньон установлено, региональный флаг погашен, а файл с флагами зашифрован, так что прочитать его нельзя — только эмпирика.
Я проверил три пути сменить регион, и все три закрыты:
Сброс к заводским настройкам с местной симкой. Сделал. Код региона в служебном разделе не меняется сбросом, а правило подхвата региона по симке работает только тогда, когда заводского кода нет. Здесь он есть.
Прошивка местной сборкой. Бессмысленно: обе прошивки в одной мультирегиональной группе и несут один и тот же файл региона.
Служебные коды. Заблокированы: в звонилке отвечают «неверная команда», через отладочный мост системная рассылка отклоняется по правам.
Вывод для продукта вышел неожиданный и полезный: у части пользователей записи звонков не будет никогда, и приложение обязано это честно диагностировать, а не молчать. Теперь после нескольких звонков без единой найденной записи оно само говорит: вот возможные причины, включая ту, что в вашей прошивке рекордера нет.
Что стоит вместо синхронизации
Локальность без выхода наружу — это не свобода, а другой замок: данные не у меня, но и не совсем у пользователя, а внутри приложения, которое однажды перестанет обновляться.
Поэтому в приложении есть выгрузка. Вся собранная история — карточки клиентов, заметки, расшифровки разговоров, переписка — в любой момент выкладывается обычными markdown‑файлами в папку, которую человек выбирает сам. Это формат Obsidian: каталог текстовых файлов со ссылками между ними, который открывается чем угодно, вплоть до блокнота.
Что это даёт на практике: перенос на новый телефон руками, резервная копия в любом облаке по своему выбору, чтение и поиск на компьютере и, главное, возможность уйти от меня, ничего не потеряв. Выгрузка бесплатна и останется бесплатной, что бы дальше ни случилось с платными функциями.
Одна деталь оттуда куплена живым прогоном: папка выгрузки не может быть одновременно папкой, куда телефон складывает записи звонков. Человек подключил одну и ту же папку дважды, сканер записей добрался до неё первым и забрал markdown‑файлы как разговоры. Теперь одна папка — один источник, и приложение это проверяет.
Чего в приложении нет и не будет без сервера
Сведу в одно место, чтобы не выглядело, будто локальность бесплатна.
Нет синхронизации: поставил на второй телефон — там пусто. Нет автоматического восстановления: потерял аппарат — потерял то, что не успел выгрузить (облачный бэкап, напомню, я сам и отключил). Нет поиска по всему объёму записей на сервере, есть только то, что телефон вытянет сам. Нет аналитики: я не знаю, каким экраном пользуются, а каким нет, и узнаю это только если мне напишут.
Последнее для разработчика больнее всего. Приложение без телеметрии это разработка вслепую, и цена осознанная: телеметрия в приложении, которое живёт внутри чужих разговоров, слишком легко превращается в утечку.
Возражения
«Всё это решается сервером за неделю». Да, решается. И записи чужих разговоров лежали бы у меня. Вопрос не в сложности, а в том, чего я не хочу иметь.
«Шесть минут на разговор, это же долго». По сравнению с сервером долго. По сравнению с «никак» нормально: это разовая работа в фоне, а не то, чего человек ждёт, глядя на экран.
«Кому вообще нужна эта локальность». Мне и жене, для которой всё начиналось. Дальше посмотрим: приложение выложено и бесплатно, счётчик установок ответит на этот вопрос лучше меня.
«А если пользователь потеряет телефон?» Потеряет всё, что не выгрузил. Поэтому выгрузка в markdown сделана бесплатной и доступной в один экран, а не спрятана в платный тариф: запирать чужие данные внутри приложения ради удержания — последнее, чем стоит заниматься.
Что я из этого вынес
Локальность — не лозунг, а счёт: платишь скоростью и половиной возможностей. Сумму стоит знать заранее, но она оказалась меньше, чем я боялся.
Тестовое устройство берут как пол рынка, а не как середину: то, что не проявится на середине, проявится здесь.
Публикуют худший замер из полученных. У пользователя телефон не стерильный.
Настройки по умолчанию способны отменить вашу архитектуру целиком: один включённый флаг отправлял в облако ровно то, что я сознательно держал на устройстве.
Диагностика, которая не отличает «запрещено» от «не видно», врёт увереннее, чем молчит. Сначала проверяйте видимость, потом права.
Часть ограничений платформы не обходится, и правильный ответ на них — честно сказать пользователю, что именно у него не заработает.
У локального хранения обязан быть выход наружу в открытом формате. Иначе получается не приватность, а замок, только ключ от него у разработчика.
Приложение называется Sales Assistant, лежит в Google Play, сейчас бесплатно. Ссылка на сайт приложения в моем профиле. Код не открыт, но замеры, на которые я ссылаюсь, воспроизводятся любым, у кого есть whisper.cpp и дешёвый Android: проверьте на своём железе, интересно сравнить.
