fancai — читалка книг со справочником без спойлеров и иллюстрациями к сценам
Собственный продукт · Чтение / EdTech / потребительское веб-приложение (PWA)
Собственный продукт, сделанный в одиночку — от идеи до сервера. Работает на fancai.ru; первая версия прошла приёмку по всем 52 требованиям.
- Python
- FastAPI
- Celery
- PostgreSQL
- pgvector
- Redis
- SQLAlchemy
- Alembic
- TypeScript
- React
- Vite
- Tailwind CSS
- TanStack Query
- Zustand
- epub.js
- IndexedDB
- PWA
- Docker
- docker-compose
- Caddy/Nginx
- VPS
- REST API
- WebSocket
- OpenRouter/платежи
- Gemini
- Vertex AI
- FLUX.2
- pytest
- Playwright
- Vitest
В цифрах
- ~40 390 строк Python (backend/app)
- объём backend-кода (прямой подсчёт)
- ~70 571 строка TS/TSX в 343 файлах (frontend/src)
- объём frontend-кода (прямой подсчёт)
- 28 router-файлов, 97 routes + 1 websocket
- поверхность API (routes — по README)
- 38 сервисов, 20 моделей, 54 Alembic-миграции, 84 backend-теста
- масштаб backend (прямой подсчёт)
- v1.0: 9 фаз, 23 плана, 52 требования, UAT 10/10, ~9 дней
- первый production-релиз
Ключевое
- Справочник по книге строится из самого её текста: приложение находит персонажей, места и предметы, склеивает разные написания одного имени и составляет биографию.
- Спойлер-защита сделана частью модели данных, а не фильтром в интерфейсе: карточка героя показывает только то, что встречалось до главы, на которой читатель остановился.
- Мобильное чтение доведено до привычного по книжным приложениям уровня: листание пальцем, полноэкранный режим на iPhone, чтение без интернета.
- Сервис развёрнут и эксплуатируется мной: сертификаты, мониторинг, резервные копии базы, дежурство по сбоям внешних сервисов.
- Два подхода к обработке текста проверены на замерах и отброшены — вместе с уже написанным кодом. Отказ от вложенного труда тоже часть работы.
Задача
В длинной книге с десятками персонажей легко потерять нить: кто этот человек, где он встречался раньше, чем связан с остальными. Обычные фан-вики отвечают на такой вопрос, но заодно рассказывают, чем всё кончится, — открыв карточку героя на третьей главе, читатель узнаёт его судьбу.
Вторая нехватка — визуальная: текст описывает сцену словами, но показать её не может.
Что сделано
- Сделал продукт один и целиком: идея, серверная и клиентская части, конвейер обработки книг, сервер, запуск и эксплуатация.
- Построил справочник, который собирается из самого текста книги: приложение находит персонажей, места и предметы, склеивает разные написания одного имени в одну карточку и составляет биографию по тому, что встретилось.
- Сделал спойлер-защиту частью модели данных, а не фильтром в интерфейсе: у каждой карточки хранится глава, где герой впервые упомянут, и показывается только то, что было до главы читателя. На это написаны тесты, которые проверяют правило на случайных данных, а не на паре примеров.
- Довёл мобильное чтение до уровня, к которому приучили книжные приложения: листание пальцем с инерцией, полноэкранный режим на iPhone, чтение без интернета, установка как приложение.
- Развернул и веду сервис сам: автоматические сертификаты, мониторинг, резервные копии базы, защита от типовых атак и устойчивость к сбоям внешних сервисов.
Итог
Сервис работает на fancai.ru: книги загружаются, справочник строится, иллюстрации к сценам генерируются. Первая версия прошла приёмку по всем 52 требованиям и по всем 10 сценариям приёмочных тестов. Мобильная версия, чтение без интернета и установка как приложение — в проде с марта 2026.
Продукт остаётся моим личным полигоном: на нём я проверяю решения, которые потом несу в заказную работу.
Технические детали
Решение и подход
Пайплайн «книга → текст → сущности/сцены → иллюстрации → читалка» (по docs/architecture/ai-pipeline.md и коду backend/app/services):
- Загрузка книги (EPUB/FB2) → парсинг в главы (
book_parser.py, ebooklib/lxml/BeautifulSoup). - Глава режется на чанки (~100K символов, ~15% overlap для устойчивости на границах).
- Чанк уходит в LLM через единый клиент-провайдер → извлекаются сущности (персонажи/локации/объекты), описания для картинок и связи между сущностями.
- Дедупликация (
entity_deduplication_service.py, fuzzy-matching для русских имён, порог 0.75; рекурсивный batched-reduce, BATCH_SIZE=50/MAX_DEPTH=2 для 500+ сущностей) и синтез биографий (entity_synthesis_service.py); консистентность —consistency_manager.py. - Спойлер-безопасность: сущность хранит
first_mention_chapter/first_mention_cfi(models/entity.py), UI показывает информацию только до текущей главы читателя; фильтрация покрыта property-based тестами (hypothesis). - Иллюстрации: описание → при необходимости перевод RU→EN → генерация изображения; Celery-задача
generate_image_task, отдельный circuit breaker, чтобы сбои LLM не блокировали генерацию картинок.
Архитектура сервисов (prod-compose, 8 сервисов): caddy (reverse proxy/TLS/HTTP-3) → frontend (статика) + backend (FastAPI/Gunicorn) → celery-worker + celery-beat (очереди book: soft-limit 3ч, image: 300с) → postgres (pgvector) + redis (кэш/pubsub/Celery-broker/JWT-blacklist) + pgbackup. Связь фронта с бэком — REST + WebSocket (прогресс обработки книг в реальном времени).
Читалка (нетривиальные места, из CHANGELOG): epub.js рендерит EPUB через CFI (не номера страниц), три системы подсветки (descriptions/entities/annotations) координируются через TreeWalker skip-фильтры; жесты — единый FSM-контроллер (заменил 3 параллельные системы), follow-finger свайпы со spring-физикой 60fps, полноэкранный iOS-overlay (обход бага: iOS Safari не доставляет touch-события в iframe); оффлайн — кэш глав в IndexedDB (Dexie) + PWA с graduated resume.
Стек и обоснование
- Backend: Python 3.12 + FastAPI (Pydantic v2, строгие типы), SQLAlchemy 2 + PostgreSQL 17/pgvector (векторные эмбеддинги глав в наработках), Redis 7.4 + Celery 5.6 (тяжёлая обработка книг асинхронно), Alembic, пакет-менеджер uv. Resilience — tenacity (экспоненциальный backoff) + circuitbreaker для внешних AI-API.
- Frontend: React 19 + TypeScript 5.7 + Vite 8, Tailwind 4 + Radix + Vaul, TanStack Query (серверное состояние) + Zustand (клиентское), epub.js (CFI-рендеринг), Dexie/IndexedDB (оффлайн), vite-plugin-pwa/Workbox. Тесты — Vitest + Playwright.
- Инфраструктура: Caddy 2.11 (выбран вместо nginx — 748 строк конфига → ~80, auto-HTTPS, HTTP/3), Docker Compose (dev/prod/monitoring профили), бэкап-контейнер БД.
- AI (продукт): единый клиент через OpenRouter (LLM —
google/gemini-2.5-flashprimary +-flash-litefallback; изображения —black-forest-labs/flux.2-klein-4b). Обоснование (Key Decisions): один провайдер с fallback-цепочкой проще и дешевле, чем зоопарк SDK. - Мониторинг (активный compose): Netdata + VictoriaMetrics + Uptime-Kuma + Dozzle + Flower, error-tracking — Hawk. (В
monitoring/остались артефакты Grafana/Prometheus от ранних экспериментов — в активный compose не подключены.)
Инженерные вызовы
- Спойлер-безопасность как корректность, а не UI-фильтр. Нужно гарантировать, что ни одна карточка не «протечёт» в будущее сюжета. Решение — модель данных с первым упоминанием (глава/CFI) + property-based тесты (hypothesis) на инварианты фильтрации.
- Качество извлечения на русском. Дедупликация имён («Гарри» → «Гарри Поттер»), синтез биографий без противоречий, устойчивость к границам чанков: fuzzy-matching с порогом 0.75, token-overlap-эвристики, рекурсивный batched-reduce для книг с 500+ сущностями.
- Стоимость/надёжность LLM. Дорогой и нестабильный внешний AI потребовал circuit breaker'ов (раздельные для LLM и изображений), tenacity-ретраев, литерального кэша ответов в Redis, классификатора ошибок (5 типов) и структурированного per-chapter логирования.
- Серия дорогих разворотов с дисциплиной отказа. Self-hosted NLP → Modal (vLLM Qwen3.5): на staging реальный батч занял 40+ минут вместо ожидаемых 7–8 — Modal отменён, наработки списаны; курс возвращён на оптимизацию OpenRouter, затем — на флаговую миграцию к Gemini Direct/Vertex. Зрелое решение «не тащить sunk cost».
- Мобильный ридер на iOS. Корневая причина (STATE.md): iOS Safari не доставляет touch-события в iframe
contentDocument— потребовался полноэкранный overlay с FSM-жестами, shared-FSM через dependency injection (дедупликация ~454 строк), координация трёх систем подсветки через TreeWalker, follow-finger физика 60fps. UAT на физическом iPhone 15 Pro (8/8 проверок). - Production-инцидент-готовность. Token blacklist для безопасного logout, бэкапы БД, пакет аварийной миграции сервера (recon/план/runbook, RTO ≤ 4ч) как страховка.
Услуги в проекте
- разработка
- деплой
- devops
- мониторинг
- аудит
- 152-ФЗ/ПДн