"""Execução paralela de tarefas independentes. O gargalo da indexação é o Tesseract, um processo externo. Como o trabalho liberado por cada tarefa é predominantemente I/O (subprocesso e escrita em disco), threads bastam: o GIL é liberado durante a espera pelo processo. `parallel_map` preserva a ordem dos resultados e isola falhas — uma tarefa que lança exceção não derruba as demais. """ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor from typing import Callable, Iterable, Sequence, TypeVar T = TypeVar("T") R = TypeVar("R") def parallel_map( items: Sequence[T], function: Callable[[T], R], workers: int = 4, default: R | None = None, ) -> list[R | None]: """Aplica `function` a cada item em paralelo, preservando a ordem. Itens cuja função lança exceção recebem o valor `default`. """ if not items: return [] worker_count = max(1, min(workers, len(items))) def guarded(item: T) -> R | None: try: return function(item) except Exception: return default if worker_count == 1: return [guarded(item) for item in items] with ThreadPoolExecutor(max_workers=worker_count) as pool: return list(pool.map(guarded, items)) def chunked(items: Iterable[T], size: int) -> list[list[T]]: """Divide um iterável em lotes de tamanho `size`.""" chunk: list[T] = [] batches: list[list[T]] = [] for item in items: chunk.append(item) if len(chunk) >= size: batches.append(chunk) chunk = [] if chunk: batches.append(chunk) return batches