Перейти к содержимому
Эльдар Шахвалиев
Обсудить проект
← Все проекты
202608.2025 — presentВ продес нуля

fancai — читалка книг со справочником без спойлеров и иллюстрациями к сценам

Собственный продукт · Чтение / EdTech / потребительское веб-приложение (PWA)

Автор продукта и единственный разработчик (full-stack: backend, frontend, конвейер обработки, инфраструктура, деплой)

Собственный продукт, сделанный в одиночку — от идеи до сервера. Работает на 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):

  1. Загрузка книги (EPUB/FB2) → парсинг в главы (book_parser.py, ebooklib/lxml/BeautifulSoup).
  2. Глава режется на чанки (~100K символов, ~15% overlap для устойчивости на границах).
  3. Чанк уходит в LLM через единый клиент-провайдер → извлекаются сущности (персонажи/локации/объекты), описания для картинок и связи между сущностями.
  4. Дедупликация (entity_deduplication_service.py, fuzzy-matching для русских имён, порог 0.75; рекурсивный batched-reduce, BATCH_SIZE=50/MAX_DEPTH=2 для 500+ сущностей) и синтез биографий (entity_synthesis_service.py); консистентность — consistency_manager.py.
  5. Спойлер-безопасность: сущность хранит first_mention_chapter/first_mention_cfi (models/entity.py), UI показывает информацию только до текущей главы читателя; фильтрация покрыта property-based тестами (hypothesis).
  6. Иллюстрации: описание → при необходимости перевод 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-flash primary + -flash-lite fallback; изображения — 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-ФЗ/ПДн