ImageProcessor_MainServer/worker/celery_worker.py

669 lines
27 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import annotations
import onnxruntime as ort
if not hasattr(ort, "sessionoptions") and hasattr(ort, "SessionOptions"):
# 일부 라이브러리가 CPU alias를 기대할 때 대비
ort.sessionoptions = ort.SessionOptions
print("[RMBG][ORT] providers:", ort.get_available_providers())
import os, uuid, base64, logging, re, time
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple
import cv2, numpy as np
from PIL import Image
from celery import Celery
from celery.utils.log import get_task_logger
# # ── SimpleLama 가중치 경로 설정 (임포트 전에 환경 구성)
# try:
# torch_home = "/app/torch_cache"
# os.makedirs(torch_home, exist_ok=True)
# os.environ.setdefault("TORCH_HOME", torch_home)
# # ✅ 환경변수로 FP16 사용 여부를 제어 (기본: 0=끄기)
# use_lama_fp16 = os.getenv("USE_LAMA_FP16", "0").strip() in {"1", "true", "True"}
# fp16_path = os.path.join(torch_home, "Big-LaMa.fp16.pt")
# default_ckpt = os.path.join(torch_home, "big-lama.pt")
# # 기본은 FP32 체크포인트를 우선
# if os.path.isfile(default_ckpt):
# os.environ.setdefault("SIMPLE_LAMA_CKPT", default_ckpt)
# elif os.path.isfile(fp16_path) and use_lama_fp16:
# os.environ.setdefault("SIMPLE_LAMA_CKPT", fp16_path)
# # 🔧 [기존 문제 원인] FP16 → big-lama.pt 강제 링크/복사 로직 제거
# # 필요 시에만 FP16을 직접 지정해서 쓰도록 함.
# except Exception:
# pass
#from worker.ocr_module import OCRModule # ndarray 지원 버전
from worker.mask_module_for_paddle import MaskModule
from worker.text_rendering_module import TextRenderingModule
# from worker.text_rendering_module2 import TextRenderingModule
from worker.rembg_module import RembgRemover
from worker.loggerModule import Logger
# from simple_lama_inpainting import SimpleLama
# from worker.inpaint_module import Inpainter, InpaintBackends
from worker.utils_debug import save_debug_artifacts, draw_ocr_overlay
from worker.roi_inpainting_module import ROIInpaintingModule
# from deep_translator import GoogleTranslator
from worker.translator import get_translator # ✅ 어댑터만 사용
# ★ GPU 메모리 추적 유틸
from worker.gpu_mem_monitor import GPUMemTracker
from contextlib import contextmanager
import ctypes, os
def _warm_up_cuda():
try:
lib = ctypes.CDLL('libcudart.so')
c = ctypes.c_int()
lib.cudaGetDeviceCount(ctypes.byref(c))
print(f"[CUDA-WARMUP] device_count = {c.value}")
except OSError as e:
print(f"[CUDA-WARMUP] failed: {e}")
_warm_up_cuda()
# ───────────────────────────────── Celery 앱
BROKER = os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0")
BACK = os.getenv("CELERY_RESULT_BACKEND", BROKER.replace("/0", "/1"))
celery_app = Celery("image_worker", broker=BROKER, backend=BACK,
include=[]) # 패키지 전체 자동 로드
# ───────────────────────────────── Logger
logger = get_task_logger(__name__)
logger.setLevel(logging.INFO)
clogger = Logger()
# ───────────────────────── GPU Tracker (전역)
GPU_INDEX = int(os.getenv("GPU_INDEX", "0"))
_gpu_tracker = GPUMemTracker(logger=clogger, device_index=GPU_INDEX)
@contextmanager
def track_phase(phase: str, trace_id: Optional[str] = None):
"""
단계별 VRAM + 경과시간 로깅 컨텍스트
- phase: 'DECODE' | 'OCR' | 'TRANSLATE' | 'MASK' | 'INPAINT' | 'RENDER'
- trace_id: 요청 식별자 (celery task_id)
"""
t0 = time.time()
snap0 = _gpu_tracker.snapshot()
_gpu_tracker.log_snapshot(tag=f"before {phase}", trace=trace_id or "")
try:
yield
finally:
snap1 = _gpu_tracker.snapshot()
_gpu_tracker.log_snapshot(tag=f"after {phase}", trace=trace_id or "")
if snap0 and snap1:
delta = _gpu_tracker.diff_used(snap0, snap1) or 0
clogger.log(
f"[GPUMem][{trace_id or ''}][{phase}] Δused={delta/1024/1024:.1f}MB, elapsed={time.time()-t0:.3f}s",
level=logging.INFO
)
# ───────────────────────────────── 전역 리소스
_TEMP = Path(os.getenv("TEMP_STORAGE", "/app/temp_files"))
_TEMP.mkdir(exist_ok=True, parents=True)
# _lama: SimpleLama | None = None
_ocr = None
_mask: MaskModule | None = None
_text: TextRenderingModule | None = None
# _inpainter: Inpainter | None = None
_roi_inpainter: ROIInpaintingModule | None = None
_translator = get_translator() # ✅ 워커 부팅 시 생성 & 재사용
# def get_lama():
# global _lama
# if _lama is None:
# _lama = SimpleLama()
# # 라마 초기화 직후 VRAM 스냅샷
# _gpu_tracker.log_snapshot(tag="after SimpleLama init")
# return _lama
# def get_inpainter() -> Inpainter:
# global _inpainter
# if _inpainter is None:
# _inpainter = Inpainter(
# logger=clogger,
# default_backend=InpaintBackends.LAMA, # 기본값은 자유롭게
# # lama_onnx_fd_path="/app/worker/models/inpainting_lama_2025jan.onnx",
# # lama_onnx_fd_device="gpu", # "cpu"도 가능
# # lama_onnx_fd_backend="trt" # "ort"=ONNX Runtime 기본 CPU/GPU 실행(CUDA 환경이면 GPU 사용 가능) "trt"=TensorRT 실행, "cuda"=ONNX Runtime CUDA Execution Provider, "cpu"=ONNX Runtime CPU Execution Provider
# )
# return _inpainter
def get_ocr():
from worker.ocr_module import OCRModule
global _ocr
if _ocr is None:
_ocr = OCRModule(logger=clogger) # ndarray 직접 처리
# OCR 초기화 직후 VRAM 스냅샷
_gpu_tracker.log_snapshot(tag="after OCR init")
return _ocr
def get_mask():
global _mask
if _mask is None:
_mask = MaskModule(logger=clogger)
return _mask
def get_text():
global _text
if _text is None:
_text = TextRenderingModule(logger=clogger)
return _text
def get_roi_inpainter():
global _roi_inpainter
if _roi_inpainter is None:
_roi_inpainter = ROIInpaintingModule(logger=clogger)
# ROI 인페인팅 초기화 직후 VRAM 스냅샷
_gpu_tracker.log_snapshot(tag="after ROI Inpainter init")
return _roi_inpainter
# 번역기 워밍업
try:
_ = _translator.translate_batch(["测试"], src="zh-CN", dest="ko")
except Exception:
pass
# ── Celery 워커 프로세스 시작 시 모델 예열(선택)
from celery.signals import worker_process_init
@worker_process_init.connect
def _warm_up_models(**_):
"""워커 프로세스 초기화 시 모델들을 사전 로딩"""
try:
# 🔥 PyTorch 성능 최적화 설정
import torch
if torch.cuda.is_available():
# cuDNN 최적화
torch.backends.cudnn.benchmark = True
torch.backends.cudnn.deterministic = False
# TF32 활성화 (Ampere 이상 GPU에서 성능 향상)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
# 메모리 형식 최적화
torch.set_float32_matmul_precision('high')
logger.info(
f"🔧 PyTorch 최적화 완료: "
f"cudnn.benchmark={torch.backends.cudnn.benchmark}, "
f"allow_tf32={torch.backends.cuda.matmul.allow_tf32}"
)
# 모델 사전 로딩
get_ocr()
get_mask()
get_text()
# 🔥 ROI 인페인팅 모듈 사전 초기화
roi_inpainter = get_roi_inpainter()
roi_inpainter._get_simple_lama() # SimpleLama 사전 로딩
logger.info("✅ 모델 사전 로딩 완료 (성능 최적화 포함)")
except Exception as e:
logger.warning(f"⚠️ 모델 사전 로딩 건너뜀: {e}")
_warm_up_models()
# ───────────────────────────────── 공통 헬퍼
def ok(img_bgr: np.ndarray, msg="done"):
_, enc = cv2.imencode(".png", img_bgr)
return {"status": "SUCCESS",
"result_image": base64.b64encode(enc).decode(),
"message": msg}
def fail(code, msg): return {"status":"FAIL", "error_code":code, "message":msg}
def _parse_font_number_from_toggle(toggle_states: Dict[str, Any]) -> int | None:
"""
toggle_states의 'font_type'에서 폰트 번호를 추출한다.
- 예: '폰트2' -> 2
- 정수가 직접 들어오면 그 값을 사용
- 없거나 파싱 실패 시 None 반환 (=> 기본 폰트(3번) 사용)
"""
try:
val = toggle_states.get("font_type", None)
if val is None:
return None
if isinstance(val, int):
return val
if isinstance(val, str):
m = re.search(r"\d+", val)
if m:
return int(m.group())
# 'font3' 같은 영어 표기도 고려
m = re.search(r"font\s*(\d+)", val, flags=re.IGNORECASE)
if m:
return int(m.group(1))
except Exception as e:
logger.warning(f"[font] font_type 파싱 실패: {e}")
return None
# def _parse_inpaint_backend(
# toggle_states: Dict[str, Any],
# *,
# default_method: str = "lama",
# default_backend: str = "ort",
# default_min_masks_for_lama: int = 4
# ) -> Tuple[str, str, int]:
# """
# toggle_states에서 inpaint_method, backend, min_masks_for_lama 를 파싱.
# 허용 표기(대소문자/공백 무시):
# - method:
# "opencv", "cv"
# "lama", "lama_torch", "torch"
# "lama_onnx_ort", "onnx_ort" # OpenCV DNN 경로
# "lama_onnx_fd", "onnx_fd", "fd" # FastDeploy(ORT/TRT/CUDA/CPU)
# - backend (lama_onnx_fd / lama_onnx 전용):
# "ort", "trt", "cuda", "cpu"
# - 콜론 구분 지원: "lama_onnx_fd:trt", "lama_onnx_fd:ort"
# 키 없음/실패 시 기본값:
# method = default_method ("lama_onnx_fd")
# backend = default_backend ("ort")
# min_masks_for_lama = default_min_masks_for_lama (4)
# Returns:
# (method_enum, backend_str, min_masks_for_lama:int)
# """
# # 1) 안전하게 읽기
# try:
# raw = str((toggle_states or {}).get("inpaint_method", "")).strip().lower()
# except Exception:
# raw = ""
# if not raw:
# raw = f"{default_method}:{default_backend}"
# # 2) method / backend 분리
# if ":" in raw:
# method_tok, backend_tok = [t.strip() for t in raw.split(":", 1)]
# else:
# method_tok, backend_tok = raw, default_backend
# # 3) method 매핑
# method_map = {
# "opencv": InpaintBackends.OPENCV,
# "cv": InpaintBackends.OPENCV,
# "lama": InpaintBackends.LAMA,
# "lama_torch": InpaintBackends.LAMA,
# "torch": InpaintBackends.LAMA,
# # ⬇️ 새 별칭들
# "lama_torch_amp": InpaintBackends.LAMA_TORCH_AMP,
# "torch_amp": InpaintBackends.LAMA_TORCH_AMP,
# "amp": InpaintBackends.LAMA_TORCH_AMP,
# }
# method_enum = method_map.get(
# method_tok,
# method_map.get(default_method, InpaintBackends.LAMA)
# )
# # 4) backend 정규화
# backend_tok = (backend_tok or default_backend).lower()
# backend_enum = backend_tok if backend_tok in {"ort", "trt", "cuda", "cpu"} else default_backend
# # 5) min_masks_for_lama 파싱
# try:
# mmfl = int((toggle_states or {}).get("min_masks_for_lama", default_min_masks_for_lama))
# except (TypeError, ValueError):
# mmfl = default_min_masks_for_lama
# return method_enum, backend_enum, mmfl
# def run_inpaint(
# src_bgr,
# polygons,
# toggle_states: Dict[str, Any],
# *,
# max_side: int = 1024,
# auto_opencv_if_few: bool = True
# ):
# method_enum, backend_enum, min_masks_for_lama = _parse_inpaint_backend(toggle_states)
# inpainter = get_inpainter()
# # ---- 추가: ROI/풀프레임/저장 토글 파싱
# roi_strategy = str((toggle_states or {}).get("roi_strategy", "components")).lower() # "components" | "full"
# pad_ratio = float((toggle_states or {}).get("pad_ratio", 0.12))
# merge_thresh_factor = float((toggle_states or {}).get("merge_thresh_factor", 0.7))
# comp_min_area = int((toggle_states or {}).get("comp_min_area", 30))
# soft_dilate_px = int((toggle_states or {}).get("soft_dilate_px", 10))
# soft_blur_px = int((toggle_states or {}).get("soft_blur_px", 17))
# save_rois = bool((toggle_states or {}).get("debug_save_rois", False))
# debug_dir = os.getenv("DEBUG_DUMP_DIR", "/app/temp_files/debug")
# req_id = (toggle_states or {}).get("_trace_id", None) # 선택: 상위에서 넣어주면 파일명에 반영
# # (선택) 풀프레임 비교할 땐 max_side 조금 키우는 게 품질에 유리
# max_side = int((toggle_states or {}).get("max_side", 1600 if roi_strategy == "full" else 1600))
# return inpainter.inpaint(
# src_bgr,
# polygons,
# backend=method_enum, # "lama" 추천
# roi_strategy=roi_strategy,
# max_side=max_side,
# auto_opencv_if_few=bool((toggle_states or {}).get("auto_opencv_if_few", False)),
# few_threshold=int((toggle_states or {}).get("few_threshold", 0)),
# comp_min_area=comp_min_area,
# pad_ratio=pad_ratio,
# merge_thresh_factor=merge_thresh_factor,
# merge_abs_min_px=int((toggle_states or {}).get("merge_abs_min_px", 8)),
# soft_dilate_px=soft_dilate_px,
# soft_blur_px=soft_blur_px,
# debug_save_rois=save_rois,
# debug_dir=os.path.join(debug_dir, "ROIs"),
# request_id=req_id
# )
# def run_inpaint(
# src_bgr,
# polygons,
# toggle_states: Dict[str, Any],
# *,
# max_side: int = 1024,
# auto_opencv_if_few: bool = True
# ):
# """
# 기존 호출부 유지. toggle_states 로 A/B 모드 제어:
# - ab_mode: "A" | "B" | "A+B" (기본 "A")
# - A = components ROI (확대/근접 병합/소프트블렌딩)
# - B = full-frame (ROI 미사용, 비교용)
# """
# # 기존 파라미터 파싱 유지
# method_enum, backend_enum, min_masks_for_lama = _parse_inpaint_backend(toggle_states)
# inpainter = get_inpainter()
# # ── 공통 토글
# ab_mode = str((toggle_states or {}).get("ab_mode", "A")).upper() # "A" | "B" | "A+B"
# trace_id = (toggle_states or {}).get("_trace_id", None)
# debug_root = os.getenv("DEBUG_DUMP_DIR", "/app/temp_files/debug")
# ab_dir = os.path.join(debug_root, "AB")
# try:
# os.makedirs(ab_dir, exist_ok=True)
# except Exception:
# pass
# # ── A(components ROI)용 kwargs: (값 없으면 기본 추천값 사용)
# A_kwargs = dict(
# backend=method_enum, # "lama" 권장
# roi_strategy=str((toggle_states or {}).get("roi_strategy_A", "components")).lower(), # "components"
# max_side=int((toggle_states or {}).get("max_side_A", 1600)),
# auto_opencv_if_few=bool((toggle_states or {}).get("auto_opencv_if_few", False)),
# few_threshold=int((toggle_states or {}).get("few_threshold", 0)),
# comp_min_area=int((toggle_states or {}).get("comp_min_area", 30)),
# pad_ratio=float((toggle_states or {}).get("pad_ratio", 0.12)),
# merge_thresh_factor=float((toggle_states or {}).get("merge_thresh_factor", 0.7)),
# merge_abs_min_px=int((toggle_states or {}).get("merge_abs_min_px", 8)),
# soft_dilate_px=int((toggle_states or {}).get("soft_dilate_px", 10)),
# soft_blur_px=int((toggle_states or {}).get("soft_blur_px", 17)),
# debug_save_rois=bool((toggle_states or {}).get("debug_save_rois", False)),
# debug_dir=os.path.join(debug_root, "ROIs"),
# request_id=trace_id
# )
# # ── B(full-frame)용 kwargs
# B_kwargs = dict(
# backend=method_enum,
# roi_strategy=str((toggle_states or {}).get("roi_strategy_B", "full")).lower(), # "full"
# max_side=int((toggle_states or {}).get("max_side_B", 1600)),
# auto_opencv_if_few=False,
# few_threshold=0,
# # full 도 얇게 블렌딩
# soft_dilate_px=int((toggle_states or {}).get("soft_dilate_px_full", (toggle_states or {}).get("soft_dilate_px", 10))),
# soft_blur_px=int((toggle_states or {}).get("soft_blur_px_full", (toggle_states or {}).get("soft_blur_px", 17))),
# # 아래는 시그니처 호환용
# comp_min_area=int((toggle_states or {}).get("comp_min_area", 30)),
# pad_ratio=float((toggle_states or {}).get("pad_ratio", 0.12)),
# merge_thresh_factor=float((toggle_states or {}).get("merge_thresh_factor", 0.7)),
# merge_abs_min_px=int((toggle_states or {}).get("merge_abs_min_px", 8)),
# debug_save_rois=False,
# debug_dir=None,
# request_id=trace_id
# )
# # ── 실행 래퍼 (결과 파일도 저장)
# def _run_and_save(label: str, kwargs: Dict[str, Any]) -> np.ndarray:
# out = inpainter.inpaint(src_bgr, polygons, **kwargs)
# try:
# fname = f"{(trace_id or 'ab')}_{label}.png"
# cv2.imwrite(os.path.join(ab_dir, fname), out)
# except Exception:
# pass
# return out
# # ── 모드 분기
# if ab_mode == "A":
# return _run_and_save("A_components", A_kwargs)
# if ab_mode == "B":
# return _run_and_save("B_full", B_kwargs)
# # ── "A+B": 좌우 합성 프리뷰 반환 (단일 결과는 파일로 저장됨)
# outA = _run_and_save("A_components", A_kwargs)
# outB = _run_and_save("B_full", B_kwargs)
# # 높이 맞춰 좌우 스택
# h = min(outA.shape[0], outB.shape[0])
# def _resize_to_h(img, h):
# if img.shape[0] == h:
# return img
# ratio = h / img.shape[0]
# new_w = int(round(img.shape[1] * ratio))
# return cv2.resize(img, (new_w, h), interpolation=cv2.INTER_CUBIC)
# a2 = _resize_to_h(outA, h)
# b2 = _resize_to_h(outB, h)
# combo = np.hstack([a2, b2])
# # 레이블(있으면 편함)
# try:
# cv2.putText(combo, "A: components ROI", (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,0,0), 4, cv2.LINE_AA)
# cv2.putText(combo, "A: components ROI", (10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA)
# cv2.putText(combo, "B: full-frame", (a2.shape[1] + 10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,0,0), 4, cv2.LINE_AA)
# cv2.putText(combo, "B: full-frame", (a2.shape[1] + 10, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2, cv2.LINE_AA)
# cv2.imwrite(os.path.join(ab_dir, f"{(trace_id or 'ab')}_AplusB.png"), combo)
# except Exception:
# pass
# return combo
# ───────────────────────────────── translate_task
@celery_app.task(name="worker.translate_task",
bind=True,
autoretry_for=(Exception,),
retry_backoff=True,
retry_kwargs={"max_retries":2})
def translate_task(self, *, image_b64: str, filename: str,
user_id: str, toggle_states: Dict,
unwanted_texts: Dict, ocr_method: str,
inpaint_method: str, **_):
trace_id = getattr(self.request, "id", str(uuid.uuid4()))
clogger.log(f"[TRACE][{trace_id}] translate_task start (file={filename}, user={user_id})", level=logging.INFO)
guid = str(uuid.uuid4())[:8]
debug_dir = os.getenv("DEBUG_DUMP_DIR", "/app/temp_files/debug") # ← 하드코딩 제거
# 0. decode
with track_phase("DECODE", trace_id):
img_arr = np.frombuffer(base64.b64decode(image_b64), np.uint8)
src_bgr = cv2.imdecode(img_arr, cv2.IMREAD_COLOR)
if src_bgr is None:
return fail("DECODE_ERR", "base64 decode fail")
# 1. OCR (ndarray 직접)
with track_phase("OCR", trace_id):
ocr = get_ocr()
ocr_res = ocr.detect_text_ndarray(src_bgr, method="polygon") # <─ ndarray 지원
chn = ocr.filter_chinese_text(ocr_res)
if not chn:
clogger.log(f"[TRACE][{trace_id}] no_chinese -> early return", level=logging.INFO)
return ok(src_bgr, "no_chinese")
# OCR 후
overlay = draw_ocr_overlay(src_bgr, ocr_res)
save_debug_artifacts(debug_dir, guid, ocr=overlay, _stage="OCR")
with track_phase("TRANSLATE", trace_id):
texts = [r["text"] for r in chn]
ko = _translator.translate_batch(texts, src="zh-CN", dest="ko")
if len(ko) != len(texts):
logger.warning(f"[TRACE][{trace_id}] 번역 결과 개수 불일치: in={len(texts)} out={len(ko)} -> per-sentence retry")
ko = [_translator.translate_batch([t], src="zh-CN", dest="ko")[0] for t in texts]
# # 2. 번역
with track_phase("MASK", trace_id):
# 🔥 A/B 테스트: 기존 방식 vs ROI 최적화 방식
use_roi_optimized_mask = toggle_states.get('use_roi_optimized_mask', False) # True → False로 변경
if use_roi_optimized_mask:
# 🔥 ROI 최적화: 적응형 마스크 생성
mask = get_mask().create_masks_np(
src_bgr, chn,
for_roi_processing=True,
# 🔥 텍스트 개수에 따른 적응형 expansion
expansion_size=min(8, max(4, 10 - len(chn))), # 텍스트 많으면 작게, 적으면 크게
blur_size=0 # ROI 모드에서는 블러 없음
)
mask_type = "ROI최적화"
else:
# 기존 방식: 전체 후처리 적용
mask = get_mask().create_masks_np(src_bgr, chn)
mask_type = "기존방식"
# 🔥 마스크 통계 로깅
mask_pixels = np.sum(mask > 0)
total_pixels = mask.shape[0] * mask.shape[1]
mask_coverage = mask_pixels / total_pixels * 100
clogger.log(
f"🔧 {mask_type} 마스크 사용: 커버리지 {mask_coverage:.2f}% ({mask_pixels:,}/{total_pixels:,} 픽셀)",
level=logging.INFO
)
if mask is None:
return fail("MASK_ERR", "mask failed")
# 마스크 후
save_debug_artifacts(debug_dir, guid, mask=mask, _stage="MASK")
with track_phase("INPAINT", trace_id):
# 🔥 ROI 기반 인페인팅 최적화 (모듈화)
roi_inpainter = get_roi_inpainter()
# ROI 처리 설정 (toggle_states에서 오버라이드 가능)
roi_config = {
'min_component_area': toggle_states.get('min_component_area', 100),
'merge_distance': toggle_states.get('merge_distance', 50),
'margin_ratio': toggle_states.get('margin_ratio', 0.15),
'large_mask_threshold': toggle_states.get('large_mask_threshold', 0.5),
# 🔥 마스크 정제 비활성화 (마스크 모듈에서 이미 최적화됨)
'enable_mask_refinement': toggle_states.get('enable_mask_refinement', False),
'mask_erosion_kernel': 0, # 비활성화
'mask_dilation_kernel': 0, # 비활성화
'mask_blur_kernel': 0, # 비활성화
'context_expansion_ratio': toggle_states.get('context_expansion_ratio', 0.1), # 줄임
'blend_mode': toggle_states.get('blend_mode', 'simple'), # 단순 블렌딩
'feather_blend_size': toggle_states.get('feather_blend_size', 5), # 줄임
# 🔥 형상 최적화 설정
'enable_shape_optimization': toggle_states.get('enable_shape_optimization', True),
'performance_tracking': toggle_states.get('performance_tracking', True),
}
# 처리 전 통계 로깅
stats = roi_inpainter.get_processing_stats(src_bgr, mask)
clogger.log(
f"[INPAINT] 처리 통계: {stats['num_components']}개 컴포넌트 → "
f"{stats['num_merged_rois']}개 ROI, 메모리 효율성: {stats['memory_efficiency']*100:.1f}%",
level=logging.INFO
)
# ROI 기반 인페인팅 실행
dst_bgr = roi_inpainter.inpaint_with_roi(src_bgr, mask, config=roi_config)
# 메모리 정리
roi_inpainter.cleanup_memory()
# 인페인트 후 (렌더 전)
save_debug_artifacts(debug_dir, guid, inpaint=dst_bgr, _stage="INPAINT")
font_number = _parse_font_number_from_toggle(toggle_states)
if font_number is not None:
logger.info(f"[TRACE][{trace_id}][font] 요청된 폰트 번호: {font_number}")
else:
logger.info(f"[TRACE][{trace_id}][font] 폰트 지정 없음 -> 기본 폰트(3번) 사용")
with track_phase("RENDER", trace_id):
out = get_text().render_text(dst_bgr, chn, ko, font_number=font_number)
# out = get_text().render_with_market_preset(
# image_bgr=dst_bgr,
# ocr_results=chn, # [{'polygon': [[x,y]...], 'text':...}, ...]
# translated_texts=ko,
# market=toggle_states.get("market", "coupang"), # 'coupang'|'naver'
# preset=toggle_states.get("preset", "basic"), # 'basic'|'badge'|'price'
# font_number=font_number
# )
# 최종
save_debug_artifacts(debug_dir, guid, final=out, _stage="RENDER")
logger.info(f"[DEBUG] artifacts saved: {debug_dir}/{guid}_*.png")
clogger.log(f"[TRACE][{trace_id}] translate_task done", level=logging.INFO)
return ok(out, "translated")
# ───────────────────────── rembg_task
@celery_app.task(name="worker.rembg_task",
bind=True,
autoretry_for=(),
retry_backoff=True)
def rembg_task(self, *, image_b64: str, filename: str, **_):
trace_id = getattr(self.request, "id", str(uuid.uuid4()))
with track_phase("DECODE", trace_id):
arr = np.frombuffer(base64.b64decode(image_b64), np.uint8)
src = cv2.imdecode(arr, cv2.IMREAD_COLOR)
with track_phase("RMBG", trace_id):
dst = RembgRemover(logger=clogger).remove_background(src)
if dst is None:
return fail("RMBG_ERR","rembg fail")
return ok(dst, "background_removed")
# # ───────────────────────────────── rembg_task
# @celery_app.task(name="worker.rembg_task",
# bind=True,
# autoretry_for=(),
# retry_backoff=True)
# def rembg_task(self, *, image_b64: str, filename: str, **_):
# arr = np.frombuffer(base64.b64decode(image_b64), np.uint8)
# src = cv2.imdecode(arr, cv2.IMREAD_COLOR)
# dst = RembgRemover(logger=clogger).remove_background(src)
# if dst is None:
# return fail("RMBG_ERR","rembg fail")
# return ok(dst, "background_removed")