processing: clarify per-file stages and outcomes
Expose extraction, analysis, and replacement progress in the file table; preserve row mapping for duplicates and collapsed archives. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+39
-21
@@ -32,7 +32,7 @@ class LLMIncompleteError(Exception):
|
||||
|
||||
def _dedupe_file_names(
|
||||
files: List[Tuple[str, bytes, str]]
|
||||
) -> List[Tuple[str, bytes, str]]:
|
||||
) -> Tuple[List[Tuple[str, bytes, str]], List[List[int]]]:
|
||||
"""Уникализировать имена файлов перед обработкой.
|
||||
|
||||
Точный дубль (одинаковое имя + одинаковое содержимое) пропускается.
|
||||
@@ -42,17 +42,24 @@ def _dedupe_file_names(
|
||||
files: [(filename, content_bytes, content_type), ...]
|
||||
|
||||
Returns:
|
||||
Новый список с уникальными именами (порядок сохранён).
|
||||
Новый список с уникальными именами (порядок сохранён) и исходные индексы,
|
||||
соответствующие каждому файлу. Точные дубликаты ссылаются на один результат.
|
||||
"""
|
||||
seen: Dict[str, bytes] = {}
|
||||
out: List[Tuple[str, bytes, str]] = []
|
||||
source_indices: List[List[int]] = []
|
||||
source_to_output: Dict[Tuple[str, bytes], int] = {}
|
||||
|
||||
for fname, content, ctype in files:
|
||||
for source_idx, (fname, content, ctype) in enumerate(files):
|
||||
candidate = resolve_name(fname, content, seen)
|
||||
if candidate is not None:
|
||||
out.append((candidate, content, ctype))
|
||||
source_to_output[(fname, content)] = len(out) - 1
|
||||
source_indices.append([source_idx])
|
||||
else:
|
||||
source_indices[source_to_output[(fname, content)]].append(source_idx)
|
||||
|
||||
return out
|
||||
return out, source_indices
|
||||
|
||||
|
||||
class TwoPassObfuscator:
|
||||
@@ -89,7 +96,7 @@ class TwoPassObfuscator:
|
||||
|
||||
def obfuscate(
|
||||
self, files: List[Tuple[str, bytes, str]],
|
||||
progress_cb: Optional[Callable[[str, int, int, str], None]] = None,
|
||||
progress_cb: Optional[Callable[[str, int, int, str, float, List[int]], None]] = None,
|
||||
cancel_event=None, file_progress=None
|
||||
) -> Tuple[bytes, str, Optional[dict]]:
|
||||
"""Обфусцировать список файлов.
|
||||
@@ -98,14 +105,18 @@ class TwoPassObfuscator:
|
||||
|
||||
Args:
|
||||
files: [(filename, content_bytes, content_type), ...]
|
||||
progress_cb: Опциональный коллбек (phase, idx, total, fname, elapsed),
|
||||
где phase ∈ {"start", "done"}, idx — 0-based индекс,
|
||||
elapsed — время обработки конкретного файла (сек).
|
||||
progress_cb: Опциональный коллбек (phase, idx, total, fname, elapsed, source_idxs),
|
||||
phase — extract_start/extract_done/extract_failed или
|
||||
replace_start/replace_done/replace_failed; idx — 0-based индекс,
|
||||
elapsed — время обработки конкретного файла (сек),
|
||||
source_idxs — индексы строк исходного списка, включая дубликаты.
|
||||
cancel_event: threading.Event — мягкая остановка. Проверяется между
|
||||
файлами LLM и между файлами замены; текущий файл добирается.
|
||||
file_progress: callable(event, fname, **fields) — прогресс по файлам/чанкам.
|
||||
События: extract_done{total_chars}, file_start{chars,chunks,idx},
|
||||
file_chunk{chunks_done,chunks_total,idx}, file_done{elapsed,idx}.
|
||||
События: extract_complete{total_chars},
|
||||
file_start{chars,chunks,idx,source_idxs},
|
||||
file_chunk{chunks_done,chunks_total,idx,source_idxs},
|
||||
file_done{elapsed,idx,source_idxs}.
|
||||
|
||||
Returns:
|
||||
(zip_bytes, csv_string, meta):
|
||||
@@ -116,7 +127,7 @@ class TwoPassObfuscator:
|
||||
"""
|
||||
# ── Предобработка: распаковать ZIP + уникализировать имена ──
|
||||
files = extractor.expand_zips(files)
|
||||
files = _dedupe_file_names(files)
|
||||
files, source_indices = _dedupe_file_names(files)
|
||||
|
||||
# имя файла (внутр. ключ all_texts) -> idx в files (для событий file_progress)
|
||||
name_to_idx = {fname: i for i, (fname, _, _) in enumerate(files)}
|
||||
@@ -126,7 +137,9 @@ class TwoPassObfuscator:
|
||||
if event == "file_done":
|
||||
llm_done.add(fname)
|
||||
if file_progress:
|
||||
fields["idx"] = name_to_idx.get(fname)
|
||||
idx = name_to_idx.get(fname)
|
||||
fields["idx"] = idx
|
||||
fields["source_idxs"] = source_indices[idx] if idx is not None else []
|
||||
file_progress(event, fname, **fields)
|
||||
|
||||
try:
|
||||
@@ -150,7 +163,7 @@ class TwoPassObfuscator:
|
||||
break
|
||||
display_name = os.path.basename(fname) or fname
|
||||
if progress_cb:
|
||||
progress_cb("start", i, total, display_name, 0.0)
|
||||
progress_cb("extract_start", i, total, display_name, 0.0, source_indices[i])
|
||||
|
||||
t0 = time.time()
|
||||
try:
|
||||
@@ -159,9 +172,11 @@ class TwoPassObfuscator:
|
||||
log.warning("obfuscate: skip file %r: %r", fname, e)
|
||||
skipped.add(fname)
|
||||
if progress_cb:
|
||||
progress_cb("done", i, total, display_name, 0.0)
|
||||
progress_cb("extract_failed", i, total, display_name, 0.0, source_indices[i])
|
||||
continue
|
||||
all_texts[fname] = text
|
||||
if progress_cb:
|
||||
progress_cb("extract_done", i, total, display_name, 0.0, source_indices[i])
|
||||
|
||||
# Regex-сканирование (быстрое, локальное)
|
||||
if text and not text.startswith("[DOC binary"):
|
||||
@@ -171,7 +186,7 @@ class TwoPassObfuscator:
|
||||
# Объём извлечённого текста — для оценки времени (глобальной ETA)
|
||||
total_chars = sum(len(t) for t in all_texts.values())
|
||||
if file_progress:
|
||||
file_progress("extract_done", None, total_chars=total_chars,
|
||||
file_progress("extract_complete", None, total_chars=total_chars,
|
||||
per_file={fname: len(t) for fname, t in all_texts.items()})
|
||||
|
||||
# LLM-сканирование (получает уже найденное regex'ом чтобы не дублировать)
|
||||
@@ -208,8 +223,6 @@ class TwoPassObfuscator:
|
||||
|
||||
# Битый файл (не удалось извлечь текст) — пропускаем
|
||||
if fname in skipped:
|
||||
if progress_cb:
|
||||
progress_cb("done", i, total, display_name, 0.0)
|
||||
continue
|
||||
|
||||
# При прерывании в LLM — в результат идут ТОЛЬКО файлы с завершённым LLM
|
||||
@@ -223,6 +236,8 @@ class TwoPassObfuscator:
|
||||
cancelled = True
|
||||
break
|
||||
|
||||
if progress_cb:
|
||||
progress_cb("replace_start", i, total, display_name, 0.0, source_indices[i])
|
||||
t0 = time.time()
|
||||
obf_content = content # По умолчанию — без изменений
|
||||
|
||||
@@ -235,7 +250,7 @@ class TwoPassObfuscator:
|
||||
log.warning("obfuscate: skip CSV %r during replacement: %r", fname, e)
|
||||
skipped.add(fname)
|
||||
if progress_cb:
|
||||
progress_cb("done", i, total, display_name, 0.0)
|
||||
progress_cb("replace_failed", i, total, display_name, 0.0, source_indices[i])
|
||||
continue
|
||||
elif fname.endswith('.doc'):
|
||||
# .doc — конвертируется через liberta, обрабатывается как .docx
|
||||
@@ -260,7 +275,10 @@ class TwoPassObfuscator:
|
||||
file_times[i] += time.time() - t0
|
||||
results.append((fname, obf_content))
|
||||
if progress_cb:
|
||||
progress_cb("done", i, total, display_name, round(file_times[i], 2))
|
||||
progress_cb(
|
||||
"replace_done", i, total, display_name,
|
||||
round(file_times[i], 2), source_indices[i],
|
||||
)
|
||||
|
||||
# ── Сборка результата ──
|
||||
csv_str = builder.build_mapping_csv(self._mapping)
|
||||
@@ -286,7 +304,7 @@ class TwoPassObfuscator:
|
||||
|
||||
def obfuscate_files(
|
||||
files: List[Tuple[str, bytes, str]], llm_client=None,
|
||||
progress_cb: Optional[Callable[[str, int, int, str], None]] = None,
|
||||
progress_cb: Optional[Callable[[str, int, int, str, float, List[int]], None]] = None,
|
||||
cancel_event=None, file_progress=None
|
||||
) -> Tuple[bytes, str, Optional[dict]]:
|
||||
"""Обфусцировать список файлов — удобная функция.
|
||||
@@ -296,7 +314,7 @@ def obfuscate_files(
|
||||
Args:
|
||||
files: [(filename, content_bytes, content_type), ...]
|
||||
llm_client: Опциональный LLM-клиент
|
||||
progress_cb: Опциональный коллбек (phase, idx, total, fname)
|
||||
progress_cb: Опциональный коллбек (phase, idx, total, fname, elapsed, source_idxs)
|
||||
cancel_event: threading.Event — мягкая остановка (см. TwoPassObfuscator.obfuscate)
|
||||
file_progress: callable(event, fname, **fields) — прогресс по файлам/чанкам
|
||||
|
||||
|
||||
+13
-4
@@ -14,7 +14,7 @@ import json
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .config import (
|
||||
@@ -294,13 +294,22 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str],
|
||||
with ThreadPoolExecutor(max_workers=_LLM_CONCURRENCY) as _ex:
|
||||
futures = [_ex.submit(_call_llm, _build_llm_prompt(c, mapping.keys()), llm_client)
|
||||
for c in chunks]
|
||||
all_ent = []
|
||||
for fut in futures:
|
||||
chunk_results = [None] * len(futures)
|
||||
future_indexes = {future: index for index, future in enumerate(futures)}
|
||||
completed = 0
|
||||
for fut in as_completed(futures):
|
||||
index = future_indexes[fut]
|
||||
try:
|
||||
all_ent.extend(fut.result())
|
||||
chunk_results[index] = fut.result()
|
||||
except Exception as e:
|
||||
llm_failures += 1
|
||||
log.warning("LLM NER chunk failed in %r: %s", fname, e)
|
||||
chunk_results[index] = []
|
||||
completed += 1
|
||||
if file_progress:
|
||||
file_progress("file_chunk", fname,
|
||||
chunks_done=completed, chunks_total=len(chunks))
|
||||
all_ent = [ent for chunk_result in chunk_results for ent in chunk_result]
|
||||
else:
|
||||
all_ent = []
|
||||
for k, c in enumerate(chunks):
|
||||
|
||||
Reference in New Issue
Block a user