669 lines
27 KiB
Python
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")
|