브라우저 컨트롤러에서 이미지 프로세서 초기화 상태를 클래스 속성으로 변경하고, 메모 입력 로직에서 예외 처리 개선. 이미지 프로세서의 고해상도 이미지 다운스케일 기능 추가 및 메모리 관리 최적화. 메인 UI에서 토글 상태 관리 개선.
This commit is contained in:
parent
4a6128e51b
commit
b8c659c89d
|
|
@ -99,6 +99,7 @@ class BrowserController(QThread):
|
|||
self.is_valid_level = False
|
||||
|
||||
self.image_processor = None
|
||||
self.is_image_processor_init = False
|
||||
|
||||
self.route_registered = False
|
||||
self.browser_task_started = False
|
||||
|
|
@ -394,15 +395,14 @@ class BrowserController(QThread):
|
|||
await asyncio.sleep(0.53)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
|
||||
async def start_browser_async(self):
|
||||
"""비동기 Playwright 초기화 및 로그인 수행"""
|
||||
try:
|
||||
|
||||
is_image_processor_init = self.init_image_processor()
|
||||
self.is_image_processor_init = self.init_image_processor()
|
||||
|
||||
|
||||
if not is_image_processor_init:
|
||||
if not self.is_image_processor_init:
|
||||
self.logger.log(f"이미지 프로세서를 사용하지 않습니다.", level=logging.INFO)
|
||||
self.image_processor_error.emit("이미지 프로세서 초기화 오류로 대체번역을 사용합니다.\n 최대 3,000회/일 사용가능하나 사용상 주의 바랍니다.")
|
||||
|
||||
|
|
@ -2707,8 +2707,8 @@ class BrowserController(QThread):
|
|||
|
||||
# save_result == "SAVED"인 경우는 정상 저장이므로 그대로 진행
|
||||
|
||||
# 메모 입력 (상품이 삭제되지 않은 경우에만)
|
||||
if save_result != "DELETED" and memo_button and self.toggle_states['memo']:
|
||||
# 메모 입력: 저장이 정상 완료(SAVED) 또는 중복 처리 완료(DUPLICATE_HANDLED)된 경우에만 진행
|
||||
if save_result in ("SAVED", "DUPLICATE_HANDLED") and memo_button and self.toggle_states['memo']:
|
||||
self.check_pause() # 일시중지 상태 확인
|
||||
self.logger.log(f'{index}/{len(product_buttons)}: 메모 입력 중...', level=logging.DEBUG)
|
||||
await self.insert_product_infos_to_memo(memo_button, title_infos)
|
||||
|
|
@ -2720,6 +2720,14 @@ class BrowserController(QThread):
|
|||
self.logger.log(f" ▶ 상품 {index}/{len(product_buttons)} 처리중 오류: {item_err}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
finally:
|
||||
# 이전 상품 처리 중 예외로 다이얼로그가 남아 있을 수 있으므로 먼저 닫아준다.
|
||||
try:
|
||||
if await self.page.is_visible(self.product_dialog_close_btn):
|
||||
await self.page.click(self.product_dialog_close_btn)
|
||||
await asyncio.sleep(0.2)
|
||||
except Exception as close_err:
|
||||
self.logger.log(f"잔존 다이얼로그 닫기 실패: {close_err}", level=logging.DEBUG)
|
||||
|
||||
# 성공·실패 관계없이 진행 카운트와 프로그레스바 업데이트
|
||||
completed_count += 1
|
||||
self.logger.log(f'{completed_count}/[{total_products}]개 상품 수정 완료.', level=logging.INFO)
|
||||
|
|
@ -2727,9 +2735,10 @@ class BrowserController(QThread):
|
|||
title_infos.clear() # 메모 입력 후 초기화
|
||||
self.logger.log(f"title_infos 초기화", level=logging.DEBUG)
|
||||
|
||||
is_reset_ocr = self.image_processor.reset_ocr_module()
|
||||
if is_reset_ocr:
|
||||
self.logger.log(f"상품수정 완료로 인해 OCR 모듈 초기화", level=logging.ERROR)
|
||||
if self.is_image_processor_init:
|
||||
is_reset_ocr = self.image_processor.reset_ocr_module()
|
||||
if is_reset_ocr:
|
||||
self.logger.log(f"상품수정 완료로 인해 OCR 모듈 초기화", level=logging.DEBUG)
|
||||
|
||||
self.total_progressbar_signal.emit(completed_count, total_products)
|
||||
|
||||
|
|
@ -2955,11 +2964,12 @@ class BrowserController(QThread):
|
|||
except Exception as e2:
|
||||
screenshot_path = await self.save_error_screenshot()
|
||||
self.logger.log(f"재시도 후에도 메모 입력 중 오류 발생: {e2}", level=logging.ERROR, exc_info=True)
|
||||
# ESC 키 2번 전송하여 팝업 제거 후 다음 상품으로 넘어감
|
||||
self.logger.log("재시도 실패: ESC 키를 2번 전송하여 창을 닫고 다음 상품으로 넘어갑니다.", level=logging.WARNING)
|
||||
# ESC 키 2번 전송하여 팝업 제거 후 재시도 로직을 제거하고, 이번 상품의 메모 입력을 건너뛰고 반환합니다.
|
||||
self.logger.log("ESC 키를 2번 전송하여 메모 창을 닫고 해당 상품의 메모 입력을 건너뜁니다.", level=logging.WARNING)
|
||||
await self.page.keyboard.press("Escape")
|
||||
await self.page.keyboard.press("Escape")
|
||||
# 다음 상품으로 넘어가기 위해 예외를 재발생하지 않고 그냥 리턴합니다.
|
||||
await asyncio.sleep(0.5) # 잠시 대기
|
||||
await asyncio.sleep(0.5)
|
||||
return
|
||||
|
||||
def generate_titles_with_prices(self, title_infos):
|
||||
|
|
|
|||
|
|
@ -1406,13 +1406,15 @@ class MAIN_GUI(QMainWindow):
|
|||
self.logger.log(f"한시적 누끼토글 OFF", level=logging.INFO)
|
||||
self.thumb_nukki_toggle.setChecked(False)
|
||||
self.thumb_nukki_toggle.setEnabled(False)
|
||||
self.toggle_states['thumb_nukki'] = False
|
||||
|
||||
self.thumb_rmb_count_input.setValue(0)
|
||||
self.thumb_rmb_count_input.setEnabled(False)
|
||||
|
||||
self.optionTrnas_method_toggle.setEnabled(False)
|
||||
self.toggle_states['thumb_rmb_count'] = 0
|
||||
|
||||
self.optionTrnas_method_toggle.setEnabled(True)
|
||||
self.optionTrnas_method_toggle.setChecked(True)
|
||||
|
||||
self.toggle_states['optionTrnas_method'] = True
|
||||
|
||||
|
||||
def on_title_toggle_for_interlock(self, checked):
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class ImageProcessor3:
|
|||
self.logger.log("🔄 OCR 모듈 재초기화 시작", level=logging.INFO)
|
||||
# 기존 모듈 참조 해제
|
||||
del self.ocr_module
|
||||
gc.collect()
|
||||
# gc.collect()
|
||||
# 새 인스턴스 생성
|
||||
self.ocr_module = OCRModule(logger=self.logger, base_dir=self.base_dir)
|
||||
self._ocr_call_count = 0
|
||||
|
|
@ -167,7 +167,13 @@ class ImageProcessor3:
|
|||
if not local_image_path:
|
||||
self.logger.log(f"이미지 {index+1} 다운로드 실패, 원본 URL 반환", level=logging.WARNING)
|
||||
return {'status': 'failed', 'path': original_image_url, 'error': '다운로드 실패'}
|
||||
self.logger.log(f"이미지 {index+1} 로컬 저장위치: {local_image_path}", level=logging.DEBUG)
|
||||
|
||||
# 1-A. 고해상도 입력 다운스케일 (메모리 절약)
|
||||
# toggle_states 에 max_image_resolution(예: 2048) 값이 있으면 사용, 없으면 2048px 기준
|
||||
max_dim = self.toggle_states.get('max_image_resolution', 2048)
|
||||
local_image_path = self.downscale_image_if_large(local_image_path, max_dim=max_dim)
|
||||
|
||||
self.logger.log(f"이미지 {index+1} 로컬 저장위치(스케일 처리후): {local_image_path}", level=logging.DEBUG)
|
||||
|
||||
# 2. OCR 텍스트 감지
|
||||
ocr_results = self.ocr_module.detect_text(local_image_path)
|
||||
|
|
@ -230,9 +236,9 @@ class ImageProcessor3:
|
|||
)
|
||||
self.logger.log(f"마스크 생성 완료", level=logging.DEBUG)
|
||||
|
||||
if not self.is_frozen():
|
||||
# 디버깅 이미지 저장 (OCR 박스 + 마스크 시각화)
|
||||
self.save_debug_images(local_image_path, filter_ocr_results, masks, index, file_prefix)
|
||||
# if not self.is_frozen():
|
||||
# # 디버깅 이미지 저장 (OCR 박스 + 마스크 시각화)
|
||||
# self.save_debug_images(local_image_path, filter_ocr_results, masks, index, file_prefix)
|
||||
|
||||
# 인페인팅
|
||||
# is_member_valid = self.toggle_states.get('membership_level', 'basic') == 'premium' or self.toggle_states.get('membership_level', 'basic') == 'vip'
|
||||
|
|
@ -280,23 +286,21 @@ class ImageProcessor3:
|
|||
return {'status': 'failed', 'path': local_image_path or original_image_url, 'error': str(e)}
|
||||
|
||||
finally:
|
||||
# ─── 메모리 해제 ───
|
||||
for var in ('ocr_results', 'filter_ocr_results', 'translated_texts',
|
||||
'masks', 'inpainted_image', 'text_rendered_image'):
|
||||
if var in locals():
|
||||
try:
|
||||
del locals()[var]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# download_image 에서 읽어들인 로컬 파일을 바로 OpenCV가 메모리에 올렸다면
|
||||
# 해당 이미지도 해제
|
||||
# (detect_text 내부에서 이미지 변수를 del 못했을 경우)
|
||||
# ---- 메모리 해제 ----
|
||||
try:
|
||||
del page, local_image_path
|
||||
except:
|
||||
ocr_results = None
|
||||
filter_ocr_results = None
|
||||
translated_texts = None
|
||||
masks = None
|
||||
inpainted_image = None
|
||||
text_rendered_image = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# download_image 단계에서 사용한 page, local_image_path 도 참조 제거
|
||||
page = None
|
||||
local_image_path = None
|
||||
|
||||
# 최종 GC 강제 실행
|
||||
gc.collect()
|
||||
|
||||
|
|
@ -346,7 +350,7 @@ class ImageProcessor3:
|
|||
is_watermark_enabled = watermark_text != ""
|
||||
|
||||
# file_prefix가 'detail' 또는 'option'일 때만 워터마크 추가
|
||||
if is_watermark_enabled and file_prefix in ["detail", "option"]:
|
||||
if is_watermark_enabled and file_prefix in ["detail"]:
|
||||
image_data_to_save = self.postImageManager.add_watermark(
|
||||
image_data=text_rendered_image,
|
||||
watermark_text=watermark_text
|
||||
|
|
@ -947,3 +951,28 @@ class ImageProcessor3:
|
|||
# print(f"배경 제거 PNG 저장: {output_path}")
|
||||
# print(f"알파 마스크 저장: {alpha_path}")
|
||||
# return foreground, alpha_img
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 고해상도 이미지 다운스케일 유틸리티 (메모리 절감용)
|
||||
# ------------------------------------------------------------------
|
||||
def downscale_image_if_large(self, image_path, max_dim=2048):
|
||||
"""주어진 이미지가 max_dim 픽셀을 초과하면 축소하여 같은 경로에 저장하고 경로를 반환합니다"""
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
width, height = img.size
|
||||
if max(width, height) <= max_dim:
|
||||
return image_path # 변경 없음
|
||||
|
||||
scale = float(max_dim) / float(max(width, height))
|
||||
new_size = (int(width * scale), int(height * scale))
|
||||
|
||||
self.logger.log(
|
||||
f"고해상도({width}x{height}) -> {new_size}로 리사이즈 후 저장", level=logging.INFO)
|
||||
|
||||
resized = img.resize(new_size, Image.LANCZOS)
|
||||
# JPG, PNG 등에 관계없이 원본 확장자를 유지하여 덮어쓰기
|
||||
resized.save(image_path)
|
||||
return image_path
|
||||
except Exception as e:
|
||||
self.logger.log(f"다운스케일 실패: {e}", level=logging.WARNING)
|
||||
return image_path
|
||||
|
|
|
|||
|
|
@ -80,8 +80,9 @@ class PostImageManager:
|
|||
self.logger.log("폰트가 로드되지 않아 워터마크를 추가할 수 없습니다. 원본 이미지를 반환합니다.", level=logging.WARNING)
|
||||
return image_data
|
||||
|
||||
# 이미지 복사본 생성
|
||||
watermark_image = image_data.copy()
|
||||
# RGBA 모드로 변환하여 투명 채널을 포함한 복사본 생성 (메모리 절약)
|
||||
# 기존 copy() 후 다시 convert() 를 수행하던 구조를 간소화하여 한 번의 변환만 수행합니다.
|
||||
watermark_image = image_data.convert("RGBA")
|
||||
|
||||
# 폰트 설정 (안전한 폰트 로딩)
|
||||
try:
|
||||
|
|
@ -106,9 +107,6 @@ class PostImageManager:
|
|||
# 이미지 크기
|
||||
width, height = image_data.size
|
||||
|
||||
# 워터마크 레이어 생성
|
||||
watermark_layer = Image.new("RGBA", (width, height)) # RGBA 이미지 생성
|
||||
|
||||
# 지그재그 간격 설정
|
||||
zigzag_step = int(text_height * 2) # Y축의 지그재그 간격
|
||||
|
||||
|
|
@ -129,12 +127,9 @@ class PostImageManager:
|
|||
# 텍스트 회전
|
||||
rotated_text_layer = text_layer.rotate(angle, expand=1)
|
||||
|
||||
# 회전된 텍스트를 워터마크 레이어에 추가
|
||||
watermark_layer.paste(rotated_text_layer, (x + x_offset, y), rotated_text_layer)
|
||||
# 회전된 텍스트를 원본 이미지에 직접 추가
|
||||
watermark_image.paste(rotated_text_layer, (x + x_offset, y), rotated_text_layer)
|
||||
|
||||
# 원본 이미지와 워터마크 레이어 합성
|
||||
watermark_image = Image.alpha_composite(watermark_image.convert("RGBA"), watermark_layer)
|
||||
|
||||
# 최종적으로 RGB 형식으로 변환 후 반환
|
||||
return watermark_image.convert("RGB")
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,13 +1,17 @@
|
|||
|
||||
|
||||
# 3.9.7 업데이트 로그
|
||||
# 3.9.8 업데이트 로그
|
||||
|
||||
### 오류 수정
|
||||
- OCR 주기적 초기화로 primitive 예방
|
||||
- 수집오류발생 상품 이후 메모입력이 밀리는 현상 수정
|
||||
|
||||
|
||||
- 옵션명 번역방식에 파파고 방식 추가 - 취사선택
|
||||
|
||||
### 기능 추가
|
||||
- 비밀번호 변경 추가
|
||||
|
||||
- 이미지프로세서의 고해상도이미지 다운스케일링
|
||||
|
||||
# 3.9.6 업데이트 로그
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue