@@ -1,10 +1,11 @@
import asyncio
import io
import logging
import re
import fitz
from config . model_settings import CROP_MODE , MODEL_PATH , PROMPT
from PIL import Image
from PIL import Image , ImageOps
from vllm import AsyncLLMEngine , SamplingParams
from vllm . engine . arg_utils import AsyncEngineArgs
from vllm . model_executor . models . registry import ModelRegistry
@@ -52,12 +53,66 @@ sampling_params = SamplingParams(
processor = DeepseekOCRProcessor ( )
# --------------------------------------------------------------------------
# 2. 핵심 처리 함수
# 2. 후 처리 함수 (공식 코드 기반)
# --------------------------------------------------------------------------
async def _process_single_image ( image : Image . Image ) - > str :
""" 단일 PIL 이미지를 받아 OCR을 수행하고 텍스트를 반환합니다. """
def _post process_text ( text : str , page_num : int = 0 ) - > str :
"""
모델의 원본 출력에서 태그를 제거/변경하고 텍스트를 정리합니다.
(test/test.py의 후처리 로직 기반으로 수정)
"""
if not text :
return " "
# 1. 정규식으로 모든 ref/det 태그 블록을 찾음
# 패턴은 (전체 매치, ref 내용, det 내용)을 캡처
pattern = r " (< \ |ref \ |>(.*?)< \ |/ref \ |>< \ |det \ |>(.*?)< \ |/det \ |>) "
matches = re . findall ( pattern , text , re . DOTALL )
# 2. 전체 매치된 문자열을 이미지 태그와 기타 태그로 분리
matches_images = [ ]
matches_other = [ ]
for match_tuple in matches :
full_match_str = match_tuple [ 0 ] # 전체 매치된 부분
ref_content = match_tuple [ 1 ] # <|ref|> 안의 내용
if " image " in ref_content :
matches_images . append ( full_match_str )
else :
matches_other . append ( full_match_str )
processed_text = text
# 3. 이미지 태그는 마크다운 링크로 대체
for idx , img_tag in enumerate ( matches_images ) :
img_link = f "  \n "
processed_text = processed_text . replace ( img_tag , img_link )
# 4. 이미지가 아닌 다른 모든 태그는 제거
for other_tag in matches_other :
processed_text = processed_text . replace ( other_tag , " " )
# 5. 특수 문자, 불필요한 토큰 및 추가 공백 정리
processed_text = (
processed_text . replace ( " <| end of sentence| > " , " " )
. replace ( " \\ coloneqq " , " := " )
. replace ( " \\ eqqcolon " , " =: " )
. replace ( " \n \n \n " , " \n \n " )
. strip ( )
)
return processed_text
# --------------------------------------------------------------------------
# 3. 핵심 처리 함수
# --------------------------------------------------------------------------
async def _process_single_image ( image : Image . Image , page_num : int = 0 ) - > str :
""" 단일 PIL 이미지를 받아 OCR을 수행하고 후처리된 텍스트를 반환합니다. """
if _engine is None :
raise RuntimeError ( " vLLM engine not initialized yet " )
if " <image> " not in PROMPT :
@@ -70,16 +125,18 @@ async def _process_single_image(image: Image.Image) -> str:
request = { " prompt " : PROMPT , " multi_modal_data " : { " image " : image_features } }
request_id = f " request- { asyncio . get_running_loop ( ) . time ( ) } "
final _output = " "
raw _output = " "
async for request_output in _engine . generate ( request , sampling_params , request_id ) :
if request_output . outputs :
final _output = request_output . outputs [ 0 ] . text
raw _output = request_output . outputs [ 0 ] . text
return final_output
# 후처리 적용 (페이지 번호 전달)
clean_text = _postprocess_text ( raw_output , page_num )
return clean_text
def _pdf_to_images ( pdf_bytes : bytes , dpi = 144 ) - > list [ Image . Image ] :
""" PDF 바이트를 받아 페이지별 PIL 이미지 리스트를 반환합니다. """
def _pdf_to_images_high_quality ( pdf_bytes : bytes , dpi = 144 ) - > list [ Image . Image ] :
""" PDF 바이트를 받아 페이지별 고품질 PIL 이미지 리스트를 반환합니다. """
images = [ ]
pdf_document = fitz . open ( stream = pdf_bytes , filetype = " pdf " )
zoom = dpi / 72.0
@@ -88,8 +145,14 @@ def _pdf_to_images(pdf_bytes: bytes, dpi=144) -> list[Image.Image]:
for page_num in range ( pdf_document . page_count ) :
page = pdf_document [ page_num ]
pixmap = page . get_pixmap ( matrix = matrix , alpha = False )
Image . MAX_IMAGE_PIXELS = None
img_data = pixmap . tobytes ( " png " )
img = Image . open ( io . BytesIO ( img_data ) )
if img . mode in ( " RGBA " , " LA " ) :
background = Image . new ( " RGB " , img . size , ( 255 , 255 , 255 ) )
background . paste ( img , mask = img . split ( ) [ - 1 ] if img . mode == " RGBA " else None )
img = background
images . append ( img )
pdf_document . close ( )
@@ -100,26 +163,37 @@ async def process_document(file_bytes: bytes, content_type: str, filename: str)
"""
업로드된 파일(이미지 또는 PDF)을 처리하여 OCR 결과를 반환합니다.
"""
if c ontent_t ype. startswith ( " image/ " ) :
# C ontent-T ype이 generic할 경우, 파일 확장자로 타입을 유추
inferred_content_type = content_type
if content_type == " application/octet-stream " :
if filename . lower ( ) . endswith ( " .pdf " ) :
inferred_content_type = " application/pdf "
elif any ( filename . lower ( ) . endswith ( ext ) for ext in [ " .jpg " , " .jpeg " , " .png " , " .bmp " , " .gif " , " .webp " ] ) :
inferred_content_type = " image/jpeg " # 구체적인 타입은 중요하지 않음
if inferred_content_type . startswith ( " image/ " ) :
try :
image = Image . open ( io . BytesIO ( file_bytes ) ) . convert ( " RGB " )
image = Image . open ( io . BytesIO ( file_bytes ) )
image = ImageOps . exif_transpose ( image ) . convert ( " RGB " )
except Exception as e :
raise ValueError ( f " 이미지 파일을 여는 데 실패했습니다: { e } " )
result_text = await _process_single_image ( image )
# 단일 이미지는 페이지 번호를 0으로 간주
result_text = await _process_single_image ( image , page_num = 0 )
return { " filename " : filename , " text " : result_text }
elif content_type == " application/pdf " :
elif inferred_ content_type == " application/pdf " :
try :
images = _pdf_to_images ( file_bytes )
images = _pdf_to_images_high_quality ( file_bytes )
except Exception as e :
raise ValueError ( f " PDF 파일을 처리하는 데 실패했습니다: { e } " )
# 각 페이지를 비동기적으로 처리
tasks = [ _process_single_image ( img ) for img in images ]
# 각 페이지를 비동기적으로 처리 (페이지 번호 전달)
tasks = [
_process_single_image ( img , page_num = i ) for i , img in enumerate ( images )
]
page_results = await asyncio . gather ( * tasks )
# 페이지 구분자를 넣어 전체 텍스트 합치기
full_text = " \n <--- Page Split ---> \n " . join ( page_results )
return { " filename " : filename , " text " : full_text , " page_count " : len ( images ) }
@@ -128,3 +202,4 @@ async def process_document(file_bytes: bytes, content_type: str, filename: str)
f " 지원하지 않는 파일 형식입니다: { content_type } . "
" 이미지(JPEG, PNG 등) 또는 PDF 파일을 업로드해주세요. "
)