Compare commits

..

20 Commits

Author SHA1 Message Date
admins 69a9884db5 feat(ingest+поиск): приём ZIP через интерфейс, регистронезависимый поиск, чистка очереди
- Загрузка архива (§4.1): POST /ingest распаковывает ZIP, дораспознаёт и
  догружает прайсы в базу (append). Виджет в админке. Кэш эмбеддингов справочника
  (dict_emb.npy) — догрузка считает только новый файл, эмбеддинги best-effort с
  откатом на fuzzy при сбое Gemini.
- Поиск по name_norm (lowercase): «УЗИ» и «узи» дают одинаковый результат —
  SQLite LIKE не сворачивает регистр для кириллицы.
- Фильтр названий: код-мнемоники («МОЗО.15») больше не попадают в очередь.
- /stats считается из базы — цифры на дашборде обновляются после загрузки.
- Деплой через Dockerfile (зависимости в образе, код монтируется томом).
- ruff: ядро и весь репозиторий проходят проверку.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:35:12 +05:00
admins 89928511ba feat(верификация): рабочая очередь с пагинацией + /skip
- автодогрузка порций + кнопка «Загрузить ещё»; разбор сверху по уверенности
- POST /skip фиксирует пропуск (таблица skipped_item), очередь его исключает

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:58:10 +05:00
admins 1ff5d93cd8 feat(день2): экран верификации с предложениями + обучение, детектор переплаты
- очередь верификации: предложенный кандидат (топ эмбеддинг), подтвердить/другое/пропустить
- POST /match пополняет синонимы (learned_synonym) — обучающаяся нормализация
- сравнение: медиана + разброс цен + отклонение клиники (переплата/выгода)
- эмбеддер: дедуп названий + ретрай при 429 (квота Gemini)
- развёрнуто на med.secondbrain.tools

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:47:57 +05:00
admins 9c0ae2c0bf fix(pitch): финальный слайд — без жаргона, понятные формулировки и ссылки
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:15:16 +05:00
admins 117cf6221e fix(api): root_path=/api — Swagger тянет спеку за прокси, дока работает
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:13:04 +05:00
admins d0e0532053 fix(pitch): слайд «что осталось» — компактнее, влезает в полный экран
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:09:22 +05:00
admins 79e308cb0c fix(pitch): слайд решений не влезал в полноэкранном — компактнее
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:03:34 +05:00
admins 1ce11155f0 docs(pitch): отчётная презентация дня 1 на /day1
- pitch/day1/ — reveal-дека: стек, решения, готовность по ТЗ, результаты
- развёрнута: https://med.secondbrain.tools/day1/ (маршрут /day1 в Caddy)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:56:03 +05:00
admins 3d7b12d63d deploy: система живая на med.secondbrain.tools + актуальный README
- infra/deploy/ — реальный compose (API-контейнер за Caddy) + Caddyfile
- docs/quality_report.json — отчёт: 8871 позиций, 73.9% автонормализации
- README переписан под факт: стек, результаты, запуск (ru-text 8.6)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:47:28 +05:00
admins 787092dc02 fix(api): сравнение — одна строка на клинику с показательной ценой
Проверено end-to-end на реальной БД: 8871 позиций, 73.9% автонормализации,
живое сравнение цен по клиникам во фронте.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:37:33 +05:00
admins 985faadf36 feat: конвейер ingest→SQLite, API на БД, фронт сравнения цен
- etl/pipeline.py — extract→normalize→validate→SQLite, дедуп партнёров, версии цен, отчёт качества
- api/main.py — 7 эндпоинтов на SQLite; ядро /services/{id}/partners (сравнение)
- web/index.html — одностраничный фронт (палитра Nomad): поиск, сравнение, дашборд, unmatched
- загрузчик справочника: уникальный service_id

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:30:10 +05:00
admins 1de66eaa85 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>
2026-06-26 17:17:06 +05:00
admins 14462e8c10 feat(etl): нормализация (каскад), валидация (8 правил), Vision-fallback
- etl/normalize/matcher.py — каскад код→точное→эмбеддинги→fuzzy→unmatched
- etl/validate/rules.py — правила ТЗ §4.4; проверено: 15588 done / 11 needs_review
- etl/extractors/vision.py — Gemini Vision для битых/нестандартных PDF
- замер % автонормализации — после установки модели эмбеддингов

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:46:18 +05:00
admins efae54fbad feat(etl): извлечение по форматам (агент B) — docx/xlsx/xls/pdf
- единый грид-экстрактор: поиск шапки, роли колонок, секции, N тарифов
- отсев колонок-индексов и не-цен; название по доле кириллицы
- PDF: реконструкция таблицы по координатам слов (нет линий → extract_tables пуст)
- детектор битого текстового слоя → пометка scan_pdf для Vision
- проверено на реальном архиве: 6/8 клиник нативно, 15599 позиций; 2 файла → Vision

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:40:27 +05:00
admins a6a6609c48 feat: Фаза 0 — каркас, контракты, загрузчик справочника
- db/migrations/001_init.sql — схема (4 сущности, prices jsonb, pgvector/pg_trgm)
- contracts/models.py — общие модели (RawRow, ParsedDocument, MatchResult, *Out)
- etl/dictionary.py — загрузчик справочника (1281 услуга, нормализация)
- api/main.py — FastAPI: 7 эндпоинтов ТЗ + /stats (контракт)
- infra/ — docker-compose (pg+pgvector, api, web), Caddyfile
- scripts/proof/phase0_proof.py — проверка каскада на реальных данных (35% без эмбеддингов)
- data/ в .gitignore: данные клиник в публичный репо не коммитятся

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:23:06 +05:00
admins f35957a31e docs: план реализации MedArchive, заземлённый на реальный датасет
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 14:59:04 +05:00
admins 39b45567a1 chore: убрать имя заказчика из артефактов + правки деки
- вычищен бренд из деки/дизайна/README/outline (нейтральные --brand токены)
- слайд 1: «на разных языках» вместо «на казахском»
- убрана лишняя угловая плашка в презентации

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:21:18 +05:00
admins 1e2ce2caf7 feat(pitch): reveal.js презентация на /demo (палитра Nomad, 7 слайдов)
- pitch/deck/index.html — колода reveal.js (fullscreen F, листание, speaker view S)
- pitch/deck/fetch-reveal.sh — вендоринг reveal локально (offline-proof)
- развёрнуто: https://med.secondbrain.tools/demo

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:12:29 +05:00
admins 062ff5fc11 feat(pre-flight): канон-справочник + golden-нормализация, дизайн Nomad, питч
- contracts/service_canon.json — 15 канонов медуслуг RU/KZ/EN + синонимы
- fixtures/normalized/golden.json — 52 эталонные строки (оракул TDD)
- design/ — tailwind-токены + shadcn-тема Nomad
- pitch/ — структура слайдов + демо-сценарий

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:03:08 +05:00
admins fa2fbc59b0 chore: seed repo — README, .gitignore, synthetic price fixtures (6 форматов)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:53:04 +05:00
5 changed files with 226 additions and 19 deletions
+146
View File
@@ -0,0 +1,146 @@
# MedArchive — план реализации (Кейс 2, MedPartners)
**Цель:** собрать систему, которая принимает архив разнородных прайс-листов клиник, извлекает услуги и цены, нормализует их к справочнику из 1287 услуг и отдаёт API и интерфейс для поиска «кто оказывает услугу и по какой цене».
**Архитектура:** Python/FastAPI обрабатывает документы по формату (нативные парсеры + Gemini Vision как универсальный запасной путь), нормализует через коды тарификатора и эмбеддинги, складывает в PostgreSQL и отдаёт REST API. Поверх — Next.js: админка оператора и публичное сравнение цен.
**Стек:** Python 3.12, FastAPI, PostgreSQL + pgvector, pdfplumber, PyMuPDF, openpyxl, xlrd, python-docx, sentence-transformers (multilingual), RapidFuzz, Gemini Vision, Next.js + Tailwind + shadcn/ui, Docker Compose + Caddy.
## Сквозные ограничения
- **Репозиторий публичный** (требование организаторов) → ни одного секрета в коде. Все ключи (Gemini и прочее) — только через переменные окружения и `.env`, который в `.gitignore`. В репо — `.env.example` с пустыми значениями.
- **Качество кода оценивается** → чистая структура модулей, типизация, docstrings и осмысленные комментарии в нетривиальных местах. Линтер (ruff) и форматтер прогоняются перед коммитом.
- **Любой русский текст** (README, комментарии, тексты интерфейса, презентация) проходит проверку `ru-text` с оценкой не ниже 8.5.
- **ИИ-движок:** Gemini Vision для сканов и сложной вёрстки; сопоставление со справочником — локальными эмбеддингами без платных вызовов.
- **Деплой:** живая версия на `med.secondbrain.tools` (Selectel KZ), плюс репозиторий с инструкцией запуска.
- **Целевой показатель ТЗ:** не менее 70% позиций нормализуются автоматически.
---
## Что реально в данных (разведка датасета)
8 клиник, 10 файлов. Реальность заметно сложнее, чем «4 формата + OCR» из ТЗ.
| Клиника | Файл(ы) | Особенности |
|---|---|---|
| 1 | docx 2024 (таблица 2727 строк) + pdf 2026 (85 стр., текст) | Колонки `Код / Наименование / Стоимость, тенге`. Коды `U1.1` (внутренние, не тарификатор). Секции «Раздел 1…». **История цен** 2024→2026. |
| 2 | pdf 2025 + pdf 2026 (текст) | **Резидент / нерезидент / страховые** — несколько колонок цен. Грязные многострочные заголовки. История цен. |
| 3 | PDF 2026 (текст, но **битый текстовый слой** — «Yliliepiia.i») | Колонки `Код тарификатора / Цена / Цена партнёра`. **Коды тарификатора `В02.110.002` совпадают со справочником** → точное сопоставление. Нужен Vision-путь поверх битого текста. |
| 4 | pdf 2026 (текст) | Несколько ценовых категорий по гражданству (РК/оралманы, СНГ, дальнее зарубежье). |
| 5 | pdf 2025 (текст) | Нестандартная вёрстка: первичный/повторный приём, ФИО врача, специальность. Цены «9000 тг». |
| 6 | xlsx (лист 5181×12) | Много пустых ячеек, шапка-преамбула («Приложение 1, к приказу…»), данные начинаются глубоко. |
| 7 | xls (3065×6) | Колонки: №, наименование, ед.изм., **цена РК / СНГ / дальнее зарубежье** (3 тарифа). Шапка не в первой строке. |
| 8 | xlsx (2 листа: 1816×11 и 155×40) | Преамбула-приказ, несколько листов, очень широкие таблицы. |
**Справочник услуг** (`Справочник услуг.xlsx`): 1 лист, **1287 строк × 5 колонок**`ID | Специальность | Code | Name_ru | TarificatrCode`. **Синонимов и категорий (кроме специальности) нет** — значит, автосопоставление держится на эмбеддингах и кодах тарификатора, синонимы наращиваем сами.
**Выводы, меняющие дизайн:**
1. **Не «резидент/нерезидент», а N ценовых тарифов** (резидент, нерезидент, СНГ, дальнее, страховые, партнёр) → гибкая модель цен, а не две колонки.
2. **Код тарификатора — золотой ключ:** где клиника его даёт (Клиника 3), сопоставление точное и бесплатное. Проверяем в первую очередь.
3. **«Скан» в реальности — это битый текстовый слой** (Клиника 3). Детектируем мусор и уходим в Gemini Vision по отрисованной странице. Универсальный Vision-путь закрывает и сканы, и битый текст.
4. **Файлы большие** (десятки страниц, тысячи строк) → нативные парсеры тянут основной объём, Vision бережём для трудных страниц (стоимость и время).
5. **Шапка не в первой строке, секции-заголовки задают специальность** → логика поиска строки заголовков и контекста секции. Специальность из секции сужает поиск по справочнику и повышает точность.
---
## Модель данных
**Service** (справочник, загружается из xlsx): `service_id` (из ID+Code), `specialty`, `name_ru`, `tarificator_code`, `synonyms jsonb` (наращиваем), `embedding vector(384)`, `is_active`.
**Partner:** `partner_id`, `name`, `city`, `address`, `bin`, `contact_email`, `contact_phone`, `is_active`, `created_at`, `updated_at`. Имя/город — из шапки документа или имени файла; дедуп по БИН, иначе по нормализованному имени.
**PriceDocument:** `doc_id`, `partner_id`, `file_name`, `file_format` (pdf/scan_pdf/docx/xlsx/xls), `effective_date` (год из имени файла), `parsed_at`, `parse_status` (pending/processing/done/error/needs_review), `parse_log`, `raw_content`.
**PriceItem:** `item_id`, `doc_id`, `partner_id`, `service_name_raw`, `service_code_source`, `service_id` (nullable), `unit`, **`prices jsonb`** (карта `тариф → сумма`: `resident`, `nonresident`, `cis`, `far`, `insurance`, `partner`…), `price_resident_kzt` и `price_nonresident_kzt` (денормализация двух главных тарифов под ТЗ и API), `currency_original`, `effective_date`, `map_method` (code/exact/embedding/fuzzy/manual), `map_confidence`, `is_verified`, `verification_note`, `is_active`.
Уникальность активной позиции: `(partner_id, service_name_raw, effective_date)`. Версионирование: при новом прайсе старые позиции той же клиники архивируются (`is_active=false`), не удаляются.
---
## Конвейер извлечения (по форматам)
Единый контракт выхода парсера: список «сырых строк» `{service_name_raw, service_code_source, prices{}, unit, section}` + метаданные клиники/даты.
- **XLSX / XLS** (openpyxl / xlrd): обойти все листы; найти строку заголовков (содержит «Наименование» и «Цена»); распознать все колонки цен по заголовкам; строки из одной непустой ячейки = заголовок секции (задаёт специальность); пропустить преамбулу.
- **DOCX** (python-docx): принять все правки (работаем с финальным текстом); вытащить таблицы; обработать секции и большие таблицы.
- **PDF текстовый** (pdfplumber): извлечь таблицы по линиям/координатам; для многоколоночных цен — кластеризация по x-координате; заголовок секции из строк без цены.
- **PDF с битым слоем / скан** (детектор мусора → PyMuPDF рендер → **Gemini Vision** со строгой JSON-схемой): универсальный путь для Клиники 3 и любых сложных страниц.
Детектор формата: расширение + проба текстового слоя (доля кириллицы/словарных слов). Низкое качество → Vision.
---
## Нормализация (ядро 25% оценки, цель ≥70%)
Каскад по убыванию надёжности:
1. **Код тарификатора:** `service_code_source` матчит `Service.tarificator_code` → точное сопоставление, `confidence=1.0`.
2. **Точное совпадение имени** (после нормализации: нижний регистр, схлопывание пробелов/дефисов) с `name_ru`.
3. **Эмбеддинги** (multilingual sentence-transformers, предрасчёт по 1287 `name_ru`, pgvector): топ-1 по косинусу; если ≥0.85 → автосопоставление. Если известна специальность секции — поиск сужается внутри специальности (точнее и быстрее).
4. **Нечёткий поиск** (RapidFuzz token_set_ratio) как вторичный сигнал/тай-брейк.
5. Иначе → очередь `unmatched` на ручную разметку.
Порог конфигурируется. Подтверждённые операторы-сопоставления пополняют `synonyms` — система учится по ходу.
---
## Валидация (20% оценки, правила ТЗ §4.4)
Движок проверок при парсинге: цена > 0 и число; нерезидент ≥ резидент; имя услуги не пустое; дата не в будущем; дубликат `(клиника, услуга, дата)` → архивировать старую; изменение цены > 50% к прошлой версии → флаг аномалии; валюта ≠ KZT → конвертация по курсу на дату, оригинал сохраняем; документ без распознаваемых данных → статус `error`. Нарушения пишутся в `parse_log` и/или ставят `needs_review`.
---
## API (15%) + WOW-слой
REST (FastAPI, OpenAPI обязателен):
`GET /services` (фильтр по специальности) · `GET /services/{id}/partners` (кто оказывает + цены) · `GET /partners` (фильтр город/статус) · `GET /partners/{id}/services` · `GET /search?q=` (полнотекст по услугам и клиникам) · `GET /unmatched` · `POST /match`. Плюс `GET /stats` для дашборда и отчёта о качестве.
**WOW-слой (тонкий, из ТЗ MedPrice):** публичная страница сравнения — поиск услуги → таблица клиник с ценами резидент/нерезидент, сортировка, подсветка самой выгодной. Это «Aviasales для медицины» поверх наших данных — показывает заказчику готовый кусок второго продукта.
---
## Роли агентов (worktrees) и владение
| Агент | Владеет | Ответственность |
|---|---|---|
| **A — Контракты и данные** | `contracts/`, `db/migrations/`, `fixtures/` | Схема БД, Pydantic/Zod-модели, `openapi.yaml`, загрузчик справочника (1287 услуг + предрасчёт эмбеддингов) |
| **B — Извлечение** | `etl/extractors/` | Парсеры xlsx/xls/docx/pdf + Vision-путь + детектор формата/мусора + поиск заголовков |
| **C — Нормализация и валидация** | `etl/normalize/`, `etl/validate/` | Каскад сопоставления, очередь unmatched, движок проверок, версионирование |
| **D — API** | `api/` | Эндпоинты, поиск, OpenAPI, `/stats` |
| **E — Фронт** | `web/` | Админка (загрузка, статусы, очередь верификации, дашборд) + публичное сравнение цен |
| **F — Инфра/деплой/доки** | `infra/`, `docs/`, `pitch/` | Docker Compose, Caddy, деплой, README, отчёт о качестве, презентация |
Человек-оркестратор: держит контракты, мёрджит в `main`, ведёт борд статусов.
---
## MVP cut-line
- **MUST (ядро 75% оценки):** загрузка архива → детект формата → парсеры 4 форматов + Vision-fallback → извлечение PriceItem с N-тарифами → нормализация (код→точное→эмбеддинги→unmatched) → схема Postgres → движок валидации → API (7 эндпоинтов) → **отчёт о качестве** (обработано, % автонормализации, очередь).
- **SHOULD (воскресенье, вариант «Шире»):** UI очереди верификации + `POST /match`, версионирование цен, конвертация валют, детектор аномалий, публичное сравнение цен (WOW).
- **NICE:** дашборд-метрики, полировка дизайна, демо-видео, дедуп по БИН, наращивание синонимов.
---
## Тайм-план (T0 = старт сборки, датасет на руках)
- **T0–4 ч — Фаза 0:** скелет монорепо + worktrees + per-role `CLAUDE.md`; A замораживает схему БД + `openapi.yaml` + загрузчик справочника; E поднимает Compose. ✅ контракты заморожены.
- **4–14 ч:** B — парсеры xlsx/docx (самые чистые форматы) на реальных файлах; C — каскад нормализации на справочнике; D — API на сид-данных; E — каркас админки. ✅ smoke: 2 клиники сквозь весь конвейер.
- **1426 ч:** B — pdf-парсер + Vision для Клиники 3; C — валидация + версионирование; прогон всего архива; отчёт о качестве. ✅ MVP на реальных данных, % автонормализации измерен.
- **26–34 ч:** деплой на домен, наполнение БД, проверка по HTTPS; начало варианта «Шире» (очередь верификации, сравнение цен), если ядро закрыто.
- **34–44 ч:** полировка, отчёт, README; **заморозка фич**; репетиция демо.
- **4448 ч:** буфер, fallback-видео, финальная проверка прода.
---
## Что сдаём (ТЗ §7)
Рабочий MVP + README · обработанная БД по архиву · загруженный справочник · OpenAPI/Swagger · **отчёт о качестве** (документов, % автонормализации, позиций в очереди) · презентация 5–7 слайдов · демо-видео (опционально).
---
## Контракты, которые замораживаем в Фазе 0
1. **Схема БД** (4 сущности выше, модель цен `prices jsonb` + две денормализованные колонки).
2. **`openapi.yaml`** (7 эндпоинтов + `/stats`).
3. **Контракт парсера** (единый формат «сырой строки»).
4. **Формат загрузки справочника** (маппинг колонок xlsx → Service).
+1 -1
View File
@@ -1,5 +1,5 @@
{
email admin@secondbrain.tools
email laukhin@dementiy.com
acme_ca https://acme-v02.api.letsencrypt.org/directory
}
+24
View File
@@ -0,0 +1,24 @@
# Демо-сценарий (≈3 минуты)
Один эффект, доведённый до «вау»: **хаос форматов на входе → сравнимая таблица за минуту**.
## Шаги
| Время | Действие | Что говорю |
|---|---|---|
| 0:00–0:15 | Открыт пустой экран загрузки | «У клиник-партнёров прайсы в разных форматах. Смотрите, что система с этим делает.» |
| 0:15–0:35 | Перетаскиваю **пёструю пачку** из `fixtures/raw/`: чистый Excel + грязный Excel (секции, «от») + CSV + текст + **фото-скан (наклонённый)** + PDF; часть на казахском, 1 заведомо мусорный. Жму «Обработать» | «Бросаю всё разом — Excel, скан, фото, казахский.» |
| 0:35–1:05 | Видно прогресс: у каждого файла статус и **путь извлечения** (structured / vision). Строки появляются в реальном времени | «Система сама выбрала быстрый путь для Excel и распознавание для фото.» |
| 1:05–2:05 | Открывается **единая таблица**. Ввожу поиск «УЗИ брюшной полости» → сводятся `УЗИ ОБП`, `Abdominal US`, `Құрсақ қуысы УДЗ` из разных клиник в **одну строку сравнения цен** | «Разные формулировки, три языка — одна услуга. Вот сравнение цен между клиниками.» (момент «вау») |
| 2:05–2:25 | Клик по строке с **жёлтой подсветкой** (low confidence) → всплывает оригинальный фрагмент файла рядом с извлечённым значением | «Система честно помечает, что проверить вручную — для медданных это важно.» |
| 2:252:40 | Жму **Экспорт в Excel** → скачивается чистый сводный прайс | «Из архива хаоса — готовый сравнительный прайс за минуту.» |
## ⚠️ Чек-лист «не провалить live-demo»
- [ ] **Fallback-видео демо (60–90 сек) записано накануне ночью** — переключаюсь без паузы, если упало.
- [ ] Демо работает **локально без интернета** (или предобработанный результат в кэше на случай тупящего ИИ-вызова на сцене).
- [ ] Данные демо прогнаны заранее, известно — на какой секунде что нажимать (**10+ репетиций с секундомером**).
- [ ] Запасное устройство (второй ноут / телефон-точка).
- [ ] Скриншоты ключевых экранов в слайдах (крайний fallback).
- [ ] Браузер: лишние вкладки закрыты, нотификации выкл, шрифт крупный (видно с проектора).
- [ ] Прод `med.secondbrain.tools` проверен с чужого устройства/сети перед выходом.
+37
View File
@@ -0,0 +1,37 @@
# Питч — структура (6–7 слайдов, ~3 мин речи + демо)
Цель: ~400 слов речи. Первые 20 сек — крючок, без «здравствуйте, мы команда». Один назначенный спикер (Дмитрий). Демо в центре.
## Слайд 1 — Хук + проблема (20–30 сек)
«У страховой/сети — десятки клиник-партнёров. Каждая шлёт прайс как умеет: Excel, скан, фото на телефон, половина на казахском. Свести это в единый прайс для ДМС/сравнения — недели ручной работы и ошибки в цене, которую видит пациент».
> Назвать масштаб боли и конкретного бенефициара (прайс-аналитик / менеджер партнёрской сети).
## Слайд 2 — Решение в одном предложении (15 сек)
«Система, которая автоматически читает прайс **в любом формате** и превращает архив в единую актуальную базу цен — с поиском и сравнением между клиниками».
## Слайд 3 — 🔴 ЖИВОЕ ДЕМО (60–90 сек) — ядро
См. `demo-script.md`. Перетащил пёструю пачку → одна кнопка → единая таблица → поиск сводит RU/KZ/EN формулировки в одну строку сравнения → клик по low-confidence показывает оригинал → экспорт.
## Слайд 4 — Как это работает (20 сек)
Простая схема: **любые форматы → ИИ-извлечение + нормализация → единый каталог услуг**. Без технического занудства. Подчеркнуть: один путь на Excel/PDF/скан/фото, RU/KZ/EN.
## Слайд 5 — Метрики / impact (20 сек)
- Онбординг прайса клиники: **часы ручного ввода → минуты**.
- % автоматически распознанных строк, точность матчинга, % строк на ручную проверку.
- «Административная автоматизация — самый окупаемый сегмент medtech AI».
## Слайд 6 — Бизнес и масштаб (20 сек)
- Кому: страховщик с ДМС, сеть клиник, мед-маркетплейс.
- Кейс 1 = data-движок Кейса 2 (агрегатор/сравнение цен) → монетизация.
- Масштаб: с десятков клиник → на сотни по РК → СНГ. Язык Terricon: **экспорт, продажи, масштаб**.
## Слайд 7 — Закрытие (15 сек)
Сильная фраза: «Из архива хаоса — готовый сравнительный прайс за минуту». + «что дальше за неделю» (доменный справочник услуг РК, интеграция в CRM страховщика). Ничего не просить — оставить впечатление.
---
### Помнить
- Доверие к медданным: confidence-score + флаги ручной проверки + ссылка на оригинал.
- Регуляторно безопасно: работаем с публичными коммерческими прайсами, не с данными пациентов — проговорить.
- Прогресс именно за 48 ч (связка с ИИ-агентами — честное преимущество по скорости).
- Слайдов максимум 7. Демо > слова.
+18 -18
View File
@@ -9,18 +9,18 @@
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { theme: { extend: {
colors: { brand: { DEFAULT: '#ff4713', soft: '#ffede7', dark: '#e63e0f' },
colors: { nomad: { DEFAULT: '#ff4713', soft: '#ffede7', dark: '#e63e0f' },
tg: '#6b788e', lg: '#f5f7f9', ink: '#111827' },
fontFamily: { sans: ['Geologica', 'ui-sans-serif', 'system-ui', 'sans-serif'] } } } };
</script>
<style>body{font-family:'Geologica',system-ui,sans-serif}</style>
</head>
<body class="bg-[#f3f4f6] text-ink min-h-screen">
<div class="h-1 bg-brand w-full"></div>
<div class="h-1 bg-nomad w-full"></div>
<header class="max-w-5xl mx-auto px-5 pt-8 pb-2 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-2xl font-bold">Med<span class="text-brand">Archive</span></span>
<span class="text-2xl font-bold">Med<span class="text-nomad">Archive</span></span>
<span class="text-xs text-tg bg-lg border border-[#ebedf0] rounded-full px-2 py-0.5">прайсы клиник-партнёров</span>
</div>
<nav class="text-sm text-tg flex gap-4">
@@ -33,12 +33,12 @@
<!-- Поиск + сравнение -->
<section id="view-search">
<h1 class="text-3xl sm:text-4xl font-bold mt-6 leading-tight">Найдите услугу — <span class="text-brand">сравните цены</span> в клиниках</h1>
<h1 class="text-3xl sm:text-4xl font-bold mt-6 leading-tight">Найдите услугу — <span class="text-nomad">сравните цены</span> в клиниках</h1>
<p class="text-tg mt-2">Единый каталог услуг и цен из архива прайсов клиник-партнёров.</p>
<div class="mt-5 flex gap-2">
<input id="q" type="text" placeholder="например: УЗИ брюшной полости, приём кардиолога, общий анализ крови"
class="flex-1 bg-white border border-[#d1d5db] rounded-lg px-4 py-3 outline-none focus:border-brand focus:ring-2 focus:ring-brand/40"
class="flex-1 bg-white border border-[#d1d5db] rounded-lg px-4 py-3 outline-none focus:border-nomad focus:ring-2 focus:ring-nomad/40"
oninput="debouncedSearch()" autocomplete="off">
</div>
@@ -60,7 +60,7 @@
</div>
<div class="shrink-0">
<input id="zip" type="file" accept=".zip" class="hidden" onchange="uploadArchive(this)">
<button onclick="$('zip').click()" class="bg-brand text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-brand-dark">Выбрать ZIP</button>
<button onclick="$('zip').click()" class="bg-nomad text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-nomad-dark">Выбрать ZIP</button>
</div>
</div>
<div id="ingest-result" class="mt-3 text-sm hidden"></div>
@@ -72,7 +72,7 @@
</div>
<p class="text-sm text-tg mt-1">Система предлагает кандидата из справочника — подтвердите, выберите другой или пропустите. Подтверждение обучает систему.</p>
<div id="queue" class="mt-3 space-y-2"></div>
<button id="loadmore" onclick="loadQueue()" class="mt-3 text-sm text-brand hover:underline hidden">Загрузить ещё →</button>
<button id="loadmore" onclick="loadQueue()" class="mt-3 text-sm text-nomad hover:underline hidden">Загрузить ещё →</button>
</section>
</main>
@@ -98,7 +98,7 @@ async function search() {
const data = await fetch(`${API}/search?q=${encodeURIComponent(q)}&limit=8`).then(r => r.json());
const items = (data.services || []);
box.innerHTML = items.length
? items.map(s => `<button class="block w-full text-left px-4 py-2.5 hover:bg-brand-soft"
? items.map(s => `<button class="block w-full text-left px-4 py-2.5 hover:bg-nomad-soft"
onclick="compare('${s.service_id}', ${JSON.stringify(s.name_ru).replace(/"/g, '&quot;')})">
<span class="font-medium">${s.name_ru}</span>
<span class="text-xs text-tg ml-2">${s.specialty || ''}</span></button>`).join('')
@@ -119,7 +119,7 @@ async function compare(serviceId, name) {
<div class="bg-white border border-[#ebedf0] rounded-2xl shadow-sm overflow-hidden">
<div class="px-5 py-4 border-b border-[#ebedf0]">
<div class="text-lg font-semibold">${name}</div>
<div class="text-sm text-tg">${rows.length} клиник(и) · медиана ${money(median)}${spread ? ' · разброс цен до <b class="text-brand">' + money(spread) + '</b>' : ''}</div>
<div class="text-sm text-tg">${rows.length} клиник(и) · медиана ${money(median)}${spread ? ' · разброс цен до <b class="text-nomad">' + money(spread) + '</b>' : ''}</div>
</div>
<table class="w-full text-sm">
<thead class="bg-lg text-tg text-left">
@@ -136,8 +136,8 @@ async function compare(serviceId, name) {
const badge = d == null ? '—' : (d > 0
? `<span class="text-[#dc2626] font-medium">+${d}%</span>`
: `<span class="text-[#0f973d] font-medium">${d}%</span>`);
return `<tr class="${cheapest ? 'bg-brand-soft' : ''}">
<td class="px-5 py-3">${r.partner_name}${cheapest ? ' <span class="text-brand text-xs font-semibold">· выгодно</span>' : ''}</td>
return `<tr class="${cheapest ? 'bg-nomad-soft' : ''}">
<td class="px-5 py-3">${r.partner_name}${cheapest ? ' <span class="text-nomad text-xs font-semibold">· выгодно</span>' : ''}</td>
<td class="px-5 py-3 text-right tabular-nums font-medium">${money(r.price_resident_kzt)}</td>
<td class="px-5 py-3 text-right tabular-nums text-xs">${badge}</td>
<td class="px-5 py-3 text-right tabular-nums text-tg">${money(r.price_nonresident_kzt)}</td>
@@ -155,7 +155,7 @@ async function loadAdmin() {
['Автонормализация', s.auto_matched_pct + '%'], ['В очереди', s.unmatched_total]];
$('stats').innerHTML = cards.map(([l, v]) => `
<div class="bg-white border border-[#ebedf0] rounded-xl p-4">
<div class="text-2xl font-bold text-brand">${v}</div>
<div class="text-2xl font-bold text-nomad">${v}</div>
<div class="text-xs text-tg mt-1">${l}</div></div>`).join('');
loadQueue();
}
@@ -175,9 +175,9 @@ async function uploadArchive(input) {
box.innerHTML = `<span class="text-[#dc2626]">Ошибка: ${r.detail || res.status}</span>`;
} else {
const note = r.embeddings ? '' : ' <span class="text-tg">(нормализация по кодам и нечёткому совпадению — без эмбеддингов)</span>';
box.innerHTML = `<div class="bg-brand-soft border border-[#ffd9cc] rounded-lg p-3">
box.innerHTML = `<div class="bg-nomad-soft border border-[#ffd9cc] rounded-lg p-3">
✓ Архив обработан: <b>${r.documents}</b> прайс(ов), <b>${r.items}</b> позиций,
автонормализация <b class="text-brand">${r.auto_matched_pct}%</b>${r.new_partners ? ', новых клиник ' + r.new_partners : ''}.${note}
автонормализация <b class="text-nomad">${r.auto_matched_pct}%</b>${r.new_partners ? ', новых клиник ' + r.new_partners : ''}.${note}
<div class="text-tg mt-1">Новые позиции уже доступны в поиске и в очереди верификации.</div></div>`;
loadAdmin(); // обновить дашборд и очередь под новые данные
}
@@ -201,7 +201,7 @@ function maybeRefill() {
async function loadCounts() {
const c = await fetch(`${API}/unmatched/count`).then(r => r.json());
$('vprogress').innerHTML = `осталось <b>${c.remaining}</b> · обучено синонимов <b class="text-brand">${c.learned_synonyms}</b>`;
$('vprogress').innerHTML = `осталось <b>${c.remaining}</b> · обучено синонимов <b class="text-nomad">${c.learned_synonyms}</b>`;
}
function renderCard(it) {
@@ -211,7 +211,7 @@ function renderCard(it) {
? `<div class="text-sm mt-1">предложение: <b>${sug.name_ru}</b> <span class="text-tg">${sug.specialty || ''} · ${(sug.score || 0).toFixed(2)}</span></div>`
: `<div class="text-sm text-tg mt-1">кандидата нет</div>`;
const confirm = sug
? `<button onclick="doMatch(${it.item_id}, '${sug.service_id}', this)" class="bg-brand text-white text-sm rounded-lg px-3 py-1.5 hover:bg-brand-dark">✓ Подтвердить</button>`
? `<button onclick="doMatch(${it.item_id}, '${sug.service_id}', this)" class="bg-nomad text-white text-sm rounded-lg px-3 py-1.5 hover:bg-nomad-dark">✓ Подтвердить</button>`
: '';
return `<div id="card-${it.item_id}" class="bg-white border border-[#ebedf0] rounded-xl p-4">
<div class="flex justify-between items-start gap-3">
@@ -228,7 +228,7 @@ function renderCard(it) {
</div>
<div id="pick-${it.item_id}" class="hidden mt-3">
<input oninput="searchDict(${it.item_id}, this.value)" placeholder="найти услугу в справочнике…"
class="w-full bg-white border border-[#d1d5db] rounded-lg px-3 py-2 text-sm outline-none focus:border-brand">
class="w-full bg-white border border-[#d1d5db] rounded-lg px-3 py-2 text-sm outline-none focus:border-nomad">
<div id="dict-${it.item_id}" class="mt-1 bg-white border border-[#ebedf0] rounded-lg divide-y divide-[#f0f0f0] max-h-44 overflow-auto"></div>
</div>
</div>`;
@@ -256,7 +256,7 @@ function searchDict(itemId, q) {
if (q.trim().length < 2) return;
const items = await fetch(`${API}/services?q=${encodeURIComponent(q)}&limit=8`).then(r => r.json());
$(`dict-${itemId}`).innerHTML = items.map(s =>
`<button class="block w-full text-left px-3 py-2 text-sm hover:bg-brand-soft" onclick="doMatch(${itemId}, '${s.service_id}', null)">${s.name_ru} <span class="text-tg text-xs">${s.specialty || ''}</span></button>`).join('');
`<button class="block w-full text-left px-3 py-2 text-sm hover:bg-nomad-soft" onclick="doMatch(${itemId}, '${s.service_id}', null)">${s.name_ru} <span class="text-tg text-xs">${s.specialty || ''}</span></button>`).join('');
}, 250);
}
</script>