← AI Document Processing
👁

03 / 11

OCR 파이프라인

PDF → Label Studio → EasyOCR Backend → 텍스트/박스 추출, API 토큰 적용

EasyOCRTesseractLabel Studio
개요환경 구성OCR 파이프라인모델 학습모델 / 데이터 처리LayoutLM 분석성능 관리배포 & 운영PaddleOCR 도입BIO 라벨 & 학습 개선문제 해결

Label Studio 모델 연결

무료 버전

모델 1개만 연결 가능

프로젝트당 ML Backend 하나만 연결 가능

Enterprise 버전

여러 모델 연결 가능

다중 ML Backend 동시 연결 지원

Label Studio API 토큰 적용

업로드 파일 경로가 http://labelstudio:8080/data/upload/... 처럼 LS 서버 내부 URL로 제공됨. ML 백엔드는 그 URL을 호출해 파일을 받아야 하고, 이때 API KEY로 인증해야 함. 같은 docker 네트워크라 호스트네임 labelstudio로 접근하며, 이를 LABEL_STUDIO_URL로 알려줘야 함.

설정 과정

1

Personal Access Token 발급

2

ls-stack/.env 파일에 값 설정

3

docker-compose.yml에서 환경변수 확인

4

컨테이너 재시작

.env 파일 설정

# Label Studio 내부 네트워크 주소
# (docker-compose의 labelstudio 서비스명 사용)
LABEL_STUDIO_URL=http://labelstudio:8080
LABEL_STUDIO_API_KEY=ls_여기에_방금_발급한_토큰_붙여넣기

docker-compose.yml 환경변수

easyocr:
    environment:
      - LABEL_STUDIO_URL=${LABEL_STUDIO_URL}
      - LABEL_STUDIO_API_KEY=${LABEL_STUDIO_API_KEY}
  ner:
    environment:
      - LABEL_STUDIO_URL=${LABEL_STUDIO_URL}
      - LABEL_STUDIO_API_KEY=${LABEL_STUDIO_API_KEY}

컨테이너 재시작

cd ~/ls-stack
docker compose --env-file .env up -d
# 로그 확인
docker compose logs -f easyocr

관련 명령어 및 파일

자주 사용하는 명령어

# 도커 이미지 빌드
docker build -t ls-ml-backend:local .

# 컨테이너 상태 확인
docker compose -f ~/ls-stack/docker-compose.yml ps

# 로그 확인
docker compose -f ~/ls-stack/docker-compose.yml logs -f easyocr
docker compose -f ~/ls-stack/docker-compose.yml logs -f ner
docker compose -f ~/ls-stack/docker-compose.yml logs -f labelstudio

# 컨테이너 재시작
docker compose -f ~/ls-stack/docker-compose.yml down
docker compose -f ~/ls-stack/docker-compose.yml up -d

# 컨테이너 내부 확인
docker exec -it easyocr bash

# model.py 수정
vi ~/label-studio-ml-backend/label_studio_ml/\
examples/easyocr/model.py

관련 파일 경로

~/ls-stack/.env환경변수 설정 파일
~/ls-stack/docker-compose.ymlDocker Compose 구성
~/label-studio-ml-backend/DockerfileML Backend 도커 이미지
~/label-studio-ml-backend/.../easyocr/model.pyEasyOCR 모델 코드

Troubleshooting

Label Studio + ML Backend(EasyOCR) 충돌 해결

1. 초기 문제: 401 Unauthorized

증상

Label Studio에서 Retrieve Predictions 실행 시 EasyOCR 컨테이너에서 에러 발생

401 Unauthorized
Failed to download via get_local_path

원인: Label Studio 1.21.0에서 API 인증 방식이 변경됨 → 기존 Token 인증 무효화. PAT → refresh → bearer access token 형식으로 변경 필요.

2. PAT → Access Token 교환 테스트

수동 curl로 refresh 테스트 → 성공. EasyOCR 컨테이너 내에서도 수동 실행 시 정상 동작. 네트워크 문제는 아님을 확인.

