Реализация мультимодального AI-ввода (текст + документ) в мобильном приложении
Пользователь открывает мобильное приложение, прикрепляет PDF контракта и спрашивает: «На каком сроке расторгается договор?» Задача выглядит простой, но между file_picker и осмысленным ответом модели — десяток нетривиальных решений.
Проблема номер один: как передать документ в LLM
Большинство LLM принимают текст, а не PDF. Значит нужна конвертация. Варианты:
Прямой upload через Files API. OpenAI Assistants API и Gemini Files API принимают PDF, DOCX, TXT напрямую. Для мобильного приложения это самый чистый путь: загружаем файл, получаем file_id, вставляем в messages[]. Но есть ограничения — у OpenAI лимит 512 МБ на файл и 100 файлов на assistant, и Files API привязан к Assistants/Batch, не к Chat Completions.
Извлечение текста на клиенте. Для PDF на Android — PdfRenderer (встроен с API 21) для рендеринга страниц в Bitmap + OCR через ML Kit TextRecognizer, или Apache PDFBox порт. На iOS — PDFKit + PDFPage.string для машинописного PDF; для сканов — Vision framework с VNRecognizeTextRequest. Текст уходит в content[] как строка.
Проблема сканированных документов. PDFKit.string вернёт пустую строку для PDF из отсканированных страниц — там нет текстового слоя. ML Kit TextRecognizer справляется, но нужно рендерить каждую страницу в Bitmap/CGImage и прогонять через OCR. Для 50-страничного документа это 2–5 секунд на устройстве.
Извлечение текста: подводные камни
На Android PdfRenderer требует ParcelFileDescriptor с флагом MODE_READ_ONLY. Если файл пришёл через content:// URI от FileProvider, нужен contentResolver.openFileDescriptor(). Прямой File() от content:// бросает FileNotFoundException — распространённая ошибка у тех, кто не работал с SAF (Storage Access Framework).
Многостраничные документы нужно обрабатывать постранично, не грузя всё в память сразу. PdfRenderer.Page нужно закрывать после каждой страницы — page.close() обязателен, иначе IllegalStateException на следующей итерации.
На iOS PDFDocument(url:) может вернуть nil для зашифрованных PDF. Обрабатывайте isEncrypted и запрашивайте пароль через UI, а не крашитесь молча.
Архитектурное решение для больших документов
Полный текст 100-страничного договора не влезет в контекстное окно большинства моделей — или влезет, но дорого. Правильный путь для объёмных документов — RAG: разбиваем на чанки по 500–1000 токенов с перекрытием 50–100 токенов, индексируем в векторную БД, при запросе ищем топ-5 релевантных чанков и только их передаём в context.
Для мобильного приложения это обычно означает серверную обработку: клиент загружает файл на бэкенд, бэкенд занимается чанкингом и эмбеддингами. На клиенте остаётся только UI запроса и рендеринг ответа. Реализовывать векторный поиск прямо на телефоне имеет смысл только для оффлайн-сценариев (см. услугу по ChromaDB/Weaviate интеграции).
Форматы и лимиты
| Формат | Android | iOS | Лимит API (OpenAI) |
|---|---|---|---|
| PDF (текст) | PdfRenderer + PDFBox | PDFKit | 512 МБ |
| PDF (скан) | ML Kit OCR | Vision VNRecognizeTextRequest | — (нужна предобработка) |
| DOCX | Apache POI (Java) | — | 512 МБ (через Files API) |
| TXT / MD | Нативно | Нативно | Без ограничений |
| XLSX | Apache POI | — | 512 МБ |
DOCX на iOS без сторонних библиотек — боль. Либо серверная конвертация (LibreOffice headless), либо ограничиваете поддержку форматов PDF + TXT для мобильного клиента.
Процесс работы
Аудит форматов документов в вашем продукте → выбор стратегии (Files API vs клиентская экстракция vs RAG) → реализация загрузки файлов (file_picker, SAF, UIDocumentPickerViewController) → конвертация и очистка текста → интеграция с LLM → индикаторы прогресса для долгих операций → тестирование на реальных документах разного качества.
Сроки: базовая поддержка PDF + TXT с прямой передачей — 1–2 недели. Полноценный pipeline с OCR, несколькими форматами и RAG для больших документов — 4–6 недель.







