chore: seed repo — README, .gitignore, synthetic price fixtures (6 форматов)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Node / Next.js
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
*.log
|
||||
|
||||
# Claude Code worktrees (изоляция агентов)
|
||||
.claude/worktrees/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Данные (реальные прайсы НЕ коммитим; синтетика в fixtures/raw — коммитим)
|
||||
data/raw/
|
||||
data/uploads/
|
||||
*.sqlite
|
||||
@@ -0,0 +1,37 @@
|
||||
# medtech-hackathon
|
||||
|
||||
Terricon Valley · Medtech Hackathon (26–28.06.2026) · **Кейс 1 — автоматическая обработка архива прайсов клиник-партнёров.**
|
||||
|
||||
Превращаем архив разнородных прайс-листов клиник (Excel / PDF / скан / фото / .doc, RU/KZ/EN, грязные данные) в единый сравнимый каталог **услуга → клиника → цена** с поиском, сравнением цен и экспортом.
|
||||
|
||||
## Стек
|
||||
|
||||
- **ETL/бэкенд:** Python + FastAPI (pandas для Excel, LLM-vision для грязного, pdfplumber, Tesseract `kaz+rus` fallback)
|
||||
- **Канонизация услуг:** sentence-transformers + pgvector + детерминированная матрица конфликтов (см. словарик)
|
||||
- **БД:** PostgreSQL + pgvector
|
||||
- **Фронт:** Next.js + Tailwind + shadcn/ui (дизайн на палитре Nomad)
|
||||
- **Деплой:** Docker Compose + Caddy → `med.secondbrain.tools` (Selectel KZ)
|
||||
|
||||
## Языки
|
||||
|
||||
KAZ / RUS / ENG — сквозное требование (данные и UI).
|
||||
|
||||
## Структура (план)
|
||||
|
||||
```
|
||||
contracts/ — модель данных + Zod-схемы + openapi.yaml (агент A)
|
||||
db/ — миграции (агент A)
|
||||
etl/ — парсинг и нормализация (агент B)
|
||||
api/ — REST API поиска/сравнения (агент C)
|
||||
web/ — Next.js UI (агент D)
|
||||
infra/ — docker-compose, Caddyfile (агент E)
|
||||
docs/ — документация, ADR (агент F)
|
||||
pitch/ — слайды и демо-сценарий (агент F)
|
||||
fixtures/ — синтетические + эталонные данные
|
||||
```
|
||||
|
||||
Скелет кода поднимается в Фазе 0 хакатона после заморозки контрактов.
|
||||
|
||||
## Полный план
|
||||
|
||||
В хранилище Obsidian: `Base/_Inbox/Хакатон Medtech/` (план, словарик нормализации, дизайн-система, памятка).
|
||||
@@ -0,0 +1,20 @@
|
||||
# Fixtures — синтетические прайсы клиник
|
||||
|
||||
Сгенерированы скриптом `scripts/gen_fixtures.py` (seed=42, детерминированно) для отладки pipeline **до** выдачи реальных данных от организатора. Намеренно разнородные и «грязные».
|
||||
|
||||
## `raw/` — входные прайсы (6 форматов)
|
||||
|
||||
| Файл | Формат | Особенности (что тестирует) |
|
||||
|---|---|---|
|
||||
| `clinic_alpha_clean.xlsx` | Excel | Чистая таблица — быстрый путь pandas, без LLM |
|
||||
| `clinic_beta_dirty.xlsx` | Excel | Секции-заголовки, «от 12000», KZ-названия, лишние колонки |
|
||||
| `clinic_gamma.csv` | CSV (`;`) | Смешанные RU/KZ, единицы измерения |
|
||||
| `clinic_delta_messy.txt` | Текст | Прайс абзацем, разные разделители и валюты (тг/тенге/₸/KZT) |
|
||||
| `clinic_epsilon_scan.png` | Картинка | Имитация фото/скана (наклон) → путь LLM-vision / OCR |
|
||||
| `clinic_zeta_prices.pdf` | PDF | Текстовый PDF, RU/EN заголовок |
|
||||
|
||||
Одни и те же услуги названы по-разному между файлами (ОАК / Общий анализ крови / Қанның жалпы анализі) — это материал для канонизации и дедупликации.
|
||||
|
||||
## TODO (Фаза 0 хакатона)
|
||||
- `normalized/*.json` — эталонные нормализованные записи (для TDD ETL)
|
||||
- `api/*.json` — эталонные ответы API (для MSW-моков фронта)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
МЕДЦЕНТР «ТЕНГРИ» — действующие цены
|
||||
|
||||
Қан биохимиясы — 8300 KZT (1 иссл.)
|
||||
Терапевт қабылдауы 7000 KZT (1 приём)
|
||||
Глюкоза крови: 1100 тг (1 иссл.)
|
||||
Құрсақ қуысы УДЗ — 6700 ₸ (1 иссл.)
|
||||
МРТ головного мозга: 21600 тг (1 иссл.)
|
||||
Рентгенография ОГК: 5100 KZT (1 иссл.)
|
||||
Удаление зуба простое ... 12900 KZT (1 зуб)
|
||||
|
||||
* Цены справочные, уточняйте в регистратуре. Бағалар анықтамалық.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,9 @@
|
||||
service;price_kzt;unit
|
||||
УЗИ ОБП;11600;1 иссл.
|
||||
Приём врача-терапевта;4500;1 приём
|
||||
Электрокардиография;2100;1 иссл.
|
||||
Флюорография;4100;1 иссл.
|
||||
Б/х крови;4000;1 иссл.
|
||||
Консультация кардиолога;9600;1 приём
|
||||
Экстракция зуба;9500;1 зуб
|
||||
Қандағы глюкоза;1600;1 иссл.
|
||||
|
Binary file not shown.
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Генератор синтетических прайсов клиник (RU/KZ) в разных форматах для отладки pipeline."""
|
||||
import csv, os, random, subprocess, sys
|
||||
|
||||
OUT = sys.argv[1] if len(sys.argv) > 1 else "fixtures/raw"
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
random.seed(42) # детерминированно
|
||||
|
||||
def ensure(pkg, mod=None):
|
||||
try:
|
||||
__import__(mod or pkg)
|
||||
except ImportError:
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pkg])
|
||||
|
||||
# Базовый каталог: (канон RU, канон KZ, [варианты названий], мин, макс, единица)
|
||||
CATALOG = [
|
||||
("Общий анализ крови", "Қанның жалпы анализі",
|
||||
["Общий анализ крови", "ОАК", "Общий анализ крови (развернутый)", "Клинический анализ крови", "Қанның жалпы анализі"],
|
||||
1200, 4500, "1 иссл."),
|
||||
("УЗИ органов брюшной полости", "Құрсақ қуысы ағзаларының УДЗ",
|
||||
["УЗИ органов брюшной полости", "УЗИ ОБП", "УЗИ брюшной полости", "Құрсақ қуысы УДЗ"],
|
||||
6000, 13000, "1 иссл."),
|
||||
("Приём врача-терапевта", "Терапевт дәрігердің қабылдауы",
|
||||
["Прием терапевта", "Консультация терапевта первичная", "Приём врача-терапевта", "Терапевт қабылдауы"],
|
||||
4000, 9000, "1 приём"),
|
||||
("Электрокардиография (ЭКГ)", "Электрокардиография (ЭКГ)",
|
||||
["ЭКГ", "Электрокардиография", "ЭКГ с расшифровкой", "ЭКГ + описание"],
|
||||
2000, 5000, "1 иссл."),
|
||||
("Глюкоза крови", "Қандағы глюкоза",
|
||||
["Глюкоза", "Глюкоза крови", "Сахар крови", "Қандағы глюкоза"],
|
||||
800, 2200, "1 иссл."),
|
||||
("Консультация кардиолога", "Кардиолог кеңесі",
|
||||
["Консультация кардиолога", "Прием кардиолога", "Кардиолог консультация первичная"],
|
||||
5000, 12000, "1 приём"),
|
||||
("МРТ головного мозга", "Бас миының МРТ",
|
||||
["МРТ головного мозга", "Магнитно-резонансная томография головного мозга", "МРТ г/м", "Бас миы МРТ"],
|
||||
18000, 35000, "1 иссл."),
|
||||
("Биохимический анализ крови", "Қанның биохимиялық анализі",
|
||||
["Биохимия крови", "Биохимический анализ крови (базовый)", "Б/х крови", "Қан биохимиясы"],
|
||||
3500, 9000, "1 иссл."),
|
||||
("Удаление зуба", "Тісті жұлу",
|
||||
["Удаление зуба", "Удаление зуба простое", "Экстракция зуба", "Тіс жұлу"],
|
||||
6000, 15000, "1 зуб"),
|
||||
("Рентген грудной клетки", "Кеуде клеткасының рентгені",
|
||||
["Рентген грудной клетки", "Рентгенография ОГК", "Флюорография", "Кеуде рентгені"],
|
||||
2500, 6000, "1 иссл."),
|
||||
]
|
||||
|
||||
CLINICS = ["Клиника «Аль-Шифа»", "Медцентр «Сункар»", "Diagnostic Lab Astana", "Клиника «Береке»", "Медцентр «Тенгри»"]
|
||||
|
||||
def price(lo, hi):
|
||||
return round(random.randint(lo, hi), -2) # круглые сотни
|
||||
|
||||
def variants_for(clinic_idx, n):
|
||||
"""Выбрать n услуг с разными вариантами написания и ценами."""
|
||||
rows = []
|
||||
chosen = random.sample(CATALOG, k=min(n, len(CATALOG)))
|
||||
for ru, kz, names, lo, hi, unit in chosen:
|
||||
name = random.choice(names)
|
||||
rows.append((name, price(lo, hi), unit))
|
||||
return rows
|
||||
|
||||
ensure("openpyxl")
|
||||
from openpyxl import Workbook
|
||||
|
||||
# 1) Чистый Excel
|
||||
wb = Workbook(); ws = wb.active; ws.title = "Прайс"
|
||||
ws.append(["Наименование услуги", "Цена, тг"])
|
||||
for name, p, _ in variants_for(0, 9):
|
||||
ws.append([name, p])
|
||||
wb.save(f"{OUT}/clinic_alpha_clean.xlsx")
|
||||
|
||||
# 2) Грязный Excel: секции-заголовки, "от", разные колонки, KZ
|
||||
wb = Workbook(); ws = wb.active; ws.title = "PRICE 2026"
|
||||
ws.append(["Прайс-лист медцентра «Сункар» (с 01.06.2026)"])
|
||||
ws.append([])
|
||||
ws.append(["№", "Услуга / Қызмет", "Стоимость (тенге)", "Ед."])
|
||||
section = ["— Лаборатория / Зертхана —", "— Диагностика —", "— Приём специалистов —"]
|
||||
i = 1
|
||||
for s in section:
|
||||
ws.append([s, None, None, None])
|
||||
for name, p, unit in variants_for(1, 3):
|
||||
val = f"от {p}" if random.random() < 0.3 else p
|
||||
ws.append([i, name, val, unit]); i += 1
|
||||
wb.save(f"{OUT}/clinic_beta_dirty.xlsx")
|
||||
|
||||
# 3) CSV с разделителем ; и смешанными языками
|
||||
with open(f"{OUT}/clinic_gamma.csv", "w", encoding="utf-8", newline="") as f:
|
||||
w = csv.writer(f, delimiter=";")
|
||||
w.writerow(["service", "price_kzt", "unit"])
|
||||
for name, p, unit in variants_for(2, 8):
|
||||
w.writerow([name, p, unit])
|
||||
|
||||
# 4) Грязный текст (прайс абзацем)
|
||||
with open(f"{OUT}/clinic_delta_messy.txt", "w", encoding="utf-8") as f:
|
||||
f.write("МЕДЦЕНТР «ТЕНГРИ» — действующие цены\n\n")
|
||||
for name, p, unit in variants_for(3, 7):
|
||||
sep = random.choice([" — ", " ... ", ": ", " "])
|
||||
cur = random.choice(["тг", "тенге", "₸", "KZT"])
|
||||
f.write(f"{name}{sep}{p} {cur} ({unit})\n")
|
||||
f.write("\n* Цены справочные, уточняйте в регистратуре. Бағалар анықтамалық.\n")
|
||||
|
||||
# 5) Картинка-«скан» прайса (имитация фото)
|
||||
ensure("Pillow", "PIL")
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
img = Image.new("RGB", (900, 700), (250, 248, 244))
|
||||
d = ImageDraw.Draw(img)
|
||||
def font(sz):
|
||||
for p in ["/System/Library/Fonts/Supplemental/Arial.ttf", "/Library/Fonts/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"]:
|
||||
if os.path.exists(p):
|
||||
return ImageFont.truetype(p, sz)
|
||||
return ImageFont.load_default()
|
||||
d.text((40, 30), "Клиника «Береке» — прайс-лист", fill=(20, 20, 20), font=font(34))
|
||||
d.line((40, 85, 860, 85), fill=(180, 180, 180), width=2)
|
||||
y = 110
|
||||
for name, p, unit in variants_for(4, 9):
|
||||
d.text((50, y), name, fill=(30, 30, 30), font=font(24))
|
||||
d.text((720, y), f"{p} тг", fill=(30, 30, 30), font=font(24))
|
||||
y += 58
|
||||
img = img.rotate(-1.4, expand=False, fillcolor=(250, 248, 244)) # лёгкий наклон как у фото
|
||||
img.save(f"{OUT}/clinic_epsilon_scan.png")
|
||||
|
||||
# 6) PDF (если есть reportlab)
|
||||
try:
|
||||
ensure("reportlab")
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
fp = next((p for p in ["/Library/Fonts/Arial.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"] if os.path.exists(p)), None)
|
||||
if fp:
|
||||
pdfmetrics.registerFont(TTFont("U", fp))
|
||||
c = canvas.Canvas(f"{OUT}/clinic_zeta_prices.pdf", pagesize=A4)
|
||||
c.setFont("U", 18); c.drawString(60, 790, "Diagnostic Lab Astana — Price list / Прайс")
|
||||
c.setFont("U", 12); y = 750
|
||||
for name, p, unit in variants_for(2, 10):
|
||||
c.drawString(60, y, name[:55]); c.drawRightString(540, y, f"{p} KZT"); y -= 28
|
||||
c.save()
|
||||
pdf = "ok"
|
||||
else:
|
||||
pdf = "no font"
|
||||
except Exception as e:
|
||||
pdf = f"skip ({e})"
|
||||
|
||||
print("PDF:", pdf)
|
||||
print("Files:", sorted(os.listdir(OUT)))
|
||||
Reference in New Issue
Block a user