curl -X POST http://localhost:7070/api/token/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh": "<PAT_TOKEN>"}'
# → 200 OK + access 토큰 반환

3. 실제 문제: model.py 인증 로직

이미지 다운로드 시 Label Studio Tools IO SDK가 Authorization: Token <PAT> 헤더를 사용
PAT는 Token 인증이 아님 → 모두 401 발생
이미지 URL이 /data/upload/... 형식인데 hostname prefix를 잘못 붙이는 문제

4. 최종 해결: 인증/다운로드 로직 개선

1

PAT 감지 → Bearer Access Token 자동 Refresh

def _is_pat(self, token):
    return token.count(".") == 2
2

PAT → Access Token 자동 교환

POST /api/token/refresh → Bearer access token 캐싱 후 사용

3

이미지 다운로드 시 Bearer 토큰 우선 적용

Authorization: Bearer <access>

4

Label Studio 내부 도메인(hostname) 제거

/data/upload/... 같은 상대 URL은 prefix 추가하지 않고, 완전한 URL만 prefix 처리

5

get_local_path는 fallback으로만 사용

PAT 환경에서는 반드시 Bearer 모드로 다운로드

토큰 검증 코드

docker exec -it easyocr bash -lc "python - <<'PY'
import os, requests
base = os.environ['LABEL_STUDIO_URL'].rstrip('/')
pat  = os.environ.get('LABEL_STUDIO_API_KEY','').strip()

# 1) PAT -> access 토큰
r = requests.post(
    base+'/api/token/refresh',
    json={'refresh': pat}, timeout=10
)
print('refresh STATUS:', r.status_code, r.text[:200])
acc = r.json().get('access')

# 2) access 토큰으로 API 호출
if acc:
    r2 = requests.get(
        base+'/api/projects',
        headers={'Authorization': f'Bearer {acc}'},
        timeout=10
    )
    print('projects STATUS:', r2.status_code, r2.text[:200])
PY"

해결 결과

모든 이미지 정상 다운로드
Retrieve Predictions 완전 정상 작동
Label Studio 화면에 하이라이트 polygon + 텍스트 정상 표시

참고: GPU 없는 CPU-only 환경에서 뜨는 PyTorch pin_memory Warning은 무시 가능

전체 해결 흐름

단계문제/행동결과
1Label Studio → EasyOCR 호출이미지 다운로드에서 401 계속 발생
2PAT을 Token 헤더에 넣어 요청LS 1.21.0에서 폐기된 방식
3curl로 /api/token/refresh 테스트PAT 정상 → access 발급됨
4EasyOCR 컨테이너에서도 테스트refresh + /api/projects = 200 OK
5실제 OCR 호출에서는 계속 401이미지 다운로드 경로/토큰 방식 문제
6model.py 전체 인증 로직 재작성Bearer 방식 + URL 정상화
7최종 테스트OCR prediction 정상 표시

LayoutLM Integration

LayoutLM / Label Studio 연동

모델 선정 조건

1안 뽑아지거나 잘못 뽑은 토큰에 대한 학습이 가능
2학습에 대한 비용이 적게 듦
3OUTPUT이 key-value 형식으로 나와야 함 (ex. invoice_number: 723423423)
4필드가 fixed되지 않는 방향 (key를 정해놓고 value를 뽑는 방식이 아닌)
5Label Studio에 연결 가능

AWS Textract

탈락

학습 불가

Donut

탈락

fixed된 필드 필요 (CORD notebook 기준, 사전 정의된 key 목록 필수)

LayoutLM

채택

SER + RE 기반 유연한 추출

모델 선정 고민 과정

2023년에 같은 고민을 한 사람이 있었음 — Transformers-Tutorials GitHub Issue #308에서 'LayoutXLM SER + RE'를 언급.

"I have briefly looked through the notebook, seems the CORD notebook for key-value requires a list of pre-defined keys (which means the keys are fixed), is there a way to extract random key-values like what LayoutXLM SER + RE does?"

— NielsRogge/Transformers-Tutorials #308

