Compare commits
3 Commits
0b12e4583b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d212e1a9a | |||
| 88a6204f1b | |||
| e871476868 |
@@ -0,0 +1,94 @@
|
|||||||
|
# Локальный OCR-сервис Xiaomi-OCR-0
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
На этой машине развёрнута и работает локальная OCR-модель Xiaomi-OCR-0 (SeerRay-Lab/Xiaomi-OCR-0). Это vision-language модель на 0.8B параметров (основа — Qwen3.5-0.8B, архитектура Qwen3_5ForConditionalGeneration). Она умеет:
|
||||||
|
|
||||||
|
- Разбор документов → Markdown (таблицы в OTSL → HTML, формулы в LaTeX, порядок чтения).
|
||||||
|
- KIE (извлечение ключевой информации) → JSON по заданной схеме полей.
|
||||||
|
- VQA — короткие ответы на вопросы по изображению.
|
||||||
|
- Разбор отдельных регионов: текст, таблица, формула.
|
||||||
|
- Сканы, чеки, формулы (в т.ч. рукописные), рукописный текст, каллиграфию, PDF (через рендер страниц).
|
||||||
|
- Всё работает только локально, без облачных API. Ключ API не нужен.
|
||||||
|
|
||||||
|
## Окружение
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| ОС | Ubuntu 26.04, Linux x86_64 |
|
||||||
|
| GPU | NVIDIA RTX 4060 Laptop, 8 ГБ VRAM (vLLM занимает ~75%) |
|
||||||
|
| Каталог проекта | `/home/alexives/OCR Xiaomi neuro` (в пути есть пробел — всегда бери его в кавычки) |
|
||||||
|
| Python venv | `/home/alexives/OCR Xiaomi neuro/.venv` → симлинк на `/mnt/data/alexives_cache/xiaomi_ocr_venv` (Python 3.12) |
|
||||||
|
| Кэш HF (веса модели) | `/mnt/data/alexives_cache/huggingface` |
|
||||||
|
| Движок инференса | vLLM 0.31.0, CUDA 13 |
|
||||||
|
| Обязательная переменная окружения | `VLLM_USE_FLASHINFER_SAMPLER=0` (без неё vLLM падает: нет системного nvcc/CUDA toolkit для JIT FlashInfer) |
|
||||||
|
|
||||||
|
> На системном разделе `/` мало места (~3.7 ГБ). Любые тяжёлые кэши и зависимости клади в `/mnt/data/alexives_cache/`.
|
||||||
|
|
||||||
|
## Запуск и остановка
|
||||||
|
```bash
|
||||||
|
"/home/alexives/OCR Xiaomi neuro/start.sh" # запускает vLLM (:8000) и веб-демо (:8787), ждёт готовности
|
||||||
|
"/home/alexives/OCR Xiaomi neuro/stop.sh" # останавливает оба процесса
|
||||||
|
```
|
||||||
|
Холодный старт vLLM занимает ~2 минуты (torch.compile + захват графов CUDA). Перед запросами опрашивай `GET /v1/models`, пока не придёт ответ.
|
||||||
|
Логи: `vllm.log` и `demo.log` в каталоге проекта.
|
||||||
|
|
||||||
|
## Сервисы
|
||||||
|
| Сервис | URL | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| vLLM (OpenAI-совместимый API) | `http://127.0.0.1:8000/v1` | Прямой инференс. Основная точка интеграции. |
|
||||||
|
| Веб-демо + REST | `http://127.0.0.1:8787` | UI в браузере и упрощённый REST с готовой пред- и постобработкой |
|
||||||
|
|
||||||
|
- Имя модели в запросах: `SeerRay-Lab/Xiaomi-OCR-0` (именно так, с префиксом).
|
||||||
|
- Проверка готовности: `curl -s http://127.0.0.1:8000/v1/models` и `curl -s http://127.0.0.1:8787/api/health` (`"ready": true`).
|
||||||
|
- Сервисы слушают только loopback (`127.0.0.1`). Не открывай их наружу.
|
||||||
|
|
||||||
|
## Вариант A: прямой вызов vLLM (рекомендуется)
|
||||||
|
|
||||||
|
### Обязательные правила запроса
|
||||||
|
1. Ровно одно изображение на запрос, сначала изображение, потом текст, в одном user-сообщении.
|
||||||
|
2. Изображение передаётся как data-URL: `data:image/png;base64,...`.
|
||||||
|
3. Thinking-режим выключен: `"chat_template_kwargs": {"enable_thinking": false}`.
|
||||||
|
4. Жадное декодирование (как в эталонном пайплайне):
|
||||||
|
`"temperature": 0, "top_p": 0.8, "presence_penalty": 0.0, "repetition_penalty": 1.0`
|
||||||
|
5. `max_tokens`: 4096 по умолчанию для целой страницы (максимум 16384, но картинка + промпт + вывод должны влезть в контекст 16384).
|
||||||
|
6. Если `finish_reason == "length"` — вывод обрезан: увеличь `max_tokens` или уменьши картинку / раздели её на части.
|
||||||
|
7. Без системного сообщения. Промпты — на английском, дословно.
|
||||||
|
|
||||||
|
### Промпты (дословно)
|
||||||
|
- **Целая страница документа → Markdown:**
|
||||||
|
`Extract all information from the main body of the document image and represent it in markdown format, ignoring headers and footers. Tables should be expressed in OTSL format, formulas in the document should be represented using LATEX format, and the parsing should be organized according to the reading order.`
|
||||||
|
- **Регион с текстом / заголовком:**
|
||||||
|
`Extract the text in the image.`
|
||||||
|
- **Регион с таблицей:**
|
||||||
|
`Parse the table in the image into OTSL.`
|
||||||
|
- **Регион с формулой:**
|
||||||
|
`Identify the formula in the image and represent it using LATEX format.`
|
||||||
|
- **KIE с известными полями:**
|
||||||
|
`Extract key information in the image`
|
||||||
|
`Please output the key information in JSON format according to the following schema:`
|
||||||
|
`{"company": "", "date": "", "total": ""}`
|
||||||
|
- **KIE без заданных полей:**
|
||||||
|
`Extract the key information from the document image as a JSON object. Use concise field names. Return JSON only; use an empty string for missing values.`
|
||||||
|
- **VQA:**
|
||||||
|
`<ВОПРОС>
|
||||||
|
Answer the question using a single word or phrase.`
|
||||||
|
|
||||||
|
### Постобработка
|
||||||
|
Таблицы приходят в OTSL (токены `<fcel>`, `<ecel>`, `<lcel>`, `<ucel>`, `<xcel>`, `<nl>`). Конвертируй в HTML:
|
||||||
|
```python
|
||||||
|
import sys; sys.path.insert(0, "/home/alexives/OCR Xiaomi neuro")
|
||||||
|
from postprocess.otsl import convert_otsl_to_html
|
||||||
|
from postprocess.document import finalize_document
|
||||||
|
md = finalize_document(raw_model_text)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Вариант B: REST веб-демо (:8787)
|
||||||
|
POST `http://127.0.0.1:8787/api/ocr`, `Content-Type: application/json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"task": "document",
|
||||||
|
"image_base64": "<base64 или data:image/...;base64,...>",
|
||||||
|
"image_mime": "image/jpeg",
|
||||||
|
"max_tokens": 4096,
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
File diff suppressed because one or more lines are too long
+2078
-2692
File diff suppressed because one or more lines are too long
@@ -73937,29 +73937,29 @@
|
|||||||
"state_destructive": "Перемена места и рождение, которые путают с вечным переездом.",
|
"state_destructive": "Перемена места и рождение, которые путают с вечным переездом.",
|
||||||
"culture_sumer_light": "",
|
"culture_sumer_light": "",
|
||||||
"culture_sumer_shadow": "",
|
"culture_sumer_shadow": "",
|
||||||
"culture_egypt_light": "",
|
"culture_egypt_light": "[Дж. Купер · «Энциклопедия символов»]: «У египтян аист олицетворял сыновнюю почтительность, так как считалось, что он кормит своих родителей в их старости.»",
|
||||||
"culture_egypt_shadow": "",
|
"culture_egypt_shadow": "",
|
||||||
"culture_kabbalah_light": "",
|
"culture_kabbalah_light": "",
|
||||||
"culture_kabbalah_shadow": "",
|
"culture_kabbalah_shadow": "",
|
||||||
"culture_antiquity_light": "[Джек Тресиддер · «Словарь символов»]: «На Востоке - популярная эмблема долголетия, а в даосизме - бессмертия. Во многих культурах аист символизирует дочернюю и сыновнюю преданность из-за поверья, что аисты кормят своих родителей, как и своих детей. Эта трогательная забота и ассоциация с новой жизнью (аисты прилетают весной) явились причиной того, что аист стал священной птицей древнегреческой богини Геры (в римской...»",
|
"culture_antiquity_light": "[Джек Тресиддер · «Словарь символов»]: «На Востоке - популярная эмблема долголетия, а в даосизме - бессмертия. Во многих культурах аист символизирует дочернюю и сыновнюю преданность из-за поверья, что аисты кормят своих родителей, как и своих детей. Эта трогательная забота и ассоциация с новой жизнью (аисты прилетают весной) явились причиной того, что аист стал священной птицей древнегреческой богини Геры (в римской...»\n\n[Дж. Купер · «Энциклопедия символов»]: «В греческих мистериях богиня аистов изображалась в виде женщины — подательницы жизни, кормилицы и являлась атрибутом Геры. У римлян аист символизировал почтительность, сыновнюю привязанность и считался атрибутом Юноны.»",
|
||||||
"culture_antiquity_shadow": "",
|
"culture_antiquity_shadow": "",
|
||||||
"culture_celtic_light": "",
|
"culture_celtic_light": "",
|
||||||
"culture_celtic_shadow": "",
|
"culture_celtic_shadow": "",
|
||||||
"culture_nordic_light": "",
|
"culture_nordic_light": "",
|
||||||
"culture_nordic_shadow": "",
|
"culture_nordic_shadow": "",
|
||||||
"culture_slavic_light": "",
|
"culture_slavic_light": "[Дж. Купер · «Энциклопедия символов»]: «В славянских поверьях и христианской традиции аист символизирует целомудрие, чистоту, благочестие, покровительство семейному очагу и наступление весны.»",
|
||||||
"culture_slavic_shadow": "",
|
"culture_slavic_shadow": "",
|
||||||
"culture_india_light": "",
|
"culture_india_light": "",
|
||||||
"culture_india_shadow": "",
|
"culture_india_shadow": "",
|
||||||
"culture_persia_light": "",
|
"culture_persia_light": "",
|
||||||
"culture_persia_shadow": "",
|
"culture_persia_shadow": "",
|
||||||
"culture_china_light": "",
|
"culture_china_light": "[Дж. Купер · «Энциклопедия символов»]: «У китайцев аист означает долгожительство, счастливую и умиротворенную старость, сыновнюю почтительность достойного, чуждого соблазнов, живущего в уединении отшельника.»",
|
||||||
"culture_china_shadow": "",
|
"culture_china_shadow": "",
|
||||||
"culture_japan_light": "",
|
"culture_japan_light": "",
|
||||||
"culture_japan_shadow": "",
|
"culture_japan_shadow": "",
|
||||||
"culture_islam_light": "",
|
"culture_islam_light": "",
|
||||||
"culture_islam_shadow": "",
|
"culture_islam_shadow": "",
|
||||||
"culture_hermetic_light": "",
|
"culture_hermetic_light": "[Дж. Купер · «Энциклопедия символов»]: «Наряду с орлом и ибисом, аист уничтожает зловещих рептилий и является потому солнечной птицей. Но, как существо, связанное с водой и рыбой, он ассоциируется с водами творения. \"Приносимые аистом\" дети в виде эмбрионов находятся в лоне Матери-Земли и в водах творения, где их и находят при ловле рыбы аисты. Символизирует приход весны и новой жизни, благовещенская птица.»",
|
||||||
"culture_hermetic_shadow": "",
|
"culture_hermetic_shadow": "",
|
||||||
"culture_mesoamerica_light": "",
|
"culture_mesoamerica_light": "",
|
||||||
"culture_mesoamerica_shadow": "",
|
"culture_mesoamerica_shadow": "",
|
||||||
@@ -73973,7 +73973,7 @@
|
|||||||
"will_vector": "Ты несёшь новое в гнездо или только меняешь крышу?",
|
"will_vector": "Ты несёшь новое в гнездо или только меняешь крышу?",
|
||||||
"shadow_warning": "Перемена места и рождение, которые путают с вечным переездом."
|
"shadow_warning": "Перемена места и рождение, которые путают с вечным переездом."
|
||||||
},
|
},
|
||||||
"filled_count": 14,
|
"filled_count": 18,
|
||||||
"is_complete": false,
|
"is_complete": false,
|
||||||
"updated_at": "2026-10-08T13:39:29.998946",
|
"updated_at": "2026-10-08T13:39:29.998946",
|
||||||
"illustrations": []
|
"illustrations": []
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class CrossSystemComparisonScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CrossSystemComparisonScreenState extends State<CrossSystemComparisonScreen> {
|
class _CrossSystemComparisonScreenState extends State<CrossSystemComparisonScreen> {
|
||||||
|
final _repo = WordsRepository();
|
||||||
late SyncreticSession _session;
|
late SyncreticSession _session;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -152,6 +153,35 @@ class _CrossSystemComparisonScreenState extends State<CrossSystemComparisonScree
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _confirmDeleteReading(SyncreticReading reading) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
backgroundColor: const Color(0xFF151829),
|
||||||
|
title: const Text('Удалить расклад?', style: TextStyle(color: Color(0xFFFFF9C4), fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
content: Text('Удалить «${reading.systemName} (${reading.layoutTitle})» из текущей сессии дня?', style: const TextStyle(color: Color(0xFFECEFF1), fontSize: 13)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Отмена', style: TextStyle(color: Color(0xFF8C93B8))),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFEF5350), foregroundColor: Colors.white),
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
await _repo.removeReadingFromSyncreticSession(reading.id);
|
||||||
|
setState(() {
|
||||||
|
_session = _repo.currentSyncreticSession;
|
||||||
|
});
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
|
},
|
||||||
|
child: const Text('Удалить'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final readings = _session.readings;
|
final readings = _session.readings;
|
||||||
@@ -332,6 +362,11 @@ class _CrossSystemComparisonScreenState extends State<CrossSystemComparisonScree
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close_rounded, size: 20, color: Color(0xFFEF5350)),
|
||||||
|
tooltip: 'Удалить из сессии',
|
||||||
|
onPressed: () => _confirmDeleteReading(reading),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -415,22 +415,6 @@ class _DivinationHubScreenState extends State<DivinationHubScreen> {
|
|||||||
// Section 1: Core 999 Systems
|
// Section 1: Core 999 Systems
|
||||||
_buildSectionHeader('СИСТЕМЫ ОРАКУЛА 999', Icons.auto_awesome_rounded),
|
_buildSectionHeader('СИСТЕМЫ ОРАКУЛА 999', Icons.auto_awesome_rounded),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildSystemCard(
|
|
||||||
title: 'Слово Дня',
|
|
||||||
instructionId: 'word_of_day',
|
|
||||||
subtitle: 'Календарь & Квест',
|
|
||||||
icon: '☀️',
|
|
||||||
desc: 'Ежедневный архетип, коан для ума, микродействие и дневник',
|
|
||||||
accentColor: const Color(0xFFFFD54F),
|
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const WordOfTheDayScreen())).then((_) => setState(() {})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -715,6 +715,13 @@ class WordsRepository {
|
|||||||
await saveSyncreticSession(updatedSession);
|
await saveSyncreticSession(updatedSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> removeReadingFromSyncreticSession(String readingId) async {
|
||||||
|
final current = currentSyncreticSession;
|
||||||
|
final updatedReadings = current.readings.where((r) => r.id != readingId).toList();
|
||||||
|
final updatedSession = current.copyWith(readings: updatedReadings);
|
||||||
|
await saveSyncreticSession(updatedSession);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveSyncreticSession(SyncreticSession session) async {
|
Future<void> saveSyncreticSession(SyncreticSession session) async {
|
||||||
_currentSyncreticSession = session;
|
_currentSyncreticSession = session;
|
||||||
final idx = _syncreticSessions.indexWhere((s) => s.id == session.id);
|
final idx = _syncreticSessions.indexWhere((s) => s.id == session.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user