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

Привет, Хабр! В прошлых статьях я рассказывал про локальный переводчик с синтезом голоса на кабардинском и про то, как собрать полный NLP-стек для «финального босса лингвистики» из старых словарей. За прошедшие месяцы весь этот стек - лемматизация, POS, NER, тональность, эмбеддинги, синтез - переехал с Python/Flask на десктопе в Swift-приложение на iOS и заодно перерос сам себя: начиналось всё как синтезатор речи для одного языка, а выросло в персональную библиотеку для любого контента - книги, статьи, подкасты, видео, музыка, файлами или ссылками. Полгода на голом Swift 6, вечерами и выходными.

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

Коротко: что это и как попробовать

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

Источником может быть файл, ссылка на статью, выпуск подкаста или видео - или целая подписка: RSS новостного сайта, лента подкаста, YouTube-канал. Статья приходит уже очищенной от рекламы и баннеров, одним текстом; и текст, и транскрипт остаются в вашей библиотеке навсегда, даже если издатель удалит оригинал или пропадёт интернет.

Требования. iOS 18.6+, iPhone 12 и новее. Apple Intelligence требует iPhone 15 Pro и новее - ограничение Apple; на старых моделях работает языковой пакет перевода и чат с цитатами, но без перефразирования локальной моделью.

Вес. Приложение - около 50 МБ. Остальное по требованию: Silero-синтез для языков СНГ - 100 МБ, ударения для славянских - 50 МБ, латышский голос - 60 МБ, пунктуация для русского - 80 МБ.

Цена. На время беты всё бесплатно; после релиза один объект с полным ИИ-анализом останется бесплатным навсегда, дальше - поштучно или подписка.

Ссылка: https://sayfable.com/ - тестирование анонимное, отдельный аккаунт не нужен, только TestFlight.

Почему такая библиотека стала возможна именно сейчас

Раньше на телефоне каждый тип контента жил в своём приложении: музыка в одном, подкасты в другом, YouTube в третьем, статьи - если повезёт, в read-it-later вроде закрывшегося Pocket. У аудио нет текста, у текста нет озвучки: нужен транскрипт подкаста - платите за API, хотите спросить у модели, о чём книга, - грузите её в чат-бот и тратьте сотни тысяч токенов, причём результат разбора нигде не сохранится.

С появлением в iOS 26 доступа к локальной языковой модели на уровне системы мы смогли соединить все типы контента в одном приложении и выполнять всю обработку медиа локально на устройстве. Контекстное окно у FM-модели Apple небольшое, но в связке с собственной разметкой этого хватает, чтобы отвечать на вопросы по личной библиотеке без галлюцинаций и со ссылкой на источник. SayFable обогащает любое медиа текстом (транскрипцией или субтитрами), локально размечает и режет на сцены - дальше с ним можно работать как с индексированным текстом: выбрать только интересные сцены из подкаста, собрать из десятка статей абзацы по своей теме, слушать книгу плейлистом сцен с одним персонажем.

Что можно делать в приложении уже сейчас

Импорт книг почти в любом формате. EPUB, FB2, DOCX/DOC, RTF, TXT, PDF - файл разбирается на главы по оглавлению, а внутри главы текст режется на сцены по смыслу.

Карточка книги, которую можно листать во время прослушивания. Не останавливая воспроизведение, видно оглавление: сколько в главе сцен и как они называются - по названиям обычно уже понятно, о чём глава. Рядом «Атмосфера» со статистикой: объём, доли частей речи, соотношение действия и описания, средняя важность и эмоциональность, доля диалогов, типы сцен и эмоциональная кривая по главам. И «Сущности» - персонажи, места и организации из текста; там же персонажу назначается свой голос (по умолчанию вся книга читается одним). Единственное исключение - YouTube: его встроенный плеер останавливается, как только уходишь с экрана видео, и это политика Google, а не наша недоработка.

Карточка книги, экран «Атмосфера» со статистикой и редактор сущностей
Карточка книги, экран «Атмосфера» со статистикой и редактор сущностей

Слева направо: карточка книги с главами и сценами, «Атмосфера» со статистикой разбора и редактор сущностей - 52 персонажа с числом упоминаний, где каждому назначается голос.

Своя подборка по любой теме. Объект в библиотеке - не обязательно одна книга. Можно начать с одной понравившейся статьи и собирать вокруг неё свою тему - робототехника, спорт, что угодно: кнопка «добавить по ссылке» кладёт каждую новую статью, выпуск подкаста или видео с субтитрами отдельной главой. Дальше по всей подборке работают поиск, слайдер важности, озвучка и чат.

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

Перевод и дубляж на лету. Вместо оригинала - перевод, вплоть до синтезированной озвучки поверх оригинальной дорожки: закадровый перевод, собранный на телефоне без платного API.

YouTube без рекламы. Видео играет внутри приложения, ниже - субтитры, которые можно переводить и озвучивать. Плеер Google останавливается при блокировке экрана, поэтому есть кнопка «Не блокировать экран»: автоблокировка выключается, яркость гаснет до минимума - телефон можно убрать в карман.

Показать в статье все режимы не выйдет - она и так на пределе размера. Ниже только два кадра, а разборы остального, с видео и скриншотами по каждому экрану, лежат на sayfable.com: кому интересно, там видно куда больше, чем можно втиснуть в текст.

Как это считается на телефоне: таблица вместо векторного чёрного ящика

Чтобы навигация по сценам, плейлисты и чат работали быстро и офлайн, обычного RAG поверх эмбеддингов не хватило. Стандартный подход - нарезать книгу на чанки, заэмбеддить, искать по косинусу - наследует две проблемы: вектор одной модели ничего не значит для другой, а голое косинусное сходство у контекстных эмбеддингов регулярно промахивается мимо смысла (анизотропия - «всё похоже на всё» с косинусом около 0.9, пока вектор не отцентрировать).

SayFable вместо этого один раз при импорте строит из книги колоночную таблицу - плоские массивы «одна колонка на признак», по которым можно сканировать без разбора JSON. Уровней три, различаются они гранулярностью: слово → предложение и абзац → сцена.

Слово. Лемма, часть речи, номера предложения / абзаца / сцены, позиция ударной гласной и salience - частота слова внутри своей сцены, умноженная на обратную частоту по сценам книги: «насколько слово необычно вот здесь», а не «насколько оно редкое вообще». Отдельной колонкой - эмоциональная валентность из словаря Уорринера (для трёх десятков языков перенесена на леммы через глоссы тем же мостом, что описан во второй статье).

Предложение и абзац. Реплика это или авторская речь, вопрос, восклицание, кто говорит, доля диалога, плотности частей речи. И здесь же - важность, которую крутит слайдер:

  • важность предложения = сумма salience его знаменательных слов с фиксированным весом части речи (существительное 1.0, глагол 0.9, прилагательное 0.7), умноженная на центральность предложения к центроиду своей сцены - центрированный косинус его эмбеддинга. Именно умножение: набор редких слов в предложении, к теме сцены отношения не имеющем, важности не даёт;

  • важность абзаца = среднее по предложениям. Красивая гипотеза «взять топ-K лучших и пик» проиграла бенчмарку на всех трёх тестовых книгах, так что в коде осталось скучное среднее;

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

Целиком закон важности выглядит скучнее своего описания - и это, пожалуй, лучшее, что можно про него сказать:

static func posWeight(_ p: POSCode) -> Double {
    switch p {
    case .noun, .propn: return 1.0
    case .verb:         return 0.9
    case .adj:          return 0.7
    case .num:          return 0.5
    default:            return 0.2
    }
}

/// Масса значимости предложения: Σ salience × вес части речи.
static func salienceMass(_ lemmas: [WeightedLemma]) -> Double {
    lemmas.reduce(0) { $0 + $1.salience * $1.posWeight }
}

/// Центральность к центроиду сцены. Нет вектора — нейтральная 1.0.
static func centrality(_ v: [Float]?, _ centroid: [Float]?) -> Double {
    guard let v, let centroid else { return 1.0 }
    return max(0, cosine(v, centroid))
}

static func sentenceScore(salienceMass: Double, centrality: Double) -> Double {
    salienceMass * centrality
}

Ни одной обучаемой величины, ни одного вызова модели - чистая арифметика по колонкам, поэтому она одинаково считается и на телефоне, и в Mac-утилите, которая готовит .say.

Сцена. Границы считаются по сдвигу плотностей частей речи и по семантическому разрыву эмбеддингов - не по длине абзаца и не по одному сигналу. Дальше сцена получает свёрнутые сверху метрики: средняя и пиковая важность, эмоция и её размах, доля диалога, доминирующий говорящий, тип сцены. Арифметика по готовым колонкам, никакой модели.

Где нужна модель побольше. Локальная Foundation Model от Apple подключается точечно и только на верхнем уровне: назвать сцену заголовком в шесть слов и разметить её паспорт - точка зрения, место, время, ключевые события. Принципиальный момент: модель не пишет прозу, которая потом хранится и переигрывается, - она возвращает ссылки на предложения самой книги. Поэтому чат отвечает цитатами из текста: галлюцинировать нечем, максимум промахнуться ссылкой. Честно про потолок: разметка сущностей и говорящих на телефоне пока эвристическая, без валидации большой моделью - точность 80–90%, список сущностей иногда приходится править руками.

На эту же таблицу опираются слайдер важности от 5% до 100% (прокрутить книгу, оставив только несущий сюжет), чат с цитатами вместо пересказа и синтез: колонки «говорящий» и «эмоция» отдаются озвучке, поэтому у персонажа свой голос, а темп и высота меняются вместе с тем, что происходит в предложении. Как эта таблица выглядит глазами пользователя - на скриншоте иммерсивного режима ниже: там у каждого слова видны ровно эти колонки.

Что это значит в цифрах

Всё мерилось на iPhone 15 Pro Max, на релизной сборке и на реальных книгах — цифры ниже это время стены из логов, а не оценка.

Операция

Замер

В пересчёте

Полный анализ книги - разметка, эмбеддинги, сцены

2069 абзацев, 32 главы - 2,5 минуты

около 85% этого времени уходит на фазу эмбеддингов

Транскрипция аудиокниги

11 ч 24 мин звука - 50 минут

≈14× быстрее реального времени

Перевод на языковом пакете Apple

6,5 абзаца в секунду

книга в 3400 абзацев - минут за девять

Синтез речи (самый тяжёлый процесс)

9,7–10,3× быстрее реального времени

Apple и Silero на одном тексте практически поровну

Ждать всё это целиком не надо: и транскрипция, и синтез идут по главам, так что слушать можно, пока готовятся следующие. А вот анализ стоит первым в очереди не случайно - он дешевле всего остального на порядок и при этом обязателен: из его колонок синтез берёт, где ускориться, где понизить тон и где поставить паузу. Без него озвучка получается ровной и плоской, каким бы хорошим ни был голос.

Эти цифры держатся на сознательном ограничении: всё построено на инфраструктуре самой Apple. Распознавание речи, перевод, эмбеддинги, языковая модель, аудиосессия, CarPlay - системные фреймворки, а не сторонние библиотеки. Извне мы тащим только данные: словари для разметки и веса моделей синтеза. Единственная внешняя зависимость с кодом - ONNX Runtime, нативный бинарник с предсказуемым поведением, который мы вызываем в своём потоке. Держать приложение, часами синтезирующее речь в фоне и при этом не падающее, сложно и так - каждая лишняя зависимость делает это на порядок сложнее.

Отсюда же честный ответ про Android: там нет системной локальной модели такого уровня, и половине стека просто не на чем стоять. Портировать урезанную копию мы думаем, но опыт может выйти хуже, чем отсутствие приложения.

Два железных ограничения - и обходные манёвры

Перевод через Apple Intelligence. Когда он включён, система сама перехватывает перевод и гонит его через генеративную модель - на уровне ОС, вне контроля приложения. Замер на книге в 3431 абзац: с включённым Apple Intelligence - 0.32–0.56 абзаца в секунду и тепловая пауза каждые 50–100 абзацев; с выключенным, через обычный языковой пакет - 5.9–6.5 абзаца в секунду и одна пауза на всю книгу. Разница больше чем десятикратная, и скачанный языковой пакет при включённом Apple Intelligence просто игнорируется. Обходной путь нашёлся не в коде, а в настройках телефона: на время большого перевода Apple Intelligence отключается целиком.

CarPlay и живой синтез. Прямое воспроизведение синтезированной речи в машине заикалось: CarPlay не готов к живому потоку TTS, а перезапуск плеера на границе каждого абзаца слышно растягивал паузы. Решение двухэтажное: аудио сначала синтезируется в файл и играет из буфера, а готовые файлы соседних абзацев одной сцены склеиваются в бесшовный трек. Цена - задержка 2–3 секунды на старте ещё не подготовленной главы.

CarPlay: своя поверхность со списком «Недавние / Библиотека / Подписки / Плейлисты»
CarPlay: своя поверхность со списком «Недавние / Библиотека / Подписки / Плейлисты»

 В машине это отдельная поверхность с недавним, библиотекой, подписками и плейлистами, а не один экран «сейчас играет».

Малые языки: то, ради чего всё начиналось

Здесь сходятся все три статьи. Всё, что было в первых двух на примере кабардинского - синтез Silero, словарная лемматизация и POS-разметка, перенос тональности через языковой мост, — оказалось не разовым случаем, а методом: тем же путём полный словарный стек доведён до покрытия у 19 из 20 языков с Silero-озвучкой. Раньше это жило Python-сервисом на Mac mini, теперь работает на телефоне.

