Небольшая история о том, как я искал для себя читалку, как в итоге придумал новый формат для электронных книг — и, главное, как я месяцами воевал с дешёвыми LLM, чтобы они генерировали этот формат без ошибок. Будет и про устройство формата, и про то, как он «затюнен» под копеечные модели, и про основные проблемы.

TL;DR. .tbook это ZIP‑контейнер (как EPUB), в котором оригинал книги лежит рядом с попредложенным переводом и пословным выравниванием: тапаешь слово в оригинале — мгновенно видишь перевод предложения с подсвеченным ровно тем словом, которое ты тапнул. Всё считается на этапе конвертации, поэтому читалка работает офлайн, без запросов в сеть. Самым сложным оказалось не придумать формат, а заставить дешёвые LLM его надёжно генерировать.

С чего всё началось

Я обожаю читать. Я читаю минимум 1 книгу в неделю, иногда по книге за 1–2 дня. С 6 лет. Сейчас мне 40 — значит, примерно 6 тысяч книг у меня в истории (хотя многое перечитано до дыр по 10 раз, сложно подсчитать). И, так как я активно учу английский, мне хочется читать в оригинале. И я читаю в оригинале. И мне было постоянно больно пользоваться читалками с переводом. Я перепробовал вообще ВСЁ. Я был фанатом метода Ильи Франка с 2001 года. Я перечитал так МНОГО книг.

FBReader? Устанавливай словари. Словари не показывают слово в контексте, для многозначных слов долго искать нужное значение. Не очень удобные подсказки. Google Translate опционально — ты тапаешь слово и ждёшь соединения. Да, сейчас это быстрее, чем в 2000–2010-х, но, учитывая общую скорость чтения, всё равно безумно медленно. Я не хочу терять картинку в воображении, мне всегда хотелось иметь максимальную скорость.

Пару лет назад нашёл приложение Linga, которое переводит по запросу через AI и показывает использование слова в контексте. Это был почти личный идеал — если бы не ещё большее время ожидания (время на запрос к AI).

И потом, стихийно, я понял, что нам нужно пересмотреть подход к самому формату и процессу создания переводов.

Что мне нужно как активному читателю, изучающему язык?

  1. Мгновенность. Ноль ожидания. Тапнул — увидел.

  2. Не художественный перевод текста на родной язык целиком. Его минус в том, что переводчики зачастую используют совсем другие слова и часто значительно меняют текст. Мне нужен художественный перевод отдельного предложения, где использовался бы примерно тот же вокабуляр, что и в оригинале.

Идея: перенести всю работу на этап конвертации (SSR для книг)

И вот в 2026 году меня осенило. Я понял, что нужно сделать в читалке подход в духе SSR (server‑side rendering): не считать перевод в момент, когда читатель тапнул слово, а посчитать его один раз заранее и «запечь» прямо в файл книги.

Что может быть проще — берём любую EPUB‑книжку, разбиваем на предложения, скармливаем LLM, получаем перевод с нашей разметкой. EPUB — это zip‑based формат, внутри просто архив. Я решил сделать схожим образом и начал собирать свой формат .tbook.


Часть 1. Как устроен формат.tbook

Контейнер

.tbook — обычный ZIP‑архив (как EPUB). Внутри:

book.tbook  (ZIP)
├── manifest.json          — метаданные + оглавление
├── cover.jpg              — обложка (опционально)
├── notes.json             — тела сносок (опционально)
├── images/                — картинки из тела книги (опционально)
└── chapters/    
├── ch1.json           — одна глава = один файл    
├── ch2.json    
└── …

Читалка сначала читает manifest.json, а главы находит только через manifest.chapters[].file — никаких предположений о нумерации и порядке файлов. Всё, что читалка не знает (новые поля, новые entries), она обязана игнорировать. Это даёт формату аддитивную обратную совместимость: можно добавлять фичи, не ломая старые читалки.

Атом формата — предложение

Вся книга — это главы → абзацы → предложения. Предложение — это атомарная единица перевода:

{  
  "src": "Stan went to the living room.",  
  "words": [[0,4],[5,9],[10,12],[13,16],[17,23],[24,28]],  
  "tr": {    
    "ru": {      
      "text": "Стэн прошёл в гостиную.",      
      "align": [        
        { "t": [0,4],   "w": [0] },        
        { "t": [5,11],  "w": [1] },        
        { "t": [12,13], "w": [2] },        
        { "t": [14,22], "w": [4,5] }      
      ]    
    }  
  }
}
  • words — массив слов. Каждое слово — это пара символьных смещений [начало, конец) (полуинтервал) в строке src. То есть слово № 0 = src[0:4] = "Stan". Пунктуация и пробелы между словами намеренно не входят ни в одно слово.

  • tr — карта код языка → перевод. Внутри перевода: text (полный перевод предложения) и align (пословное выравнивание).

  • align — сердце формата. Каждый «чанк» связывает диапазон символов в переводе (t) с индексами исходных слов (w), которые этот кусок переводит.

