From 6047592988019ac7194f8fad1a33b810fbc52513 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:18:51 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20Refactor=20and=20optimize=20engi?= =?UTF-8?q?ne=20thread=20pool,=20event=20loop,=20and=20resultobj=20initial?= =?UTF-8?q?ization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bounded ThreadPoolExecutor max_workers to min(4, cpu_count) to avoid contention. - Wrapped synchronous PIL and base64 operations in asyncio.to_thread. - Cleaned up resultobj initialization using dictionary comprehension. Co-authored-by: chottokun <29515187+chottokun@users.noreply.github.com> --- src/api/main.py | 10 +++++----- src/core/engine.py | 21 +++++++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index 4fb72e2..ac8a097 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -241,8 +241,8 @@ async def openai_vision_endpoint( try: # Remove data URI prefix if present header, encoded = img_data.split(",", 1) if "," in img_data else (None, img_data) - contents = base64.b64decode(encoded) - img = Image.open(io.BytesIO(contents)) + contents = await asyncio.to_thread(base64.b64decode, encoded) + img = await asyncio.to_thread(Image.open, io.BytesIO(contents)) # Dimension check if img.width * img.height > MAX_PIXELS: @@ -321,7 +321,7 @@ async def _get_image_from_request(request: Request, file: Optional[UploadFile]): raise HTTPException(status_code=499, detail="Client closed connection") if len(contents) > MAX_IMAGE_SIZE: raise HTTPException(status_code=413, detail="File too large") - img = Image.open(io.BytesIO(contents)) + img = await asyncio.to_thread(Image.open, io.BytesIO(contents)) filename = file.filename or "uploaded_image.jpg" else: # Handle JSON body (Base64) @@ -346,8 +346,8 @@ async def _get_image_from_request(request: Request, file: Optional[UploadFile]): ocr_req = OCRRequest(**body) # Remove data URI prefix if present header, encoded = ocr_req.image.split(",", 1) if "," in ocr_req.image else (None, ocr_req.image) - contents = base64.b64decode(encoded) - img = Image.open(io.BytesIO(contents)) + contents = await asyncio.to_thread(base64.b64decode, encoded) + img = await asyncio.to_thread(Image.open, io.BytesIO(contents)) filename = "base64_image.jpg" except HTTPException: raise diff --git a/src/core/engine.py b/src/core/engine.py index fc6c2c8..2f0aa08 100644 --- a/src/core/engine.py +++ b/src/core/engine.py @@ -86,8 +86,17 @@ def __init__( self.recognizer50 = None # ThreadPoolExecutor for parallelizing character recognition across lines + # Allow override via environment variable OCR_MAX_WORKERS, or use a bounded default. + # Too many concurrent threads calling ORT sessions on CPU causes high contention and thrashing. + max_workers_env = os.getenv("OCR_MAX_WORKERS") + if max_workers_env: + max_workers = int(max_workers_env) + else: + cpu_count = os.cpu_count() or 4 + max_workers = min(4, cpu_count) + self.executor = ThreadPoolExecutor( - max_workers=os.cpu_count() or 4, + max_workers=max_workers, thread_name_prefix="ocr_worker" ) @@ -221,14 +230,14 @@ def ocr(self, pil_image: Image.Image, img_name: str = "image.jpg") -> Dict[str, classeslist = list(self.detector.classes.values()) # Prepare data for NDL-style XML conversion - resultobj = [dict(), dict()] - resultobj[0][0] = list() - for i in range(17): - resultobj[1][i] = [] + resultobj = [ + {0: []}, + {i: [] for i in range(17)} + ] for det in detections: xmin, ymin, xmax, ymax = det["box"] conf = det["confidence"] - char_count = det.get("pred_char_count", 100.0) # v1.2.1 uses char_count in resultobj + char_count = det.get("pred_char_count", 100.0) # v1.2.1 uses char_count in resultobj if det["class_index"] == 0: resultobj[0][0].append([xmin, ymin, xmax, ymax]) # v1.2.1 adds char_count here