300 lines
13 KiB
Python
300 lines
13 KiB
Python
import os
|
|
import asyncio
|
|
import aiofiles
|
|
import logging
|
|
from urllib.parse import urlparse
|
|
import shutil
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import re
|
|
import cv2
|
|
import base64
|
|
import requests
|
|
import numpy as np
|
|
from modules.ocr_module import OCRModule
|
|
from modules.mask_module import MaskModule
|
|
from modules.text_rendering_module import TextRenderingModule
|
|
from modules.postImageManager import PostImageManager
|
|
|
|
class ImageProcessor:
|
|
"""이미지 다운로드, OCR, 번역 처리를 담당하는 클래스"""
|
|
|
|
def __init__(self, logger, gpt_client, base_dir, font_path):
|
|
self.logger = logger
|
|
self.base_dir = base_dir
|
|
self.gpt_client = gpt_client
|
|
|
|
# OCR 관련
|
|
self.inpaint_sv_port = 8080
|
|
|
|
self.font_path = font_path
|
|
self.TEMP_IMAGE_DIR = os.path.join(self.base_dir, "temp_images")
|
|
os.makedirs(self.TEMP_IMAGE_DIR, exist_ok=True)
|
|
|
|
self.ocr_module = OCRModule(logger=self.logger, base_dir=self.base_dir)
|
|
self.mask_module = MaskModule(logger=self.logger, base_dir=self.base_dir)
|
|
self.text_rendering_module = TextRenderingModule(logger=self.logger, font_path=self.font_path)
|
|
self.postImageManager = PostImageManager(logger=self.logger, font_path=self.font_path)
|
|
|
|
def __del__(self):
|
|
"""소멸자에서 리소스 정리"""
|
|
self.cleanup()
|
|
|
|
def cleanup(self):
|
|
"""리소스 정리"""
|
|
try:
|
|
|
|
# 임시 폴더 삭제
|
|
if hasattr(self, 'TEMP_IMAGE_DIR') and os.path.exists(self.TEMP_IMAGE_DIR):
|
|
shutil.rmtree(self.TEMP_IMAGE_DIR)
|
|
self.logger.log(f"임시 폴더 삭제됨: {self.TEMP_IMAGE_DIR}", level=logging.INFO)
|
|
except Exception as e:
|
|
self.logger.log(f"리소스 정리 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
|
|
|
def is_valid_image_path(self, path):
|
|
# http/https 또는 로컬 파일(.jpg, .png 등) 모두 허용
|
|
if re.match(r'^(http|https)://.*\\.(jpg|jpeg|png|bmp|gif|webp|tiff?)(\\?.*)?$', path, re.IGNORECASE):
|
|
return True
|
|
if os.path.isfile(path) and path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tif', '.tiff')):
|
|
return True
|
|
return False
|
|
|
|
async def process_single_image(self, toggle_states, unwanted_texts, local_image_path, index, file_prefix=""):
|
|
"""
|
|
단일 이미지를 처리합니다 (다운로드 -> OCR -> 인페인팅)
|
|
|
|
Args:
|
|
toggle_states: 토글 상태 딕셔너리
|
|
local_image_path (str): 처리할 이미지 경로
|
|
index (int): 이미지 인덱스
|
|
unwanted_texts: 치환할 텍스트 딕셔너리
|
|
file_prefix (str): 파일명에 추가할 접두사 (예: "detail", "option")
|
|
|
|
Returns:
|
|
dict: 처리 결과를 포함한 딕셔너리
|
|
- status: 'inpainted', 'original', 'exclude', 'error' 중 하나
|
|
- path: 처리된 이미지 파일 경로 또는 원본 이미지 파일 경로
|
|
- error: 오류 메시지 (status가 'error'인 경우에만 포함)
|
|
"""
|
|
ocr_enabled = toggle_states.get('ocr', False)
|
|
unwanted_texts = unwanted_texts
|
|
|
|
try:
|
|
ocr_results = self.ocr_module.detect_text(local_image_path)
|
|
|
|
# 3. 중국어 텍스트 없는 경우 원본 이미지 반환
|
|
if not self.ocr_module.filter_chinese_text(ocr_results):
|
|
self.logger.log(f"이미지 {index+1} 중국어 텍스트 없음, 원본 이미지 반환", level=logging.INFO)
|
|
return local_image_path
|
|
|
|
# 4. 텍스트 번역 (GPT)
|
|
translated_texts = self.gpt_translate_texts(ocr_results, self.gpt_client)
|
|
|
|
if ocr_enabled:
|
|
filtered_translated_texts = self.process_translated_texts(translated_texts, unwanted_texts, local_image_path, index)
|
|
if not filtered_translated_texts:
|
|
self.logger.log(f"이미지 {index+1} 제외됨", level=logging.INFO)
|
|
return None
|
|
else:
|
|
self.logger.log(f"이미지 {index+1} 치환됨", level=logging.INFO)
|
|
|
|
# 마스크 생성 (basic 방식만 사용)
|
|
masks = self.mask_module.create_masks(
|
|
image_path=local_image_path, ocr_results=ocr_results, mask_option="basic"
|
|
)
|
|
self.logger.log(f"마스크 생성 완료", level=logging.INFO)
|
|
|
|
# 인페인팅
|
|
inpainted_image = self.call_inpaint_api(local_image_path, masks)
|
|
self.logger.log(f"인페인팅 완료", level=logging.INFO)
|
|
|
|
# 텍스트 렌더링
|
|
text_rendered_image = self.text_rendering_module.render_text(
|
|
inpainted_image, ocr_results, filtered_translated_texts, font_path=self.font_path)
|
|
self.logger.log(f"텍스트 렌더링 완료", level=logging.INFO)
|
|
|
|
# 결과 저장
|
|
translated_img_path = await self.postProcess_and_save_image(local_image_path, text_rendered_image, index, file_prefix, toggle_states)
|
|
self.logger.log(f"이미지 {index+1} 번역 완료: {translated_img_path}", level=logging.INFO)
|
|
return translated_img_path
|
|
|
|
except Exception as e:
|
|
self.logger.log(f"이미지 {index+1} 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
|
return {'status': 'failed', 'path': local_image_path, 'error': str(e)}
|
|
|
|
async def postProcess_and_save_image(self, local_image_path, text_rendered_image, index, file_prefix, toggle_states):
|
|
"""로컬 서버 URL을 사용해 이미지를 번역하고 로컬에 저장합니다"""
|
|
try:
|
|
# 파일명에 접두사 포함
|
|
if file_prefix:
|
|
img_path = os.path.join(self.TEMP_IMAGE_DIR, f"translated_{file_prefix}_img_{index+1}.png")
|
|
else:
|
|
img_path = os.path.join(self.TEMP_IMAGE_DIR, f"translated_img_{index+1}.png")
|
|
|
|
watermarked_image_data = self.postImageManager.add_watermark(image_data=text_rendered_image, watermark_text=toggle_states.get("watermark_text", "워터마크"))
|
|
final_image_path = self.postImageManager.save_image_to_path(watermarked_image_data, img_path)
|
|
|
|
return final_image_path
|
|
|
|
except Exception as e:
|
|
self.logger.log(f"이미지 {index+1} 번역 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
|
return local_image_path
|
|
|
|
def is_valid_image_data(self, image_data):
|
|
"""이미지 데이터가 유효한지 확인합니다"""
|
|
if not image_data or len(image_data) < 100:
|
|
return False
|
|
|
|
# 일반적인 이미지 파일 시그니처 확인
|
|
image_signatures = [
|
|
b'\xFF\xD8\xFF', # JPEG
|
|
b'\x89PNG\r\n\x1a\n', # PNG
|
|
b'GIF87a', # GIF87a
|
|
b'GIF89a', # GIF89a
|
|
b'RIFF', # WebP (RIFF 컨테이너)
|
|
]
|
|
|
|
return any(image_data.startswith(sig) for sig in image_signatures)
|
|
|
|
def call_inpaint_api(self, image, mask):
|
|
"""
|
|
인페인팅 API를 호출하여 이미지를 인페인팅합니다.
|
|
"""
|
|
try:
|
|
# 이미지 처리
|
|
if isinstance(image, str):
|
|
image_np = cv2.imread(image)
|
|
if image_np is None:
|
|
self.logger.log(f"이미지 로딩 실패: {image}", level=logging.ERROR)
|
|
return None
|
|
else:
|
|
image_np = image
|
|
|
|
# 마스크 처리
|
|
if isinstance(mask, str):
|
|
mask_np = cv2.imread(mask, cv2.IMREAD_GRAYSCALE)
|
|
if mask_np is None:
|
|
self.logger.log(f"마스크 로딩 실패: {mask}", level=logging.ERROR)
|
|
return None
|
|
else:
|
|
mask_np = mask
|
|
|
|
api_url = f"http://localhost:{self.inpaint_sv_port}/api/v1/inpaint"
|
|
_, img_encoded = cv2.imencode('.png', image_np)
|
|
_, mask_encoded = cv2.imencode('.png', mask_np)
|
|
img_b64 = base64.b64encode(img_encoded).decode('utf-8')
|
|
mask_b64 = base64.b64encode(mask_encoded).decode('utf-8')
|
|
payload = {
|
|
"image": img_b64,
|
|
"mask": mask_b64
|
|
}
|
|
response = requests.post(api_url, json=payload)
|
|
if response.status_code != 200:
|
|
self.logger.log(f"IOPaint 서버 에러: {response.text}", level=logging.ERROR)
|
|
return None
|
|
nparr = np.frombuffer(response.content, np.uint8)
|
|
result = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
|
return result
|
|
except Exception as e:
|
|
self.logger.log(f"인페인팅 API 호출 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
|
return None
|
|
|
|
|
|
def process_translated_texts(self, translated_texts, unwanted_texts, local_image_path, index):
|
|
"""
|
|
번역된 단어 리스트(translated_texts)에서 unwanted_texts의 원본값이
|
|
앞이나 뒤에 포함되면 치환값으로 바꿉니다.
|
|
치환값이 '이미지삭제'라면 None 반환(이미지 제외)
|
|
"""
|
|
|
|
new_texts = []
|
|
for text in translated_texts:
|
|
replaced = False
|
|
for origin, replace in unwanted_texts.items():
|
|
# 앞/뒤에 원본값이 있는지 확인
|
|
if text.startswith(origin) or text.endswith(origin):
|
|
self.logger.log(f"[{text}] -> [{replace}] (치환)", level=logging.INFO)
|
|
if replace == "이미지삭제":
|
|
self.logger.log(f"이미지 {index+1} 제외됨: {local_image_path}", level=logging.INFO)
|
|
return None
|
|
# 앞/뒤 원본값만 치환
|
|
if text.startswith(origin):
|
|
new = replace + text[len(origin):]
|
|
elif text.endswith(origin):
|
|
new = text[:-len(origin)] + replace
|
|
new_texts.append(new)
|
|
replaced = True
|
|
break
|
|
if not replaced:
|
|
new_texts.append(text)
|
|
|
|
self.logger.log(f"최종 치환 결과: {new_texts}", level=logging.INFO)
|
|
return new_texts
|
|
|
|
|
|
async def process_image_list(self, image_urls, delay=1.0, file_prefix="", use_inpainting=False):
|
|
"""
|
|
이미지 리스트를 순차적으로 처리합니다.
|
|
"""
|
|
if not image_urls:
|
|
self.logger.log("처리할 이미지가 없습니다.", level=logging.INFO)
|
|
return []
|
|
|
|
processing_mode = "인페인팅" if use_inpainting else "웨일 번역"
|
|
self.logger.log(f"이미지 {len(image_urls)}개를 {processing_mode} 모드로 처리 시작", level=logging.INFO)
|
|
|
|
processed_images = []
|
|
|
|
for i, url in enumerate(image_urls):
|
|
self.logger.log(f"이미지 {i+1}/{len(image_urls)} 처리 중... ({processing_mode} 모드)", level=logging.INFO)
|
|
|
|
result = await self.process_single_image(
|
|
url, i, delay, file_prefix, use_inpainting
|
|
)
|
|
|
|
# 결과 처리
|
|
if isinstance(result, dict):
|
|
status = result.get('status')
|
|
path = result.get('path')
|
|
|
|
if status == 'inpainted':
|
|
processed_images.append(path)
|
|
self.logger.log(f"이미지 {i+1} 인페인팅 처리 완료", level=logging.INFO)
|
|
elif status == 'original':
|
|
processed_images.append(path)
|
|
self.logger.log(f"이미지 {i+1} 원본 사용", level=logging.INFO)
|
|
elif status == 'exclude':
|
|
self.logger.log(f"이미지 {i+1} 제외됨", level=logging.INFO)
|
|
# 제외된 이미지는 리스트에 추가하지 않음
|
|
else: # failed
|
|
self.logger.log(f"이미지 {i+1} 처리 실패: {result.get('error', '알 수 없는 오류')}", level=logging.WARNING)
|
|
# 실패한 이미지는 원본 경로 추가
|
|
processed_images.append(path)
|
|
else:
|
|
# 이전 버전과의 호환성을 위한 처리
|
|
if result:
|
|
processed_images.append(result)
|
|
|
|
self.logger.log(f"이미지 처리 완료: 총 {len(processed_images)}개 ({processing_mode} 모드)", level=logging.INFO)
|
|
return processed_images
|
|
|
|
|
|
def gpt_translate_texts(self, ocr_results, gpt_client):
|
|
texts = [result['text'] for result in ocr_results]
|
|
if not texts:
|
|
return []
|
|
prompt = (
|
|
"다음 중국어 문장들을 한국어로 자연스럽고 의미가 잘 전달되게 번역해줘. "
|
|
"순서와 개수는 반드시 그대로 유지하고, 결과는 JSON 배열(리스트)로만 반환해. "
|
|
"중국어 리스트:\n" +
|
|
str(texts)
|
|
)
|
|
response = gpt_client.ask(prompt)
|
|
if isinstance(response, list):
|
|
return response
|
|
elif isinstance(response, dict) and 'result' in response:
|
|
return response['result']
|
|
else:
|
|
print("GPT 번역 결과 파싱 실패, 원본 반환")
|
|
return texts
|