diff --git a/History/2026-08-20-sonnet-query-review-speed-followup.md b/History/2026-08-20-sonnet-query-review-speed-followup.md new file mode 100644 index 0000000..dd8e0a8 --- /dev/null +++ b/History/2026-08-20-sonnet-query-review-speed-followup.md @@ -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 скрытый повторный парсинг (например, повторное чтение + объекта при каждом вызове), который можно убрать без смены библиотеки? + +## Формат ответа +- По каждому вопросу: короткий ответ + «факт»/«допущение» + что проверить, если допущение. +- Без воды, без лирики. diff --git a/History/2026-08-20-sonnet-query-review-speed.md b/History/2026-08-20-sonnet-query-review-speed.md new file mode 100644 index 0000000..d22dca9 --- /dev/null +++ b/History/2026-08-20-sonnet-query-review-speed.md @@ -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 самых важных действий по приоритету. +- Максимум — сухо, без воды. diff --git a/History/2026-08-20-sonnet-review-qa.md b/History/2026-08-20-sonnet-review-qa.md new file mode 100644 index 0000000..5821e4b --- /dev/null +++ b/History/2026-08-20-sonnet-review-qa.md @@ -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.