핵심 문제: Donut 등 기존 모델은 key를 미리 정의해야 하지만, 실제 인보이스는 회사마다 필드가 다르고 계속 바뀜. LayoutXLM SER + RE 방식이라면 fixed key 없이 유연하게 key-value를 추출할 수 있음. → 이 방향으로 LayoutLM 채택을 결정.

라이센스 이슈

LayoutLM, LayoutXLM은 상업적 이용 불가 라이센스. 상업적 활용을 위해서는 Microsoft Research Licensing Team에 연락하여 별도 license grant 또는 commercial agreement 체결이 필요.

LayoutLMv2 / v3 / LayoutXLM

CC BY-NC-SA 4.0 (비상업적)

RE 모델 포함 — 상업적 사용 불가

LayoutLM v1

MIT License (상업적 사용 가능)

'LayoutLM v1 + 커스텀 Fine-Tuning' 방향으로 진행

단, v2/v3의 weight는 못 쓰지만 로직(코드)은 사용 가능. RE 모델이 "어떻게 linking을 예측하는지" 알고리즘은 저작권 위반이 아님: bounding-box normalization, linking 방식, loss 함수, entity merge 알고리즘, distance heuristic, post-processing pipeline 등.

LayoutLM 핵심 원리

문서 구조는 다양하고 계속 바뀜 → 구조에 대한 학습 필요. Position Embedding + Text Embedding + Image Embedding + Faster R-CNN 기반.

LayoutLM 적용해서 결과가 나오면 Label Studio에서 보여주고, 빠진 데이터나 수정해야 할 데이터가 있다면 사용자 입력을 통해 수정. 수정한 데이터를 기반으로 재학습 진행. (Human-in-the-Loop)

⚠ LayoutLM은 GPU 기반 모델이라 GPU 포함된 상태로 돌아가야 함

layoutlm_preprocess.py 역할

한 장의 이미지에 대해 전처리부터 추론까지 수행하는 핵심 모듈

1

pytesseract로 OCR 수행

2

OCR 결과를 LayoutLM 형식에 맞게 단어·bbox 추출

3

LayoutLM 입력 포맷으로 변환 (convert_example_to_features)

4

fine-tuned LayoutLM 모델로 inference (convert_to_features)

Output

  • word_level_predictions: 각 단어의 label id 리스트 (BIOES, ANSWER/QUESTION 등)
  • final_boxes: 각 단어의 실제 픽셀 좌표 [x0, y0, x1, y1]

→ 이 함수들만 있으면 "이미지 1장 → 단어별 예측 라벨 + 실제 박스 좌표"까지 완성

Label Studio → ML Backend 호출 흐름

1

사용자가 Label Studio에서 문서 이미지를 열면

2

Label Studio가 해당 task JSON을 백엔드에 HTTP로 전송 (/predict)

3

백엔드에서 layoutlm_preprocess.py 기반으로 추론 수행

4

word별 라벨 + bbox를 Label Studio JSON 포맷으로 변환

5

Label Studio 화면에 자동으로 그려진 박스 + 라벨이 표시

사용자는 자동 생성된 박스를 보고: 잘못된 라벨 수정, 박스 위치 조정/삭제, 누락된 토큰 추가 등의 검수 작업 수행.

Label Studio 설정 (RectangleLabels)

문서 + LayoutLM은 이미지 + 사각형 박스(RectangleLabels) 구조로 구성

<View>
  <Image name="image" value="$image" />
  <RectangleLabels name="label" toName="image">
    <Label value="QUESTION" background="#1f77b4" />
    <Label value="ANSWER" background="#2ca02c" />
    <Label value="HEADER" background="#ff7f0e" />
    <Label value="OTHER" background="#9467bd" />
  </RectangleLabels>
</View>

ML Backend 코드 구조 (model.py)

layoutlm_preprocess.py를 Label Studio ML Backend로 포장한 핵심 클래스