Ядро - портированная на iPhone Silero base под лицензией MIT: 43 голоса на 20 языков, полностью офлайн. По группам: славянские (русский, украинский, белорусский - с ударениями), тюркские (восемь языков), финно-угорские (три), кавказские (кабардинский, грузинский, армянский), плюс таджикский и калмыцкий. Ударения ставит отдельная портированная модель - русский без правильных ударений это не отдых, а наказание для слушателя. Ещё одна закрывает похожую дыру в распознавании: Apple прекрасно распознаёт русскую речь, но не расставляет знаки препинания (замер модели пунктуации против авторских субтитров подкаста: совпадение знаков 88.5%, капитализации 93.6%). Нативную лемматизацию, POS и NER от Apple из этих 20 языков получает только русский; для остальных 18 всё собрано словарным методом из второй статьи.

Масштаб в цифрах: не только Silero

Silero - только один слой. Синтез, распознавание речи, перевод и NLP-разметка считаются отдельно, и покрытие у них разное:

Функция

Языков и вариантов

Основные группы

Синтез речи (Apple + Silero + Piper)

68

европейские, славянские, тюркские, финно-угорские, кавказские, азиатские

Распознавание речи

47

европейские, славянские, азиатские

Машинный перевод

22

европейские, славянские, азиатские

Полный NLP-стек (лемма + POS + NER + тональность)

52

германские, романские, славянские, тюркские, финно-угорские, кавказские

Полный список кодов - на сайте проекта. Отдельная история - эмбеддинги для смыслового поиска: у Apple они привязаны к письменности, а не к языку, поэтому латиница и кириллица считаются напрямую, а для белорусского, греческого, армянского, грузинского и кабардинского не работает ни та, ни другая модель. Там включается мост через глоссы: предложение слово за словом переводится в русские или английские глоссы, и эмбеддинг считается уже по ним. Каждый такой маршрут включался после замера с контрольным языком, а не «на всякий случай». Словари и модели для «обделённых» языков выложены отдельно, с открытыми лицензиями: github.com/sayfable-models.

Иммерсивный режим: побочный эффект полезнее исходной идеи

Планировался он как окно в то, как модель видит текст: у каждого слова лемма, часть речи, сущности, вес важности, валентность и настройки синтеза. Просто показать двигатель изнутри.

Но стоило подставить в то же окно глоссу - тот самый мост из предыдущего раздела, - как получился режим чтения на другом языке: под каждой леммой не перевод всей фразы, а её собственный аналог.

Иммерсивный режим: часть речи, кабардинская глосса, параметры синтеза, важность, эмоция и говорящий
Иммерсивный режим: часть речи, кабардинская глосса, параметры синтеза, важность, эмоция и говорящий

 «Остров сокровищ» с кабардинскими глоссами: у текущего слова - часть речи, глосса ар, езы, рассчитанные rate / pitch / volume / pause и голос говорящего, ниже - важность, эмоция и сцена абзаца. Это и есть та самая таблица, показанная человеку.

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

Формат .say: разбор, который не приходится пересчитывать

.say - книга вместе с уже посчитанной таблицей: весами слов и валентностью, настройками синтеза, транскрипцией, сущностями, эмбеддингами предложений, переводом. Разобрали один раз - переслали файл на другой телефон, и там ничего не пересчитывается: книга открывается со сценами и персонажами сразу. 600 страниц весят около 15 МБ вместе с исходным текстом - в 50 раз меньше средней аудиокниги. Открытую спецификацию опубликуем на GitHub после тестов.

Отсюда же план, который снимает оговорку про эвристику выше: издавать книги из общественного достояния сразу в .say, с разметкой, прогнанной на большой модели. Тогда не придётся ни ждать анализа, ни править сущности руками: у каждой реплики проставлен правильный персонаж, а границы сцен подтверждены сменой времени, места и состава действующих лиц, а не только статистикой.

Как помочь с тестированием

Движок, кажется, получился. А удобное приложение рождается только из того, как им пользуются живые люди, - как любому родителю, мне сложно увидеть недостатки своего детища. Что интереснее всего проверить:

  • Заменяет ли SayFable у вас несколько отдельных приложений - или удобнее держать их порознь.

  • Синтез и подбор голоса под персонажа; для славянских - качество ударений и помогает ли личный словарь исправлений.

  • Насколько осмысленно собираются плейлисты по важности, эмоции и персонажам на вашей книге.

  • Ловит ли чат себя за руку там, где обычный чат-бот тихо бы что-то придумал: совпадают ли цитаты с текстом.

  • Как ведёт себя CarPlay - есть ли заикания на стыках абзацев на ещё не подготовленных главах.

  • Если вы носитель малоресурсного языка - как звучит синтез и насколько адекватна разметка.

Ссылка: https://sayfable.com/ - тестирование анонимное. Буду рад любым отзывам, восторженным и не очень.