Metadata-Version: 2.5
Name: nyshporka
Version: 0.2.1
Summary: Читання рукописних архівних справ і пошук прізвища в них — для генеалогів
Project-URL: Homepage, https://github.com/SERGIUSH-UA/nyshporka
Project-URL: Source, https://github.com/SERGIUSH-UA/nyshporka
Project-URL: Issues, https://github.com/SERGIUSH-UA/nyshporka/issues
Project-URL: Changelog, https://github.com/SERGIUSH-UA/nyshporka/blob/main/CHANGELOG.md
Author: Serhii Dalishchynskyi
License-Expression: AGPL-3.0-or-later
License-File: LICENSE
Keywords: archives,genealogy,handwriting,htr,kraken,ocr,parseq
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Ukrainian
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Topic :: Sociology :: Genealogy
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: loguru>=0.7
Requires-Dist: pillow>=10.4
Requires-Dist: platformdirs>=4.3
Requires-Dist: psutil>=6.0
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: pydantic>=2.9
Requires-Dist: pypdfium2>=4.30
Requires-Dist: python-frontmatter>=1.1
Requires-Dist: pyyaml>=6
Requires-Dist: rapidfuzz>=3.10
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Dist: unidecode>=1.3
Provides-Extra: agent
Requires-Dist: anyio>=4.0; extra == 'agent'
Requires-Dist: mcp>=1.2; extra == 'agent'
Provides-Extra: all
Requires-Dist: aiolimiter>=1.1; extra == 'all'
Requires-Dist: aiosqlite>=0.20; extra == 'all'
Requires-Dist: anyio>=4.0; extra == 'all'
Requires-Dist: fastapi>=0.115; extra == 'all'
Requires-Dist: ged4py>=0.5; extra == 'all'
Requires-Dist: httpx[socks]>=0.27; extra == 'all'
Requires-Dist: keyring>=25; extra == 'all'
Requires-Dist: lxml>=5.3; extra == 'all'
Requires-Dist: mcp>=1.2; extra == 'all'
Requires-Dist: pdfplumber>=0.11; extra == 'all'
Requires-Dist: selectolax>=0.3.21; extra == 'all'
Requires-Dist: tenacity>=9; extra == 'all'
Requires-Dist: timm>=1.0; extra == 'all'
Requires-Dist: torch>=2.2; extra == 'all'
Requires-Dist: torchvision>=0.20; extra == 'all'
Requires-Dist: uvicorn[standard]>=0.32; extra == 'all'
Provides-Extra: app
Requires-Dist: fastapi>=0.115; extra == 'app'
Requires-Dist: uvicorn[standard]>=0.32; extra == 'app'
Provides-Extra: archives
Requires-Dist: aiolimiter>=1.1; extra == 'archives'
Requires-Dist: aiosqlite>=0.20; extra == 'archives'
Requires-Dist: httpx[socks]>=0.27; extra == 'archives'
Requires-Dist: keyring>=25; extra == 'archives'
Requires-Dist: lxml>=5.3; extra == 'archives'
Requires-Dist: pdfplumber>=0.11; extra == 'archives'
Requires-Dist: selectolax>=0.3.21; extra == 'archives'
Requires-Dist: tenacity>=9; extra == 'archives'
Provides-Extra: gedcom
Requires-Dist: ged4py>=0.5; extra == 'gedcom'
Provides-Extra: htr
Requires-Dist: timm>=1.0; extra == 'htr'
Requires-Dist: torch>=2.2; extra == 'htr'
Requires-Dist: torchvision>=0.20; extra == 'htr'
Provides-Extra: ocr
Requires-Dist: paddleocr>=3.5; extra == 'ocr'
Description-Content-Type: text/markdown

# Нишпорка

Читання рукописних архівних справ і пошук прізвища в них — для генеалогів.

Комерційні OCR не читають скоропис XVIII–XIX ст., а платформи, які читають,
платні й не мають моделей під матеріал українських, молдовських і польських
архівів. Нишпорка закриває саме цю прогалину: беремо теку сканів із архіву й
отримуємо текст, у якому можна шукати прізвище.