class LayoutLMv1Backend(LabelStudioMLBase):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.device = torch.device(
            "cuda" if torch.cuda.is_available() else "cpu"
        )
        self.labels = load_labels(LABELS_PATH)
        self.id2label = {i: lab for i, lab in enumerate(self.labels)}
        self.model = model_load(MODEL_PATH, num_labels)
        self.model.to(self.device)
        self.model.eval()

    def predict(self, tasks, **kwargs):
        predictions = []
        for task in tasks:
            image_url = task["data"]["image"]
            image, words, boxes, actual_boxes = preprocess(image_url)
            word_level_predictions, final_boxes = convert_to_features(
                image, words, boxes, actual_boxes, self.model
            )
            img_w, img_h = image.size

            ls_results = []
            for pred_id, box in zip(
                word_level_predictions, final_boxes
            ):
                raw_label = self.id2label[pred_id]
                entity = iob_to_label(raw_label).upper()
                if entity == "":
                    continue

                x0, y0, x1, y1 = box
                # Label Studio는 퍼센트 좌표(0~100) 사용
                result = {
                    "from_name": "label",
                    "to_name": "image",
                    "type": "rectanglelabels",
                    "value": {
                        "x": x0 / img_w * 100,
                        "y": y0 / img_h * 100,
                        "width": (x1 - x0) / img_w * 100,
                        "height": (y1 - y0) / img_h * 100,
                        "rotation": 0,
                        "labels": [entity]
                    }
                }
                ls_results.append(result)

            predictions.append({"result": ls_results})
        return predictions

전체 진행 과정

1

PDF 300장 업로드 → S3 또는 Label Studio에 직접 업로드

2

PDF → PNG 변환 (pdf2image 등으로 일괄 처리)

3

LayoutLM v1 적용 — ML Backend에서 preprocess() + convert_to_features() 수행

4

LayoutLM 결과를 Label Studio로 전달 (Model Predictions)

5

Label Studio에서 검수/수정 (RectangleLabels UI)

6

검수된 데이터를 기반으로 재학습 (Export JSON → FUNSD 형식 변환 → Fine-tuning)

전체 구조

[Label Studio 컨테이너]  포트 7070
       |
       |  HTTP /predict
       v
[LayoutLM ML 백엔드 컨테이너]  포트 7073 → 9090
       |
       |  내부에서:
       |   - PNG 이미지 로드
       |   - layoutlm_preprocess.py
       |     (pytesseract + bbox + normalize)
       |   - LayoutLM v1 fine-tuned 모델 로드
       |   - 단어별 BIOES 라벨 예측
       |     → RECTANGLE 박스로 변환
       v
Label Studio UI에 자동 박스 + 라벨 노출

Automation

Retrieve Prediction 자동화

문제: UI에서 대량 처리 시 멈춤

UI에서 "Retrieve Predictions"로 대량(15~50개) 처리하다가 멈추는 문제 발생. API 호출로 전환하면 상당 부분 완화 가능.

UI에서 대량이 깨지는 원인

브라우저 요청이 길어지면서 프론트 타임아웃/재시도 실패
한 번에 너무 많은 task에 대해 backend 요청 발생 → 서버측 큐/워커 병목
응답 payload가 크면 브라우저가 렌더링/상태관리에서 터짐

API(서버 스크립트)로 처리하면

요청을 작게 쪼개서(batch size 5~10) 순차 처리 가능
실패한 task만 재시도 가능
로그/재처리/스킵 기준을 코드로 제어 가능

해결 방향

핵심은 "Label Studio API로 Retrieve Predictions 버튼을 그대로 호출"이 아니라, Tesseract ML Backend(=BBOXOCR)가 제공하는 /predict 엔드포인트를 서버에서 직접 호출하고, 그 결과를 Label Studio의 /api/predictions로 저장하는 방식.

1

Tesseract ML Backend /predict 엔드포인트 직접 호출

2

결과를 Label Studio /api/predictions로 저장

3

layoutlm_batch_predict.py가 LayoutLM 결과를 올리는 것과 동일한 방식

구현에 필요한 값

1. Tesseract ML Backend URL

TESSERACT_ML_URL = "http://localhost:9090"