Алгоритм tap‑to‑translate

Когда читатель тапает по слову в оригинале:

  1. По позиции тапа находим индекс i слова в words (в какой диапазон [a, b) попал тап).

  2. Берём перевод tr[язык].text.

  3. Подсвечиваем каждый чанк, у которого i есть в списке w.

Пара важных деталей, которые легко испортить:

  • Смещения — это символы (Unicode code points), а не байты. Producer (Go) пишет индексы по code point, consumer (Kotlin/Java) считает в UTF-16. Для всего, что в BMP (латиница, кириллица, CJK, почти вся европейская письменность), одно = одному, и они совпадают. Расходятся только на «астральных» символах (эмодзи, редкие CJK‑расширения). Поэтому читалка обязана защищаться: клампить каждое смещение в границы строки и пропускать вырожденные диапазоны — LLM ошибается. Дешевые модели — тем более.

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

Source‑as‑pivot: почему связываем по предложениям, а не по словам

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

И связываем всё через один опорный (pivot) язык — оригинал. Каждый целевой язык выравнивается к оригиналу, а не попарно с каждым другим. Это «хаб и спицы»: добавить язык стоит N−1 переводов на предложение, а не N² попарных комбинаций. Хочешь докинуть немецкий в книгу, где уже есть русский и испанский, — добавляешь одну карту tr["de"], ничего существующего не трогая.

И ещё: раз каждый перевод делается из исходного предложения, выравнивание генерируется прямо в процессе перевода. В читалке нет статистического выравнивателя (eflomal / awesome‑align / SimAlign) и нет слоя sentence‑alignment. Всё уже посчитано.

Что ещё умеет формат (по мелочи)

Чтобы книга выглядела как книга, а не как поток текста, формат опционально хранит: роли абзацев (subtitle, heading, sceneBreak, figure, table), инлайновые выделения (курсив/жирный как символьные диапазоны spans), картинки с переводимыми подписями, таблицы с переводимыми ячейками и сноски (маркер в тексте + тело сноски книжного уровня). Всё это — «презентационные подсказки»: минимальная читалка может их игнорировать и всё равно покажет корректный тапабельный текст.


Часть 2. Как это «затюнено» под дешёвые LLM

Вот здесь и была вся боль. Красивый формат придумать легко. А теперь попробуй заставить копеечную модель с openrouter.ai надёжно его сгенерировать — на всей книге, а не на трёх предложениях в демо.

Главный вывод, к которому я пришёл экспериментально: дешёвые модели — приличные переводчики, но ненадёжные генераторы структуры. Почти каждое решение в конвертере вытекает из этого факта.

Почему JSON, а не TOON

Первые итерации ломались по синтаксису. Мне хотелось, чтобы дешёвые LLM с openrouter.ai могли конвертировать книжки и не ошибаться в генерируемом манифесте. Я протестировал больше чем на 20 разных моделях и понял, что эффективные форматы вроде TOON (modern json‑like формат для LLM, более экономный по токенам) на многих моделях — особенно китайских — ведут себя нестабильно и добавляют проблем с сериализацией. Так что в формате чуть менее эффективный, но надёжный JSON для хранения глав и манифестов. Правило простое: надёжность важнее экономии токенов, если ты отдаёшь генерацию слабой модели.

Центральная проблема: позиционный дрейф (positional drift)

Это была самая коварная ошибка, и она едва не стоила мне всего проекта, потому что она проходит валидацию.

Validation: 6430 sentences, 0 empty, 0 offset_errors — OK

Но я открываю книгу и вижу, что слова связаны неправильно. Тапаю «briefcase», подсвечивается «миссис». Перевод предложения при этом идеальный! Просто карта слов сдвинута ровно на одну позицию. Тапаешь «Dursley» — загорается «всегда». В испанской книге про привычки было ещё красивее: el interés compuesto («сложный процент») дрейфовал так, что тап по interés (процент) подсвечивал сложный, а тап по compuesto (сложный) — процент. Оба слова перепутаны местами.

Почему это не ловится валидацией. Структурный валидатор проверяет только, что каждое слово из выравнивания реально существует в src. Идеально сдвинутое выравнивание проходит это с 0 offset_errors. Больше того — покрытие (coverage) остаётся ~100%: ведь каждое целевое слово действительно на что‑то маппится, просто не на то. Отсюда важнейший урок:

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

Корень проблемы. Изначально один промпт просил модель одновременно переводить И строить пословное выравнивание. Слабая модель под нагрузкой (большой батч + двойная когнитивная задача) сваливается в самое дешёвое решение: «русское слово № 1 ↔ английское слово № 1». Позиционно. Структурно валидно. Но по факту мусор.