> **Стан: alpha.** Каталоги, завантаження, читання рукопису, гортач, сховище
> прочитаного, пошук, браузерне обличчя й установлення працюють. Ваги моделей
> ще не викладені релізом — доти читати можна лише з власними вагами. Що саме
> не готове, у розділі «Чого ще немає» нижче, без замовчувань.

## Установлення

На чистій машині — **без Python і без прав адміністратора**. Інсталятор
приносить `uv`, `uv` приносить власний інтерпретатор:

```powershell
# Windows
powershell -ExecutionPolicy Bypass -File install\windows.ps1
```
```sh
# Linux / macOS
sh install/unix.sh
```

Або звичайним способом, якщо Python уже є:

```bash
pip install 'nyshporka[app,archives,htr]'
nysh init                      # створити робочий простір
nysh doctor                    # перевірити те, що ламається тихо
nysh serve                     # відкрити застосунок у браузері
```

### Де лежить дослідження

Простір — тека, у якій живуть скани, прочитане й звіти. Типово `nysh init`
кладе її в `Документи/Нишпорка` (або поруч із домівкою, якщо ця тека
синхронізується з хмарою: обхід справ став би мережевим і виглядав би як
зависання).

```bash
nysh init D:/Дослідження          # обрати місце при створенні
export NYSHPORKA_WORKSPACE=D:/Дослідження   # закріпити для ВСІХ команд
nysh --workspace D:/Дослідження doctor      # разово, на один запуск
```
```powershell
$env:NYSHPORKA_WORKSPACE = 'D:\Дослідження'   # PowerShell, на поточну сесію
```

Без змінної команди шукають файл `nyshporka.toml` угору від поточної теки — тож
усередині простору нічого вказувати не треба. Куди дивиться застосунок зараз,
каже `nysh doctor`: перевірка «Робочий простір» друкує корінь і джерело
(`env:…`, `marker`, `explicit`).

### Що саме ставимо

Нишпорку ставлять дуже різні люди, і показувати всім однакове — означає комусь
брехати. `nysh init` питає, чим ви користуватиметесь; змінити відповідь можна
будь-коли (`nysh sections`), нічого не перевстановлюючи.

| Набір | Що є | Вага |
|---|---|---|
| `catalog` | каталоги, газетир, описи фондів | без рушіїв |
| `amateur` | + читання рукопису й гортач | + torch |
| `researcher` (типово) | + пошук у прочитаному, облік переглянутого, експорт | + torch |
| `lab` | + місце для розмітки й тренування (поки порожнє) | + torch |

```powershell
powershell -ExecutionPolicy Bypass -File install\windows.ps1 -Preset catalog
```
```sh
NYSH_PRESET=catalog sh install/unix.sh
```

🔴 **Вимкнена частина вимкнена всюди**, а не лише в шапці: її дії відмовляють і
в браузері, і в командному рядку, і в агента — з назвою секції та командою, якою
її увімкнути. Напівстан («кнопки немає, але команда працює») тут гірший за
відсутність: він читається як несправність.

`catalog` — єдиний набір без `torch` (~2.5 ГБ). Він точно описує найпершого
відвідувача: сканів ще немає, відеокарти теж, а питання вже є — «де взагалі
метрики мого села». Обидва вкладені зрізи відповідають на нього одразу після
встановлення, тож це робочий набір, а не урізаний.

Типово ставиться CPU-збірка torch. Задача читання впирається в **ядра
процесора**, не у відеокарту (виміряно: 92% часу CPU при завантаженні карти
близько нуля), тож на CPU все працює — просто повільніше: ~2 хв на сторінку
проти ~20 с. Прискорення відеокартою доставляється окремим кроком
(`nysh doctor` підкаже яким), а не вимагається збіркою.

## Що вже працює

**Два зрізи їдуть разом із пакетом**, тож `nysh find "моє село"` працює
**одразу після встановлення** — ні сканів, ні відеокарти, ні обходу чужих
сайтів:

* **каталог ДАХмО** — 9020 справ 47 фондів, роки 1648-2025;
* **поаркушевий покажчик плівок** — 62 412 записів, 1594 населені пункти:
  яке село на яких КАДРАХ якої плівки. Це найкоротша відповідь на «де метрики
  мого села», і вона не вимагає завантажити жодного байта сканів.

🔴 Зрізи старіють, тож кожна відповідь несе їхню дату й межі покриття
(покажчик плівок сьогодні накриває лише Молдову — в інших регіонах дзеркала
поаркушевого покажчика немає в принципі). Зібране на місці має пріоритет над
вкладеним.

**Каталоги й завантаження.** Переглядачі архівів (ARCHIUM — обласні та
центральні), дзеркало плівок FamilySearch і Wikimedia Commons — за спільним
контрактом джерела: `search` / `browse` / `manifest` / `fetch`. Найцінніше в
дзеркалі — **поаркушевий покажчик плівки**: він відповідає «де метрики мого
села» без жодного завантаження. Найцінніше в Commons — **повний файл**: дзеркала
обрізають великі справи в рази (25 МБ проти 771 МБ), і виглядає обрізане як
нормальна копія.

```bash
nysh find "Ракулешты"                  # де взагалі є щось про село
nysh browse fsfilm moldova             # що лежить у регіоні
nysh get fsfilm "<плівка>" --out . --frames 6-10
nysh crawl archium                     # зібрати каталог справ для пошуку
```

**Що взагалі існує у фонді.** Окреме питання від «що вже оцифровано», і саме
воно вирішує, чи має сенс замовляти документ в архіві. Складають відповідь
збирачі реєстру опису:

```bash
nysh registry sources                                  # які збирачі є
nysh registry plan  duck --repo ДАВіО --fond 904       # скільки це коштуватиме
nysh registry collect archium --repo ЦДІАК --fond 224 --fond-id 198
nysh registry rate                                     # чи витримали темп
```

Приймач збирання — **не число рядків**. Позиційний розбір таблиці опису вже
одного разу віддав 2944 справи з однаковим заголовком, і за кількістю це
виглядало успіхом. Тому кожен запуск друкує, скільки рядків мають роки, аркуші
й заголовок, і **чого джерело не бачить**: позиції «вільний номер» і «Справа
вибула» лягають окремо — пущені в реєстр, вони стають фантомами в черзі, за
якою замовляють документи.

🔴 Duck Inspector — безкоштовний волонтерський сервіс, і його ліміт (5 запитів
на 10 с) міряється **по клієнту**, а не по процесу. Тому запити йдуть через
чергу, спільну на всю машину: дві сесії з бездоганною паузою кожна дали б
подвійний темп. `nysh registry rate` показує максимум у вікні за журналом
фактичних відправок — не за наміром.

⚠ Зібрані джерела ще не зводяться в один реєстр опису: складати `registry/*.tsv`
пакет уміє, зливати їх — поки ні.

**Читання рукопису.** `nysh read <тека>` або екран «Читання»: план (скільки
кадрів, яке письмо, яка модель) показується ДО запуску, бо справа читається
годинами. Модель обирається за письмом і за файлом бойових ваг — «найновіша» ≠
«найкраща». Другий рушій читає ті самі кропи: він помиляється ІНАКШЕ й витягує
те, де перший підставив правдоподібне слово.

**Гортач.** Вирізка рядка з рамкою — щоб було видно, ЗВІДКИ взявся текст.
Виявити ≠ перевірити: машина подає кандидата, вирішує око. Дефолт — рядок, бо
сторінка коштує в десятки разів дорожче (виміряно: 15 КБ проти 1.1 МБ).

