docs: сортировка History по темам (sonnet, opus, llm, sse, ux-frontend, upload, infra, plans, tests, base, code-fixes)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# v0.0.32 — фиксы по ревью обфускатора — 2026-08-18
|
||||
|
||||
**Дата:** 2026-08-18
|
||||
**Версия:** 0.0.31 → 0.0.32
|
||||
**Ветка:** master (коммит `94e588a`, после rebase над `c3c6e5b`)
|
||||
**Источник:** ревью `History/2026-08-18-review-sonnet-bugs.md`
|
||||
|
||||
---
|
||||
|
||||
## Внесённые изменения
|
||||
|
||||
### 1. Единый обфускатор на все файлы (ТОП-1 — согласованность токенов)
|
||||
- **Было:** `api_bp.py` обрабатывал каждый файл отдельным `obfuscate_files()` →
|
||||
новая инстанс `TwoPassObfuscator` на файл → одна сущность получала РАЗНЫЕ токены
|
||||
в разных файлах, `all_mapping` (ручной разбор CSV) был неверен.
|
||||
- **Стало:** все файлы сессии → один `obfuscate_files(...)` с общим mapping.
|
||||
Проверено: «ООО Ромашка» в двух файлах → один токен «Сфера_0001».
|
||||
- **Побочно:** убран хрупкий ручной разбор CSV `split(",", 2)` (ломавшийся на
|
||||
запятых в значениях) — больше не нужен при едином вызове.
|
||||
|
||||
### 2. Рекурсивный `expand_zips` (ТОП-2)
|
||||
- `extractor.py:expand_zips` переписан: очередь + распаковка вложенных ZIP.
|
||||
ZIP внутри ZIP теперь раскрывается. Проверено: вложенный `doc1.txt` извлекается.
|
||||
|
||||
### 3. SSE-дисконнект (ТОП-3)
|
||||
- `api_bp.py:generate()`: ловится `(GeneratorExit, BrokenPipeError,
|
||||
ConnectionResetError)` вместо одного `GeneratorExit`.
|
||||
- `obfuscate_files` выполняется в отдельном потоке; прогресс через очередь;
|
||||
флаг отмены (`threading.Event`) при разрыве соединения.
|
||||
- Коллбек прогресса `(phase, idx, total, fname)` где phase ∈ {start, done} —
|
||||
согласуется с фронт-протоколом (`start`/`done` по `idx`).
|
||||
|
||||
### 4. Кириллица 1С (CP437→CP866)
|
||||
- `extractor.py._decode_name`: если имя не-UTF-8 (флаг 0x800 снят) и содержит
|
||||
не-ASCII → `name.encode("cp437").decode("cp866")`. Проверено изолированно.
|
||||
|
||||
### 5. Дедупликация имён
|
||||
- `builder.py:build_zip`: одинаковое имя+контент → пропуск; имя+разный контент →
|
||||
суффикс `_2`, `_3`... Проверено: `['a.md', 'a_2.md']`.
|
||||
- `obfuscator.py:_dedupe_file_names`: уникализация имён на входе до прохода 1
|
||||
(защита `all_texts[fname]` от перезаписи).
|
||||
|
||||
### 6. upload(): цикл по всем файлам
|
||||
- `api_bp.py:upload()`: обрабатывает все `request.files.getlist("files")`
|
||||
(поддержка загрузки целых папок через `webkitdirectory`).
|
||||
- Сохранено поведение ошибок: файл без имени → «No filename» (совместимо с тестом).
|
||||
|
||||
### 7. Лимит объёма сессии
|
||||
- `session.py`: `MAX_SESSION_BYTES = 500 MB`; в `add_file` — скип при превышении.
|
||||
|
||||
### 8. Защита от ZIP-бомб
|
||||
- `extractor.py`: проверка `total + file_size > LIMIT` ДО `zf.read`; скип архива
|
||||
при превышении ratio/лимита/числа файлов; лимиты глобально на вызов.
|
||||
|
||||
---
|
||||
|
||||
## Тесты
|
||||
|
||||
| Набор | Результат |
|
||||
|-------|-----------|
|
||||
| test_builder | 20/20 OK |
|
||||
| test_zip | 28/28 OK |
|
||||
| test_replacer | 10/10 OK |
|
||||
| test_scanner | 20/20 OK |
|
||||
| test_upload | 13/13 OK |
|
||||
| test_extractor | **14/15** — падает `.doc` (внешний сервис liberta) |
|
||||
|
||||
`test_extractor` `.doc`: отправляется мусорный OLE2-магик
|
||||
`b"\xD0\xCF\x11\xE0\x00"*10`, сервис `liberta.containerk8s.dev.nubes.ru/convert`
|
||||
возвращает «conversion produced no .docx». НЕ регрессия от v0.0.32 — ветка
|
||||
`.doc`/`doc_to_markdown` не менялась. Вероятно, сервис изменил поведение или
|
||||
не принимает невалидный контент. Требует отдельного разбора.
|
||||
|
||||
---
|
||||
|
||||
## Git
|
||||
|
||||
- Remote был впереди (посторонний коммит `c3c6e5b` про магистратуру) → `git pull --rebase`.
|
||||
- Push успешен: `c3c6e5b..94e588a`.
|
||||
- Ветка-сохранение документации: `save-docs-2026-08-18` (`de072d8`).
|
||||
|
||||
## TODO / открытые вопросы
|
||||
- [ ] Разобраться с тестом `.doc` (возможно, обновить тест на актуальный сервис).
|
||||
- [ ] Визуально проверить SSE-прогресс во фронте после рефакторинга.
|
||||
@@ -0,0 +1,45 @@
|
||||
# v0.0.39 — оптимизация apply_replacements (однопроходная замена) — 2026-08-19
|
||||
|
||||
**Дата:** 2026-08-19
|
||||
**Версия:** 0.0.38 → 0.0.39
|
||||
**Файл:** `drhider/replacer.py`, `site/app.py` (версия)
|
||||
|
||||
---
|
||||
|
||||
## Суть
|
||||
|
||||
Обфускация PDF 19 МБ шла 132.7с, из них ИИ (LLM) всего 7с — остальное (~125с)
|
||||
тратил основной код. Замер выявил узкое место: `apply_replacements`.
|
||||
|
||||
## Причина
|
||||
|
||||
`apply_replacements` для **каждой** сущности из mapping делала **2 полных
|
||||
прохода `re.sub` по всему тексту** (основной + фоллбэк `_md_tolerant_pattern`).
|
||||
Сложность O(N_сущностей × размер_текста × 2) — квадратичная по числу сущностей.
|
||||
При 300+ сущностей и тексте в несколько МБ — десятки секунд.
|
||||
|
||||
Замер (0.9 МБ, 304 сущности): **17.3 с**.
|
||||
|
||||
## Решение
|
||||
|
||||
Однопроходная замена:
|
||||
- Все ключи собираются в **один** regex-паттерн через альтернацию `(?:...|...)`
|
||||
(по убыванию длины, с `re.escape`, границы `(?<!\w)...(?!\w)` для alnum-сущностей).
|
||||
- Один `re.sub` с callback (lookup в mapping), O(text).
|
||||
- Фоллбэк `_md_tolerant_pattern` применяется **только к ключам, не найденным**
|
||||
основной заменой (отслеживание через `matched_keys`), а не ко всем.
|
||||
|
||||
## Результат замера
|
||||
- 300 сущностей реально в тексте (1.3 МБ): **1.17 с** (было бы десятки сек).
|
||||
- Реалистичный сценарий (10 сущностей): **0.29 с**.
|
||||
- Согласованность токенов и mapping.csv сохранены (проверено через `obfuscate`).
|
||||
|
||||
## Проверка
|
||||
- `test_replacer` 10/10, `test_builder` 20/20, `test_scanner` 20/20, `test_zip` 28/28.
|
||||
- Пайплайн `obfuscate`: «ООО Ромашка»→«Вектор_0001» в двух файлах согласовано,
|
||||
mapping.csv корректен.
|
||||
|
||||
## Примечание
|
||||
- `scan_regex` (0.4с) и upload (сеть ~1.7 МБ/с) — не узкие места, не трогали.
|
||||
- `pdf_to_markdown` (pdfplumber) локально не замерен (нет библиотеки); остаётся
|
||||
потенциальным вторым узким местом для будущей оптимизации (pypdf/pdfminer).
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-08-20 — Устойчивость к битым файлам (v0.0.53)
|
||||
|
||||
## Root cause (найден по логу /tmp/drhider.log)
|
||||
SSE "connection failed" при обработке НЕ был обрывом шлюза. Воркер падал целиком:
|
||||
```
|
||||
worker: exception: PdfminerException(PDFSyntaxError('No /Root object! - Is this really a PDF?'))
|
||||
```
|
||||
на битом PDF (миниатюра 142 байта). Исключение в `extract_text` не было обёрнуто
|
||||
по-файлово → воркер ронялся → SSE слал `error` → фронт показывал "SSE connection failed".
|
||||
|
||||
## Фикс (drhider/obfuscator.py)
|
||||
Проход 1: `extract_text` обёрнут в try/except. При ошибке файл добавляется в
|
||||
`skipped`, логируется warning, файл пропускается, остальные обрабатываются.
|
||||
Проход 2: файлы из `skipped` пропускаются (не попадают в результат).
|
||||
|
||||
Проверено на flat/ (10 файлов: 5 валидных + 5 битых миниатюр):
|
||||
- битые PDF/docx пропущены (PDFSyntaxError/BadZipFile), валидные 5 → в zip.
|
||||
- воркер не падает, complete доходит.
|
||||
|
||||
## Примечание
|
||||
Миниатюры 54-142 B в /mnt/y/T/flat — НЕ валидные PDF/docx (мусор из корзины).
|
||||
Их можно удалить из тестового набора (или оставить как кейс устойчивости).
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-08-20 — КРИТИЧНО: mapping.csv (ключ расшифровки) убран из выходного ZIP (v0.0.56)
|
||||
|
||||
## Проблема (утечка секретных данных)
|
||||
Выходной ZIP содержал `mapping.csv` — таблицу соответствия «оригинал → замена»,
|
||||
т.е. ключ расшифровки (реальные ФИО/телефоны/ИНН ↔ токены). Если ZIP передать
|
||||
третьему лицу — тот получал и обфусцированные файлы, и таблицу расшифровки.
|
||||
CSV должен храниться/скачиваться ОТДЕЛЬНО от обфусцированных файлов.
|
||||
|
||||
## Фикс (drhider/builder.py `build_zip`)
|
||||
- Убрана запись `mapping.csv` в архив. Теперь в ZIP — ТОЛЬКО обфусцированные файлы.
|
||||
- Параметр `mapping_csv` сохранён в сигнатуре (для совместимости вызовов), но в
|
||||
архив не пишется.
|
||||
- CSV продолжает генерироваться (`build_mapping_csv`) и отдаваться отдельно
|
||||
через `/api/csv/<sid>` (кнопка «Скачать CSV»), как написано на странице.
|
||||
|
||||
## Тесты (tests/test_builder.py)
|
||||
- Блок «ZIP с mapping.csv» переписан: теперь проверяет, что mapping.csv НЕ в архиве.
|
||||
- Интеграционный блок: ZIP содержит только doc.md, CSV проверяется отдельной строкой.
|
||||
- Все 106 тестов OK.
|
||||
|
||||
## Проверка
|
||||
- python3 tests/test_builder.py — 20/20 OK (включая новый сценарий «mapping не в zip»).
|
||||
- Остальные тесты — без изменений поведения, OK.
|
||||
- VERSION 0.0.56.
|
||||
@@ -0,0 +1,44 @@
|
||||
# 2026-08-20 — Честный ответ Sonnet об ускорении + реализация А (v0.0.55)
|
||||
|
||||
## Честный ответ Sonnet (History/2026-08-20-sonnet-query-honest-speed.md)
|
||||
Вопрос: можно ли значительно ускорить обработку, не рискуя устойчивостью/таблицами?
|
||||
|
||||
Ответ Sonnet (принято, согласуется с замерами):
|
||||
- **Безопасно ускорить extract_text() на pdfplumber НЕЛЬЗЯ** — pdfminer pure Python,
|
||||
CPU-bound, GIL полностью блокирует threading. (Подтверждено: extract_text 64.4с на Spartan10.)
|
||||
- A) Безопасно-просто:
|
||||
- A1. Threading для .doc (liberta, IO-bound) — N×~120с → ~120с при нескольких .doc.
|
||||
- A2. Кэш compiled regex между файлами (проход 2).
|
||||
- B) Заметный выигрыш, но с рисками (ОТЛОЖЕНО):
|
||||
- B1. ProcessPool по файлам (spawn) — 3 PDF × 25с → ~30с (только 2+ PDF, CPU=2).
|
||||
- B2. ProcessPool по страницам (spawn) — Spartan10 64с → ~35с (сложно: content, порядок).
|
||||
- C) Не стоит:
|
||||
- fork ProcessPool при session в памяти (COW-раздутие, 4Gi на пределе) — только spawn.
|
||||
- Threading по страницам/файлам PDF — GIL, ноль.
|
||||
- Итог Sonnet: безопасного значительного ускорения ОДНОГО PDF нет. Для набора 2+ PDF —
|
||||
B1. При CPU=2 потолок 2x.
|
||||
|
||||
## Решение пользователя
|
||||
«Делай по А, безопасно» — реализованы ТОЛЬКО A1 и A2. B — НЕ отложено (не делаем).
|
||||
|
||||
## Реализация (v0.0.55)
|
||||
|
||||
### A1 — threading для .doc (obfuscator.py, проход 1)
|
||||
- .doc файлы (конвертация через HTTP liberta, IO-bound) запускаются в
|
||||
ThreadPoolExecutor(max_workers=min(4, N_doc)).
|
||||
- Основной цикл сохраняет порядок: для .doc берёт future.result(), остальные —
|
||||
последовательно. progress_cb/порядок start/done не меняются.
|
||||
- Выигрыш: HTTP-конвертация .doc перекрывается с CPU-обработкой PDF/docx.
|
||||
|
||||
### A2 — кэш compiled regex (replacer.py + obfuscator.py)
|
||||
- replacer.py: вынесен `_build_combined_re(sorted_keys)`, `apply_replacements` получил
|
||||
параметр `compiled_re=None` (если None — компилирует сам).
|
||||
- obfuscator.py: после формирования `self._sorted_keys` компилируется один раз
|
||||
`self._compiled_re`, передаётся во все вызовы apply_replacements (проход 2).
|
||||
- __init__/finally: _compiled_re инициализируется/очищается.
|
||||
|
||||
## Проверка
|
||||
- Все 106 тестов OK (test_replacer 10/10 — apply_replacements с compiled_re).
|
||||
- Интеграция: flat/ (10 файлов) → 5 обработано (EB.md, EB1.md, Spartan10, TKM, mini_78b),
|
||||
битые пропущены, .doc через liberta в потоках. 78.3с (в основном Spartan10 64с extract_text).
|
||||
- VERSION 0.0.55.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-08-20 — Файловый лог в поде (v0.0.52)
|
||||
|
||||
## Проблема
|
||||
В v0.0.51 добавили логирование в stderr, но на платформе Штурвал
|
||||
`kubectl logs` НЕ показывает stdout/stderr приложения (fd 1/2 python -> pipe,
|
||||
который читает супервизор платформы, а не kubelet). Логи приложения недоступны.
|
||||
|
||||
## Решение
|
||||
`setup_logging()` в site/app.py:
|
||||
- логи в stderr (для локального запуска)
|
||||
- + `FileHandler` в `/tmp/drhider.log` (путь через env `LOG_FILE`)
|
||||
- уровень через env `LOG_LEVEL` (DEBUG/INFO/WARNING)
|
||||
- root.handlers.clear() + свои handler'ы (не полагаемся на basicConfig)
|
||||
- если файл не открылся — не падаем, пишем в stderr
|
||||
|
||||
## Как читать логи в поде
|
||||
```
|
||||
kubectl exec -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> -c app -- cat /tmp/drhider.log
|
||||
```
|
||||
(или tail -f). Версия 0.0.52.
|
||||
|
||||
## Проверено локально
|
||||
Файл /tmp/drhider.log создаётся, пишет "Logging configured, version=0.0.52"
|
||||
и сообщения логгеров.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 2026-08-20 — Максимальное логирование (v0.0.51)
|
||||
|
||||
## Проблема
|
||||
SSE рвётся на проде (пару минут), обработка при этом продолжается (воркер жив).
|
||||
Логов в поде не было — приложение не писало в stdout, нельзя было диагностировать
|
||||
обрыв (внешний шлюз vs генератор).
|
||||
|
||||
## Решение
|
||||
1. `site/app.py` — `setup_logging()`:
|
||||
- уровень из env `LOG_LEVEL` (DEBUG/INFO/WARNING), default INFO
|
||||
- `logging.basicConfig(..., stream=sys.stderr, force=True)` — в stdout/stderr пода
|
||||
- формат с таймстампом; уровни для логгеров drhider/app/routes/session
|
||||
- `LOG_LEVEL` задаётся через env-переменные приложения на платформе
|
||||
2. `site/routes/api_bp.py` — подробные логи:
|
||||
- upload: каждый файл (имя, размер), итог, ошибки
|
||||
- process_stream: start, worker start/done (время, токены, zip_len),
|
||||
каждое событие progress (start/done) с idx/name/elapsed,
|
||||
disconnect на heartbeat/progress/complete/error (с причиной),
|
||||
result/complete, error event
|
||||
3. VERSION поднята до 0.0.51
|
||||
|
||||
## Проверка (локально, DEBUG)
|
||||
Upload 1 файла + SSE: видны все события от upload до complete.
|
||||
`LLM NER failed: Illegal header value b'Bearer '` — ожидаемо без ключа (локально).
|
||||
|
||||
## Как читать логи пода после деплоя
|
||||
```
|
||||
kubectl logs -n 20a75175-a58c-49cb-b8fa-e86367b1a8dc <pod> --tail=500 --timestamps
|
||||
```
|
||||
- Если `process_stream: disconnect on progress` — клиент/шлюз оборвал.
|
||||
- Если worker дошёл до `complete`, а клиент не получил — рвёт шлюз/браузер.
|
||||
- Если `worker: exception` — ошибка обработки.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 2026-08-20 — В1+В2 из ревью Sonnet (v0.0.54)
|
||||
|
||||
## В2 — фикс кракозябр имён zip (extractor.py `_decode_name`)
|
||||
Добавлена попытка decode("utf-8") ПЕРЕД decode("cp866"), с валидацией диапазона:
|
||||
- результат UTF-8 принимается, только если все символы — ASCII или кириллица (U+0400–U+04FF)
|
||||
(отсекает случайную коллизию CP866→UTF-8, напр. «Т»+«г» = U+04A3);
|
||||
- при ошибке или выходе из диапазона — фоллбэк decode("cp866") (реальные 1С).
|
||||
|
||||
Проверено:
|
||||
- CP866-зип (zip_subfolders_cp866.zip) → имена корректны (1_Металлургия.pdf…)
|
||||
- UTF-8 с флагом (zip_subfolders_utf8.zip) → корректны
|
||||
- test_zip 28/28 OK
|
||||
|
||||
## В1 — фильтр перед extract_tables() (extractor.py `pdf_to_markdown`)
|
||||
Добавлен: `if page.lines or page.curves or page.rects:` перед `extract_tables()`.
|
||||
Задумывалось как ускорение сканов (extract_tables на страницах без линий впустую).
|
||||
|
||||
### ⚠️ ФАКТ замера (опровергает гипотезу Sonnet)
|
||||
На Spartan10Manual.pdf (14.8 МБ, 619 стр):
|
||||
- extract_text суммарно 64.4с (104мс/стр) — УЗКОЕ МЕСТО
|
||||
- extract_tables суммарно 0.2с (0мс/стр) — ничтожно
|
||||
- на 272 страницах без линий extract_tables суммарно 0.0с — почти мгновенно
|
||||
|
||||
ВЫВОД: фильтр В1 НЕ даёт ускорения (extract_tables и так дешёвый). Реальное узкое
|
||||
место — extract_text (pdfminer). Фильтр оставлен как безопасная защита (таблицы
|
||||
не теряет: на 0144-03-2023_отчет об оценке.pdf 94=94 таблицы, потеряно страниц 0),
|
||||
но эффект ускорения ≈ 0.
|
||||
|
||||
### Реальный путь ускорения (отложено)
|
||||
Узкое место — extract_text (104мс/стр × 619 стр = 64с). Ускорение только через:
|
||||
- распараллеливание extract_text по страницам/файлам (ProcessPool) — отложено,
|
||||
требует замера рисков (fork/память, CPU=2);
|
||||
- или смену извлечения текста без потери таблиц (риск: таблицы критичны).
|
||||
|
||||
## Тесты
|
||||
Все 106 тестов OK (test_zip 28, test_extractor 15, test_builder 20, test_replacer 10,
|
||||
test_scanner 20, test_upload 13). VERSION 0.0.54.
|
||||
Reference in New Issue
Block a user