Решение № 1: разделить перевод и выравнивание на два прохода

Я поставил контролируемый эксперимент на «золотом» наборе трудных предложений с перестановкой слов, на реальном дешёвом Haiku (temperature 0):

Формат промпта

маленький батч

батч 16 (боевой)

Совмещённый (перевод + выравнивание за один проход)

15/19

0/14 — и JSON порвался посреди вывода

Выравнивание по числовым индексам

9/19

худший, отклонён

Только выравнивание (перевод дан отдельно)

19/19

14/14

Вот в этом вся суть ловушки: совмещённый промпт даёт правдоподобные 15/19 на трёх предложениях — именно поэтому он проходил ручные спот‑чеки и «выглядел нормально». А на боевом батче из 16 предложений он падает до 0/14 с позиционным дрейфом и синтаксической ошибкой в JSON.

Разделение на два прохода (сначала перевести {id, src}, потом отдельно выровнять {id, src, tr}) само по себе поднимает батч-16 с 0/14 до 14/14!!!

И это не причуда Haiku. Тот же A/B на OpenRouter (батч 16, 14 золотых пар):

Модель

Совмещённый проход

Два прохода (align‑only)

Gemini 2.5 Flash

сломанный JSON посреди вывода

14/14

DeepSeek V3

4/14 (миссис→briefcase, руку→shaking)

14/14

Haiku

0/14

14/14

Совмещённый проход разваливается у всех дешёвых моделей: Gemini выдаёт битый JSON, DeepSeek и Haiku дрейфуют. А align‑only — 14/14 у всех троих. Разделение проходов — вот что вообще делает дешёвую модель пригодной, независимо от того, какая это дешёвая модель.

Цена разделения: примерно ×2 запросов, но <×2 токенов (перевод — короткий вывод, тяжёлый вывод — это выравнивание). И кэш разнесён по namespace, чтобы «переведённое, но не выровненное» предложение никогда не считалось готовым.

Решение № 2: «считать нельзя, потому что нечего считать»

Даже с пословными чанками ранняя версия просила модель возвращать числовые индексы исходных слов. И модель их теряла: после перестановки фразы все индексы уезжали на единицу. Просить LLM держать точный счёт 0-based индексов через реордеринг — заведомо хрупко.

Решение оказалось элегантным: пусть модель не считает, а копирует. Вместо индекса модель возвращает текст исходного слова, а конвертер уже сам локально сопоставляет текст обратно с индексами (match‑by‑text).

Counting can’t drift because there’s no counting.

Это доросло до контракта v5 «numbered echo»: во вход выравнивания слова подаются пронумерованным списком ("0:Los 1:hábitos 2:son …"), а модель на каждое целевое слово возвращает {"tgt": "<кусок перевода>", "en": ["3:interés", …]} — и индекс, и текст. Текст работает как контрольная сумма для индекса: если модель ошиблась в номере, конвертер это ловит и откатывается на match‑by‑text. Поле называется en независимо от реального языка оригинала. Пустой en ([]) означает вставленное целевое слово без исходного соответствия (артикль, частица) — на этом дальше отдельная история.

A/B нового контракта против старого (16 трудных ES→RU предложений, судья — модель посильнее):

Вариант

Неверных пар слов

Старый промпт, батч 16 (боевой)

13.8% … 29.3% (два прогона!)

Старый промпт, батч 4

18.4%

Новый промпт, батч 16

9.5%

Новый промпт, батч 4

5.4%

Старый промпт, более сильная модель (flash), батч 16

11.2%

Заголовок этой таблицы: новый промпт + маленькие align‑батчи на дешёвой модели (5.4%) обыгрывают более сильную модель на старом промпте (11.2%) — по цене «лайт». А ещё обратите внимание на строку с двумя прогонами: 13.8% и 29.3% на одинаковом входе. Вот почему на дешёвых моделях нельзя полагаться на один удачный прогон — нужен контракт‑с-контрольной‑суммой плюс судья.

Решение № 3: маленькие батчи именно для выравнивания

Перевод отлично идёт батчами по 16. А вот выравнивание — нет: внимание модели по многим длинным элементам выравнивания в большом батче как раз и есть то место, где дешёвые модели дрейфуют. Поэтому дефолт: align-batch = batch/4 (то есть 4, когда основной батч 16). В коде это буквально так и подписано — «small align batches curb positional drift».

Решение № 4: каскады off‑by‑one и вставленные слова

Механику дрейфа удалось локализовать точно: когда в русском появляется слово без испанского соответствия («Вы», «и», «это»), модель норовит примапить его к следующему исходному слову вместо того, чтобы пометить как вставленное — и дальше каждая пара сдвигается на единицу. Хвост предложения уезжает, последние исходные слова осыпаются в пустые чанки. Лечение: явное правило в промпте, что вставленное слово должно иметь en: [] (не потреблять ни одного исходного слова), плюс контрастный пример CORRECT/WRONG с перекрёстным порядком слов.

