Compare commits

...

2 Commits

Author SHA1 Message Date
keboss-m
9c08128a9e Fix numpy float32 JSON serialization in OCR bbox 2026-06-01 13:48:40 +03:00
keboss-m
b344ef6f59 Fix _run_rapidocr: rapidocr API 2026-06-01 13:24:49 +03:00
7 changed files with 790 additions and 7 deletions

View File

@ -85,7 +85,7 @@ def run_tiling_ocr(img_path: Path, conf_threshold: float = 0.5):
for txt, box, score in zip(res.txts, res.boxes, res.scores): for txt, box, score in zip(res.txts, res.boxes, res.scores):
if score < conf_threshold: if score < conf_threshold:
continue continue
shifted = [[pt[0] + off_x, pt[1] + off_y] for pt in box] shifted = [[float(pt[0]) + off_x, float(pt[1]) + off_y] for pt in box]
all_results.append({"text": txt, "confidence": float(score), "bbox": shifted}) all_results.append({"text": txt, "confidence": float(score), "bbox": shifted})
# Дедупликация по IoU # Дедупликация по IoU
@ -157,13 +157,14 @@ def process_pdf(pdf_path: Path, out_dir: Path, use_qwen: bool = False, use_tilin
def _run_rapidocr(img_path: Path): def _run_rapidocr(img_path: Path):
res = engine(img_path) res = engine(img_path)
ocr_lines = [] ocr_lines = []
if res and res[0] is not None: if res and res.txts is not None:
for item in res[0]: for txt, box, score in zip(res.txts, res.boxes, res.scores):
box, txt, score = item # Convert numpy float32 to Python float for JSON serialization
bbox = [[float(pt[0]), float(pt[1])] for pt in box] if hasattr(box, '__iter__') else box
ocr_lines.append({ ocr_lines.append({
"text": txt, "text": txt,
"confidence": float(score), "confidence": float(score),
"bbox": box "bbox": bbox
}) })
return ocr_lines return ocr_lines

View File

@ -0,0 +1,10 @@
# OpenCode API (DeepSeek V4 Flash Free)
OPENCODE_API_KEY=ваш-ключ-здесь
OPENCODE_URL=https://opencode.ai/zen/v1
# LM Studio (VLM для описаний PNG)
LMSTUDIO_URL=http://127.0.0.1:1234/v1
LMSTUDIO_API_KEY=lm-studio
# OpenAI (опционально)
# OPENAI_API_KEY=sk-...

208
rag_standalone/README.md Normal file
View File

@ -0,0 +1,208 @@
# RAG / LightRAG для анализа чертежей
Этот модуль добавляет **Retrieval-Augmented Generation (RAG)** поверх существующего OCR-pipeline. Он превращает распознанный текст из `full_ocr_results.json` в **граф знаний** и позволяет задавать вопросы по чертежам на естественном языке.
## Архитектура
```
PDF -> PyMuPDF + RapidOCR -> full_ocr_results.json
|
v
rag_indexer.py (LightRAG)
|
v
lightrag_cache/ (граф знаний)
|
v
rag_query.py (вопросы)
```
**Почему LightRAG?**
- Строит **граф сущностей и отношений** (этажи, оси, размеры, помещения)
- Поддерживает 4 режима поиска: `naive`, `local`, `global`, `hybrid`
- Для чертежей рекомендуется **`hybrid`** — объединяет локальный поиск по соседним сущностям и глобальный контекст проекта
## Установка
```bash
# 1. Установите зависимости
pip install -r requirements.txt
# 2. Выберите LLM backend:
```
### Вариант A: OpenAI (быстро, требует интернет)
```bash
export OPENAI_API_KEY="sk-..."
# или в Windows: set OPENAI_API_KEY=sk-...
```
### Вариант B: Ollama (локально, приватно, требует RAM/VRAM)
```bash
# Установите Ollama: https://ollama.com
# Скачайте модели (пример)
ollama pull qwen2.5:14b # LLM для ответов
ollama pull nomic-embed-text # embeddings
# Запустите сервер
ollama serve
```
### Вариант C: LM Studio (локальный GUI, подходит для VLM)
```bash
# Установите LM Studio: https://lmstudio.ai
# 1. Загрузите VLM модель (например, qwen3-vl-4b) и запустите Local Server
# 2. Убедитесь, что сервер доступен: http://127.0.0.1:1234/v1
# Переменные окружения (при необходимости)
export LMSTUDIO_URL="http://127.0.0.1:1234/v1"
export LMSTUDIO_API_KEY="lm-studio"
```
### Вариант D: OpenCode API (DeepSeek V4 Flash Free)
```bash
# Требуется API-ключ OpenCode (бесплатный тариф)
# 1. Получите ключ на https://opencode.ai
# 2. Экспортируйте переменные:
export OPENCODE_API_KEY="ваш-ключ"
export OPENCODE_URL="https://opencode.ai/zen/v1" # опционально, уже задан по умолчанию
```
## Использование
### Шаг 1: OCR (если еще не сделано)
```bash
python process_any_pdf.py "Кронштадтский 16-18 НК1_ОСК (v3).pdf" output_kronshtadt
```
Результат: папка `output_kronshtadt/` с `full_ocr_results.json`.
### Шаг 2: Построение индекса
**OpenAI:**
```bash
python rag_indexer.py output_kronshtadt --backend openai --model gpt-4o-mini
```
**Ollama (локально):**
```bash
python rag_indexer.py output_kronshtadt --backend ollama --model qwen2.5:14b --embed-model nomic-embed-text
```
Результат: папка `output_kronshtadt/lightrag_cache/` с графом знаний.
**LM Studio (VLM для описаний):**
```bash
# Сгенерировать VLM-описания PNG (локальная Qwen)
python vlm_describer.py output_kronshtadt --model qwen/qwen3-vl-4b
# Построить индекс через OpenCode API (LLM)
python rag_indexer.py output_kronshtadt --backend opencode --vlm-desc
```
**OpenCode API (DeepSeek V4 Flash Free):**
```bash
# Построение индекса через бесплатный DeepSeek
python rag_indexer.py output_kronshtadt --backend opencode --model opencode/deepseek-v4-flash-free
# С VLM-описаниями (локальная Qwen + бесплатный DeepSeek)
python rag_indexer.py output_kronshtadt --backend opencode --vlm-desc
```
### Шаг 3 (опционально): VLM-описания через LM Studio
Если у вас в LM Studio загружена **VLM модель** (например, `qwen3-vl-4b`), можно сгенерировать текстовые описания PNG перед индексацией:
```bash
python vlm_describer.py output_kronshtadt --model qwen/qwen3-vl-4b
```
Это создаст `output_kronshtadt/vlm_descriptions.json` — текстовое описание каждой страницы. При `--vlm-desc` в `rag_indexer.py` эти описания добавятся к OCR-тексту, давая RAG понимание пространственной компоновки.
**Важно:** 4B VLM хороша для базовых описаний, но на сложных А0-чертежах может галлюцинировать. Для лучшего качества используйте 7B+ VLM (Qwen2-VL 7B, InternVL 8B).
### Шаг 4: Запросы
```bash
# Через OpenCode API (рекомендуется — бесплатно)
python rag_query.py output_kronshtadt "На каких страницах показан план 3-го этажа?" --backend opencode --mode hybrid
python rag_query.py output_kronshtadt "Какие размеры указаны между осями А и Б?" --backend opencode --mode hybrid
python rag_query.py output_kronshtadt "Какие квартиры есть на этаже 2?" --backend opencode --mode hybrid
# Режим local — ищет соседей сущностей в графе
python rag_query.py output_kronshtadt "Что находится рядом с осью В?" --backend opencode --mode local
# Режим global — общий контекст проекта
python rag_query.py output_kronshtadt "Опиши общую структуру здания по чертежам." --backend opencode --mode global
```
## Как это работает под капотом
### 1. Формирование документов
`rag_indexer.py` читает `full_ocr_results.json` и для каждой страницы создает текстовый документ вида:
```
=== Чертеж: страница 5 из 120 ===
Изображение: page_005.png
--- Текстовый слой PDF ---
План 3-го этажа...
--- Распознанный текст (OCR) ---
"3-й этаж" (confidence=0.95, bbox=[100, 200, 150, 220])
"Ось А" (confidence=0.88, bbox=[300, 400, 350, 420])
"5400 мм" (confidence=0.92, bbox=[500, 600, 580, 620])
--- Извлеченные сущности ---
Этажи: 3-й этаж
Оси: А
Размеры: 5400 мм
```
### 2. Построение графа (LightRAG)
LightRAG пропускает каждый документ через LLM и извлекает:
- **Entities**: `3-й этаж`, `Ось А`, `5400 мм`
- **Relationships**: `3-й этаж -> содержит -> Ось А`, `Ось А -> имеет размер -> 5400 мм`
Это сохраняется в `lightrag_cache/` как граф + векторные эмбеддинги.
### 3. Поиск ответа
При запросе `"Какие размеры между осями А и Б?"`:
- **Hybrid** = Local (ищет узлы "Ось А", "Ось Б" и их соседей) + Global (общий контекст "размеры между осями")
- LLM генерирует ответ на основе найденных фрагментов
## Советы по использованию для чертежей
1. **Качество OCR критично** — если RapidOCR пропускает цифры размеров, RAG не найдет их. Проверяйте `full_ocr_results.json`.
2. **Структурируйте сущности** — в `rag_indexer.py` уже встроен парсер этажей, осей, размеров. При необходимости расширьте регулярные выражения под ваши ГОСТ-овские обозначения.
3. **Страницы как узлы** — если чертеж очень большой (А0), можно разбить PNG на фрагменты и индексировать их отдельно.
4. **VLM для сложных схем** — если нужно понимать геометрию (а не только текст), добавьте описания от зрительной модели (Qwen2-VL, LLaVA) в текст документов перед индексацией.
## Расширение: добавление VLM-описаний
Если у вас есть GPU, можно улучшить понимание чертежей:
```python
# Псевдокод — добавить в rag_indexer.py перед insert
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
vlm = Qwen2VLForConditionalGeneration.from_pretrained("Qwen/Qwen2-VL-7B-Instruct")
processor = AutoProcessor.from_pretrained("Qwen/Qwen2-VL-7B-Instruct")
desc = vlm_describe_image(page_image_path, "Опиши этот чертеж: этажи, оси, размеры, помещения.")
doc_text += f"\n--- Описание модели зрения ---\n{desc}\n"
await rag.ainsert(doc_text)
```
Это даст RAG понимание пространственной компоновки, даже если OCR плохо распознал текст.
## Файлы
| Файл | Назначение |
|------|-----------|
| `rag_indexer.py` | Построение индекса LightRAG из OCR-результатов |
| `rag_query.py` | Запросы к индексу |
| `requirements.txt` | Зависимости (lightrag-hku, openai, etc.) |

View File

@ -0,0 +1,352 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Построение LightRAG индекса из OCR-результатов чертежей.
Использование:
# С OpenAI (требуется OPENAI_API_KEY)
python rag_indexer.py <output_folder> --backend openai
# С локальной моделью через Ollama (требуется запущенный Ollama)
python rag_indexer.py <output_folder> --backend ollama --model qwen2.5:14b
# Через LM Studio (локальный OpenAI-compatible сервер)
python rag_indexer.py <output_folder> --backend lmstudio --model qwen2.5:14b
# С VLM-описаниями (предварительно запустить vlm_describer.py)
python rag_indexer.py <output_folder> --backend lmstudio --vlm-desc
Результат: папка <output_folder>/lightrag_cache с графом знаний.
"""
import os
import sys
import json
import asyncio
import argparse
from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
load_dotenv()
# ------------------------------------------------------------------
# LightRAG imports
# ------------------------------------------------------------------
try:
from lightrag import LightRAG, QueryParam
from lightrag.utils import EmbeddingFunc
except ImportError as e:
print(f"[ERR] LightRAG не установлен: {e}")
print(" Установите: pip install lightrag-hku")
sys.exit(1)
# ------------------------------------------------------------------
# LLM backends
# ------------------------------------------------------------------
def get_openai_backend(model: str = "gpt-4o-mini"):
"""OpenAI backend (требует OPENAI_API_KEY)."""
from lightrag.llm import openai_complete_if_cache
from lightrag.llm import openai_embedding
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return await openai_complete_if_cache(
model, prompt,
system_prompt=system_prompt,
history_messages=history_messages,
**kwargs
)
async def embed_func(texts: list[str]) -> list[list[float]]:
return await openai_embedding(texts, model="text-embedding-3-small")
embed_cfg = EmbeddingFunc(
embedding_dim=1536,
max_token_size=8192,
func=embed_func,
)
return llm_func, embed_cfg
def get_ollama_backend(model: str = "qwen2.5:14b", embed_model: str = "nomic-embed-text"):
"""Ollama backend (требует запущенный ollama serve)."""
from lightrag.llm import ollama_model_complete, ollama_embedding
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return await ollama_model_complete(
model, prompt,
system_prompt=system_prompt,
history_messages=history_messages,
**kwargs
)
async def embed_func(texts: list[str]) -> list[list[float]]:
return await ollama_embedding(texts, model=embed_model)
# nomic-embed-text -> 768 dim, check your model
embed_cfg = EmbeddingFunc(
embedding_dim=768,
max_token_size=8192,
func=embed_func,
)
return llm_func, embed_cfg
def get_lmstudio_backend(model: str = "qwen2.5:14b"):
"""LM Studio backend (OpenAI-compatible API на http://127.0.0.1:1234/v1)."""
from lightrag.llm import openai_complete_if_cache
from sentence_transformers import SentenceTransformer
base_url = os.environ.get("LMSTUDIO_URL", "http://127.0.0.1:1234/v1")
api_key = os.environ.get("LMSTUDIO_API_KEY", "lm-studio")
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return await openai_complete_if_cache(
model, prompt,
system_prompt=system_prompt,
history_messages=history_messages,
api_key=api_key,
base_url=base_url,
**kwargs
)
# Для эмбеддингов используем локальную sentence-transformers
# (qwen3-vl-4b не умеет embeddings, поэтому нужна отдельная модель)
embed_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
async def embed_func(texts: list[str]) -> list[list[float]]:
import numpy as np
embeddings = embed_model.encode(texts, convert_to_numpy=True)
return embeddings.tolist()
embed_cfg = EmbeddingFunc(
embedding_dim=384,
max_token_size=512,
func=embed_func,
)
return llm_func, embed_cfg
def get_opencode_backend(model: str = "opencode/deepseek-v4-flash-free"):
"""OpenCode backend (DeepSeek V4 Flash Free, OpenAI-compatible API)."""
from openai import AsyncOpenAI
from sentence_transformers import SentenceTransformer
base_url = os.environ.get("OPENCODE_URL", "https://opencode.ai/zen/v1")
api_key = os.environ.get("OPENCODE_API_KEY", "")
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
if history_messages:
messages.extend(history_messages)
messages.append({"role": "user", "content": prompt})
response = await client.chat.completions.create(
model=model,
messages=messages,
temperature=kwargs.get("temperature", 0.3),
max_tokens=kwargs.get("max_tokens", 1024),
)
return response.choices[0].message.content
# Эмбеддинги — локальные sentence-transformers
embed_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
async def embed_func(texts: list[str]) -> list[list[float]]:
import numpy as np
embeddings = embed_model.encode(texts, convert_to_numpy=True)
return embeddings
embed_cfg = EmbeddingFunc(
embedding_dim=384,
max_token_size=512,
func=embed_func,
)
return llm_func, embed_cfg
# ------------------------------------------------------------------
# Подготовка текстов из OCR
# ------------------------------------------------------------------
def format_page_document(page: dict, page_idx: int, total: int, vlm_descs: dict = None) -> str:
"""Превращает данные одной страницы в текстовый документ для RAG."""
lines = []
lines.append(f"=== Чертеж: страница {page['page_number']} из {total} ===")
lines.append(f"Изображение: {page['image']}")
lines.append("")
# VLM-описание изображения (если есть)
if vlm_descs and page["image"] in vlm_descs:
lines.append("--- Описание модели зрения (VLM) ---")
lines.append(vlm_descs[page["image"]])
lines.append("")
# Текстовый слой PDF (если есть)
pdf_text = page.get("pdf_text_layer", "").strip()
if pdf_text:
lines.append("--- Текстовый слой PDF ---")
lines.append(pdf_text[:2000]) # обрежем, чтобы не перегружать
lines.append("")
# OCR результаты
ocr_lines = page.get("ocr_lines", [])
if ocr_lines:
lines.append("--- Распознанный текст (OCR) ---")
for entry in ocr_lines:
txt = entry["text"].strip()
conf = entry.get("confidence", 0.0)
bbox = entry.get("bbox", [])
lines.append(f' "{txt}" (confidence={conf:.2f}, bbox={bbox})')
lines.append("")
# Извлечь и явно перечислить сущности (помогает LLM построить граф)
entities = extract_entities_from_page(ocr_lines)
if any(entities.values()):
lines.append("--- Извлеченные сущности ---")
if entities["floors"]:
lines.append(f"Этажи: {', '.join(entities['floors'])}")
if entities["axes"]:
lines.append(f"Оси: {', '.join(entities['axes'])}")
if entities["dimensions"]:
lines.append(f"Размеры: {', '.join(entities['dimensions'])}")
if entities["rooms"]:
lines.append(f"Помещения: {', '.join(entities['rooms'])}")
lines.append("")
return "\n".join(lines)
def extract_entities_from_page(ocr_lines: list[dict]) -> dict:
"""Извлекает сущности из OCR-строк страницы."""
import re
floors, axes, dims, rooms = set(), set(), set(), set()
floor_re = re.compile(r"(\d+)[-\s]?й\s*этаж", re.I)
axis_re = re.compile(r"\b([А-Я])\b")
num_axis_re = re.compile(r"\b(\d{1,2})\b")
dim_re = re.compile(r"\b(\d{3,5})\s*м?[мм]?\b")
room_re = re.compile(r"([Кк]вартира|[Кк]в\.?\s*\d+|[Пп]омещение\s*\d+)", re.I)
for entry in ocr_lines:
t = entry["text"]
for m in floor_re.finditer(t):
floors.add(m.group(0))
for m in axis_re.finditer(t):
axes.add(m.group(1))
for m in num_axis_re.finditer(t):
v = m.group(1)
if v.isdigit() and 1 <= int(v) <= 50:
axes.add(v)
for m in dim_re.finditer(t):
v = m.group(1)
if 100 <= int(v) <= 20000:
dims.add(v + " мм")
for m in room_re.finditer(t):
rooms.add(m.group(0))
return {
"floors": sorted(floors),
"axes": sorted(axes),
"dimensions": sorted(dims),
"rooms": sorted(rooms),
}
def build_documents(ocr_path: Path, vlm_desc_path: Optional[Path] = None) -> list[str]:
"""Собирает список текстовых документов из full_ocr_results.json и VLM-описаний."""
data = json.loads(ocr_path.read_text(encoding="utf-8"))
pages = data["pages"]
vlm_descs = {}
if vlm_desc_path and vlm_desc_path.exists():
vlm_descs = json.loads(vlm_desc_path.read_text(encoding="utf-8"))
print(f" VLM-описания загружены: {len(vlm_descs)} шт.")
docs = []
total = len(pages)
for i, page in enumerate(pages):
doc_text = format_page_document(page, i, total, vlm_descs)
docs.append(doc_text)
return docs
# ------------------------------------------------------------------
# Основной pipeline
# ------------------------------------------------------------------
async def index_folder(folder: Path, backend: str, model: str, embed_model: Optional[str], use_vlm: bool = False):
ocr_path = folder / "full_ocr_results.json"
if not ocr_path.exists():
print(f"[ERR] Не найден {ocr_path}")
sys.exit(1)
cache_dir = folder / "lightrag_cache"
cache_dir.mkdir(exist_ok=True)
vlm_desc_path = folder / "vlm_descriptions.json" if use_vlm else None
print(f"[1/4] Загрузка OCR-данных из {ocr_path}")
docs = build_documents(ocr_path, vlm_desc_path)
print(f" Страниц: {len(docs)}")
print(f"[2/4] Инициализация LightRAG (backend={backend}, model={model})")
if backend == "openai":
llm_func, embed_cfg = get_openai_backend(model)
elif backend == "ollama":
embed = embed_model or "nomic-embed-text"
llm_func, embed_cfg = get_ollama_backend(model, embed)
elif backend == "lmstudio":
llm_func, embed_cfg = get_lmstudio_backend(model)
elif backend == "opencode":
llm_func, embed_cfg = get_opencode_backend(model)
else:
print(f"[ERR] Неизвестный backend: {backend}")
sys.exit(1)
rag = LightRAG(
working_dir=str(cache_dir),
llm_model_func=llm_func,
embedding_func=embed_cfg,
)
print(f"[2.5/4] Инициализация хранилищ...")
await rag.initialize_storages()
print(f"[3/4] Вставка документов в индекс...")
for i, doc_text in enumerate(docs, 1):
print(f" [{i}/{len(docs)}] Страница {i}")
await rag.ainsert(doc_text)
print(f"[4/4] Готово. Индекс сохранен в {cache_dir}")
print(f" Для запросов используйте: python rag_query.py \"{folder}\"")
def main():
parser = argparse.ArgumentParser(description="Построение LightRAG индекса из OCR")
parser.add_argument("folder", help="Папка с full_ocr_results.json")
parser.add_argument("--backend", choices=["openai", "ollama", "lmstudio", "opencode"], default="openai",
help="LLM backend (default: openai)")
parser.add_argument("--model", default="gpt-4o-mini",
help="Название модели LLM (default: gpt-4o-mini, opencode: deepseek-v4-flash-free)")
parser.add_argument("--embed-model", default=None,
help="Модель эмбеддингов (только для Ollama, default: nomic-embed-text)")
parser.add_argument("--vlm-desc", action="store_true",
help="Использовать VLM-описания из vlm_descriptions.json")
args = parser.parse_args()
folder = Path(args.folder)
model = args.model
if args.backend in ("ollama", "lmstudio") and model == "gpt-4o-mini":
model = "qwen2.5:14b"
if args.backend == "opencode" and model == "gpt-4o-mini":
model = "mimo-v2.5-free"
asyncio.run(index_folder(folder, args.backend, model, args.embed_model, args.vlm_desc))
if __name__ == "__main__":
main()

195
rag_standalone/rag_query.py Normal file
View File

@ -0,0 +1,195 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Запросы к LightRAG индексу по чертежам.
Использование:
python rag_query.py <output_folder> "Какие размеры между осями А и Б?"
Режимы поиска:
naive прямой поиск по чанкам (без графа)
local поиск по локальному графу соседей сущностей
global поиск по глобальному контексту проекта
hybrid комбинация local + global (рекомендуется для чертежей)
"""
import os
import sys
import asyncio
import argparse
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
try:
from lightrag import LightRAG, QueryParam
from lightrag.utils import EmbeddingFunc
except ImportError as e:
print(f"[ERR] LightRAG не установлен: {e}")
sys.exit(1)
def get_openai_backend(model: str = "gpt-4o-mini"):
from lightrag.llm import openai_complete_if_cache, openai_embedding
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return await openai_complete_if_cache(
model, prompt, system_prompt=system_prompt,
history_messages=history_messages, **kwargs
)
async def embed_func(texts: list[str]) -> list[list[float]]:
return await openai_embedding(texts, model="text-embedding-3-small")
return llm_func, EmbeddingFunc(embedding_dim=1536, max_token_size=8192, func=embed_func)
def get_ollama_backend(model: str = "qwen2.5:14b", embed_model: str = "nomic-embed-text"):
from lightrag.llm import ollama_model_complete, ollama_embedding
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
return await ollama_model_complete(
model, prompt, system_prompt=system_prompt,
history_messages=history_messages, **kwargs
)
async def embed_func(texts: list[str]) -> list[list[float]]:
return await ollama_embedding(texts, model=embed_model)
return llm_func, EmbeddingFunc(embedding_dim=768, max_token_size=8192, func=embed_func)
def get_lmstudio_backend(model: str = "qwen2.5:14b"):
"""LM Studio backend."""
from openai import AsyncOpenAI
from sentence_transformers import SentenceTransformer
base_url = os.environ.get("LMSTUDIO_URL", "http://127.0.0.1:1234/v1")
api_key = os.environ.get("LMSTUDIO_API_KEY", "lm-studio")
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
if history_messages:
messages.extend(history_messages)
messages.append({"role": "user", "content": prompt})
response = await client.chat.completions.create(
model=model,
messages=messages,
temperature=kwargs.get("temperature", 0.3),
max_tokens=kwargs.get("max_tokens", 1024),
)
content = response.choices[0].message.content
return content if content is not None else ""
embed_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
async def embed_func(texts: list[str]) -> list[list[float]]:
import numpy as np
return embed_model.encode(texts, convert_to_numpy=True)
return llm_func, EmbeddingFunc(embedding_dim=384, max_token_size=512, func=embed_func)
def get_opencode_backend(model: str = "nemotron-3-super-free"):
"""OpenCode backend (DeepSeek V4 Flash Free)."""
from openai import AsyncOpenAI
from sentence_transformers import SentenceTransformer
base_url = os.environ.get("OPENCODE_URL", "https://opencode.ai/zen/v1")
api_key = os.environ.get("OPENCODE_API_KEY", "")
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
async def llm_func(prompt, system_prompt=None, history_messages=[], **kwargs):
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
if history_messages:
messages.extend(history_messages)
messages.append({"role": "user", "content": prompt})
response = await client.chat.completions.create(
model=model,
messages=messages,
temperature=kwargs.get("temperature", 0.3),
max_tokens=kwargs.get("max_tokens", 1024),
)
content = response.choices[0].message.content
return content if content is not None else ""
embed_model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
async def embed_func(texts: list[str]) -> list[list[float]]:
import numpy as np
return embed_model.encode(texts, convert_to_numpy=True)
return llm_func, EmbeddingFunc(embedding_dim=384, max_token_size=512, func=embed_func)
async def query_index(folder: Path, question: str, mode: str, backend: str, model: str, embed_model: str):
cache_dir = folder / "lightrag_cache"
if not cache_dir.exists():
print(f"[ERR] Не найден индекс {cache_dir}. Сначала запустите rag_indexer.py")
sys.exit(1)
print(f"[INIT] Загрузка индекса из {cache_dir} (backend={backend}, model={model})")
if backend == "openai":
llm_func, embed_cfg = get_openai_backend(model)
elif backend == "ollama":
llm_func, embed_cfg = get_ollama_backend(model, embed_model)
elif backend == "lmstudio":
llm_func, embed_cfg = get_lmstudio_backend(model)
elif backend == "opencode":
llm_func, embed_cfg = get_opencode_backend(model)
else:
print(f"[ERR] Неизвестный backend: {backend}")
sys.exit(1)
rag = LightRAG(
working_dir=str(cache_dir),
llm_model_func=llm_func,
embedding_func=embed_cfg,
)
print(f"[INIT] Инициализация хранилищ...")
await rag.initialize_storages()
print(f"[QUERY] Режим: {mode}")
print(f"[QUERY] Вопрос: {question}\n")
print("=" * 60)
result = await rag.aquery(question, param=QueryParam(mode=mode))
print("=" * 60)
print("\n[RESULT] Ответ:")
print(result)
print("")
def main():
parser = argparse.ArgumentParser(description="Запросы к LightRAG по чертежам")
parser.add_argument("folder", help="Папка с lightrag_cache")
parser.add_argument("question", help="Вопрос на естественном языке")
parser.add_argument("--mode", choices=["naive", "local", "global", "hybrid"],
default="hybrid", help="Режим поиска (default: hybrid)")
parser.add_argument("--backend", choices=["openai", "ollama", "lmstudio", "opencode"], default="openai")
parser.add_argument("--model", default="gpt-4o-mini")
parser.add_argument("--embed-model", default="nomic-embed-text")
args = parser.parse_args()
folder = Path(args.folder)
model = args.model
if args.backend in ("ollama", "lmstudio") and model == "gpt-4o-mini":
model = "qwen2.5:14b"
if args.backend == "opencode" and model == "gpt-4o-mini":
model = "mimo-v2.5-free"
asyncio.run(query_index(folder, args.question, args.mode, args.backend, model, args.embed_model))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,17 @@
# LightRAG standalone requirements
# Install: pip install -r requirements.txt
# Core RAG engine
lightrag-hku>=1.4.0
# LLM backends
openai>=1.0.0
# Environment variables
python-dotenv>=1.0.0
# Optional: local embeddings (for Ollama/LM Studio local backends)
sentence-transformers>=3.0.0
# Utilities
numpy>=1.24.0

View File

@ -94,8 +94,8 @@ def run_tiling_ocr(png_path: Path, tile_size: int = 2000, overlap: int = 200, co
continue continue
# Сдвинуть bbox на offset кропа # Сдвинуть bbox на offset кропа
shifted_box = [] shifted_box = []
for pt in box.tolist() if hasattr(box, "tolist") else box: for pt in box:
shifted_box.append([pt[0] + off_x, pt[1] + off_y]) shifted_box.append([float(pt[0]) + off_x, float(pt[1]) + off_y])
all_results.append({ all_results.append({
"text": txt, "text": txt,
"confidence": float(score), "confidence": float(score),