Docker로 띄웠으면 컨테이너 포트/서비스 주소 확인. Label Studio UI의 ML Backend 설정 화면에서도 URL 확인 가능.

2. /predict 요청/응답 포맷

// 요청
{ "tasks": [{ "id": 198, "data": {...} }] }

// 응답
{ "results": [{
    "result": [...],
    "score": 0.95,
    "model_version": "BBOXOCR"
}]}

핵심 함수: run_tesseract_and_upload_prediction

Tesseract ML Backend를 직접 호출하고 결과를 Label Studio에 업로드하는 함수

TESSERACT_ML_URL = "http://localhost:9090"
TESSERACT_MODEL_VERSION = "BBOXOCR-auto"

def run_tesseract_and_upload_prediction(task: dict) -> bool:
    """
    1) Tesseract ML backend /predict 호출
    2) Label Studio /api/predictions 로 업로드
    """
    task_id = task.get("id")
    if not task_id:
        return False

    payload = {"tasks": [task]}

    try:
        r = requests.post(
            f"{TESSERACT_ML_URL}/predict",
            headers={"Content-Type": "application/json"},
            data=json.dumps(payload),
            timeout=300,
        )
        r.raise_for_status()
        pred = r.json()
    except Exception as e:
        print(f"[WARN] task {task_id}: "
              f"tesseract /predict 실패 - {e}")
        return False

    # 결과 파싱 (환경마다 키가 다를 수 있음)
    result_item = None
    if isinstance(pred, dict):
        if isinstance(pred.get("results"), list) \
                and pred["results"]:
            result_item = pred["results"][0]
        elif isinstance(pred.get("predictions"), list) \
                and pred["predictions"]:
            result_item = pred["predictions"][0]

    if not result_item:
        return False

    ls_result = result_item.get("result")
    if not isinstance(ls_result, list) or not ls_result:
        return False

    # Label Studio에 prediction 생성
    post_payload = {
        "task": task_id,
        "result": ls_result,
        "model_version": result_item.get(
            "model_version") or TESSERACT_MODEL_VERSION,
        "score": float(result_item.get("score") or 0.0),
    }

    try:
        resp = requests.post(
            f"{LS_URL}/api/predictions",
            headers=HEADERS,
            data=json.dumps(post_payload),
            timeout=60,
        )
        resp.raise_for_status()
        print(f"[INFO] task {task_id}: "
              f"Tesseract prediction 생성 완료(API)")
        return True
    except Exception as e:
        print(f"[WARN] task {task_id}: "
              f"prediction 업로드 실패 - {e}")
        return False

기존 배치 루프 수정

Before (SKIP)

preds = fetch_predictions_for_task(task_id)
if not preds:
    print(f"[SKIP] task {task_id}: "
          f"prediction 없음")
    continue

After (자동 Tesseract)

preds = fetch_predictions_for_task(task_id)
if not preds:
    ok = run_tesseract_and_upload_prediction(task)
    if not ok:
        print(f"[SKIP] task {task_id}: "
              f"tesseract 자동 수행 실패")
        continue
    preds = fetch_predictions_for_task(task_id)
    if not preds:
        print(f"[SKIP] task {task_id}: "
              f"tesseract 후에도 prediction 없음")
        continue

→ 이렇게 하면 UI에서 "Retrieve Predictions"를 누를 필요가 거의 없어짐. prediction이 없는 task를 만나면 자동으로 Tesseract를 돌려서 prediction을 생성한 뒤 LayoutLM이 정상 진행.

운영 팁 (대량 처리 안정화)

배치 사이즈 조절

한 번에 50개를 던지지 말고, task를 5~10개 단위로 /predict 호출

딜레이 추가

task별 호출이면 sleep(0.2~0.5s) 같은 소량 딜레이도 도움

재시도 제한

실패 task는 최대 2~3회 재시도만 하고 로그로 남겨서 따로 처리

로그 관리

성공/실패/스킵을 구분하여 로그 기록, 실패 task 목록 별도 저장

SER → RE (다음 단계)

