feat(etl): нормализация на Gemini-эмбеддингах (API), Vision подключён

- эмбеддинги переведены с локального torch на Gemini API (лёгкий контейнер)
  GeminiEmbedder: gemini-embedding-001, 768d, RETRIEVAL query/document
- фильтр настоящих названий: коды/числа не уходят в сопоставление
- порог косинуса 0.70 → ~73% автонормализации (цель ТЗ ≥70%), остальное в unmatched
- Vision (gemini-2.5-flash, новый SDK) подключён в диспетчер для скан/нулевых PDF;
  проверено: Клиника 5 — 34 чистые позиции со страницы
- зависимости: убран torch/sentence-transformers, добавлен google-genai+numpy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 17:17:06 +05:00
parent 14462e8c10
commit 1de66eaa85
9 changed files with 189 additions and 76 deletions
+11 -8
View File
@@ -33,9 +33,9 @@ _PROMPT = """Ты извлекаешь позиции из прайс-листа
def _render_pages(path: str, max_pages: int, dpi: int) -> list[bytes]:
"""Отрисовать первые страницы PDF в PNG-картинки."""
import fitz # PyMuPDF
import pymupdf # PyMuPDF
document = fitz.open(path)
document = pymupdf.open(path)
images = []
for page in document[:max_pages]:
images.append(page.get_pixmap(dpi=dpi).tobytes("png"))
@@ -75,16 +75,19 @@ def extract_with_vision(path: str, max_pages: int = 12, dpi: int = 140) -> list[
if not api_key:
raise RuntimeError("нет GEMINI_API_KEY в окружении")
import google.generativeai as genai
from google import genai
from google.genai import types
genai.configure(api_key=api_key)
model = genai.GenerativeModel(os.environ.get("GEMINI_MODEL", "gemini-2.0-flash"))
client = genai.Client(api_key=api_key)
model = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
config = types.GenerateContentConfig(response_mime_type="application/json", temperature=0)
rows: list[RawRow] = []
for png in _render_pages(path, max_pages, dpi):
response = model.generate_content(
[_PROMPT, {"mime_type": "image/png", "data": png}],
generation_config={"response_mime_type": "application/json", "temperature": 0},
response = client.models.generate_content(
model=model,
contents=[types.Part.from_bytes(data=png, mime_type="image/png"), _PROMPT],
config=config,
)
rows.extend(_parse_response(response.text))
return rows