Решение № 5: LLM‑судья

Раз ни валидация, ни покрытие не ловят смысловые ошибки, единственный надёжный детектор — семантическая проверка другой моделью. Я пробовал ловить дрейф эвристикой «близость к диагонали» — она ловила 2 правильных предложения на каждый реальный дрейф. Не работает.

Как устроен судья (--judge): независимая модель (обязательно посильнее — Haiku слишком слаб, чтобы судить сам себя) читает {src, перевод, карта выравнивания} по каждому предложению и ставит две независимые оценки: alignment_ok (каждое ли слово примаплено к тому, что оно значит, а не позиционно) и translation_ok (верен ли сам перевод).

Что судья нашёл на 60 случайных предложениях уже доставленной книги (Harry Potter, весь Haiku):

  • выравнивание: ~23% с дефектами (14 из 60),

  • перевод: ~12% с дефектами (7 из 60).

Примеры фактических ляпов: mustache → усадьба (модель перепутала mustache с estate), перевёрнутый по смыслу «Beats me how…», обрезанные предложения.

Петля исправления: судья возвращает список сбойных src--invalidate вычищает кэш ровно этих предложений → перегенерация (можно более сильной моделью — про эскалацию ниже) → пересуд → повтор до чистоты. По деньгам это дешевле, чем кажется: тяжёлый выходной ток уже оплачен на этапе выравнивания, а судья читает примерно тот же материал, но эмитит 10–20 токенов. Полный проход «проверить всё» добавляет примерно +30–40% токенов, а не +100%.

Решение № 6: статический детектор дрейфа — словарь вместо токенов

Судья хорош, но он стоит токенов. А позиционный дрейф — ошибка с очень характерным почерком. Можно ли ловить её вообще бесплатно, офлайн? Оказалось, да — обычным двуязычным словарём (--lexcheck).

Словари берутся из OPUS OpenSubtitles v2018 — это данные пословного выравнивания на гигантском корпусе субтитров. Скрипт скачивает .dic‑файлы и ужимает их в компактный формат «слово → топ-12 переводов по частоте» (отбрасывая пары, встретившиеся меньше 3 раз). Все 15 пар языков de/en/es/fr/it/ru → 30 лексиконов (оба направления), суммарно ~25 МБ gzip. В сам .tbook словари, конечно, не попадают — это инструмент конвертера.

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

  • low‑support: словарь «видит» минимум 4 пары, и правдоподобны из них ≤34% — выравнивание в целом не похоже на правду;

  • shift‑pattern — самая красивая часть: ≥2 пар, где фрагмент перевода не подходит своему исходному слову, но подходит его соседу. И этот признак устойчив к многозначности: случайная лакуна словаря не будет систематически совпадать именно с соседним словом.

Морфология — без морфоанализатора, тремя дешёвыми приёмами: словари OPUS хранят поверхностные формы (большинство словоизменений там есть буквально), плюс консервативное префиксное сравнение («цель/целей», «совершенствование/совершенствования» — длинный общий префикс, короткий разъехавшийся хвост), плюс лёгкое срезание окончаний с исходной стороны (metasmeta). Важное свойство: ошибаться «в мягкую сторону» безопасно — более снисходительный матчинг означает меньше флагов дрейфа, не больше.

Пороги не угаданы, а выбраны по данным. Для этого есть отдельный инструмент lexeval, и у него симпатичный трюк: где взять размеченный датасет дрейфа? Сгенерировать. Берём готовую книгу; каждое предложение — предположительно хороший «негатив» (можно ограничиться теми, что одобрил LLM‑судья); а «позитив» синтезируем, сдвинув все маппинги выравнивания на одно исходное слово вправо — это ровно сигнатура позиционного дрейфа (плюс вариант со сдвигом только второй половины — каскад, начавшийся с середины предложения). Детектор обязан флагать сдвинутые копии и не флагать оригиналы. По этой сетке прогоняется перебор порогов.

Результаты на реальной книге: ~87% recall на полных каскадах, ~52% на каскадах с середины предложения, при ~2.4% ложных срабатываний (точность 97%); словарь покрывает 72% пар. Пороги сознательно сдвинуты в пользу точности, а не полноты: каждый флаг уходит на платную переделку, и ложные срабатывания — это буквально потраченные деньги.

Решение № 7: эскалация — и почему сильную модель тоже нужно проверять

Флаги обоих гейтов (lexcheck + судья) стекаются в --escalate-model: всё зафлаганное немедленно переделывается (перевод + выравнивание) более сильной моделью. Тонкость: результат пишется в кэш‑namespace основной дешёвой модели — книга остаётся одним файлом, где ~95% сделано дешёвой моделью и только трудные несколько процентов — сильной.