지금까지 했던 것은 SER(Sequence labeling for Entity Recognition)이고, 다음 단계가 RE(Relation Extraction).

OCR → LayoutLMv1 (SER: QUESTION/ANSWER 분류)
  ↓
RE 모델 (Q–A 관계 매칭)
  ↓
Key–Value Pair 생성

LayoutLMv2 / v3 / LayoutXLM 기반의 'SER+RE' 통합 모델 사용이 베스트이나, 라이센스 제약으로 인해 v1 기반 + 커스텀 RE 로직으로 진행.

Tesseract + LayoutLM Integration

Tesseract / LayoutLM 통합 로직

개요: 왜 통합이 필요한가

"Tesseract가 토큰 뿌려주고, 그 토큰을 LayoutLM이 라벨까지 달아서 한 번에 보여주는" 로직이 필요. Label Studio 입장에서는 bbox+text(OCR 결과)와 label(LayoutLM 예측)이 다 포함된 prediction JSON을 주면 됨.

1. 사용자가 PDF 업로드
2. 전처리 (png변환, 해상도 변경)
3. Tesseract OCR → 토큰 + bbox 추출
4. LayoutLM으로 각 토큰에 라벨 예측
   (INVOICE_NUMBER, CUSTOMER_NAME 등)
5. Label Studio 화면에서
   - 토큰 박스(polygon+text) 보이고
   - LayoutLM이 예측한 라벨이 미리 붙어있는 상태
   - 사람이 잘못된 라벨만 수정/추가
6. 저장된 결과 → S3 → 재학습

→ 즉, "OCR 토큰 + LayoutLM 라벨"을 한 화면에서 다루고, 어디선가 두 결과를 합치는 로직이 필요.

통합 패턴 선택

패턴 1) 콤바인 ML 백엔드

한 개의 ML backend로 통합

패턴 2) 순차 실행 ✓

Tesseract와 LayoutLM을 "순차로" 사용

현재 상태 & 목표

지금 상태

• Label Studio + Tesseract ML backend(도커) 이미 동작 중

• Labeling Interface: Image + Polygon + TextArea + Labels 구조

• LayoutLM 학습/추론 코드는 주피터/파이썬에서 이미 있음

하고 싶은 것

• Tesseract가 토큰/박스를 만들어주고

• LayoutLM이 거기에 라벨만 입혀서

• Label Studio에서 한 토큰마다 바운딩 박스 + 라벨이 붙어있게

Tesseract → LayoutLM 순차 흐름

1

PDF 업로드

사용자가 PDF 업로드 → Label Studio 프로젝트에 Task 생성

2

Tesseract ML backend 호출

이미지에 대해 Polygon + TextArea(토큰 + bbox)를 prediction으로 생성

3

파이썬 스크립트 (서버에서 직접 실행)

Label Studio API로 Task 읽기 → Tesseract prediction에서 words+bboxes 추출 → LayoutLM 모델에 넣고 라벨 예측 → prediction 형식으로 만들어서 Label Studio에 Push

4

Label Studio UI에서 확인

Tesseract가 만든 토큰 박스 그대로 + LayoutLM이 예측한 라벨 자동 입힘. 사용자는 잘못된 라벨만 수정 → 저장

5

재학습

수정된 결과를 다시 학습 데이터로 만들어 S3 → LayoutLM 재학습

준비 단계

Label 세트 맞추기

LayoutLM이 사용하는 라벨 목록과 파이썬에서 쓰는 LABELS 리스트가 일치해야 함.

Label Studio 세팅 (REST 버전)

일시적으로 사용하는 것이 아님으로 Legacy token으로 설정

# /home/user/layoutlm_batch_predict.py 세팅 부분
LS_URL = "http://localhost:7070"
TOKEN  = "<legacy-token>"

r = requests.get(
    f"{LS_URL}/api/projects/6",
    headers={"Authorization": f"Token {TOKEN}"}
)

LayoutLM 추론용 모델 로드

추론용으로 로드할 때 필요한 파일은 두 개뿐:

config.jsonmodel.safetensors