**Своя тека стає справою.** Скани, зняті в архіві чи прислані колегою, не мають
шифри — а без ключа в них немає ні обліку прочитаного, ні місця в реєстрі, ні
можливості послатись на знахідку. Екран «Завести справу» (або `nysh case`)
приймає шифру в тих формах, якими її справді пишуть — `ДАХмО 315-1-8433`,
`ф.315 оп.1 спр.8433`, `Ф. 211 Оп. 3 Д. 140`. Опис пишеться **в теку**: вона
переїжджає між дисками й потрапляє до колег, і опис їде з нею. Кнопка ✏ у
переліку відкриває записане для правки — щоб змінити одне слово, а не
передруковувати все наосліп.

**Скани можуть лежати де завгодно.** Не обов'язково всередині простору: тека на
зовнішньому диску чи в мережі береться під облік там, де лежить, — позначкою у
формі (або полем `case_roots` у `nyshporka.toml`). Файли не переносяться.
Розширення зони завжди явне: застосунок ніколи не бере теку сам, бо шлях у
гортач приходить із запиту браузера, і «дозволено все» тут коштувало б надто
дорого. Тека всередині простору лишається записаною відносним шляхом — щоб
простір можна було перенести на інший диск чи віддати колезі.

**Зразкова справа в комплекті.** `nysh sample` (або кнопка на екрані
«Перевірити цю машину») кладе в простір три аркуші справи **ДАХмО ф.315 оп.1
спр.159** — про висвячення в диякони, 1821-1822 — **разом із готовим машинним
декодом двома голосами**. Це відповідь на перше питання після встановлення:
клацнувши рядок у гортачі, видно, ЗВІДКИ взявся текст, а пошук по декоду
знаходить у ньому прізвище. Прочитати ці аркуші заново поки нічим — ваги ще не
викладені, — але весь ланцюг після читання можна пройти до того, як вкладати
власні три тисячі сканів.

**Довідники окремим комплектом.** Газетир зведеного каталогу ЦДІАК (4566
поселень, 348 408 справ) і реєстри опису чотирьох фондів ставляться окремо від
програми — дані оновлюються не тоді, коли код:

```bash
nysh catalog install --from <завантажений zip>   # releases
nysh geog find "Липовеньке"      # де взагалі є документи цього села
```

Газетир відповідає на питання, з якого починається пошук: **які взагалі метрики
цього поселення вціліли і що з них уже у вас**. Шукає обома мовами й латинкою —
`Miastkowka` знаходить те саме, що й кирилицею; раніше такий запит давав нуль, а
це найгірший вид нуля, бо його читають як «такого села немає». І показує три
конфесії окремо: метрики православної громади, костелу й рабинату лежать у
різних фондах, тож шукати лише в православному розділі означає не бачити решти.

**Сховище прочитаного.** Облік того, що вже переглянуто оком — щоб наступна
сесія не гортала ті самі аркуші вдруге. Пошук по прочитаному: у машинному
декоді, у виписаних прізвищах, в учасниках розібраних записів.

**Реєстр справ.** Що є на диску, що прочитано машиною, що прошукано, що бачило
око — з попередженням, коли зріз відстав від джерел.

**Три обличчя, одне ядро.** Браузерна консоль, командний рядок і MCP-сервер для
Claude Code / Codex — тонкі обгортки навколо одного реєстру операцій. Коли
правда одна, вони не можуть розійтись у відповідях; це перевіряється тестом, а
не домовленістю. Працювати з агентом не обов'язково — без нього застосунок
повний.

Якщо агент таки береться до роботи, він читає [`AGENTS.md`](AGENTS.md) і
[`docs/agents/`](docs/agents/): що вміє, **чого не вміє**, як читати нуль,
де межа, за якою вирішує людина, і як агенти вже помилялися на цьому матеріалі.
Причина окремої теки проста — у перелік інструментів іде однорядковий підпис
операції, а не її докстрінг, тож усе, чому саме так, довелось написати окремо.

## 🔴 Нуль мусить щось означати

Це головне правило проєкту, і воно вбудоване в код, а не в інструкцію.

Порожній результат пошуку — найдорожча відповідь у генеалогії: «немає» закриває
напрям назавжди. Тому джерело, яке **не може** шукати (каталог не зібраний,
дерево регіону не завантажене), не додає нуль до суми — воно відмовляється
відповідати й каже, чого бракує:

```
⚠ archium: каталог справ ще не зібрано, тож шукати нема де — і нуль тут нічого
  не означав би: вбудований пошук сайту індексує лише назви фондів і описів.
⚠ жодне джерело не змогло шукати — цей нуль НІЧОГО не означає
```

Кожна відповідь несе `coverage`: де саме шукали. Кожне попередження їде **полем
конверта**, а не лише в лозі, — інакше саме той читач, який не помітить нічого
поза даними (агент), лишався б без попередження.

## Чого ще немає

Чесно, без замовчувань:

* **Ваги моделей не викладені.** `nysh models get` знає, що качати, але реліз із
  вагами ще не складено; доти пак без sha256 не приймається взагалі — модель, про
  цілість якої нічого не відомо, читатиме справу годинами й видасть текст, що не
  відрізняється від поганого почерку. Власні ваги в
  `<простір>/data/spotter/models` працюють уже зараз.
* **Гортач бачить 86% прогонів.** Переміряно на 614 прогонах: готовий скан у
  519, рендер зі справи-PDF ще у 7. Решта 88 — здебільшого збірки, у яких теки
  однієї справи немає в принципі, і прогони, чия тека лишилась на чужій машині;
  другі лікуються `nysh cases bind`. Для тих, що видно, аркуш тепер показується
  правильно й на рендері теж — раніше кроп рядка там з'їжджав.
* **Покажчик плівок накриває лише Молдову.** Не наша межа: в інших регіонах
  дзеркала `folder_meta` це голий підпис теки, поаркушевого переліку там немає.
* **Описи є не для всіх фондів.** Готові зрізи чотирьох фондів приходять
  довідниками (нижче); решту довелося б збирати самому, а збирачів у цьому
  пакеті немає.
* **Зразок не читається заново — лише все після читання.** Три аркуші справи
  ДАХмО 315-1-159 їдуть у пакеті вже з машинним декодом, тож гортач, пошук і
  реєстр працюють на них одразу; а прогнати по них САМЕ ЧИТАННЯ нічим, доки
  немає ваг. Це та сама межа, що й у першому пункті, і зникне вона разом із ним.
* **Кандидатів нема кому подавати.** Людський gate `nysh review` працює, але
  пишуть у нього fetcher'и чужих сайтів, яких у цій версії немає (питання їхніх
  умов використання в роздаваному продукті). На щойно створеному просторі черга
  порожня — це стан, а не поламка.

## Розробка

```bash
uv sync --group dev
uv run pytest
uv run ruff check . && uv run mypy
pre-commit install            # ворота проти приватних даних
```

### 🔒 Ворота проти приватних даних

Пакет виділяється з приватного дослідницького репозиторію однієї родини, і
головна небезпека тут — не зловмисник, а випадковість: один `git add -A`,
скопійований для прикладу шматок коду з реальним ідентифікатором особи, шлях із
машини автора в докстрінгу.

```bash
python tools/scan_private.py             # робоче дерево
python tools/scan_private.py --staged    # індекс (стоїть у pre-commit)
python tools/scan_private.py --history   # уся історія, перед першим push
```

Перевірка стоїть **до** коміту, бо git не забуває: файл, доданий і видалений
наступним комітом, лишається в історії назавжди, а прибрати його означає
переписати вже опубліковану гілку.

## Ліцензія

[AGPL-3.0-or-later](LICENSE).

Причина конкретна: конвеєр спирається на `ultralytics` і `PyMuPDF`, обидва під
AGPL-3.0. Решта стеку читання чиста (kraken, PARSeq, timm, torch — Apache/BSD),
тож вибір був між «викинути дві залежності» і «прийняти AGPL». Прийняли AGPL:
код усе одно відкритий, а копілефт тут радше плюс.

⚠ Пакет `strhub` містить підмодуль `models/abinet` під non-commercial ліцензією
USTC. Нишпорка використовує з нього **лише PARSeq** (Apache-2.0).