Но главный урок этой части: более сильная модель — не значит проверенная модель. Поэтому эскалация замкнута в петлю перепроверки: те же гейты прогоняются по эскалированному выводу; что всё ещё флагается — получает ровно одну повторную попытку; а стойкие отказники не подшиваются в книгу молча, а деградируются явно: признанный судьёй неверный перевод отправляется в книгу непереведённым (и пере‑переводится при следующем прогоне), а сбой выравнивания — сырым текстом без подсветок. Список таких предложений пишется в <out>.unverified.json. Принцип: ничто из отвергнутого гейтами не попадает в книгу тихо — без этой петли эскалированная галлюцинация ложится в книгу незамеченной.

И тут всплыл самый контринтуитивный результат всей калибровки. Казалось бы, судьёй надо ставить модель посильнее. Я померил (на вручную вычитанной странице плюс «дрейфовых двойниках», инструмент driftdemo): сильный судья (gemini-2.5-flash) ловит дрейф идеально — 14/14, — но из перфекционизма помечает 36–53% чистого текста за придирки к маппингу. А в двухглавной петле он забраковал 69% собственного эскалированного вывода — эскалация вырождается в массовый fallback: система бесконечно переделывает то, что уже хорошо. Дешёвый же судья — та же модель, что и переводила — ловит 12/14 дрейфов почти без ложных срабатываний. Поэтому дефолтный судья = та же дешёвая модель, а сильный судья остался только для разовых аудитов.

Формулировки промпта судьи оказались столь же хрупкими: промежуточная версия с мягким «флагай только дефекты, видимые читателю» уронила recall дешёвого судьи по дрейфу до нуля — снисходительная формулировка дала слабому судье повод одобрять всё подряд. Правило с тех пор: любое изменение промпта судьи калибруется на дрейфовых двойниках до выката.

Весь боевой конвейер теперь — одна команда:

tools/fetch-lexicons.sh es-ru   # один раз на языковую пару
convert book.epub -o book.tbook --lexcheck --judge --escalate-model google/gemini-2.5-flash

Дешёвая модель переводит и выравнивает всё; словарь (бесплатно) и дешёвый судья помечают подозрительное; сильная модель переделывает только помеченное; переделанное перепроверяется; неисправимое честно деградирует и ретраится в следующем прогоне.

Экономика: почему это всё дёшево

  • Кэш на диске. Каждый результат кэшируется по ключу promptVersion | model | sourceLang | targetLang | src (SHA-256). Повторный запуск делает только то, чего нет в кэше. Так смена контракта выравнивания только выравнивает книгу, не переводя её заново.

  • Глоссарий (--glossary). Один предварительный вызов вытаскивает из книги повторяющиеся ключевые термины и имена собственные с фиксированными переводами, и они дописываются в каждый промпт перевода — чтобы терминология не плясала от батча к батчу. Раз промпт меняется, у кэша появляется суффикс с хешем глоссария, и переводы с глоссарием не смешиваются с обычными.

  • Дефолтная модель конвертера — google/gemini-2.5-flash (быстрая, ~2 с на предложение, корректный JSON). А ещё у меня есть скилы для Claude, и в свободное время я перевожу книжки на базе своей подписки: медленно, зато без затраты токенов, за ночь справляется.

Итог по деньгам: сконвертировать книгу вроде Revelation Space стоит чуть менее $1 на DeepSeek и может попасть в бесплатный лимит Gemma на openrouter (на момент написания статьи), но — 4–7USD за Хайку по токенам.

Часть 3. Факапы

История разработки — это в основном история того, как дешёвые модели (и агенты) находили новые способы всё сломать.

1. Haiku — надёжный переводчик, но ненадёжный эмиттер JSON. На большом прогоне (книга Азимова, 9508 предложений, 584 батча) 150 из 584 результатов (~26%) не распарсились нормально.

2. Агенты жульничали с валидатором. Первый валидатор проверял только, что tgt воспроизводит перевод. Агенты «проходили» его, оставляя en пустым (то есть нулевое выравнивание): один батч вернулся со 100% пустых en, другой — с 85%. Аудит нашёл 6.9% предложений (439 штук) с более чем 60% пустых en, кучкующихся целыми «жульническими» группами агентов. Пришлось делать валидатор строже и орать в промпте капсом, что так нельзя.

3. Мультисловный en как одна строка → тихая потеря подсветки. Модели эмитили "living room", "the large" как одну строку, а резолвер считал строку одним токеном и не находил соответствия — подсветки нет. Таких чанков в одной книге было ~4342. В итоге строку с пробелами бьём на токены. Самый сложный момент, это в текущем TODO, так как с точки зрения смысла это неверно, пока это выглядит хотфиксом но не решением.

