docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes)

This commit is contained in:
“Naeel”
2026-08-24 15:23:31 +03:00
parent 25e4b46e76
commit db7cdbdd84
69 changed files with 0 additions and 0 deletions
@@ -0,0 +1,32 @@
# Запрос к Соннету — ERR_TIMED_OUT при открытии нового окна (2026-07-12)
## Симптом
DrHider на Managed Flask (Штурвал/pythonk8s). При открытии страницы в НОВОМ окне браузера — ERR_TIMED_OUT (20+ секунд). Старые вкладки работают нормально.
## Факты
- Изнутри кластера (kubectl exec → curl): HTTP 200 за 2.1 секунды
- Снаружи (браузер, curl с локальной машины): таймаут 10+ секунд
- v1 работало с gunicorn (CMD: `gunicorn --bind :5000 --worker-class gevent`)
- Сейчас: `app.run(host="0.0.0.0", port=5000)` — Flask dev server (Werkzeug)
- Страница: 12 KB HTML, 2 статических файла (logo.svg, favicon.svg — оба 2.8 KB)
- Ноль внешних зависимостей (никаких CDN, шрифтов, JS-библиотек)
- HTTP-заголовки: Cache-Control: no-cache, no-store, must-revalidate
- Штурвал запускает: cd site && python app.py
## Вопросы
1. Может ли Flask dev server (Werkzeug) быть причиной ERR_TIMED_OUT на новых соединениях, в то время как gunicorn работал нормально?
2. Если да — какой механизм? (keep-alive, thread pool, socket backlog?)
3. Если нет — что ещё может вызывать таймаут именно на НОВЫХ окнах при работающих старых?
4. Нужно ли вернуть gunicorn или есть другой способ заставить app.run() работать стабильно?
5. Может ли `Cache-Control: no-store` заставлять браузер делать лишние запросы при открытии нового окна?
## Контекст платформы
Штурвал — Managed Flask на Kubernetes:
- Запускает: `cd /var/www && pip install -r requirements.txt && cd site && python app.py`
- Readiness probe: `tcp-socket :5000`
- Нет Dockerfile, нет nginx перед приложением
- Ingress: внешний LoadBalancer → ClusterIP сервис → под
@@ -0,0 +1,83 @@
# Запрос к Соннету — анализ результатов DrHider v3 (2026-07-12)
## Контекст
DrHider v3 — обфускация документов через LLM + Markdown. Результаты уже есть.
Покажи Соннету реальный mapping.csv и фрагмент обфусцированного .md.
## Промпт для Соннета
Вот текущий LLM-промпт в scanner.py:
```
You are a PII detector. Find ALL sensitive or private information in the text below.
Return a JSON array. Each item must have:
- "type": short snake_case label describing the entity
(e.g. person_name, phone, email, address, inn, company, passport,
contract_number, employee_id, bank_account — WHATEVER you see)
- "value": the EXACT string from the text (copy verbatim)
Rules:
- Copy value VERBATIM — same spaces, punctuation, case as in text
- If same value appears multiple times — include only once
- Invent the type label yourself based on what you see
- Return ONLY the JSON array, no other text, no markdown wrapping
```
Результат mapping.csv (первые 10 строк):
```csv
тип_данных,оригинал,замена
text,"""****XXX****001****""","""****IPD****084****"""
text,"""НУБЕС""","""ФИОЩЦ"""
phone,+7 (495) 789-41-35,+7 (343) 986-75-93
email,info@nubes.ru,bxkuno@company.local
company,"ЗАО ""****XXX****001****""",ООО «Спектр»
company,"ЗАО ""XXX001""",ЗАО «Меридиан»
inn_ul,ИНН 9706005293,ИНН 9325731296
text,Исполнителем Заказчику Сторонами,Морозов М.П.
kpp,КПП 772401001,КПП 051543039
```
Фрагмент обфусцированного .md:
```markdown
**ООО «Спектр»** именуемое в дальнейшем "Заказчик", в лице Генерального директора ФИО,
действующего на основании Устава, и **ООО **"**НУБЕС**"**, именуемое в дальнейшем
"Исполнитель", в лице Генерального директора Степанов Н.М.
```
## Проблемы
1. **Контекстная роль Исполнителя** — сервис должен быть УНИВЕРСАЛЬНЫМ, без хардкода конкретных названий. LLM должна из контекста понимать: «вот эта компания — Исполнитель (сторона договора, оказывающая услуги), её НЕ заменяем». Как научить LLM этому без указания конкретных имён?
2. **Тип "text" для многих сущностей** — LLM не всегда правильно определяет тип (например, "Исполнителем Заказчику Сторонами" — это не ПДн, а просто текст договора)
3. **Regex находит структурированное (ИНН, телефон), LLM — остальное** — есть дублирование
## Вопросы
1. Как научить LLM определять Исполнителя из контекста договора (без хардкода имён) и НЕ заменять его данные?
2. Как заставить LLM точнее определять тип сущности (не "text" для всего подряд)?
3. Нужно ли добавить в промпт примеры того что НЕ является ПДн (юридические термины, роли сторон)?
4. Стоит ли передавать LLM уже найденное regex'ом чтобы избежать дублирования?
5. Правильно ли что промпт на английском, а документы на русском?
## Текущий промпт (полностью)
```
You are a PII (Personally Identifiable Information) detector.
Find ALL sensitive or private information in the text below.
Return a JSON array. Each item must have:
- "type": short snake_case label describing the entity
(e.g. person_name, phone, email, address, inn, company, passport,
contract_number, employee_id, bank_account — WHATEVER you see)
- "value": the EXACT string from the text (copy verbatim)
Rules:
- Copy value VERBATIM — same spaces, punctuation, case as in text
- If same value appears multiple times — include only once
- Invent the type label yourself based on what you see
- Return ONLY the JSON array, no other text, no markdown wrapping
Text:
{combined[:8000]}
```
@@ -0,0 +1,47 @@
# Запрос к Соннету — нумерация вместо генерации фейков (2026-07-12)
## Контекст
DrHider обфусцирует документы: ищет ПДн → заменяет на фиктивные значения.
Сейчас используется генерация "похожих" фейков (Иванов И.И., ООО «Ромашка», +7(495)...).
## Проблема
1. **Адреса → абракадабра.** Ул. Стекольная → иг. 8ц Лжчюсвхшбр. Потому что fallback-генератор меняет кириллицу на случайную кириллицу.
2. **Сложность.** 11 генераторов под каждый тип данных. Каждый надо поддерживать.
3. **Ложное ощущение реальности.** Фейковые ИНН с правильной контрольной суммой выглядят как настоящие.
## Идея
Заменить всю генерацию на **нумерацию по типам:**
| Оригинал | Замена |
|---|---|
| Иванов И.И. | `Лицо_0042` |
| ООО «НУБЕС» | `Компания_0003` |
| ул. Стекольная, д.7 | `Адрес_0015` |
| +7 (495) 789-41-35 | `Телефон_0007` |
| info@nubes.ru | `Email_0004` |
| ИНН 9706005293 | `ИНН_0002` |
| Р/с 40702... | `Счёт_0001` |
| договор-XXX001.docx | `Файл_0006` |
## Вопросы
1. **Плюсы/минусы** нумерации vs генерации фейков для задачи обфускации документов?
2. **Как нумеровать?** Глобальный счётчик на каждый тип? Или в рамках одного документа?
3. **Стоит ли сохранять формат?** Например `Телефон_0007` vs `+7 (000) 000-00-07`?
4. **mapping.csv** — при нумерации он становится главным документом аудита. Это ок?
5. **UUID вместо номеров?** `Лицо_a3f4b2` — лучше или хуже последовательных номеров?
## Текущий mapping.csv (для контекста)
```csv
тип_данных,оригинал,замена
phone,+7 (495) 789-41-35,+7 (495) 906-87-80
email,info@nubes.ru,ypiorej@company.local
company,"ЗАО ""XXX001""",ООО «Альянс»
inn_ul,ИНН 9706005293,ИНН 1846691249
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
text,"115404, г. Москва, ул. 1-я Стекольная, д.7с2","084810, б. Ввжзгд, гф. 6-в Фтпжыобнзщ, я.8с1"
```
@@ -0,0 +1,54 @@
# Запрос к Соннету — нумерация vs фейки для downstream LLM (2026-07-12)
## Контекст
Я делаю сервис **DrHider** — обфускация документов. Находит ПДн → заменяет.
После обфускации документы идут на дальнейшую LLM-обработку — **сверка спецификаций**
(другой сервис contracts, извлекает типы/номера/даты/контрагентов через LLM).
Сейчас генерация фейков: `Иванов И.И.` → `Петров А.С.`, `ООО «Ромашка»` → `ООО «Спектр»`.
**Проблема:** для неизвестных типов (адреса, нестандартные ID) — character-class замена
даёт абракадабру: `ул. Стекольная` → `иг. 8ц Лжчюсвхшбр`. Downstream LLM не может это обработать.
## Варианты замены
### А. Чистая нумерация
`Иванов И.И.` → `Лицо_0042`, `ООО «Ромашка»` → `Компания_0003`, `ул. Ленина` → `Адрес_0015`
Плюс: никакой абракадабры. Минус: LLM теряет контекст («Лицо_0042 заключил договор» — не понимает семантику).
### Б. Фейки (сейчас)
`Иванов И.И.` → `Петров А.С.`, `ООО «Ромашка»` → `ООО «Спектр»`
Плюс: LLM понимает роли. Минус: сложно (11 генераторов), ложное ощущение реальности,
для неизвестных типов — абракадабра.
### В. Гибрид: шаблон + номер
`Иванов И.И.` → `Иванов_5677`, `ООО «Ромашка»` → `ООО_Технология_0034`,
`ул. Стекольная, д.7` → `ул_Ленина_0015`, `+7 (495) 789-41-35` → `+7_495_000_0042`,
`info@nubes.ru` → `email_0007@fake`
Плюс: LLM видит что это человек/компания/адрес, номер гарантирует уникальность и обфускацию.
Минус: всё равно нужны шаблоны для каждого типа.
## Вопросы
1. Какой вариант лучше для downstream LLM-обработки (извлечение контрагентов, дат, номеров)?
2. Вариант В — достаточно ли LLM поймёт что `Иванов_5677` это персональные данные?
3. Для неизвестных типов — `[Данные_NNNN]` как fallback, ок?
4. Глобальная нумерация (сквозная через все документы пакета) vs локальная (в рамках одного файла)?
5. Стоит ли сохранять частичную структуру (например телефон `+7_495_000_0042` сохраняет формат номера)?
## Downstream задача (для контекста)
LLM в сервисе contracts получает текст документа и должна извлечь:
- Контрагент («с кем договор»)
- Номер и дату договора
- Список услуг с ценами (таблица)
Промпт для contracts LLM (фрагмент):
```
Извлеки из текста: тип документа, номер, дату, контрагента.
Верни JSON: {"type":"договор","number":"...","date":"...","counterparty":"..."}
```
+76
View File
@@ -0,0 +1,76 @@
# Запрос к Соннету — универсальная архитектура DrHider
## Контекст
Проект DrHider — обфускация документов (замена персональных данных на фиктивные). Сейчас:
- Python/Flask, без БД, Managed Flask на платформе Штурвал
- Двухпроходная архитектура: сбор сущностей → замена
- Проход 1: regex-паттерны (телефон, email, ИНН, ОГРН, КПП, БИК, счета, паспорт) + LLM NER (ФИО, компании, адреса, паспорта)
- Проход 2: замена по словарю mapping
## Проблема
Архитектура **в корне неверна** — жёстко привязана к фиксированному списку типов сущностей:
```python
# config.py — жёстко зашитые паттерны
ENTITY_PATTERNS = {
"phone": r'...',
"email": r'...',
"inn_fl": r'ИНН\s*\d{12}',
...
}
```
```python
# scanner.py — жёстко зашитый промпт
prompt = (
"Найди ВСЕ следующие сущности:\n"
"1. ФИО\n2. Компании\n3. Адреса\n4. Паспортные данные\n"
)
```
Если новый документ содержит СНИЛС, водительское удостоверение, номер договора, ИНН без префикса «ИНН» — система их НЕ обнаружит. Надо править config, generators, scanner, маппинги.
## Что нужно
**Универсальная архитектура**, где система САМА определяет что скрывать в любом документе:
1. Не привязана к списку типов
2. Не требует добавления паттернов под каждый новый вид данных
3. LLM сама решает что является персональными/конфиденциальными данными
4. Генерация фиктивных значений — тоже универсальная (не 11 отдельных функций)
## Текущая структура (полная)
```
drhider/
├── config.py # ENTITY_PATTERNS (10 regex), словари имён/городов
├── checksum.py # Контрольные суммы ИНН/ОГРН
├── random_utils.py # random_digits, random_letters
├── generators/ # 11 файлов: phone, email, inn, ogrn, kpp, bik, accounts, passport, company, person, address
├── llm_client.py # HTTP-клиент к LLM API
├── extractor.py # Извлечение текста + expand_zips + convert_pdfs_to_docx
├── scanner.py # scan_regex (regex) + scan_llm_ner (LLM NER с жёстким промптом)
├── replacer.py # apply_replacements, replace_in_docx, replace_in_text
├── builder.py # build_zip, build_mapping_csv
├── obfuscator.py # TwoPassObfuscator — оркестратор
├── site/app.py # Flask (3 blueprint'а)
└── site/templates/ # HTML-интерфейс
```
## Вопросы к Соннету
1. Как перестроить архитектуру чтобы обнаружение было универсальным (LLM сама решает что скрывать)?
2. Нужен ли regex вообще или достаточно одного LLM с правильным промптом?
3. Как сделать генерацию фиктивных значений универсальной? (Не 11 функций под каждый тип, а что-то общее)
4. Двухпроходная схема (сбор→замена) — сохранять или перейти на однопроходную (LLM сразу возвращает обфусцированный текст)?
5. Как должен выглядеть промпт чтобы LLM возвращала структурированный результат (что найдено + на что заменить)?
6. Стоит ли сохранять DOCX-форматирование (сейчас runs склеиваются-разделяются) или проще отдать LLM plain text?
## Ограничения
- Документы до 200 MB
- Форматы: .docx, .pdf, .txt, .zip
- LLM: OpenAI-совместимое API (aillm.ru, модель gpt-oss-120b, 8000 токенов)
- Без БД, всё в памяти
- Платформа: Managed Flask на Kubernetes
@@ -0,0 +1,19 @@
# Ответ Соннета — ERR_TIMED_OUT (2026-07-12)
## Причина
Werkzeug dev server: **один поток на TCP-соединение**. Браузер держит 6 keep-alive соединений на вкладку. 6 потоков заняты idle-соединениями → backlog (5) переполнен → новое окно получает SYN-дроп → таймаут.
gunicorn+gevent: async I/O, idle-соединения не занимают ресурсы.
## Решение
Три варианта:
| Вариант | Что | Изменения |
|---|---|---|
| A (waitress) | Pure Python WSGI | `requirements.txt` + 2 строки в `app.py` |
| B (gevent) | Async, как gunicorn | `requirements.txt` + 2 строки в `app.py` |
| C (gunicorn) | Точно как v1 | `requirements.txt` + смена CMD в Штурвале |
Соннет рекомендует: **A** если Штурвал не меняет CMD, **C** если меняет.
@@ -0,0 +1,47 @@
# Ответ Соннета v2 — улучшение LLM-промпта (2026-07-12)
## Q1. Как научить LLM определять Исполнителя из контекста?
**Двухфазная инструкция в промпте:**
- STEP 1: определи кто «Исполнитель» — его данные НЕ трогать
- STEP 2: найди всё остальное ПДн
Убирает хардкод `НУБЕС` из кода. Работает на любом договоре.
## Q2. Точная классификация типов
Вместо `"Invent the type label yourself"` → **фиксированный список:**
`person_name, company, phone, email, address, inn, kpp, ogrn, bik, passport, contract_number, bank_account, other_id`
Если сомневаешься — НЕ включай.
## Q3. Примеры что НЕ является ПДн
Обязательно добавить блок:
```
NOT PII:
- Юридические роли: Исполнитель, Заказчик, Стороны
- Термины договора: Услуги, Приложение, Договор, НДС
- Должности: Генеральный директор
- Сама компания-Исполнитель
```
## Q4. Передавать LLM уже найденное regex'ом
`scan_llm_ner` получает `mapping.keys()` → промпт: "Already captured (skip these): ..."
Исключает дублирование, экономит токены.
## Q5. Язык промпта
Гибрид: структурные инструкции на английском, NOT PII и роли — на русском.
---
## План реализации (scanner.py)
1. Убрать хардкод `НУБЕС` из `scan_regex`
2. `scan_llm_ner` — передавать `mapping.keys()` как "Already captured"
3. Новый промпт:
- Фиксированный список типов
- Блок NOT PII (русский)
- Двухфазная инструкция (STEP 1: Исполнитель, STEP 2: остальное)
@@ -0,0 +1,46 @@
# Ответ Соннета v3 — нумерация вместо генерации фейков (2026-07-12)
## Q1. Нумерация vs фейки
- ✅ Адреса без абракадабры, код проще, аудит прозрачнее
- ❌ Документ нечитаем (`Лицо_0042 заключил договор с Компания_0003`)
- Вывод: для аудита/хранения — отлично, для показа людям — фейки лучше
## Q2. Глобальный счётчик на весь пакет
Одна сущность в разных документах → один номер. `TwoPassObfuscator` хранит counters.
## Q3. Сохранять формат?
Рекомендация: `Тип_NNNN` — проще всего, аудит максимально прозрачен.
## Q4. mapping.csv — главный документ аудита
Да, это улучшение. mapping.csv — секретный ключ деобфускации.
## Q5. Номера vs UUID
Номера (`Лицо_0042`) — лучше для читаемости и аудита.
---
## План изменений
### Удалить
- `drhider/generators/` — все 12 файлов
- `drhider/checksum.py` — больше не нужен (ИНН без контрольных сумм)
- `drhider/random_utils.py` — больше не нужен
### Изменить
- `scanner.py`: добавить `TYPE_PREFIXES` + `_next_token()`, убрать импорты генераторов
- `obfuscator.py`: `TwoPassObfuscator` хранит `counters` dict
- `config.py`: убрать словари имён/городов (RU_SURNAMES, ...)
### Новая логика замены
```python
TYPE_PREFIXES = {
"person_name": "Лицо", "company": "Компания", "phone": "Телефон",
"email": "Email", "address": "Адрес", "inn": "ИНН", ...
}
_FALLBACK = "Данные"
def _next_token(entity_type, counters):
prefix = TYPE_PREFIXES.get(entity_type, _FALLBACK)
counters[prefix] = counters.get(prefix, 0) + 1
return f"{prefix}_{counters[prefix]:04d}"
```
@@ -0,0 +1,19 @@
# Ответ Соннета v4 — гибридный формат замен (2026-07-12)
## Итог: Вариант В (шаблон + номер)
| Тип | Формат замены | Пример |
|---|---|---|
| ФИО | `Фамилия_NNNN` | `Иванов_5677` |
| Компания | `ООО_Слово_NNNN` | `ООО_Технология_0034` |
| Адрес | `ул_Шаблон_NNNN` | `ул_Ленина_0015` |
| Телефон | `+7_код_000_NNNN` | `+7_495_000_0042` |
| Email | `email_NNNN@fake` | `email_0007@fake` |
| ИНН | `ИНН_NNNNNNNNNN` | `ИНН_0000000042` |
| Неизвестный тип | `[Тип_NNNN]` | `[Адрес_0023]` |
## Ключевые принципы
1. **Читаемо человеком** — не ужиматься, пусть будет естественно
2. **Однозначно для LLM** — префикс указывает тип сущности
3. **Глобальная нумерация** — одна сущность = один номер во всех файлах пакета
4. **Цены/суммы** — не обфусцировать (нужны для downstream сверки)
@@ -0,0 +1,78 @@
# Ответ Соннета — универсальная архитектура DrHider (2026-07-12)
Кратко: 6 вопросов → 6 ответов → карта изменений.
---
## Q1. Как сделать обнаружение универсальным?
Промпт должен звучать не «найди вот эти типы», а «найди ВСЁ, что выглядит как приватная информация, и сам назови тип».
Затрагивает: `scanner.py` (промпт), `builder.py` (тип для mapping.csv).
---
## Q2. Regex или LLM?
**Гибрид — правильный выбор:**
- **Regex = pre-pass** для структурированных данных (телефон, email, ИНН, БИК). Экономит токены LLM.
- **LLM = post-pass** для неструктурированного (имена, адреса, нестандартные ID).
- Дублирования не страшны — mapping dict сам отсеет.
---
## Q3. Генерация фиктивных значений — универсальная?
Два уровня:
1. **Известные типы** — специализированные генераторы (оставить как есть).
2. **Неизвестные типы** — fallback-генератор: character-class preserving замена (цифры→цифры, буквы→буквы той же длины).
Дополнительно: в промпт добавить `"category": "person|org|contact|id|financial|other"` — 6 категорий вместо 10+ типов.
---
## Q4. Двухпроходная или однопроходная?
**Двухпроходная — обязательно.** Причина: «Иванов» в 5 документах должен заменяться одинаково. Однопроход не может этого гарантировать.
Текущий `TwoPassObfuscator` — правильный, не трогать.
---
## Q5. Новый промпт
```
You are a PII detector. Find ALL sensitive or private information.
Return JSON array: [{"type": "short_label", "value": "exact_string"}]
Rules:
- Copy "value" VERBATIM from text
- "type" is snake_case label you invent
- Same value → include once
- Return ONLY JSON
```
Ключевое: нет ограничения на типы, value verbatim.
---
## Q6. DOCX или Markdown?
- **Внутренняя обработка:** Markdown проще парсить, LLM понимает лучше.
- **На выходе:** опция `output_format: "docx" | "md"`. По умолчанию `"docx"`.
---
## Итоговая карта изменений
```
scanner.py — новый промпт (убрать список типов, LLM сама называет типы)
generators/ — добавить fallback-генератор для неизвестных типов
obfuscator.py — вызывать fallback если тип неизвестен
api_bp.py — принять параметр output_format
replacer.py — добавить replace_to_markdown()
builder.py — упаковывать .md если output_format=md
```
**Не трогать:** двухпроходная схема, checksum-генераторы, ZIP-безопасность.
@@ -0,0 +1,39 @@
# Запрос к Соннету — архитектура пофайловой обработки с одним ZIP (2026-07-13)
## Задача
DrHider (Flask + waitress). Ограничения:
- Фронтенд должен слать файлы **по одному** (сеть не пропускает большие multipart)
- Бэкенд должен обрабатывать **по одному** (без параллельных LLM-запросов)
- Результат: **один ZIP** со всеми обфусцированными .md + **один общий** mapping.csv
- **Никакой обработки данных в JS** — фронтенд только кнопки и скачивание
## Варианты
### А. Upload → Process
1. `POST /api/upload` — один файл → `{session:"abc"}`
2. Фронтенд циклом шлёт все, получает session
3. `POST /api/process/abc` — бэкенд обрабатывает всё → ZIP
4. Фронтенд скачивает ZIP
### Б. Один запрос, все файлы
1. Фронтенд: один POST, все файлы в FormData
2. Бэкенд: получил все → обработал по одному → ZIP
3. Waitress держит долгое соединение, таймаут 600с
### В. SSE с прогрессом
1. `POST /api/start` → `{session:"abc"}`
2. `POST /api/upload/abc` — по одному файлу
3. `GET /api/process/abc` — SSE стримит прогресс каждого файла и в конце ZIP
## Вопросы
1. Какой вариант правильный для Flask + waitress?
2. Вариант Б — пройдёт ли большой multipart через ingress (6 файлов × 30-60 KB)?
3. Если А — как чистить сессии? (пользователь закрыл вкладку, файлы остались в памяти)
4. Нужен ли SSE или достаточно простого XHR с таймером?
5. mapping.csv — генерировать на лету (по мере обработки) или после всех файлов?
## Сейчас работает (но сломано)
Сейчас: фронтенд шлёт по одному файлу, каждый получает отдельный ZIP. Пользователь хочет один ZIP в конце. JS обрабатывал ZIP'ы (распаковывал/перепаковывал) — это убрали, всё должно быть на бэкенде.
@@ -0,0 +1,114 @@
# Запрос к Sonnet: анализ обфускации
**Дата:** 2026-07-13
---
**Задача:** Найти причину расхождения между ожидаемыми кодами замен и реальными заменами.
## Ожидаемое поведение
`_next_token()` в `drhider/scanner.py` генерирует коды замен формата:
- `Технология_0001`, `Прогресс_0002` (для компаний)
- `+7_000_000_0001` (для телефонов)
- `email_0001` (для email)
- `ИНН_0001` (для ИНН)
- `Иванов_0001` (для ФИО)
## Реальный результат — `TMP/mapping.csv`
```
тип_данных,оригинал,замена
phone,+7 (495) 789-41-35,+7 (495) 906-87-80 ← НЕ код замены, реальный номер
email,info@nubes.ru,ypiorej@company.local ← НЕ код замены, реальный email
company,ЗАО ****XXX****001****,АО «Прогресс» ← НЕ код замены, реальное название
company,ЗАО XXX001,ООО «Альянс» ← НЕ код замены, реальное название
inn_ul,ИНН 9706005293,ИНН 1846691249 ← НЕ код замены, реальный ИНН
kpp,КПП 772401001,КПП 350443068 ← НЕ код замены, реальный КПП
ks,Кор/сч 30101810400000000225,Кор/сч 30101201490272090211 ← НЕ код замены
ogrn,ОГРН 1207700098759,ОГРН 1048672318430 ← НЕ код замены
company,ООО "НУБЕС",ООО «Спектр» ← НЕ код замены
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
text,"Обязанности Заказчика\nСвоевременно",Степанов А.В.
text,"Обязанности Исполнителя\nОказывать",Петров П.Е.
text,"Ответственность Заказчика\nЗаказчик",Новиков А.М.
text,"Ответственность Исполнителя\nИсполнитель",Попов О.М.
company,ПАО Сбербанк,АО «Формат»
```
**Ни одного кода замены формата `XXX_0000` в колонке «замена».**
## Реальный результат — `TMP/договор-XXX001-03700_obfuscated.md` (первые строки)
```markdown
**АО «Прогресс»** ... и **ООО **"**НУБЕС**" ... Новиков И.В. ...
```
- `Новиков И.В.` — НЕ заменён (должен быть `Иванов_0001`)
- `ООО "НУБЕС"` — НЕ заменён в тексте (есть в mapping но замена не применена?)
- `г. Москва, 1-я Стекольная 7с5` — адрес не заменён
## Файлы для анализа
**Обязательно прочитать:**
1. `drhider/scanner.py` — `_next_token()`, `scan_regex()`, `scan_llm_ner()`, TYPE_POOLS, промпт LLM (строки 109-151)
2. `drhider/obfuscator.py` — порядок вызова regex → LLM, как объединяется mapping (строки 63-95)
3. `drhider/config.py` — ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN
4. `drhider/llm_client.py` — `LLMClient.complete()`
**Прочитать для контекста:**
5. `TMP/mapping.csv` — полный результат
6. `TMP/договор-XXX001-03700_obfuscated.md` — обфусцированный файл
7. `TMP/примеры_договоров_для_ИИ/договор-XXX001-03700.docx` — оригинал (для сверки)
**Не лезть:** `builder.py`, `extractor.py`, `replacer.py`, `session.py`, `templates/`, docs/, History/.
## Ключевые точки для проверки
### 1. `scanner.py` — `_next_token()` (стр. 63-82)
```python
def _next_token(entity_type, counters):
pool, prefix_key = TYPE_POOLS.get(entity_type, _FALLBACK_POOL)
template = random.choice(pool)
key = f"{prefix_key}_{template}"
counters[key] = counters.get(key, 0) + 1
return f"{template}_{counters[key]:04d}"
```
Возвращает `Технология_0001`, `+7_000_000_0001`. **Никак не может вернуть** `АО «Прогресс»` или `+7 (495) 906-87-80`.
### 2. `scanner.py` — `scan_regex()` (стр. 89-108)
```python
mapping[original] = _next_token(entity_type, counters)
```
Всегда вызывает `_next_token`. Результат должен быть кодом замены.
### 3. `scanner.py` — `scan_llm_ner()` (стр. 109-179)
```python
mapping[val] = _next_token(ent_type, counters)
```
Тоже вызывает `_next_token`. Результат должен быть кодом замены.
### 4. `obfuscator.py` — порядок вызова (стр. 86-92)
```python
scanner.scan_regex(text, self._mapping, self._counters) # сначала
scanner.scan_llm_ner(all_texts, self._mapping, ...) # потом
```
### 5. `config.py` — regex-паттерны:
- `phone`: `(?:\+7|8)[\s\-]?\(?\d{3}\)?...` — должно матчить `+7 (495) 789-41-35`
- `email`: `[a-zA-Z0-9._%+-]+@...` — должно матчить `info@nubes.ru`
- `inn_ul`: `ИНН\s*\d{10}` — должно матчить `ИНН 9706005293`
- `COMPANY_PATTERN`: `(?:ООО|ЗАО|...)\s+(?:«[^»]+»|"[^"]+"|...)` — должно матчить `ООО "НУБЕС"` и `ЗАО "XXX001"`
- `PERSON_PATTERN`: `[А-Я][а-я]+\s+[А-Я]\.[А-Я]\.` — должно матчить `Новиков И.В.`
## Главный вопрос
Каждый путь (regex и LLM) вызывает `_next_token()`. `_next_token()` физически не может выдать `АО «Прогресс»` или `+7 (495) 906-87-80`. Откуда в `mapping.csv` берутся реалистичные замены? Кто и где их генерирует В ОБХОД `_next_token()`?
@@ -0,0 +1,17 @@
# Ответ Соннета — архитектура upload+process (2026-07-13)
## Рекомендация: Вариант А + блокирующий /process
```
POST /api/upload → {session_id} (фронт шлёт по одному, N раз)
POST /api/process/{sid} → {status:"done"} (блокирует до конца обработки)
GET /api/download/{sid} → ZIP
```
Без polling — `/process` просто блокирует XHR до готовности. Waitress держит.
## Детали
- Mapping.csv — на лету, аккумулируется после каждого файла
- Сессии — dict в памяти, TTL 30 минут через threading.Timer
- Фронт: цикл upload → process → download. Три простых XHR.
- Никакого JS-кода обработки данных.
@@ -0,0 +1,70 @@
# Sonnet: анализ обфускации — TMP-файлы vs текущий код
**Дата:** 2026-07-13
---
## Главный вывод
**TMP-файлы созданы СТАРОЙ версией кода.** Они не были сгенерированы текущим `_next_token()`-кодом. Это не баг в текущем коде — это артефакт предыдущей реализации.
---
## Доказательства
### 1. Имена файлов — старый формат
Текущий `obfuscator.py` генерирует имена вида `договор-НУБЕС001-03700.md` (меняет расширение). TMP-файлы: `договор-XXX001-03700_obfuscated.md` — суффикс `_obfuscated` и `XXX`-маскировка в имени. Текущий код так не делает.
### 2. Тип `text` — fallback из builder.py, не из сканера
`build_mapping_csv()` определяет тип постфактум по паттернам. Если ни один не совпал → `"text"`. Типа `text` нет в `TYPE_POOLS` scanner.py. Значит строки вида:
```
text,Исполнителем Заказчику Сторонами,Кузнецов О.В.
```
попали из старого кода, который не фильтровал роли (Исполнитель, Заказчик) — текущий LLM-промпт их явно исключает.
### 3. Одна сущность → разные замены
```
email,info@nubes.ru,ypiorej@company.local ← файл 1
email,info@nubes.ru,qifdckke@org.example.ru ← файл 2
```
Текущий `TwoPassObfuscator` гарантирует глобальную согласованность. Старый код обрабатывал файлы по одному — отсюда расхождения.
### 4. Реалистичные замены невозможны из `_next_token()`
```python
TYPE_POOLS = {
"phone": (["+7_000_000"], "phone"), # → "+7_000_000_0001"
"email": (["email"], "email"), # → "email_0001"
}
```
`+7 (495) 906-87-80` или `ypiorej@company.local` физически не могут выйти из этих пулов.
---
## Ответ на главный вопрос
> Кто и где генерирует замены в обход `_next_token()`?
**Никто в текущем коде.** Это артефакты старого кода (версии до v3). Текущий `_next_token()` написан позже.
---
## Что реально нужно проверить
| Вопрос | Как проверить |
|--------|---------------|
| Работает ли текущий код? | Прогнать оригиналы через v0.0.12, посмотреть mapping.csv |
| Regex ловит все сущности? | Проверить `config.py` паттерны на примерах из TMP |
| Адрес `115404, г. Москва...` — почему не заменён? | `ENTITY_PATTERNS` не содержит тип `address` |
| `ООО "НУБЕС"` в тексте не заменён? | Возможно кавычки — regex матчит `«»` но не `""` |
---
## Итог
**Ложное противоречие** — сравнивались TMP-файлы (старый код) с текущим scanner.py. Нужно прогнать текущий код на тех же документах и посмотреть результат.
@@ -0,0 +1,37 @@
# Sonnet: анализ багов обфускации — fix
**Дата:** 2026-07-13
---
## Баг 1: `ООО "НУБЕС"` не заменено
**Причина:** DOCX → Markdown: `**ООО **"**НУБЕС**"`. Кавычка в отдельном run → `**` разрывает строку. Replacer ищет точное совпадение `ООО "НУБЕС"` — не находит.
**Фикс (replacer.py):** добавить `_md_tolerant_pattern()` — паттерн, допускающий `**` между частями строки. При промахе точного совпадения — фоллбэк с этим паттерном.
```python
def _md_tolerant_pattern(original: str) -> str:
MD = r'(?:\*{1,2})?'
parts = re.findall(r'[А-ЯЁа-яёA-Za-z0-9]+|[^А-ЯЁа-яёA-Za-z0-9]', original)
return MD + MD.join(re.escape(p) for p in parts) + MD
```
## Баг 2: `Новиков И.В.` → `ФИО`
**Причина:** В оригинале буквально `ФИО` — незаполненный шаблон. Не баг паттерна.
**Превентивный фикс (config.py):** `\s+` → `[\s\xa0]+` в PERSON_PATTERN — на случай неразрывных пробелов в DOCX.
```python
PERSON_PATTERN = re.compile(
r'\b[А-Я][а-я]+[\s\xa0]+[А-Я]\.[А-Я]\.'
r'|\b[А-Я][а-я]+[\s\xa0]+[А-Я][а-я]+[\s\xa0]+[А-Я][а-я]+',
)
```
## Моё мнение
**Баг 1 — делать.** Логично, минимально, решает проблему Markdown-жирности.
**Баг 2 — `\xa0` делать** (превентивно). Сам `ФИО` — не баг, документ содержит незаполненный шаблон. Можно добавить `ФИО` в список игнорируемых (исключить из mapping), но не обязательно.
+105
View File
@@ -0,0 +1,105 @@
# 2026-08-20 — Вопрос-ответ по LLM-чанкингу (Q&A)
Контекст: сервис обфускации, двухступенчатый детект ПДн (regex + LLM). Пользователь
хочет, чтобы LLM обрабатывал ВСЕ данные, а не урезку 8000 символов.
Обсуждение с Sonnet: основной вопрос + 6 уточнений.
Предыдущие файлы:
- History/2026-08-20-sonnet-query-llm-pattern.md — рекомендуемая схема
- History/2026-08-20-sonnet-query-llm-followup.md — уточнения перед внедрением
---
## Исходная проблема
`scan_llm_ner` (drhider/scanner.py) обрезал:
- каждый файл до первых 3000 символов (`t[:3000]`);
- всё вместе до 8000 символов (`combined[:8000]`);
- один вызов LLM на весь набор.
Итог: при 299 файлах LLM «видел» только ~8000 символов (~0.1–0.3% данных).
Пользователь: «всё затевалось чтобы ЛЛМ ВСЁ обрабатывал». → убрать урезку.
## Рекомендуемая схема (Sonnet, основной ответ)
- Отказаться от глобального батчинга → «один файл = один или несколько вызовов LLM», параллельно.
- Чанкинг per-file: чанк 6000 символов, overlap 400–500 символов.
- Дедупликация по файлу + верификация (найденная строка реально в тексте → отсекает галлюцинации).
- Батчинг мелких файлов — опционально, второй шаг.
## Уточнения (У.1–У.6) и ответы Sonnet
### У.1 — Алгоритм разбивки на чанки
Приоритет границ: `\n\n` → `\n` → `. ` → `? ` → `! ` → `; ` → `, ` → ` ` (пробел — крайний).
Ищем ближайшую границу с конца в диапазоне `[size//2 .. size]`. Жёсткий разрез — только если нет ни одного пробела.
Overlap: отступить назад на overlap, найти начало слова.
Псевдокод:
```python
BOUNDARIES = ['\n\n', '\n', '. ', '? ', '! ', '; ', ', ', ' ']
def split_into_chunks(text, size=6000, overlap=500):
chunks=[]; start=0
while start < len(text):
end=min(start+size, len(text))
if end==len(text):
chunks.append(text[start:]); break
cut=None
for b in BOUNDARIES:
pos=text.rfind(b, start+size//2, end)
if pos!=-1:
cut=pos+len(b); break
if cut is None: cut=end
chunks.append(text[start:cut])
ov=max(start, cut-overlap)
space=text.find(' ', ov)
start=(space+1) if (space!=-1 and space<cut) else ov
return chunks
```
### У.2 — Нормализация для дедупа
```python
def normalize_entity(s):
s=s.strip()
s=re.sub(r'\s+',' ',s)
s=s.lower()
s=re.sub(r'[«»“”‘’"\' ]','"',s) # кавычки → "
s=re.sub(r'[—–−-]','-',s) # тире → -
s=s.replace('\u00ad','') # мягкий перенос
s=s.replace('ё','е') # Ё→Е (OCR/PDF)
return s
```
Дедуп: `seen=set()` — добавлять, только если `normalize_entity(v) not in seen`.
### У.3 — Источник истины для верификации
Полный текст файла ДО обфускации (не чанк).
```python
def verify(entity, full_text):
if entity in full_text: return True
return normalize_entity(entity) in normalize_entity(full_text)
```
Если сущность из двух чанков с разным форматированием — нормализованный ключ одинаков → дедуп оставит один; хранить длиннее (или первый).
### У.4 — Батчинг мелких в первой итерации
НЕ нужен. 270 мелких: 1 файл=1 вызов ≈ 270 вызовов ≈ 101с при 4 потоках; батч по 5 → 54 вызова ≈ 20с. Разница ~80с несущественна. Батчинг — вторая итерация.
### У.5 — Промпт при чанкинге
Блок «Already captured by regex» ОСТАВИТЬ в каждом вызове (снижает дублирование с regex).
Блок правил — как есть в первую итерацию (если API с system/user — правила в system, текст чанка в user; иначе как сейчас).
Первый шаг: промпт не менять, только текстовый блок = чанк.
### У.6 — Общее время и rate-limit
Расчёт: 270 мелких (1 вызов) + 30 крупных (≈4 чанка) = 390 вызовов.
- 4 потока × 2с: 390/4×2 ≈ 195с ≈ 3.5 мин.
- 8 потоков: ~1.5–2 мин.
Rate-limit свой LLM: внешнего нет, ограничение GPU. Начать с 4, поднять до 8, если latency не растёт (>3× от базового — снижать).
---
## Итоговое решение (для внедрения, ждёт «делай»)
1. `scanner.py scan_llm_ner`: убрать `[:3000]`/`[:8000]`;
по каждому файлу из `all_texts` разбить на чанки (6000/500, split_into_chunks);
параллельные вызовы (ThreadPool, concurrency 4, крупные вперёд);
для каждого чанка — полный промпт + «regex-найденные»;
собрать сущности, normalize-дедуп, verify против полного текста файла, в mapping.
2. Добавить хелперы split_into_chunks / normalize_entity / verify в scanner.py.
3. Не дублировать regex-найденное; считать токены/время LLM корректно (llm_active/elapsed).
4. Прогнать тест на малом наборе (не прод): сколько сущностей добавит LLM, время.
Статус: НЕ внедрено (ждёт «делай»). Версия при внедрении — v0.0.57.
@@ -0,0 +1,42 @@
# Промпт для Sonnet — честный ответ: можно ли ускорить без риска устойчивости?
## Роль
Отвечай ЧЕСТНО и ПО ДЕЛУ. Без воды, без лирики. Если ускорение невозможно без
компромисса (устойчивость/таблицы/риск) — прямо скажи «нет» и объясни почему.
Не предлагай «смену библиотеки» — уже пробовали, PyMuPDF теряет таблицы (40 vs 94),
ТАБЛИЦЫ КРИТИЧНЫ для пользователя. Учитывай это ограничение железно.
## ФАКТЫ (результаты наших замеров, НЕ догадки)
1. `Spartan10Manual.pdf` (14.8 МБ, 619 страниц, скан/руководство):
- `extract_text()` суммарно = **64.4с** (104мс/стр) — УЗКОЕ МЕСТО
- `extract_tables()` суммарно = **0.2с** (0мс/стр) — ничтожно
- на 272 страницах без линий extract_tables = 0.0с
→ Твоё прошлое предположение «на сканах дорогой extract_tables» НЕ подтвердилось.
Узкое место — ИЗВЛЕЧЕНИЕ ТЕКСТА, а не таблиц. Не повторяй эту ошибку.
2. Большой PDF 19 МБ (0144-03-2023_отчет об оценке.pdf, 141 стр, 94 таблицы) — ~25-26с.
3. CPU пода = 2 ядра, Memory = 4Gi. Воркер один, последовательный.
4. Разброс времени одного файла между запусками: 70с → 470с (Spartan10). Причины
не установлены точно (подозрение: CPU throttling/нагрузка пода, декомпрессия битмапов).
5. `.doc` обрабатывается через HTTP-сервис liberta (IO-bound).
6. apply_replacements — один regex из всех ключей (уже оптимизирован).
7. Сессия хранит файлы в памяти до 500 МБ (session.py). ProcessPool с fork —
риск COW-копии памяти.
## Вопрос (ответь честно)
Можно ли ЗНАЧИТЕЛЬНО ускорить обработку (цель — сократить время на больших PDF/наборах),
НЕ рискуя:
- устойчивостью (битые файлы, SSE, память пода 4Gi),
- потерей таблиц (критично),
- сложностью поддержки (код должен остаться понятным)?
Требования к ответу:
- Дай КОНКРЕТНЫЕ пункты, каждый: что менять / где / ожидаемый эффект (с цифрами из фактов выше) / риски.
- Раздели на:
A) безопасно и просто (готов внедрить сейчас, низкий риск),
B) заметный выигрыш, но с рисками/сложностью (нужен осознанный выбор),
C) рискованно/не стоит (объясни почему).
- Если для БЕЗОПАСНОГО варианта реального выигрыша нет — так и скажи: «безопасно ускорить
значительно нельзя», и предложи что реально можно сделать без риска (даже если эффект мал).
- НЕ предлагай смену pdfplumber/PyMuPDF и не предлагай то, что ломает таблицы.
- Оцени РЕАЛЬНО: при CPU=2 параллельность текста по страницам даст хоть что-то?
(GIL/процессы, overhead fork/spawn, память). С цифрами.
@@ -0,0 +1,46 @@
# Промпт для Sonnet — уточнения по LLM-чанкингу (перед внедрением)
Ты дал рекомендуемую схему (чанкинг 6000 chars, overlap 500, параллельность 4–8,
дедуп + верификация). Перед внедрением уточни практические детали. Отвечай КРАТКО,
по пунктам, с конкретикой (алгоритм/числа). Без воды.
## У.1 — Разбивка на чанки: по чему резать
Ты сказал «разбивать по абзацам/предложениям, не по символам». Но в PDF-выгрузках
текст часто сливается в один-два гигантских «абзаца» (нет \n). Дай КОНКРЕТНЫЙ алгоритм
разбивки произвольного текста (в т.ч. без \n) на чанки ~6000 символов с overlap 500:
- по каким границам резать в приоритете (\n\n, \n, '. ', '? ', '; ', потом символы)?
- как применить overlap без разрыва слова?
- псевдокод функции split_into_chunks(text, size=6000, overlap=500).
## У.2 — Дедупликация при overlap и некорректном форматировании
LLM может вернуть один и тот же value из двух чанков с РАЗНЫМ форматированием
(лишние пробелы, регистр, тире/дефис). Проверки `strip().lower()` недостаточно.
Как нормализовать перед дедупом (например, сжать пробелы, унифицировать кавычки/тире)?
Дай конкретный набор нормализаций для русских деловых текстов.
## У.3 — Верификация против исходника
Проверку «найденного значения нет в исходном тексте → отбросить (галлюцинация)» делать
против ИСХОДНОГО извлечённого текста файла (до обфускации), верно? Или против чанка?
Уточни, что есть «источник истины» для верификации, и как быть, если value найден в
нескольких чанках, но слегка отличается (нормализованное сравнение).
## У.4 — Малые файлы: нужен ли батчинг в первую итерацию
Набор сотен мелких файлов (<1000 символов). Батчинг 5–8 в один вызов ускорит, но усложнит
(деградация разметки, больший ответ). Для ПЕРВОЙ итерации можно ли обойтись «1 файл = 1 вызов»
без батчинга, даже если файлов сотни? Оцени, насколько медленнее будет без батчинга.
## У.5 — Промпт при пофайловом чанкинге
Текущий промпт (в коде) содержит блоки «Already captured by regex» + большой список правил.
При пофайловом/почанковом вызове эти блоки повторяются в каждом запросе. Оставить промпт
как есть (меняя только текст), или его надо упростить под чанк (одна папка уже нашла — regex)?
Повлияет ли повтор правил на качество/токены?
## У.6 — Прогресс и общее время
300 файлов, многие >6000 символов → сотни LLM-вызовов × по 1–2с = минуты-десятки минут.
Пользователь готов ждать (LLM своя). Но оцени: с параллельностью 4 и ~5 вызовов/файл на
крупных — реальное общее время для набора ~300 файлов (в т.ч. 270 мелких + 30 крупных).
И: не упрёмся ли в rate-limit своего LLM-эндпоинта при 4-8 параллельных.
## Формат
- По каждому У.1–У.6: короткий ответ + конкретика (алгоритм/числа/решение).
- Если что-то некритично для первой итерации — скажи явно «можно первым шагом упростить так».
@@ -0,0 +1,30 @@
# Промпт для Sonnet — вопрос по использованию LLM (без ссылок на файлы)
## Контекст
У нас сервис обфускации документов (удаление персональных данных из русских деловых документов).
Двухступенчатый детект ПДн:
1. Regex-паттерны (телефоны, ИНН, email, типовые формы — быстро, локально, дёшево).
2. LLM (общая NER-подстраховка) — сейчас обрабатывает только урезанный фрагмент:
- от каждого файла берутся первые ~3000 символов,
- всё вместе обрезается до ~8000 символов,
- выполняется ОДИН вызов LLM на весь набор.
Пользователь хочет, чтобы LLM обрабатывал ВСЕ данные (ВСЕ файлы целиком), не только первые 8000 символов.
LLM — своя, стоимость не важна. Важно качество распознавания ПДн и разумная архитектура вызова.
## Вопрос
Как лучше организовать вызов LLM, чтобы он БЕЗ потерь покрывал все документы (набор из сотен файлов, каждый до нескольких десятков страниц), сохраняя качество распознавания русских деловых документов?
Ограничения/требования к ответу:
- Не предлагай менять модель/external API — LLM своя, она остаётся.
- Подумай про: пакетирование файлов (размер батча), максимальную длину контекста на запрос,
как избежать обрезки данных, как не потерять качество при больших объёмах,
параллельность запросов (если релевантна), дедупликацию найденного между пакетами.
- Дай конкретную рекомендуемую схему (числа: размер батча, лимит символов на файл/пакет, параллельность).
- Оцени риски: токены/время, качество, риск пропуска.
- Сухо, по делу, без воды.
## Формат
- Секция «Рекомендуемая схема» — конкретный план с числами.
- Секция «Риски» — что может пойти не так и как смягчить.
- Секция «Минимум для старта» — если хочется просто и быстро, что достаточно сделать первым шагом.
@@ -0,0 +1,53 @@
# Промпт для Sonnet — follow-up вопросы по ревью (скорость + сбои)
## Контекст
Ты дал ревью (файл: History/2026-08-20-sonnet-query-review-speed.md). Часть пунктов приняли,
часть — требуют уточнения. Отвечай ТОЛЬКО на вопросы ниже, сухо, конкретно, без воды.
Если что-то из твоих утверждений основано на допущении, а не на факте — скажи явно «допущение» и
что нужно проверить, чтобы подтвердить.
## Вопрос 1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py)
Ты предложил: не вызывать extract_tables() на страницах без vector-линий, т.к. на сканах это пустой проход.
Вопросы:
1.1. Перечисли КОНКРЕТНО типы таблиц, которые `extract_tables()` находит, но которые НЕ дают
`page.lines`/`page.curves` (текстовые сетки, таблицы на заливке/fill, встроенные картинки, что ещё?).
1.2. Есть ли в нашем реальном наборе (TMP/спецификации, 0144-03-2023_отчет об оценке.pdf, документы из
DownLoads) риск, что фильтр отбросит реальную таблицу? Это надо проверить фактом — предложи
точный способ замера: как сравнить число таблиц с фильтром и без на конкретных файлах.
1.3. Насколько `page.lines`/`page.curves` дешевле `extract_tables()`? В pdfplumber они тоже делают
парсинг объектов страницы — дай оценку реального выигрыша на скан-PDF, не «в разы», а чем измерить.
1.4. Безопасная альтернатива: может, стоит вызывать extract_tables() только если `page.find_tables()`
вернул непусто? Или это то же самое по стоимости? Уточни, что реально дорого внутри extract_tables().
## Вопрос 2. `_decode_name`: попытка decode("utf-8") перед decode("cp866")
Ты предложил порядок: raw.decode("utf-8"), при ошибке — raw.decode("cp866").
2.1. Оцени риск ложного срабатывания: когда CP866-байты случайно образуют валидный UTF-8
(например, псевдографика 0xC0-0xDF + продолжения 0x80-0xBF). Насколько это реально для имён
файлов 1С? Есть ли способ отличить «настоящий UTF-8» от «случайного» (например, проверить
диапазон символов после декодирования)?
2.2. Подтверди, что для нашего реального случая (Info-ZIP UTF-8 без флага 0x800) этот порядок даёт
корректное имя, а для CP866 1С — не ломает.
## Вопрос 3. ProcessPoolExecutor по страницам / по файлам
Ты предложил распараллелить извлечение текста. Вопросы:
3.1. Риск памяти: в контейнере сессия уже держит файлы в памяти (до 500 МБ в session.py). При fork
воркеры получают COW-копию памяти родителя. Оцени реальный риск OOM в managed-поде (лимит CPU 2,
Memory 4Gi) при 4-8 процессах. Не будет ли хуже, чем текущее последовательное?
3.2. pdfplumber сам по себе уже использует один процесс. Дай конкретный план безопасного
распараллеливания: какие данные передавать в воркер (только bytes страницы или весь файл?),
как собирать результаты в порядке индексов, как не раздуть память.
3.3. Учитывая CPU 2 (2 ядра) — какой реальный выигрыш даст ProcessPool на 2 ядрах? Стоит ли это
сложности, или сначала дёшево (П.1 + П.2)?
## Вопрос 4. Что реально тормозит в Spartan10Manual.pdf (70–470с на 14.8 МБ)
Ты утверждаешь: виноват пустой extract_tables() на скане. Но время скачет 70→470с — это подозрительно.
4.1. Как точно измерить, что дороже на ЭТОМ файле: extract_text() по страницам или extract_tables()?
Дай конкретный способ замера (тайминги по функциям, по страницам), чтобы не гадать.
4.2. Объясни разброс 70–470с: что в коде/данных может давать такой разброс между запусками
(кэши, LLM, сеть к liberta, нагрузка CPU пода)?
4.3. Есть ли в extract_text() внутри pdfplumber скрытый повторный парсинг (например, повторное чтение
объекта при каждом вызове), который можно убрать без смены библиотеки?
## Формат ответа
- По каждому вопросу: короткий ответ + «факт»/«допущение» + что проверить, если допущение.
- Без воды, без лирики.
@@ -0,0 +1,44 @@
# Промпт для Sonnet — код-ревью drhider (скорость + сбои)
## Роль
Ты — ревьюер кода. Отвечай ТОЛЬКО по делу: короткие технические тезисы. Без воды, без лирики, без «как было бы здорово», без общих фраз. Каждый тезис — конкретика: файл, функция, строка, суть.
## Ограничение: смотреть ТОЛЬКО эти файлы, нигде больше не рыться
- `drhider/obfuscator.py` — двухпроходный обфускатор (проход 1: извлечение текста + regex + LLM NER; проход 2: замена)
- `drhider/extractor.py` — pdf_to_markdown (pdfplumber), doc_to_markdown (сервис liberta), expand_zips (рекурсия zip, CP437→CP866), защита от zip-бомб
- `drhider/scanner.py` — scan_regex, scan_llm_ner
- `drhider/replacer.py` — apply_replacements (один regex из всех ключей)
- `drhider/llm_client.py` — OpenAI-совместимый клиент, счётчики токенов/времени
- `drhider/builder.py` — build_zip, build_mapping_csv
- `site/routes/api_bp.py` — upload, process_stream (SSE + воркер в потоке), download/csv
- `site/app.py` — create_app, лимиты, setup_logging (LOG_LEVEL/LOG_FILE)
- `site/session.py` — хранение сессий в памяти, TTL 30 мин, MAX_SESSION_BYTES
- `site/templates/index.html` — фронт: нативный unzip, таблица, SSE-прогресс, таймеры, лимиты 100МБ/1ГБ
Не читай README, docs, History, tests, Dockerfile, ничего про деплой.
## Контекст: сбои, которые были (объясни каждый, если увидишь первопричину в коде)
1. «SSE connection failed» при живом воркере. Выяснено: воркер падал на битом PDF — `PDFSyntaxError('No /Root object!')` в `extract_text` не был обёрнут по-файлово; после фикса (skip битого файла) SSE доходит до complete. Подтверди/опровергни по коду, есть ли ещё места, где одна ошибка файла роняет весь проход.
2. Кракозябры кириллицы в именах zip. Причина: Info-ZIP пишет UTF-8 без флага 0x800 → декодер `encode(cp437).decode(cp866)` ломает. Проверь `_decode_name` в extractor.py: покрывает ли оба реальных сценария (CP866 без флага и UTF-8 с флагом), есть ли дыры.
3. HTTP 413 при загрузке. Лимиты: MAX_CONTENT_LENGTH=200MB (запрос), MAX_SESSION_BYTES=500MB (сессия). Фронт пускает до 100МБ/файл и 1ГБ/сумма — рассинхрон фронт/бэк. Отметь.
## Главная задача: предложи БОЛЕЕ БЫСТРУЮ логику обработки
Известные факты производительности (факты, не догадки):
- pdfplumber на скане Spartan10Manual.pdf 14.8 МБ — 70–470 секунд на извлечение текста.
- Большой PDF 19 МБ — ~25–26 с на extract_text.
- LLM NER — один общий вызов на все файлы, ~1.8с на маленьком наборе, зависит от числа токенов.
- apply_replacements оптимизирован (один regex), но проверить, нет ли лишней работы.
Что хочешь от тебя:
- Где реальные узкие места в коде (не «вообще», а конкретные функции/строки).
- Конкретные предложения ускорения: что менять, как, ожидаемый эффект. Без «использовать PyMuPDF» — уже пробовали, теряет таблицы (94 vs 40), ТАБЛИЦЫ КРИТИЧНЫ. Учитывай это ограничение.
- Можно ли ускорить без смены pdfplumber (параллельность страниц, кэши, ограничение повторных парсингов, предобработка)?
- Есть ли дублирующая работа между проходами 1 и 2 (например, повторный парсинг/чтение)?
- Безопасно ли распараллелить извлечение текста по файлам (потоки/GIL)? Если да — как.
## Формат ответа
- Секция «Сбои»: по каждому — подтверждение/опровержение по коду + где именно.
- Секция «Узкие места»: список файл:строка + суть + почему.
- Секция «Предложения ускорения»: пронумерованный список, каждый пункт: что/где/как/эффект/риски.
- Секция «Итог»: 3–5 самых важных действий по приоритету.
- Максимум — сухо, без воды.
@@ -0,0 +1,127 @@
# 2026-08-20 — Ревью Sonnet: вопросы и ответы (Q&A)
Серия: ревью скорости обработки + объяснение сбоев.
Предыдущие файлы:
- History/2026-08-20-sonnet-query-review-speed.md — исходный промпт ревью
- History/2026-08-20-sonnet-query-review-speed-followup.md — вопросы по спорным пунктам
- Настоящий файл — ответы Sonnet + решения
---
## В1. Фильтр `page.lines or page.curves` перед `extract_tables()` (extractor.py)
### В1.1 — Какие таблицы extract_tables() найдёт, но фильтр отбросит?
**Sonnet: допущение (не факт).**
`extract_tables()` со стратегией по умолчанию ищет только векторные линии (strategy="lines").
- Таблицы на цветном фоне (только `page.rects`, без линий) — фильтр `lines or curves` их пропустит → **дыра**.
Нужно `or page.rects`.
- Whitespace-таблицы (только пробелы/выравнивание) — не найдёт вообще без strategy="text".
- Растровые таблицы в embedded images — не найдёт вообще.
**Решение:** фильтр = `page.lines or page.curves or page.rects`. Обязательно проверить фактом (В1.2).
### В1.2 — Как проверить, что фильтр не потеряет таблицы?
**Sonnet:**
```python
with pdfplumber.open(fname) as pdf:
for i, page in enumerate(pdf.pages):
t_all = page.extract_tables()
has_lines = bool(page.lines or page.curves or page.rects)
t_filtered = page.extract_tables() if has_lines else []
if len(t_all) != len(t_filtered):
print(f"p{i}: потеряно {len(t_all)-len(t_filtered)} таблиц")
```
Прогнать на TMP-спецификациях и 0144-03-2023_отчет об оценке.pdf.
**Решение:** сделать замер до включения фильтра в код. ТАБЛИЦЫ КРИТИЧНЫ (прецедент PyMuPDF 40 vs 94).
### В1.3 — Насколько page.lines/curves дешевле extract_tables()?
**Sonnet: допущение.**
`page.lines` — `@cached_property` (уже разобран при первом обращении к странице). Стоимость ≈ 0.
Дорогой в `extract_tables()` — `TableFinder`: строит граф пересечений линий. На скан-PDF всё равно инициализируется.
Замер:
```python
t0 = time.perf_counter(); _ = page.lines; t1 = time.perf_counter()
t2 = time.perf_counter(); _ = page.extract_tables(); t3 = time.perf_counter()
print(f"lines={t1-t0:.4f}s extract_tables={t3-t2:.4f}s")
```
### В1.4 — find_tables() как guard?
**Sonnet:** `find_tables()` и `extract_tables()` — одна стоимость (`extract_tables()` вызывает `finder.find_tables()` внутри). Guard бесполезен.
---
## В2. `_decode_name`: попытка decode("utf-8") перед decode("cp866")
### В2.1 — Риск ложного срабатывания UTF-8 на CP866-байтах?
**Sonnet:**
CP866 кириллические заглавные = 0xC0–0xDF. В UTF-8 0xC0, 0xC1 — overlong (невалидны), 0xC2–0xDF — валидное начало двухбайта, требует продолжения 0x80–0xBF. CP866 строчные = 0xA0–0xBF — попадают в диапазон UTF-8 continuation bytes.
**Реальная коллизия:** «Т» (0xD2) + «г» (0xA3) = 0xD2 0xA3 = валидный UTF-8 (U+04A3 Ң). Риск для имён «Тг…», «Рп…» и т.п.
**Защита:** после `decode("utf-8")` проверить, что все символы — в U+0400–U+04FF (кириллица) или ASCII. Если есть символы вне диапазона — ложное срабатывание → фоллбэк cp866.
### В2.2 — Подтверждение порядка
**Sonnet: факт.**
- Info-ZIP UTF-8 без флага: zipfile(cp437) → `encode("cp437")` → исходные UTF-8 байты → `decode("utf-8")` → корректно.
- CP866 1С: zipfile(cp437) → `encode("cp437")` → CP866 байты → `decode("utf-8")` → почти всегда UnicodeDecodeError (0xC0/0xC1 overlong, 0xE0–0xFF без продолжения) → фоллбэк `decode("cp866")` → корректно.
**Решение:** фикс принимается, но с валидацией диапазона кириллицы/ASCII после utf-8.
---
## В3. ProcessPoolExecutor по страницам/файлам
### В3.1 — Риск памяти при fork в managed-поде
**Sonnet: допущение.**
fork → COW. pdfplumber в дочернем создаёт новые объекты (запись → COW). 500МБ session files в родителе — COW, читаются, не копируются. Пик: pdfplumber на 14.8МБ PDF ~150–300МБ на воркер. 2 воркера + родитель ≈ 1–1.5ГБ пик. При 4Gi — умеренный риск.
`spawn` (не fork): дочерние стартуют чисто, память сессии не копируется, overhead ~0.5с/процесс.
### В3.2 — Безопасный план распараллеливания
**Sonnet:**
- Единица параллелизма: **файл** (не страница), воркер получает `(fname, content_bytes)` через pickle.
- content_bytes по pipe: 15МБ ≈ 0.1с overhead.
- Порядок: `{i: executor.submit(pdf_to_markdown, content)}`, collect `{i: future.result()}`.
- progress_cb из основного потока после future.result().
### В3.3 — Выигрыш на CPU=2
**Sonnet:** на 2 ядрах при ОДНОМ PDF — нет выигрыша (один процесс на одно ядро). Выигрыш только при нескольких PDF: 3 файла × 25с → ~30с вместо 75с.
**Рекомендация Sonnet:** сначала В1 (фильтр) + В2 (decode) — бесплатно и безопасно. ProcessPool — потом, если замер покажет, что узкое место реально там.
**Решение:** ОТЛОЖЕНО. Сначала В1+В2, замерить.
---
## В4. Разброс 70–470с на Spartan10Manual.pdf (14.8 МБ)
### В4.1 — Как измерить, что дороже: extract_text или extract_tables
**Sonnet:**
```python
with pdfplumber.open(io.BytesIO(content)) as pdf:
for i, page in enumerate(pdf.pages):
t0 = time.perf_counter()
page.extract_text()
t1 = time.perf_counter()
page.extract_tables()
t2 = time.perf_counter()
print(f"p{i:3d}: text={t1-t0:.3f}s tables={t2-t1:.3f}s")
```
### В4.2 — Причина разброса
**Sonnet:**
- Факт: pdfplumber не кэширует между запросами — каждый вызов парсит заново.
- Допущение: CPU throttling при limit=2 в managed-поде (нагрузка → 470с, пусто → 70с). Проверить `kubectl top pod` во время обработки.
- Допущение: битмапы скана разного размера (цветной vs ч/б) → разное время декомпрессии (JPEG2000/JBIG2). Проверить замером по страницам.
### В4.3 — Скрытый повторный парсинг в extract_text?
**Sonnet: факт.** `extract_text()` читает `self.chars` (@cached_property), `extract_tables()` — `self.edges` (@cached_property). Оба используют уже разобранные структуры. Повторного чтения PDF в рамках одного `with pdfplumber.open()` нет.
---
## Итоговый план (по приоритету)
1. **В2** — `_decode_name`: utf-8 + валидация диапазона (кириллица U+0400–U+04FF/ASCII) → фоллбэк cp866. Фикс Info-ZIP-зипов (кракозябры).
2. **В1** — фильтр `page.lines or page.curves or page.rects` перед `extract_tables()` + замер числа таблиц до/после на реальных файлах.
3. Замер ускорения на Spartan10Manual.pdf.
4. ProcessPool — после фактов, если узкое место там (отложено).
5. Диагностика разброса 70–470с: `kubectl top pod` во время обработки + замер по страницам.
## Статус реализации
- Пока НЕ реализовано (ждёт «делай»). Код не менялся в рамках этого Q&A.