가상 환경 설정

torch 라이브러리 때문에 Python 3.8 이상 필요

python3.8 -m venv lm_env
source lm_env/bin/activate

pip install "torch==1.13.1" "torchvision==0.14.1"
pip install "transformers>=4.38.0" pillow requests label-studio-sdk

배치 실행 설정 (30초 주기)

수동 실행

source lm_env/bin/activate
python layoutlm_batch_predict.py

루프용 쉘 스크립트 (run_layoutlm_loop.sh)

#!/bin/bash
cd /home/user
source lm_env/bin/activate

while true; do
    echo "===== $(date) : layoutlm_batch_predict.py 실행 ====="
    python /home/user/layoutlm_batch_predict.py
    echo "===== $(date) : 실행 끝 ====="
    sleep 30
done

백그라운드 실행 / 관리

# 백그라운드 실행
nohup /home/user/run_layoutlm_loop.sh \
  >> /home/user/layoutlm_batch.log 2>&1 &

# 로그 확인
tail -n 50 /home/user/layoutlm_batch.log

# 멈추기
pkill -f "layoutlm_batch_predict.py"
pkill -f "run_layoutlm_loop.sh"

리팩토링 포인트

run_layoutlm_loop.sh

이전 작업이 안 끝났다면 다시 layoutlm_batch_predict.py를 수행하지 않고 skip

layoutlm_batch_predict.py

100개의 task를 한번에 수행하지 않고 10개씩 끊어서 수행. 다음 10개 수행 시 이전꺼 제외하고 진행.

PDF Uploader 흐름

사용자 업로드
↓
서버 로컬 볼륨(/data/raw/) 저장
↓
high_quality_pdf_process 실행
↓
PNG 생성
↓
S3 preprocessed/ 업로드
↓
(선택) 로컬 원본 삭제
파일컨테이너 내부호스트 경로
PDF (원본)/data/raw/<파일명>.pdf/home/user/uploader-data/raw/
PNG (전처리)/data/processed/<base_name>.png/home/user/uploader-data/processed/

주의: 업로드 처리 끝나면 코드에서 os.remove(raw_path)로 원본 PDF는 삭제됨. PNG는 Label Studio가 가져가야 해서 삭제하지 않고 유지.

Label Studio가 보는 URL: http://<서버IP>:8000/files/<base_name>.png— FastAPI가 PROCESSED_DIR을 정적 서빙하는 경로

CloudStorage Sync 배치

Label Studio에서 Cloud Storage sync를 30초마다 배치로 돌리되, 이미 sync가 진행 중이면 skip.

• storage를 title로 찾아서 id를 얻고

• "sync 진행 중"이면 skip

• 아니면 sync API를 여러 후보 엔드포인트로 시도

# 스크립트 파일
/home/user/cloudstorage_sync.py
/home/user/run_cloudstorage_sync_loop.sh  # 30초마다, 이미 돌고있으면 skip

# 실행
chmod +x /home/user/run_cloudstorage_sync_loop.sh
nohup /home/user/run_cloudstorage_sync_loop.sh \
  >> /home/user/cloudstorage_sync_loop.log 2>&1 &

# 로그 확인
tail -f /home/user/cloudstorage_sync.log       # 파이썬 상세 로그
tail -f /home/user/cloudstorage_sync_loop.log   # 루프 실행 로그

Batch Performance Tuning

Tesseract / LayoutLM 배치 성능 튜닝

현재 설정

MAX_TESSERACT_TASKS_PER_RUN10
RETRIEVE_API_CHUNK5
HTTP_TIMEOUT30초
VERIFY_TRIES5
루프 간격60초

문제 정의: 50개에 20분

50개 정도 돌리는데 20분 정도 걸림. 병목은 LayoutLM 추론이 아니라 Retrieve Predictions(Tesseract) 쪽. 특히 timeout/재시도 때문에 더 늘어남.

Retrieve Predictions는 서버 내부에서 task N개를 모아서 ML backend에 전달하고 결과를 저장하는 무거운 작업. 한 번에 크게 보내면 응답이 늦어지고 timeout이 발생하며 retry가 중첩되어 서버에 더 큰 부하가 걸림.