4. У судьи был свой баг. Первый полный проход судьи оставил 2122 предложения «непросуженными», потому что слал 64-символьные hex‑ключи кэша в качестве id элементов, а дешёвая модель их теряла. Лечение — короткие id "1","2",… в пределах батча: их дешёвая модель эхо‑воспроизводит надёжно. (Та же тема «большой сложный промпт → хуже для дешёвой модели» заставила и судью работать батчами batch/2.)


Что есть сейчас

  1. Спека: https://tbook.vercel.app/

  2. Go‑конвертер: https://github.com/adubovskoy/tbook_converter

  3. Android‑приложение читалки

  4. Десктоп‑читалка (написана на rust, так как я на linux, она протестирована только под linux)

Всё будет OpenSource под MIT — и спека, и конвертеры. Приложение будет бесплатным и без рекламы. Мне хочется, чтобы начала формироваться библиотека книг: проще обмениваться, чем конвертировать каждый раз заново.

Нужны тестировщики

Сейчас для прохождения валидации в Google Play мне нужны тестировщики приложения. Если хотите помочь — пишите мне сюда или на alex@techlovers.team, пришлю ссылку на тестирование в Google Play. Могу бесплатно для тестеров сконвертировать любую вашу книгу — присылайте, отправлю и ссылку, и книгу.

upd: скоро будет билд для e-ink читалок на андроиде тоже.

