docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes)
This commit is contained in:
@@ -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":"..."}
|
||||
```
|
||||
@@ -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), но не обязательно.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user