이슈: Read timed out 발생 후 retry

"완전한 장애"라기보단 정상적으로도 자주 나올 수 있는 상황. 다만 과부하/지연이 꽤 있는 상태고, 배치가 불안정해질 가능성이 높음.

[TESS] retrieve ... 대상 10개 뽑음
RETRIEVE_API_CHUNK=5 → 5개씩 POST를 2번
첫 POST에서 Read timed out (read timeout=30) 발생
→ retry 하다가 어떤 attempt에서 status=200 성공
→ "Retrieved 5 predictions"
→ 다음 chunk/다음 run에서 또 timeout 반복

문제 없는 경우

• retry 끝에 결국 status=200이 자주 뜬다

• processed_items가 꾸준히 늘어난다

• 시간이 지나면 prediction이 실제로 생성된다

→ "느리지만 계속 전진"하는 상황

문제가 되는 경우

• status=200이 거의 안 나오고 timeout만 반복

• processed_items가 늘지 않음

• 7070이 계속 바빠져서 UI까지 느려짐

• flock 때문에 다음 run들이 계속 스킵

해결 방법 (3갈래)

1) 가장 효과 큰 방법: Retrieve 우회 — Tesseract를 밖에서 돌려서 prediction 직접 POST

현재: Label Studio가 dm/actions → ML backend 호출 → 결과 저장을 "동기/긴 요청"으로 처리 → timeout/대기/재시도 발생

개선: 배치가 직접 이미지 경로를 읽고 → tesseract로 words+bbox 만들고 → /api/predictions로 바로 넣으면 dm/actions 왕복이 없어져서 체감 속도 차이가 큼

단점: Tesseract 결과를 만들어주는 코드를 배치에 포함해야 함 (ML backend가 하던 일을 가져오는 것)

2) Retrieve 유지하되 속도 올리는 튜닝

(A) chunk 줄이고 동시성 올리기 (가장 안전)

RETRIEVE_API_CHUNK=3으로 줄이고, ThreadPoolExecutor(max_workers=2~4)로 3개짜리 POST를 병렬로 날리면 throughput이 올라감

(B) HTTP timeout 늘리기

timeout 늘리면 실패가 줄어서 "재시도 시간 낭비"는 줄 수 있지만, 요청 자체가 오래 걸리는 건 그대로라서 순수 처리량이 크게 늘진 않음

(C) selection 로직 최적화 (API 호출 수 줄이기)

task마다 fetch_predictions(task_id)를 호출하는 대신, LS task 응답의 total_predictions 값으로 1차 필터 → GET 호출이 확 줄고 전체 run 시간이 줄어듦

3) 인프라/구성 측면

• Label Studio gunicorn worker 수 늘리기 (worker 1개면 dm/actions가 잘 막힘)

• ML backend(tesseract docker)의 CPU/메모리 제한 확인

• 이미지 해상도가 너무 크면 OCR이 느림 → 전처리에서 DPI/리사이즈 더 aggressive하게

추천 튜닝 값

"가장 현실적 + 효과 좋은" 추천 순서:

설정기존변경
RETRIEVE_API_CHUNK53
HTTP_TIMEOUT3060
MAX_TESSERACT_TASKS_PER_RUN109 (chunk 3 × 3회)
루프 간격60초60초 유지
1순위

task selection 최적화: total_predictions로 먼저 걸러서 GET 호출 최소화

2순위

Retrieve POST를 3개 단위 + 병렬 2~3개 workers

3순위

timeout은 30→60 정도만 (과하게 늘리면 flock 때문에 loop가 길어짐)

이 3개만 해도 "50개 20분"이 보통 반 이하로 내려가는 케이스가 많음.

Tesseract + LayoutLM 통합 요약

PDF 업로드전처리 (PNG)Tesseract (토큰+bbox)LayoutLM (라벨 예측)Label Studio (수정)S3 → 재학습
환경 구성모델 학습