Комментарии (16)


  1. starfair
    08.07.2026 10:52

    Отличная идея! Я бы предложил тогда расширить вашу идею, на некое хранилицще где можно было бы меняться уже готовыми и проверенными книгами в вашем формате. Кстати, про художественные тексты это здорово! А как на счет научных, технических и т.п.? Они были бы для здешней аудитории ещё востребованее!


    1. a_dubovskoy Автор
      08.07.2026 10:52

      Технические точно прекрасно получаются, тестировал) Но их я обычно легко читаю в оригинале) Научные - думаю тоже не будет проблем.

      Хранилище - есть вопросики про авторские права... Но я думал сделать портальчик для обмена книгами, сам я буду публиковать туда только свободные к публикации переводы (как делал Илья Франк, например), ну а остальное пусть решает сообщество. )


      1. starfair
        08.07.2026 10:52

        Ну да, с копирайтом могут быть проблемы. Хотя, если что то классическое, то какие проблемы могут быть?
        Про технические - я имел ввиду, иллюстрации, таблицы и все такое - оно нормально переводится и отоброжается?


        1. a_dubovskoy Автор
          08.07.2026 10:52

          иллюстрации и сноски - да. остальное надо тестить. оно близко к fb2/epub по поддерживаемым данным.


  1. AndreyDmitriev
    08.07.2026 10:52

    Делал себе что-то похожее, когда учил французский, на LabVIEW правда. Но у меня были проблемы с восприятием на слух, так что я брал учебные тексты и диалоги с аудио рядом и делал воспроизведение по предложениям или словам с возможностью замедления воспроизведения (есть алгоритмы, позволяющие замедлить, но без изменения высоты тона). Правда с форматом не заморачивался, просто XML с маркерами времени, чтобы звук из wav брать и проигрывать. В принципе похожие программы есть, просто в каждой что-то да не устроило.


  1. hungry_la
    08.07.2026 10:52

    Здравствуйте, хочу помочь с тестированием приложения под android


    1. a_dubovskoy Автор
      08.07.2026 10:52

      пишите на почту или в тележку @adubovskoy, я вышлю инвайт) Специально не выкладываю apk - имхо надежнее поставить с гугла и потом легче получать обновления.


  1. Emelian
    08.07.2026 10:52

    Я был фанатом метода Ильи Франка с 2001 года.

    Был? А, сейчас, уже нет? Почему?

    Не художественный перевод текста на родной язык целиком.

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

    Я это называю методом БКП («Буквальный Контекстный Перевод»). Вот пример, для французского языка:

    Пример метода БКП (буквального контекстного перевода)
    Пример метода БКП (буквального контекстного перевода)

    Что мне нужно как активному читателю, изучающему язык?

    Мгновенность. Ноль ожидания. Тапнул – увидел.

    Ну, если у вас достаточно высокий уровень владения языком, то, да, достаточно и этого варианта. Для ПК, как по мне, отличная вещь – электронный словарь «Lingvo x6». Его полнота – это его огромный плюс! С его помощью я даже обнаружил ошибки в написании диакритических знаков в полной французской грамматике на французском языке (около полутора тысяч страниц в цифровом pdf).

    При этом, применяя метод БКП, можно создавать двуязычные книги, примерно, как у Ильи Франка, но только с возможностью их более удобного редактирования:

    Двуязычная книга – горизонтальное представление
    Двуязычная книга – горизонтальное представление
    Двуязычная книга – вертикальное представление
    Двуязычная книга – вертикальное представление

    Как я успел заметить, что когда уже «под сраку лет» или более, то выучить иностранный язык почти невозможно. По крайней мере, это тяжелее, чем создать обучающую программу (у меня это – https://lecole.free.nf/Prg/Lecole.php ):

    Проверка оригиналов запомненных слов, с озвучкой. Если не знаем правильный ответ – просим подсказку
    Проверка оригиналов запомненных слов, с озвучкой. Если не знаем правильный ответ – просим подсказку
    Проверка переводов запомненных слов, с озвучкой. Если не знаем правильный ответ – просим подсказку
    Проверка переводов запомненных слов, с озвучкой. Если не знаем правильный ответ – просим подсказку

    создать кучу видео с двуязычными субтитрами ( https://lecole.free.nf/video.php и др.), написать «дофейхуа» скриптов для подготовки обучающих данных и готовить разного рода двуязычные книги, вроде учебника французской грамматики, на английском языке:

    Двуязычная книга в html-формате
    Двуязычная книга в html-формате

    В таком случае, самым эффективным оказывается способ создания и освоения двуязычных книг, в частности книг по грамматике, на языке оригинала. В этом случае процесс освоения языка идет «медленно, но, верно»! Причем, как по форме (языковые конструкции), так и по содержанию.


    1. Erop22
      08.07.2026 10:52

      что когда уже «под сраку лет» или более, то выучить иностранный язык почти невозможно

      Като Ломб с вами не согласилась бы. Было бы "зачем", а "как" — найдётся.. ;)


      1. Emelian
        08.07.2026 10:52

        Като Ломб с вами не согласилась бы. Было бы “зачем”, а “как” — найдётся… ;)

        Можете привести примеры? При условии, что «чел» не знал до 40 лет никакого другого языка, кроме родного.

        Я знаю только одну «бабель», лет под 70, которая выучила, на пенсии, Эсперанто и даже ездила на международные конференции по нему.

        А, вот, Н.Ф. Замяткин и Ли Вон Янь, в своей книге: «Вас невозможно научить иностранному языку», придерживаются противоположного мнения.

        Что касается Като Ломб, то она учила свои «16 языков» с детства, так что, для нее изучение иврита, в 80 лет, не кажется таким уж большим подвигом.

        Короче говоря: «Верить никому нельзя! Мне – можно!!!». Создавайте двуязычные книги по методу БКП и, медленно, но верно, язык вы будете осваивать, особенно в зрелом возрасте (молодежи этот метод не понравится).


        1. Erop22
          08.07.2026 10:52

          Можете привести примеры? При условии, что «чел» не знал до 40 лет никакого другого языка, кроме родного.

          Дело, конечно, неблагородное, по единичным примерам судить об общем, но если вы просите… Бабуля подруги, 70+ годков. Жили с мужем в Ярике, оба — преподы в ВУЗов, технические специальности (физика или математика, не помню уже). После смерти мужа и ухудшения здоровья, родные забрали её к себе, в Калифорнию (в Ярике не осталось ни родных, ни близких). До того бабуля — порядочная коммунистка — вжисть на буржуйских языках не шпрехала. Но в штатах сама нашла курсы (она вообще бодренькая духом была), сама записалась, планировала даже обучиться вождению (после языка). Разумеется, native speaker’ом она не стала, но в магазин сходить и объясниться на бытовые темы через несколько месяцев обучилась. И всё это на фоне довольно серьёзной перемены практически вообще всей картины мира — можете предположить её стресс (действие происходило 10+ лет тому). Но тут именно мотивация важна. Бабуля не хотела становиться обузой для родных, ей надо было научиться жить в новых условиях — и она научилась. А вот как раз “просто книжки читать” для большинства людей — не мотивация вообще (особенно в век тотального СДВГ (а теперь ещё и ПТСР), быстрого поверхностного контента и прочего ИИ). А вот как один из инструментов для тех, у кого эта самая мотивация уже есть — вполне годно!

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

          Билингвой, конечно, в возрасте не стать. Но общаться на минималках доступно большинству как будто бы. Но если есть реальная мотивация, а не “хочу знать *ийский, потому что просто так”…

          Могу привести свой пример, отчасти обратный. В школе меня заставили выучить немецкий (настолько хорошо, что я бегло мог общаться с носителями в Германии, когда довелось туда съездить). А потом 25+ лет немецкого не было в моей жизни вообще. В итоге теперь я без труда могу понять, если кто-то рядом говорит на немецком (а не голландском, например), есть память о том, что когда-то я знал значения слов, которые слышу. Но смыслы утеряны навскидку для 95% лексики, которую я точно знал (а это не одна тысяча слов была, учили нас очень хорошо).

          Язык — это такой же навык, как и ходить, например. Тренируешь — развивается, не тренируешь — пропадает. Научиться паркурить в 70 конечно сложнее, чем в 16, но прокачивать скиллы не поздно никогда (пока не сыграл в ящик, конечно). Имхо, Като Ломб (да и многие другие) как раз об этом писала (в том числе)…


          1. Emelian
            08.07.2026 10:52

            если вы просите

            Спасибо за реальные истории – очень интересно.

            Да, полное погружение в среду носителей, эффект, безусловно, дает. Я знаю случаи, когда в лихие 90-тые, не только дочки, но и их одинокие мамы, выходили замуж за «буржуев». И если у «дочек» особых проблем с языком не возникало, то у их «мам» это было головной болью еще той. Одна из них рассказывала (когда приезжала в гости к своим родителям), что она ревела навзрыд, так как ни с кем не могла объясниться. Похоже, что со своим иностранным мужем она общалась на языке жестов. Тем не менее, при очередной нашей встрече (мы, почему-то, всегда случайно встречались в городе, когда она, раз в год, навещала своих родителей) она уже явно чувствовала себя уверенней, по части знания английского. Я его тоже учил, даже приходилось общаться с одним американцем, который приезжал к нам в город, к своим русским «невестам». Однако, она осваивала язык явно быстрее, чем я.

            Естественно, что в молодые годы, когда тебя обучают как спортсмена на тренировках, это дает очень хороший эффект, как показывает ваш случай. Я могу привести свой пример по учебе. Закончил Политех и получил распределение во Всесоюзный НИИ. Там я занимался проектными работами, а хотелось – наукой. Поэтому, поступил на дневное обучение, на мехмат (математика) в МГУ. Когда окончил Университет, СССР уже начал постепенно разваливаться. Тем не менее, карьера у меня начала складываться очень удачно. Однако, медленно, но верно, «лафа» закончилась, уровень жизни стал падать, а наука перестала быть востребованной. Поэтому, самостоятельно освоил программирование, написал и внедрил учетную систему на хорошем производственном предприятии, что и кормило меня двадцать лет, пока мою любимую фирму не закрыли по политическим мотивам.

            Так вот, если все знания по первой (технической) специальности у меня выветрились абсолютно, то, по математике я «кое-что» помню до сих пор, хотя я уже забыл, когда последний раз ею занимался. Ибо «натаскивали» нас в ней, как тех же спортсменов в профессиональном спорте. Похоже, как у вас – с немецким.

            И изучение языков – неплохой способ отсрочить приход Альцгеймера. Не единственный, но вполне действенный, есть довольно много исследований на эту тему.

            Я бы даже сказал больше. Изучение языка, в зрелом возрасте, продлевает жизнь и уменьшает количество «болячек». Так что, это тоже, своего рода, стимул.

            Однако, лично у меня, стимул, все же, немного другой. Мне нужен уровень, чтобы я «смог» его выучить в должной мере. Примерно, как в анекдоте: «Англичанин выглядывает в окно, видит – туман. Сегодня смог, говорит он. «– Поздравляю, сэр!», отвечает дворецкий.» :) .

            Чтобы его освоить я написал несколько версий обучающей программы, сейчас, думаю об очередной. Параллельно делаю двуязычные субтитры к оригинальным видео. Но, вот только создание двуязычной книги (по методу БКП) для французской грамматики на французском языке, дает реальный выхлоп. Да, это тяжело, трудно и медленно, но, я вижу эффект и это меня стимулирует на дальнейшие эксперименты…


  1. Pavel_nobranch
    08.07.2026 10:52

    23 года назад написал на делфи7 прогу которая озвучивала с помощью text-to-speech английскую часть текста в таких русско английских текстах. написал илье франку, он ответил я подумаю и пропал). тексты готовил сам другой прогой в котрой в левом окне был англ текст, в правом русский и я кликом мышки их совмещал(прога автоматизировала копирование, вставку скобок, окраску текста). и в том же 2003 году сумел продать две таких книги по 70р за штуку, программу распространял бесплатно. пожелаю удачи вам в вашем трудном деле.


  1. farengeyt451
    08.07.2026 10:52

    Вчера наткнулся на вот такой инструмент https://github.com/kerivin/bilingual-book-builder. Причем он делает epub, который отлично работает на всех устройствах, в том числе электронных книгах.


    1. a_dubovskoy Автор
      08.07.2026 10:52

      Ооо, какой восторг. Я увидел там несколько прекрасных идей в обработке. спасибо!


    1. a_dubovskoy Автор
      08.07.2026 10:52

      Так, посмотрел детальнее. Они используют aligner на уровне предложений, не на уровне слов. Мне не сильно он поможет. Ну и понятно, что запекая в отдельный формат, удобство будет побольше (нет проблем собрать альтернативную читалку tbook, где он будет в 2 столбца отображать, или переключать языки, например).

      Но, одну очень шикарную идею этот репозиторий подбросил, можно сильно сэкономить на токенах с LaBSE (~1.8 GB). Пока тестирую, но, кажется, я могу конвертировать книжки в 2 раза быстрее :) скоро выложу на гитхаб в конвертере.