가격 수정 로직 개선: 가격범위 초과 버튼의 동작을 별도로 분리하고, 이미지 번역 처리 후 즉시 파일을 제거하도록 수정하였습니다. 메모리 모니터링 기능을 추가하여 시스템 메모리 사용량을 실시간으로 확인할 수 있도록 하였으며, 관련 UI 요소를 개선하였습니다. 또한, 이미지 처리 요청 시 임시 파일 정리 로직을 추가하여 안정성을 높였습니다.

This commit is contained in:
9700X_PC 2025-08-28 23:56:46 +09:00
parent e26a7cae64
commit e34ec7acab
12 changed files with 705 additions and 238 deletions

View File

@ -2855,11 +2855,12 @@ class BrowserController(QThread):
# 가격 수정
self.start_stage_signal.emit(2)
is_price = self.toggle_states.get('price')
is_price = self.toggle_states.get('price', False)
remove_overprice = self.toggle_states.get('remove_overprice', False)
await self.random_human_behavior(self.page)
if is_price:
if is_price or remove_overprice:
self.check_pause() # 일시중지 상태 확인
self.logger.log(f"가격수정 : {is_price} ", level=logging.DEBUG)
self.logger.log(f"가격수정 : {is_price} + 가격범위 초과제외 :{remove_overprice}", level=logging.DEBUG)
price_result = await self.edit_price(title_infos)
if price_result == "DELETED":
self.logger.log("가격 탭에서 상품이 삭제되었습니다. 다음 상품으로 넘어갑니다.", level=logging.INFO)
@ -4103,6 +4104,8 @@ class ImageWorkerManager:
self._last_heartbeat = 0 # 마지막 하트비트 응답 시간
self._consecutive_failures = 0 # 연속 실패 횟수
self._circuit_breaker_threshold = 3 # 서킷브레이커 임계값
self._circuit_breaker_last_failure_time = 0 # 마지막 실패 시간
self._circuit_breaker_timeout = 600 # 서킷브레이커 자동 해제 시간 (10분)
self._backoff_delay = 1 # 백오프 지연 시간 (초)
self._max_backoff = 30 # 최대 백오프 지연 시간 (초)
@ -4150,7 +4153,7 @@ class ImageWorkerManager:
pass
ready = None
deadline = time.time() + 30 # 여유 있게 90초까지 대기
deadline = time.time() + 60 # 초기화가 길어질 수 있으므로 60초까지 대기
while time.time() < deadline:
try:
msg = self.result_q.get(timeout=1)
@ -4170,6 +4173,9 @@ class ImageWorkerManager:
except Exception as e:
self.logger.log(f"프로세스 정리 중 오류: {e}", level=logging.WARNING)
# 초기화 실패 시 대기 중인 요청들 정리
self._cleanup_old_generation_requests()
# 재시작은 호출자에게 위임 (무한 루프 방지)
self.logger.log("ImageWorker 초기화 실패 - 수동 재시작 필요", level=logging.ERROR)
return
@ -4346,6 +4352,46 @@ class ImageWorkerManager:
elif mem2.percent > 60:
self.logger.log(f"⚠️ 주의: 메모리 사용량이 중간 수준입니다 ({mem2.percent}%)", level=logging.INFO)
# 🔧 큐를 새로 생성하여 오염된 큐 상태 해결
self.logger.log("🔄 재시작용 새 큐 생성 중...", level=logging.INFO)
# 기존 큐 정리
if hasattr(self, 'task_q') and self.task_q:
try:
# 기존 큐에 남은 메시지들 정리
while not self.task_q.empty():
try:
self.task_q.get_nowait()
except:
break
except:
pass
if hasattr(self, 'result_q') and self.result_q:
try:
while not self.result_q.empty():
try:
self.result_q.get_nowait()
except:
break
except:
pass
# 새 큐 생성
import multiprocessing
self.task_q = multiprocessing.Queue()
self.result_q = multiprocessing.Queue()
self.log_q = multiprocessing.Queue()
self.logger.log("✅ 새 큐 생성 완료", level=logging.INFO)
# 🧹 _pending_requests 정리 (재시작 시)
with self._lock:
old_count = len(self._pending_requests)
self._pending_requests.clear()
if old_count > 0:
self.logger.log(f"🗑️ 재시작 시 {old_count}개 pending requests 정리 완료", level=logging.INFO)
# proc_args 업데이트
log_path = self.proc_args[3] # log_path 위치 수정
self.proc_args = (
@ -4363,6 +4409,9 @@ class ImageWorkerManager:
# 새 프로세스 시작
self._spawn_proc()
# 🔄 LogBridge는 BrowserController에서 관리되므로 여기서는 재시작하지 않음
# (BrowserController에서 새 log_q를 감지하여 자동으로 재시작됩니다)
except Exception as e:
self.logger.log(f"재시작 중 오류: {e}", level=logging.ERROR)
finally:
@ -4443,21 +4492,29 @@ class ImageWorkerManager:
return {"status": "failed", "path": None}
async def _call_worker(self, cmd, timeout=60, **kwargs):
# 서킷브레이커 체크
# 서킷브레이커 체크 (시간 기반 자동 해제 포함)
if self._consecutive_failures >= self._circuit_breaker_threshold:
self.logger.log(f"서킷브레이커 활성화 - 연속 실패 {self._consecutive_failures}", level=logging.WARNING)
raise RuntimeError("서킷브레이커: 워커가 너무 자주 실패하고 있습니다")
# 10분 경과 시 자동 해제
current_time = time.time()
if current_time - self._circuit_breaker_last_failure_time > self._circuit_breaker_timeout:
self.logger.log(f"서킷브레이커 자동 해제 - {self._circuit_breaker_timeout/60:.0f}분 경과", level=logging.INFO)
self._consecutive_failures = 0
self._circuit_breaker_last_failure_time = 0
else:
remaining_time = self._circuit_breaker_timeout - (current_time - self._circuit_breaker_last_failure_time)
self.logger.log(f"서킷브레이커 활성화 - 연속 실패 {self._consecutive_failures}회 (해제까지 {remaining_time/60:.1f}분)", level=logging.WARNING)
raise RuntimeError("서킷브레이커: 워커가 너무 자주 실패하고 있습니다")
# 워커 프로세스가 죽었으면 재시작
uid = uuid.uuid4().hex # 고유 UID 먼저 생성
if not self.proc.is_alive():
self.logger.log(f"image worker died (exit code: {self.proc.exitcode}) → restarting…", level=logging.WARNING)
self._schedule_restart("worker_died")
# pending_requests 정리
if uid in self._pending_requests:
del self._pending_requests[uid]
del self._pending_requests[uid]
raise RuntimeError("워커 프로세스가 죽었습니다")
uid = uuid.uuid4().hex
current_generation = self._generation
# 내부에서 ImageProcessor3 가 toggle_states, base_dir 이 필요하므로 함께 전달
@ -4492,10 +4549,12 @@ class ImageWorkerManager:
except TimeoutError:
self.logger.log("⏱ 워커 응답 지연 → 재시작 시도", level=logging.WARNING)
self._consecutive_failures += 1
self._circuit_breaker_last_failure_time = time.time()
self._schedule_restart("timeout")
raise # 상위에서 필요하다면 재시도 로직을 추가
except Exception as e: # ← RuntimeError 포함
self._consecutive_failures += 1
self._circuit_breaker_last_failure_time = time.time()
# primitive/메모리 오류 에스컬레이션 처리
msg = str(e).lower()

View File

@ -108,6 +108,9 @@ class MAIN_GUI(QMainWindow):
self.toggle_states['migan_use_cuda'] = True
self.logger.log("🎯 migan_use_cuda를 True로 강제 설정", level=logging.INFO)
# 메모리 모니터링 초기화 (UI 생성 전에 실행)
self.init_memory_monitoring()
self.initUI()
# GPU 가용성 확인 및 드롭다운 폴백 처리 (UI 초기화 완료 후)
@ -5378,6 +5381,10 @@ class MAIN_GUI(QMainWindow):
self.setting_Buttons_group = self.create_Settings_buttons()
self.main_layout.addWidget(self.setting_Buttons_group)
# 메모리 모니터링 위젯 추가
self.memory_monitor_group = self.create_memory_monitor_group()
self.main_layout.addWidget(self.memory_monitor_group)
# 토글 레이아웃 생성
toggle_main_widget = self.creat_Toggle_tab()
toggle_main_widget.setVisible(False)
@ -7269,4 +7276,286 @@ class MAIN_GUI(QMainWindow):
if msg.clickedButton() == retry_button:
return True # 다시 시도
else:
return False # 취소
return False # 취소
def init_memory_monitoring(self):
"""메모리 모니터링을 위한 초기화"""
try:
# 시스템 전체 메모리 정보
self.system_memory = psutil.virtual_memory()
self.total_memory_gb = self.system_memory.total / (1024**3) # GB 단위
# 프로그램 시작시 메모리 사용량
self.process = psutil.Process()
self.initial_memory_mb = self.process.memory_info().rss / (1024**2) # MB 단위
self.initial_memory_percent = (self.initial_memory_mb * (1024**2)) / self.system_memory.total * 100
# 메모리 업데이트 타이머
self.memory_update_timer = QTimer(self)
self.memory_update_timer.timeout.connect(self.update_memory_info)
self.memory_update_timer.start(2000) # 2초마다 업데이트
self.logger.log(f"메모리 모니터링 초기화 완료 - 시작시 메모리: {self.initial_memory_mb:.1f}MB ({self.initial_memory_percent:.1f}%)", level=logging.INFO)
except Exception as e:
self.logger.log(f"메모리 모니터링 초기화 실패: {str(e)}", level=logging.ERROR)
def create_memory_monitor_group(self):
"""메모리 모니터링 그룹 위젯 생성"""
memory_group = QGroupBox("시스템 메모리 사용률")
memory_group.setStyleSheet("""
QGroupBox {
border: 2px solid #3498db;
border-radius: 8px;
padding: 15px 10px 10px 10px;
margin-bottom: 10px;
font-size: 11pt;
font-weight: bold;
color: #2c3e50;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #ffffff, stop: 1 #f8f9fa);
}
QGroupBox::title {
subcontrol-origin: margin;
left: 15px;
padding: 0 8px 0 8px;
font-size: 10pt;
font-weight: bold;
color: #2980b9;
background-color: #ffffff;
border-radius: 4px;
}
""")
memory_layout = QVBoxLayout()
# 메모리 정보 레이블
self.memory_info_label = QLabel()
self.memory_info_label.setAlignment(Qt.AlignCenter)
self.memory_info_label.setStyleSheet("""
QLabel {
color: #2c3e50;
font-size: 9pt;
font-weight: bold;
padding: 2px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 4px;
}
""")
memory_layout.addWidget(self.memory_info_label)
# 통합 프로그레스 바 컨테이너
progress_container = QWidget()
progress_layout = QVBoxLayout(progress_container)
progress_layout.setContentsMargins(5, 5, 5, 5)
progress_layout.setSpacing(8)
# 통합 메모리 프로그레스 바
unified_memory_layout = QHBoxLayout()
# 메모리 정보 레이블
memory_info_layout = QVBoxLayout()
memory_info_layout.setSpacing(2)
# 시작시 메모리 정보
start_info_layout = QHBoxLayout()
start_label = QLabel("시작시:")
start_label.setStyleSheet("font-size: 8pt; font-weight: bold; color: #27ae60;")
start_label.setFixedWidth(50)
start_value = QLabel(f"{self.initial_memory_mb:.0f}MB")
start_value.setStyleSheet("font-size: 8pt; color: #27ae60; font-weight: bold;")
start_value.setFixedWidth(60)
start_info_layout.addWidget(start_label)
start_info_layout.addWidget(start_value)
start_info_layout.addStretch()
# 현재 메모리 정보
current_info_layout = QHBoxLayout()
current_label = QLabel("현재:")
current_label.setStyleSheet("font-size: 8pt; font-weight: bold; color: #27ae60;")
current_label.setFixedWidth(50)
self.current_value_label = QLabel()
self.current_value_label.setStyleSheet("font-size: 8pt; color: #27ae60; font-weight: bold;")
self.current_value_label.setFixedWidth(60)
current_info_layout.addWidget(current_label)
current_info_layout.addWidget(self.current_value_label)
current_info_layout.addStretch()
# 증가분 정보
increase_info_layout = QHBoxLayout()
increase_label = QLabel("증가:")
increase_label.setStyleSheet("font-size: 8pt; font-weight: bold; color: #e74c3c;")
increase_label.setFixedWidth(50)
self.increase_value_label = QLabel()
self.increase_value_label.setStyleSheet("font-size: 8pt; color: #e74c3c; font-weight: bold;")
self.increase_value_label.setFixedWidth(60)
increase_info_layout.addWidget(increase_label)
increase_info_layout.addWidget(self.increase_value_label)
increase_info_layout.addStretch()
memory_info_layout.addLayout(start_info_layout)
memory_info_layout.addLayout(current_info_layout)
memory_info_layout.addLayout(increase_info_layout)
# 중첩된 프로그레스바 컨테이너
progress_container_widget = QWidget()
progress_container_widget.setFixedHeight(25)
progress_container_widget.setStyleSheet("""
QWidget {
background-color: #ecf0f1;
border: 2px solid #3498db;
border-radius: 10px;
}
""")
# 중첩된 프로그레스바 레이아웃
nested_progress_layout = QVBoxLayout(progress_container_widget)
nested_progress_layout.setContentsMargins(2, 2, 2, 2)
nested_progress_layout.setSpacing(0)
# 시작시 메모리 프로그레스바 (고정 - 파란색)
self.start_memory_bar = QProgressBar()
self.start_memory_bar.setMaximum(100)
self.start_memory_bar.setValue(int(self.initial_memory_percent))
self.start_memory_bar.setTextVisible(True)
self.start_memory_bar.setFixedHeight(12)
self.start_memory_bar.setFormat(f"시작: {self.initial_memory_percent:.1f}%")
self.start_memory_bar.setStyleSheet("""
QProgressBar {
border: none;
background-color: transparent;
text-align: center;
font-size: 8pt;
font-weight: bold;
color: #ffffff;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #3498db, stop: 1 #2980b9);
border-radius: 8px;
}
""")
# 증가분 메모리 프로그레스바 (동적 - 그라데이션)
self.increase_memory_bar = QProgressBar()
self.increase_memory_bar.setMaximum(100)
self.increase_memory_bar.setValue(0) # 초기값은 0
self.increase_memory_bar.setTextVisible(False)
self.increase_memory_bar.setFixedHeight(12)
self.increase_memory_bar.setStyleSheet("""
QProgressBar {
border: none;
background-color: transparent;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #2ecc71, stop: 1 #27ae60);
border-radius: 8px;
}
""")
nested_progress_layout.addWidget(self.start_memory_bar)
nested_progress_layout.addWidget(self.increase_memory_bar)
unified_memory_layout.addLayout(memory_info_layout)
unified_memory_layout.addWidget(progress_container_widget, 1) # 1은 stretch factor
progress_layout.addLayout(unified_memory_layout)
memory_layout.addWidget(progress_container)
memory_group.setLayout(memory_layout)
memory_group.setFixedHeight(130) # 중첩 프로그레스바로 높이 조정
# 초기 메모리 정보 업데이트
self.update_memory_info()
return memory_group
def update_memory_info(self):
"""메모리 정보 업데이트"""
try:
# 현재 메모리 사용량
current_memory_mb = self.process.memory_info().rss / (1024**2)
current_memory_percent = (current_memory_mb * (1024**2)) / self.system_memory.total * 100
# 증가분 계산
increase_mb = current_memory_mb - self.initial_memory_mb
increase_percent = current_memory_percent - self.initial_memory_percent
# 시스템 전체 메모리 현황
system_memory_current = psutil.virtual_memory()
system_used_percent = system_memory_current.percent
# 정보 레이블 업데이트
info_text = (f"전체: {self.total_memory_gb:.1f}GB | "
f"시스템 사용률: {system_used_percent:.1f}% | "
f"프로그램 시작시: {self.initial_memory_mb:.0f}MB")
self.memory_info_label.setText(info_text)
# 현재 메모리 값 업데이트
self.current_value_label.setText(f"{current_memory_mb:.0f}MB")
# 증가분 메모리 프로그레스바 업데이트
if increase_mb > 0:
# 증가분을 전체 메모리 대비 퍼센트로 계산
increase_percent = (increase_mb * (1024**2)) / self.system_memory.total * 100
self.increase_memory_bar.setValue(int(increase_percent))
# 총합 메모리 사용률 (시작시 + 증가분)
total_program_memory_percent = self.initial_memory_percent + increase_percent
# 총합에 따른 증가분 바 색상 변경 (그라데이션)
if total_program_memory_percent >= 80: # 80% 이상 - 빨간색
increase_bar_style = """
QProgressBar {
border: none;
background-color: transparent;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #e74c3c, stop: 1 #c0392b);
border-radius: 8px;
}
"""
self.current_value_label.setStyleSheet("font-size: 8pt; color: #e74c3c; font-weight: bold;")
elif total_program_memory_percent >= 70: # 70% 이상 - 주황색
increase_bar_style = """
QProgressBar {
border: none;
background-color: transparent;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #f39c12, stop: 1 #e67e22);
border-radius: 8px;
}
"""
self.current_value_label.setStyleSheet("font-size: 8pt; color: #f39c12; font-weight: bold;")
else: # 60% 이하 - 녹색
increase_bar_style = """
QProgressBar {
border: none;
background-color: transparent;
}
QProgressBar::chunk {
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #2ecc71, stop: 1 #27ae60);
border-radius: 8px;
}
"""
self.current_value_label.setStyleSheet("font-size: 8pt; color: #27ae60; font-weight: bold;")
self.increase_memory_bar.setStyleSheet(increase_bar_style)
else:
# 증가분이 없으면 바를 0으로 설정
self.increase_memory_bar.setValue(0)
self.current_value_label.setStyleSheet("font-size: 8pt; color: #27ae60; font-weight: bold;")
# 증가분 값 업데이트
if increase_mb > 0:
self.increase_value_label.setText(f"+{increase_mb:.0f}MB")
else:
self.increase_value_label.setText("0MB")
except Exception as e:
self.logger.log(f"메모리 정보 업데이트 오류: {str(e)}", level=logging.ERROR)

View File

@ -54,23 +54,34 @@ class DetailHandler:
# self.whale_translator = self.browser_controller.get_whale()
# self.logger.log(f"whale_translator 업데이트 : {self.whale_translator}", level=logging.DEBUG)
def reset_state(self):
"""상세페이지 핸들러의 상태를 초기화합니다. (detail 관련 임시파일만 정리)"""
self.logger.log("DetailHandler 상태 초기화 - detail 관련 임시파일 정리", level=logging.DEBUG)
try:
# detail 관련 임시 디렉토리의 파일들만 삭제
detail_pattern = os.path.join(self.TEMP_IMAGE_DIR, "detail_*.png")
detail_files = glob.glob(detail_pattern)
# detail 관련 임시 디렉토리의 파일들만 삭제 (더 포괄적인 패턴 사용)
patterns = [
"detail_*.png",
"detail_*.jpg",
"detail_*.webp",
"translated_detail_*"
]
if detail_files:
for file_path in detail_files:
all_detail_files = []
for pattern in patterns:
detail_pattern = os.path.join(self.TEMP_IMAGE_DIR, pattern)
all_detail_files.extend(glob.glob(detail_pattern))
if all_detail_files:
for file_path in all_detail_files:
try:
os.remove(file_path)
self.logger.log(f"detail 임시 파일 삭제: {file_path}", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"detail 임시 파일 삭제 실패: {file_path}, 오류: {e}", level=logging.WARNING)
self.logger.log(f"{len(detail_files)}개의 detail 임시 파일 정리 완료", level=logging.INFO)
self.logger.log(f"{len(all_detail_files)}개의 detail 임시 파일 정리 완료", level=logging.INFO)
else:
self.logger.log("정리할 detail 임시 파일이 없습니다.", level=logging.DEBUG)
@ -583,6 +594,9 @@ class DetailHandler:
excluded_count = 0
error_count = 0
# 임시 파일 정리를 위한 리스트
processed_files = []
# 모든 이미지 처리
self.logger.log("이미지 처리 및 업로드 시작...", level=logging.INFO)
@ -609,7 +623,10 @@ class DetailHandler:
elif status == 'exclude':
excluded_count += 1
self.logger.log(f"이미지 {idx+1} 제외됨: {image_url}", level=logging.INFO)
# 업로드하지 않고 다음 이미지로
# 제외된 이미지도 정리 리스트에 추가
if tranlated_image_path:
processed_files.append(tranlated_image_path)
# 업로드하지 않고 다음 이미지로
elif isinstance(final_image_result, str) and final_image_result:
# (이전 방식과의 호환성)
upload_success = await self.upload_image(final_image_result)
@ -625,10 +642,17 @@ class DetailHandler:
else:
error_count += 1
self.logger.log(f"이미지 {idx+1} 업로드 실패: {tranlated_image_path}", level=logging.WARNING)
# 처리된 파일을 정리 리스트에 추가
if tranlated_image_path:
processed_files.append(tranlated_image_path)
except Exception as e:
error_count += 1
self.logger.log(f"이미지 {idx+1} 처리 중 오류: {e}", level=logging.ERROR)
# 오류 발생 시에도 정리 리스트에 추가
if 'tranlated_image_path' in locals() and tranlated_image_path:
processed_files.append(tranlated_image_path)
# 프로그레스 업데이트
self.update_detail_progress_signal.emit(idx+1, len(image_urls))
@ -637,6 +661,22 @@ class DetailHandler:
# 처리 결과 로깅
self.logger.log(f"이미지 처리 완료 - 성공: {success_count}, 제외: {excluded_count}, 오류: {error_count}, 총: {len(image_urls)}", level=logging.INFO)
# 모든 이미지 처리 완료 후 임시 파일 배치 정리
if processed_files:
self.logger.log(f"임시 파일 배치 정리 시작: {len(processed_files)}개 파일", level=logging.INFO)
deleted_count = 0
for file_path in processed_files:
try:
if file_path and os.path.exists(file_path):
os.remove(file_path)
deleted_count += 1
self.logger.log(f"임시 파일 삭제: {file_path}", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"파일 삭제 실패 (무시): {file_path} - {e}", level=logging.DEBUG)
self.logger.log(f"임시 파일 정리 완료: {deleted_count}/{len(processed_files)}개 삭제", level=logging.INFO)
# 번역 완료 후 프로그레스바 숨김
self.set_progress_visible_signal.emit(False)

View File

@ -5,6 +5,7 @@ import logging
import random
import re
import math
import glob
class OptionHandler:
def __init__(self, locator_manager, browser_controller, TEMP_IMAGE_DIR, logger, gpt_client, update_detail_progress_signal, set_progress_visible_signal, toggle_states, imageProcessor, papago_translator):
@ -103,6 +104,8 @@ class OptionHandler:
self.imageProcessor = imageProcessor
self.logger.log(f"page객체 업데이트 : {page1}", level=logging.DEBUG)
# def update_whale(self):
# self.whale_translator = self.browser_controller.get_whale()
# self.logger.log(f"whale_translator 업데이트 : {self.whale_translator}", level=logging.DEBUG)
@ -1504,6 +1507,7 @@ class OptionHandler:
translated_index = 1
self.set_progress_visible_signal.emit(True)
processed_files = [] # 임시 파일 정리를 위한 리스트
for idx, option_item in enumerate(valid_items, start=1):
try:
@ -1592,8 +1596,15 @@ class OptionHandler:
# 업로드 완료 대기
await asyncio.sleep(0.5)
# 처리된 파일을 정리 리스트에 추가
if translated_image_path:
processed_files.append(translated_image_path)
except Exception as e:
self.logger.log(f"{idx}번째 옵션의 이미지를 추가하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# 오류 발생 시에도 정리 리스트에 추가
if 'translated_image_path' in locals() and translated_image_path:
processed_files.append(translated_image_path)
except Exception as e:
self.logger.log(f"{idx}번째 옵션 이미지 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
@ -1605,6 +1616,22 @@ class OptionHandler:
self.update_detail_progress_signal.emit(idx, total_options)
self.set_progress_visible_signal.emit(False)
# 모든 옵션 처리 완료 후 임시 파일 배치 정리
if processed_files:
self.logger.log(f"옵션 임시 파일 배치 정리 시작: {len(processed_files)}개 파일", level=logging.INFO)
deleted_count = 0
for file_path in processed_files:
try:
if file_path and os.path.exists(file_path):
os.remove(file_path)
deleted_count += 1
self.logger.log(f"임시 파일 삭제: {file_path}", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"파일 삭제 실패 (무시): {file_path} - {e}", level=logging.DEBUG)
self.logger.log(f"옵션 임시 파일 정리 완료: {deleted_count}/{len(processed_files)}개 삭제", level=logging.INFO)
except Exception as e:
self.logger.log(f"옵션 이미지 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)

View File

@ -94,116 +94,118 @@ class PriceHandler:
# 상품정보 초기화
self.initialize_values()
is_group = title_infos.get('is_group_ESM', False)
category = title_infos.get('category', False)
self.logger.log(f"process_price - title_infos : {title_infos}", level=logging.DEBUG)
selling_prices = title_infos.get('top_5_prices', [])
if selling_prices:
try:
# 문자열에서 숫자만 추출하여 정수로 변환
prices = [int(price.replace(",", "")) for price in selling_prices if isinstance(price, str) and price.isdigit()]
if prices: # 유효한 숫자가 있을 경우만 계산 진행
# 중간값(median)과 평균값(mean) 계산
median_price = sorted(prices)[len(prices) // 2]
mean_price = sum(prices) // len(prices)
# 중간값과 평균값 중 높은 값 선택
selling_price = max(median_price, mean_price)
self.logger.log(f"중간값: {median_price}, 평균값: {mean_price}, 최종 선택된 판매가격: {selling_price}", level=logging.DEBUG)
else:
# 숫자가 없을 경우 기본값 설정
selling_price = 0
self.logger.log("판매 가격 데이터에 유효한 숫자가 없습니다. 기본값 0 설정", level=logging.DEBUG)
except Exception as e:
# self.logger.log(f"판매 가격 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
screenshot_path = await self.browser_controller.save_error_screenshot("price_error")
self.logger.log(f"판매 가격 처리 중 오류 발생: {e}\n 스크린샷 저장됨: {screenshot_path}", level=logging.ERROR, exc_info=True)
selling_price = 0
else:
selling_price = 0 # 데이터가 없는 경우 기본값
self.logger.log("판매 가격 데이터를 찾을 수 없음. 기본값 0 설정", level=logging.DEBUG)
# 가격범위 초과 옵션 제외 처리
if self.toggle_states.get('remove_overprice', False):
await self.click_remove_overprice_btn()
self.logger.log("가격범위 초과 옵션 제외 처리 완료", level=logging.INFO)
# 1. 페이지에서 가격 정보 수집
self.logger.log("초기 더하기마진과 해외배송비 가격 정보를 수집합니다.", level=logging.DEBUG)
initial_plusmargin, initial_shipping_price = await self.get_plusmargin_and_shipping_values()
if self.toggle_states.get('price', False):
is_group = title_infos.get('is_group_ESM', False)
category = title_infos.get('category', False)
# 판매 가격을 결정하기 위해 조건 추가
# 팔린가격(판매이력)이 없으면 None 으로 두고, 이후 적정가 계산 시 가중치에서 제외한다.
if not selling_price or selling_price == 0:
selling_price = None
self.logger.log("팔린 가격 정보가 없어 sold_price 를 None 으로 처리합니다.", level=logging.DEBUG)
else:
self.logger.log(f"title_infos에서 팔린 가격 {selling_price}을 가져옴", level=logging.DEBUG)
self.logger.log(f"process_price - title_infos : {title_infos}", level=logging.DEBUG)
self.logger.log("옵션 가격 정보를 수집하기 위해 옵션가격정렬을 클릭 합니다.", level=logging.DEBUG)
selling_prices = title_infos.get('top_5_prices', [])
if selling_prices:
try:
# 문자열에서 숫자만 추출하여 정수로 변환
prices = [int(price.replace(",", "")) for price in selling_prices if isinstance(price, str) and price.isdigit()]
if prices: # 유효한 숫자가 있을 경우만 계산 진행
# 중간값(median)과 평균값(mean) 계산
median_price = sorted(prices)[len(prices) // 2]
mean_price = sum(prices) // len(prices)
is_single = self.option_info['is_single_option']
self.logger.log(f"is_single : {is_single}", level=logging.DEBUG)
# 중간값과 평균값 중 높은 값 선택
selling_price = max(median_price, mean_price)
self.logger.log(f"중간값: {median_price}, 평균값: {mean_price}, 최종 선택된 판매가격: {selling_price}", level=logging.DEBUG)
else:
# 숫자가 없을 경우 기본값 설정
selling_price = 0
self.logger.log("판매 가격 데이터에 유효한 숫자가 없습니다. 기본값 0 설정", level=logging.DEBUG)
if not is_single:
await self.ordering_by_option_button_click()
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices(is_single, is_group) # 수집된 옵션정보를 반환
if option_data is None:
self.logger.log("상품 옵션 정보를 수집하지 못했습니다.", level=logging.ERROR, exc_info=True)
return
# 2. 원가 기반가격 계산
calculated_price = self.calc_initial_price(upper_avg_cost, 0.04, 0.24)
except Exception as e:
# self.logger.log(f"판매 가격 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
screenshot_path = await self.browser_controller.save_error_screenshot("price_error")
self.logger.log(f"판매 가격 처리 중 오류 발생: {e}\n 스크린샷 저장됨: {screenshot_path}", level=logging.ERROR, exc_info=True)
selling_price = 0
else:
selling_price = 0 # 데이터가 없는 경우 기본값
self.logger.log("판매 가격 데이터를 찾을 수 없음. 기본값 0 설정", level=logging.DEBUG)
# 3. 적정 판매가 산출
self.logger.log("적정 판매가를 계산합니다.", level=logging.DEBUG)
# 팔린가격이 None 인 경우 비율 계산에서 제외된다.
self.optimal_price_config['sold_price'] = selling_price
self.optimal_price_config['cost_price2X'] = upper_avg_cost # 원가2배 가격 : upper_avg_cost를 기준으로 계산.
self.optimal_price_config['calculated_price'] = calculated_price # 기준판매가를 기준으로 계산된 값
optimal_price = self.calculate_optimal_price()
self.logger.log(f"계산된 적정 판매가: {optimal_price}", level=logging.DEBUG)
# 1. 페이지에서 가격 정보 수집
self.logger.log("초기 더하기마진과 해외배송비 가격 정보를 수집합니다.", level=logging.DEBUG)
initial_plusmargin, initial_shipping_price = await self.get_plusmargin_and_shipping_values()
# 4. 더하기 마진을 적정 판매가 기준으로 재설정
self.logger.log("더하기 마진을 적정 판매가에 맞게 조정합니다.", level=logging.DEBUG)
additional_margin = self.calculate_adjusted_margin(optimal_price, option_data)
additional_margin = self.round_to_UP(additional_margin)
self.logger.log(f"조정된 더하기 마진: {additional_margin}", level=logging.DEBUG)
# 판매 가격을 결정하기 위해 조건 추가
# 팔린가격(판매이력)이 없으면 None 으로 두고, 이후 적정가 계산 시 가중치에서 제외한다.
if not selling_price or selling_price == 0:
selling_price = None
self.logger.log("팔린 가격 정보가 없어 sold_price 를 None 으로 처리합니다.", level=logging.DEBUG)
else:
self.logger.log(f"title_infos에서 팔린 가격 {selling_price}을 가져옴", level=logging.DEBUG)
# 5. 해외 배송비 재계산
shipping_base_price = optimal_price - upper_avg_cost - (upper_avg_cost*0.04) - (upper_avg_cost*0.24) - additional_margin
shipping_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, shipping_base_price)
shipping_cost = self.round_to_UP(shipping_cost)
self.logger.log(f"적정판매가 기준으로 재계산된 해외배송비: {shipping_cost}", level=logging.DEBUG)
self.logger.log("옵션 가격 정보를 수집하기 위해 옵션가격정렬을 클릭 합니다.", level=logging.DEBUG)
# 5. 카테고리별 추가 해외배송비 계산
extra_shipping = self.calculate_category_extra_shipping(category, optimal_price)
shipping_cost += extra_shipping
self.logger.log(f"카테고리별 추가배송비 기준으로 재계산된 해외배송비: {shipping_cost}", level=logging.DEBUG)
is_single = self.option_info['is_single_option']
self.logger.log(f"is_single : {is_single}", level=logging.DEBUG)
# 5. 계산된 값 입력
self.logger.log("계산된 값을 페이지에 입력합니다.", level=logging.DEBUG)
await self.input_calculated_values(additional_margin, shipping_cost)
if not is_single:
await self.ordering_by_option_button_click()
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices(is_single, is_group) # 수집된 옵션정보를 반환
if option_data is None:
self.logger.log("상품 옵션 정보를 수집하지 못했습니다.", level=logging.ERROR, exc_info=True)
return
# 2. 원가 기반가격 계산
calculated_price = self.calc_initial_price(upper_avg_cost, 0.04, 0.24)
# 6. 반품비, 초도배송비, 교환비 계산 및 입력
return_fee, first_delv_fee, exchange_fee = self.calculate_claim_costs(max_cost)
self.logger.log(f"반품비: {return_fee}, 초도배송비: {first_delv_fee}, 교환비: {exchange_fee}", level=logging.DEBUG)
await self.input_claim_costs(return_fee, first_delv_fee, exchange_fee)
# 3. 적정 판매가 산출
self.logger.log("적정 판매가를 계산합니다.", level=logging.DEBUG)
# 팔린가격이 None 인 경우 비율 계산에서 제외된다.
self.optimal_price_config['sold_price'] = selling_price
self.optimal_price_config['cost_price2X'] = upper_avg_cost # 원가2배 가격 : upper_avg_cost를 기준으로 계산.
self.optimal_price_config['calculated_price'] = calculated_price # 기준판매가를 기준으로 계산된 값
optimal_price = self.calculate_optimal_price()
self.logger.log(f"계산된 적정 판매가: {optimal_price}", level=logging.DEBUG)
# 7. 가격범위 초과 제외 - remove_overprice 토글 상태에 따라 실행
if self.toggle_states.get('remove_overprice', False):
await self.click_remove_overprice_btn()
self.logger.log("가격범위 초과 옵션 제외 처리 완료", level=logging.INFO)
# 4. 더하기 마진을 적정 판매가 기준으로 재설정
self.logger.log("더하기 마진을 적정 판매가에 맞게 조정합니다.", level=logging.DEBUG)
additional_margin = self.calculate_adjusted_margin(optimal_price, option_data)
additional_margin = self.round_to_UP(additional_margin)
self.logger.log(f"조정된 더하기 마진: {additional_margin}", level=logging.DEBUG)
# 8. 저장
# save_xpath = "//button[contains(.,'저장하기')]"
# click_element(driver, 'XPATH', save_xpath, 10, 'js')
# self.logger.log("가격 정리 후 저장버튼 클릭 완료", level=logging.DEBUG)
# 5. 해외 배송비 재계산
shipping_base_price = optimal_price - upper_avg_cost - (upper_avg_cost*0.04) - (upper_avg_cost*0.24) - additional_margin
shipping_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, shipping_base_price)
shipping_cost = self.round_to_UP(shipping_cost)
self.logger.log(f"적정판매가 기준으로 재계산된 해외배송비: {shipping_cost}", level=logging.DEBUG)
# 5. 카테고리별 추가 해외배송비 계산
extra_shipping = self.calculate_category_extra_shipping(category, optimal_price)
shipping_cost += extra_shipping
self.logger.log(f"카테고리별 추가배송비 기준으로 재계산된 해외배송비: {shipping_cost}", level=logging.DEBUG)
# 5. 계산된 값 입력
self.logger.log("계산된 값을 페이지에 입력합니다.", level=logging.DEBUG)
await self.input_calculated_values(additional_margin, shipping_cost)
# 6. 반품비, 초도배송비, 교환비 계산 및 입력
return_fee, first_delv_fee, exchange_fee = self.calculate_claim_costs(max_cost)
self.logger.log(f"반품비: {return_fee}, 초도배송비: {first_delv_fee}, 교환비: {exchange_fee}", level=logging.DEBUG)
await self.input_claim_costs(return_fee, first_delv_fee, exchange_fee)
# 7. 가격범위 초과 제외 - remove_overprice 토글 상태에 따라 실행
if self.toggle_states.get('remove_overprice', False):
await self.click_remove_overprice_btn()
self.logger.log("가격범위 초과 옵션 제외 처리 완료", level=logging.INFO)
# 8. 저장
# save_xpath = "//button[contains(.,'저장하기')]"
# click_element(driver, 'XPATH', save_xpath, 10, 'js')
# self.logger.log("가격 정리 후 저장버튼 클릭 완료", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"가격 수정 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)

View File

@ -64,6 +64,8 @@ class ThumbnailHandler:
self.imageProcessor = imageProcessor
self.logger.log(f"page객체 업데이트 : {page1}", level=logging.DEBUG)
# def update_whale(self):
# self.whale_translator = self.browser_controller.get_whale()
# self.logger.log(f"whale_translator 업데이트 : {self.whale_translator}", level=logging.DEBUG)
@ -125,6 +127,8 @@ class ThumbnailHandler:
self.logger.log(f"수집된 썸네일 이미지 URL 리스트: {image_urls}", level=logging.DEBUG)
# 3. 반복적으로 1번 카드의 삭제/업로드 버튼 사용
processed_files = [] # 임시 파일 정리를 위한 리스트
for idx, image_url in enumerate(image_urls):
self.logger.log(f"{idx+1}번째 썸네일 작업 시작", level=logging.DEBUG)
@ -216,11 +220,18 @@ class ThumbnailHandler:
confirm_button = await self.page.query_selector(self.confirm_upload_button_selector)
await confirm_button.click()
self.logger.log("이미지 삽입 버튼 클릭 완료", level=logging.DEBUG)
# 처리된 파일을 정리 리스트에 추가
if output_path:
processed_files.append(output_path)
else:
self.logger.log("업로드 버튼을 찾을 수 없습니다.", level=logging.ERROR)
continue
except Exception as e:
self.logger.log(f"이미지 업로드 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# 오류 발생 시에도 정리 리스트에 추가
if 'output_path' in locals() and output_path:
processed_files.append(output_path)
self.update_detail_progress_signal.emit(idx+1, len(image_urls))
await asyncio.sleep(1)
@ -319,6 +330,22 @@ class ThumbnailHandler:
else:
self.logger.log("번역 모드가 비활성화 되어 있습니다.", level=logging.INFO)
# 모든 썸네일 처리 완료 후 임시 파일 배치 정리
if processed_files:
self.logger.log(f"썸네일 임시 파일 배치 정리 시작: {len(processed_files)}개 파일", level=logging.INFO)
deleted_count = 0
for file_path in processed_files:
try:
if file_path and os.path.exists(file_path):
os.remove(file_path)
deleted_count += 1
self.logger.log(f"임시 파일 삭제: {file_path}", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"파일 삭제 실패 (무시): {file_path} - {e}", level=logging.DEBUG)
self.logger.log(f"썸네일 임시 파일 정리 완료: {deleted_count}/{len(processed_files)}개 삭제", level=logging.INFO)
except Exception as e:
self.logger.log(f"썸네일 작업 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)

View File

@ -2,55 +2,58 @@ import os
import cv2
from PIL import Image
import logging
import onnxruntime # [추가] ONNX 런타임 직접 사용을 위해 임포트
from rembg.sessions import sessions # [수정] rembg의 모델별 세션 클래스 딕셔너리 import
class BackgroundRemovalModule:
"""
rembg 기반 배경제거 모듈 (안전한 의존성 처리)
주요 지원 모델 설명:
- 'u2net': 범용성, 속도/품질 밸런스, 사람/사물 모두 OK (기본값)
- 'u2netp': u2net 경량버전, 속도 빠름(저사양PC, 실시간)
- 'u2net_human_seg': 인물(사람) 세그멘테이션 특화
- 'u2net_cloth_seg': (패션) 세그멘테이션 특화
- 'isnet-general-use': 범용, 디테일 강조, 크기 (최신 고성능)
- 'sam': Segment Anything, 사물/사람 모두, 고품질(고성능PC 권장)
- 'sam-mobile': SAM 경량화(속도, 성능), 모바일·저사양 PC도 사용 가능
- 'birefnet-general-lite': BiRefNet 경량 모델, 고품질 저용량
"""
# [수정] 사용하시려는 birefnet 모델을 지원 목록에 추가합니다.
SUPPORTED_MODELS = {
"u2net": "범용 배경제거 | 빠름 | 사람/사물 모두 양호",
"u2net": "범용 배경제거 | 빠름 | 사람/사물 모두 양호 (기본값)",
"u2netp": "u2net 경량화 | 매우 빠름 | 실시간, 저사양PC",
"u2net_human_seg": "인물 전용 | 빠름 | 사람 경계 정밀",
"u2net_cloth_seg": "옷 전용 | 빠름 | 패션/의류 특화",
"isnet-general-use": "범용 고품질 | 느림 | 디테일 중시, 대용량",
"sam": "SAM 최고 품질 | 매우 느림 | 고성능PC 권장",
"sam-mobile": "SAM 경량화 | 보통 | 모바일, 중간성능",
"birefnet-general": "BiRefNet 일반형 | 보통 | 고품질 다목적",
"birefnet-general-lite": "BiRefNet 경량 | 빠름 | 고품질 저용량 (추천)"
"birefnet-general-lite": "BiRefNet 경량 모델 | 고품질 저용량 (로컬)"
}
def __init__(self, logger=None, default_model="birefnet-general-lite", gpu_manager=None, local_rembg_model_path: str | None = None):
# [추가] SUPPORTED_MODELS 키와 실제 rembg sessions 키 간의 매핑
MODEL_NAME_MAPPING = {
"u2net": "u2net",
"u2netp": "u2netp",
"u2net_human_seg": "u2net-human-seg",
"u2net_cloth_seg": "u2net-cloth-seg",
"isnet-general-use": "dis-general-use",
"sam": "sam",
"sam-mobile": "sam", # sam-mobile은 sam과 동일하게 처리
"birefnet-general-lite": "birefnet-general-lite"
}
def __init__(self, logger=None, default_model="u2net", gpu_manager=None, local_rembg_model_path: str | None = None):
self.logger = logger
self.default_model = default_model
self.gpu_manager = gpu_manager
self.local_rembg_model_path = local_rembg_model_path
self.sessions = {} # 모델별 세션 캐시
self._rembg_available = None # rembg 사용 가능 여부 캐시
self._init_error = None # 초기화 오류 메시지
self._cuda_providers_tested = False # CUDA provider 테스트 완료 여부
self.sessions = {}
self._rembg_available = None
self._init_error = None
self._cuda_providers_tested = False
# _check_rembg_availability 메서드는 변경할 필요 없습니다. (기존 코드 유지)
def _check_rembg_availability(self):
"""rembg 모듈 사용 가능 여부를 확인하고 캐시"""
if self._rembg_available is not None:
return self._rembg_available
try:
# 동적 임포트로 rembg 사용 가능성 테스트
import rembg
# GPU 사용 가능 시 CUDA provider 우선 테스트
providers_to_test = []
if self.gpu_manager and self.gpu_manager.can_use_cuda:
providers_to_test = ['CUDAExecutionProvider', 'CPUExecutionProvider']
@ -61,14 +64,11 @@ class BackgroundRemovalModule:
if self.logger:
self.logger.log("rembg CPU-only 모드로 테스트", level=logging.INFO)
# rembg는 직접 ONNX 경로를 받지 못하므로 내장 모델명 사용
model_arg = 'birefnet-general-lite'
model_arg = 'u2net'
# 기본 세션 생성 테스트 (실제 ONNX 런타임 등 동작 확인)
test_session = rembg.new_session(model_name=model_arg, providers=providers_to_test)
self._rembg_available = True
# 실제 사용된 provider 확인 및 로깅
if hasattr(test_session, 'inner_session') and hasattr(test_session.inner_session, 'get_providers'):
actual_providers = test_session.inner_session.get_providers()
if self.logger:
@ -87,103 +87,102 @@ class BackgroundRemovalModule:
self._init_error = f"rembg 모듈이 설치되지 않음: {e}"
self._rembg_available = False
except Exception as e:
# ONNX runtime 오류, CUDA 관련 오류, 메모리 부족 등
self._init_error = f"rembg 모듈 초기화 실패 (의존성/하드웨어 문제): {e}"
self._rembg_available = False
if self.logger:
self.logger.log(self._init_error, level=logging.ERROR)
return False
# ===================================================================================
# [핵심 수정] get_session 메서드를 아래 코드로 완전히 교체하세요.
# ===================================================================================
def get_session(self, model_name):
"""
모델별 세션을 캐싱하여 반환 (CUDA 지원 포함)
모델별 세션을 캐싱하여 반환 (로컬 모델 경로 CUDA 지원 포함)
"""
if not self._check_rembg_availability():
if self.logger:
self.logger.log(f"rembg 사용 불가로 세션 생성 실패: {self._init_error}", level=logging.ERROR)
return None
# 세션 키에 CUDA 사용 여부 포함
cuda_enabled = self.gpu_manager and self.gpu_manager.can_use_cuda
session_key = f"{model_name}_cuda_{cuda_enabled}"
# 세션 키에 로컬 모델 사용 여부도 반영
is_local = bool(self.local_rembg_model_path and os.path.exists(self.local_rembg_model_path))
# 실제 모델명을 세션 키에 사용
actual_model_name = self.MODEL_NAME_MAPPING.get(model_name, model_name)
session_key = f"{actual_model_name}_cuda_{cuda_enabled}_local_{is_local}"
if session_key not in self.sessions:
if self.logger:
self.logger.log(f"🔧 rembg 새 세션 생성 필요: {session_key}", level=logging.INFO)
try:
import rembg
# Provider 설정 (GPU 관리자 우선 사용)
if self.gpu_manager and cuda_enabled:
# GPU/CPU provider 설정 (기존 코드와 동일)
providers = []
if cuda_enabled:
providers_raw = self.gpu_manager.get_optimal_onnx_providers()
# rembg는 단순 문자열 리스트를 기대하므로 tuple에서 provider 이름만 추출
providers = []
for p in providers_raw:
if isinstance(p, tuple):
providers.append(p[0]) # (provider_name, options) -> provider_name
else:
providers.append(p) # 문자열인 경우 그대로
if self.logger:
self.logger.log(f"rembg 세션 생성 (GPU 관리자 최적화): {model_name}", level=logging.INFO)
self.logger.log(f" - 원본 providers: {providers_raw}", level=logging.DEBUG)
self.logger.log(f" - rembg용 providers: {providers}", level=logging.INFO)
elif cuda_enabled:
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
if self.logger:
self.logger.log(f"rembg 세션 생성 (CUDA 우선): {model_name}", level=logging.INFO)
providers.append(p[0] if isinstance(p, tuple) else p)
if self.logger: self.logger.log(f"rembg 세션 생성 (GPU): {model_name} with {providers}", level=logging.INFO)
else:
providers = ['CPUExecutionProvider']
if self.logger:
self.logger.log(f"rembg 세션 생성 (CPU 전용): {model_name}", level=logging.INFO)
if self.logger: self.logger.log(f"rembg 세션 생성 (CPU): {model_name}", level=logging.INFO)
# 세션 생성
# rembg는 직접 ONNX 경로를 받지 못하므로 내장 모델명 사용
model_arg = model_name
session = None
session = rembg.new_session(model_name=model_arg, providers=providers)
# *** 로컬 모델 경로가 있으면 여기서 처리 ***
if is_local:
if self.logger:
self.logger.log(f"로컬 모델로 세션 생성: {self.local_rembg_model_path}", level=logging.INFO)
# 1. model_name에 맞는 rembg 세션 *클래스*를 가져옴 (e.g., BiRefNetSessionGeneral)
# 모델명 매핑을 통해 실제 sessions 키로 변환
actual_model_name = self.MODEL_NAME_MAPPING.get(model_name, model_name)
if actual_model_name not in sessions:
raise ValueError(f"지원하지 않는 모델명: {model_name} (매핑된 이름: {actual_model_name}). 사용 가능한 모델: {list(sessions.keys())}")
session_class = sessions[actual_model_name]
# 2. 클래스의 인스턴스를 생성하되, __init__을 호출하지 않아 모델 다운로드를 방지
session = session_class.__new__(session_class)
# 3. 로컬 ONNX 파일로 직접 onnxruntime 세션을 생성
session.inner_session = onnxruntime.InferenceSession(self.local_rembg_model_path, providers=providers)
# 4. 세션에 필요한 다른 속성들을 수동으로 설정
session.model_name = model_name
session.providers = providers
# 로컬 모델 경로가 없으면 기존 방식으로 처리
else:
if self.logger: self.logger.log(f"내장 모델로 세션 생성: {actual_model_name}", level=logging.INFO)
session = rembg.new_session(model_name=actual_model_name, providers=providers)
self.sessions[session_key] = session
# 실제 사용된 provider 확인
try:
# rembg 세션에서 실제 사용 중인 provider 확인 시도
actual_providers = []
if hasattr(session, 'providers'):
actual_providers = session.providers
elif hasattr(session, 'get_providers'):
actual_providers = session.get_providers()
elif hasattr(session, '_inference_session'):
# rembg 내부 ONNXRuntime 세션에 접근
if hasattr(session._inference_session, 'get_providers'):
actual_providers = session._inference_session.get_providers()
if actual_providers and self.logger:
is_gpu_accelerated = any(p in actual_providers for p in ['CUDAExecutionProvider', 'TensorrtExecutionProvider'])
gpu_status = "GPU 가속 활성화" if is_gpu_accelerated else "CPU 모드로 동작"
self.logger.log(f"✅ rembg {model_name} {gpu_status} (실제 providers: {actual_providers})", level=logging.INFO)
else:
if self.logger:
self.logger.log(f"⚠️ rembg {model_name} provider 확인 불가 (요청 providers: {providers})", level=logging.WARNING)
except Exception as e:
if self.logger:
self.logger.log(f"rembg provider 확인 중 오류: {e}", level=logging.DEBUG)
# 실제 사용된 provider 확인 및 로깅 (기존 코드와 동일)
actual_providers = session.inner_session.get_providers()
if self.logger:
is_gpu = any('CUDA' in p or 'Tensorrt' in p for p in actual_providers)
status = "GPU 가속 활성화" if is_gpu else "CPU 모드로 동작"
self.logger.log(f"✅ rembg '{actual_model_name}' {status} (실제 providers: {actual_providers})", level=logging.INFO)
except Exception as e:
if self.logger:
self.logger.log(f"rembg 세션 생성 실패 ({model_name}): {e}", level=logging.ERROR)
self.logger.log(f"rembg 세션 생성 실패 ('{actual_model_name}'): {e}", level=logging.ERROR, exc_info=True)
return None
else:
if self.logger:
self.logger.log(f"♻️ rembg 기존 세션 재사용: {session_key}", level=logging.DEBUG)
return self.sessions[session_key]
return self.sessions.get(session_key)
# ===================================================================================
# 이하 다른 메서드들은 수정할 필요 없습니다. (기존 코드 유지)
# ===================================================================================
def set_default_model(self, model_name):
"""
배경제거 기본 모델을 변경
"""
if model_name not in self.SUPPORTED_MODELS:
raise ValueError(f"지원하지 않는 모델명: {model_name}")
self.default_model = model_name
@ -191,57 +190,29 @@ class BackgroundRemovalModule:
self.logger.log(f"rembg 기본 모델이 '{model_name}'(으)로 변경됨", level=logging.INFO)
def get_default_model(self):
"""
현재 사용 중인 기본 모델 반환
"""
return self.default_model
def get_supported_models(self):
"""
지원 모델/설명 dict 반환 (UI, 도움말 활용)
"""
return self.SUPPORTED_MODELS.copy()
def get_model_description(self, model_name):
"""
모델명에 대한 설명 반환
"""
return self.SUPPORTED_MODELS.get(model_name, "모델 설명 없음")
def is_available(self):
"""rembg 모듈 사용 가능 여부 반환"""
return self._check_rembg_availability()
def get_init_error(self):
"""초기화 오류 메시지 반환 (디버깅용)"""
return self._init_error
def to_white_background(self, img: Image.Image) -> Image.Image:
"""
알파(투명) 부분을 흰색으로 합성해서 RGB 이미지로 반환
Args:
img: PIL.Image (RGBA or BGRA)
Returns:
PIL.Image (RGB, 배경이 흰색)
"""
if img.mode in ("RGBA", "BGRA"):
bg = Image.new("RGB", img.size, (255, 255, 255))
bg.paste(img, mask=img.split()[-1]) # 알파채널로 합성
bg.paste(img, mask=img.split()[-1])
return bg
else:
return img.convert("RGB")
def remove_background(self, image_path, model_name=None, **kwargs):
"""
이미지에서 배경을 제거한 결과(PIL.Image) 반환 (CUDA 가속 지원)
Args:
image_path (str): 입력 이미지 경로
model_name (str|None): None이면 기본 모델 사용
kwargs: rembg 옵션(alpha_matting )
Returns:
PIL.Image | None
"""
# rembg 사용 가능성 먼저 확인
if not self._check_rembg_availability():
if self.logger:
self.logger.log(f"rembg 사용 불가로 배경 제거 실패: {self._init_error}", level=logging.ERROR)
@ -260,22 +231,23 @@ class BackgroundRemovalModule:
return None
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
model_name = model_name or self.default_model
if model_name not in self.SUPPORTED_MODELS:
# [수정] 로컬 모델을 사용하려면, model_name을 파일과 맞는 이름으로 지정해야 합니다.
# 예를 들어, birefnet-general-lite.onnx 파일을 사용하려면 model_name='birefnet-general-lite'로 호출해야 합니다.
effective_model_name = model_name or self.default_model
if effective_model_name not in self.SUPPORTED_MODELS:
if self.logger:
self.logger.log(f"지원하지 않는 모델명: {model_name}", level=logging.ERROR)
return None
self.logger.log(f"지원하지 않는 모델명: {effective_model_name}. u2net으로 대체 사용", level=logging.WARNING)
effective_model_name = "u2net"
session = self.get_session(model_name)
session = self.get_session(effective_model_name)
if session is None:
return None
# rembg 실행 (여러 오류 가능성 대비)
import rembg
import time
# 처리 시간 측정
start_time = time.time()
result = rembg.remove(img_rgb, session=session, alpha_matting=kwargs.get("alpha_matting", False))
end_time = time.time()
@ -283,13 +255,11 @@ class BackgroundRemovalModule:
if not isinstance(result, Image.Image):
result = Image.fromarray(result)
# 성능 정보 로깅
processing_time = end_time - start_time
if self.logger:
cuda_status = "CUDA" if (self.gpu_manager and self.gpu_manager.can_use_cuda) else "CPU"
self.logger.log(f"✅ 배경 제거 성공: {model_name} ({cuda_status}, {processing_time:.2f}초)", level=logging.INFO)
self.logger.log(f"✅ 배경 제거 성공: {effective_model_name} ({cuda_status}, {processing_time:.2f}초)", level=logging.INFO)
# GPU 메모리 사용량 로깅
if self.gpu_manager and self.gpu_manager.can_use_cuda:
self.gpu_manager.log_gpu_memory_usage()
@ -299,3 +269,25 @@ class BackgroundRemovalModule:
if self.logger:
self.logger.log(f"배경 제거 처리 중 오류 ({model_name}): {e}", level=logging.ERROR, exc_info=True)
return None
def _preload_sessions(self):
"""자주 사용되는 rembg 세션들을 미리 로딩하여 첫 번째 요청 시간을 단축"""
preload_models = ["u2net", "birefnet-general-lite"]
for model_name in preload_models:
try:
if self.logger:
self.logger.log(f"🔄 {model_name} 세션 미리 로딩 중...", level=logging.INFO)
# 세션 생성하여 캐시에 저장
session = self.get_session(model_name)
if session:
if self.logger:
self.logger.log(f"{model_name} 세션 미리 로딩 완료", level=logging.INFO)
else:
if self.logger:
self.logger.log(f"⚠️ {model_name} 세션 로딩 실패", level=logging.WARNING)
except Exception as e:
if self.logger:
self.logger.log(f"{model_name} 세션 미리 로딩 중 오류: {e}", level=logging.WARNING)

View File

@ -143,13 +143,37 @@ def worker_main(
except Exception:
logger.error("READY 신호 전송 실패", exc_info=True)
# ── rembg 세션 비동기 로딩 ──────────────────────────────
def preload_rembg_sessions():
"""백그라운드에서 rembg 세션을 미리 로딩"""
try:
if processor and hasattr(processor, 'background_removal_module'):
logger.info("🔄 rembg 세션 백그라운드 로딩 시작...")
# 기본 세션들을 미리 로딩
processor.background_removal_module._preload_sessions()
logger.info("✅ rembg 세션 백그라운드 로딩 완료")
except Exception as e:
logger.warning(f"rembg 세션 백그라운드 로딩 실패: {e}")
# rembg 세션 로딩을 별도 Thread에서 실행
import threading
preload_thread = threading.Thread(target=preload_rembg_sessions, daemon=True)
preload_thread.start()
# ── 작업 루프 ─────────────────────────────────────────────
# 컨트롤러가 즉시 pick-up 할 수 있도록 READY 신호를 추가 전송
try:
result_q.put({"id": "__READY__", "cmd": "__READY__", "kwargs": {}})
logger.info("📡 추가 READY 신호 전송 완료")
except Exception as e:
logger.error(f"추가 READY 신호 전송 실패: {e}")
while True:
logger.debug("작업 대기 중...")
try:
# 주기적 상태 출력을 위해 타임아웃 설정
logger.debug(f"큐에서 작업 대기 중... (PID: {os.getpid()})")
task = task_q.get(timeout=30) # 30초 타임아웃
task = task_q.get(timeout=60) # 60초 타임아웃
logger.info(f"🔥 작업 수신 성공: {task}")
except queue.Empty:
logger.debug("30초간 작업 없음 - 계속 대기...")

View File

@ -199,7 +199,7 @@ class Request_AI_Server:
self.logger.log(f"백업 rembg 모듈 사용 불가: {error_msg}", level=logging.ERROR)
return None
# 모델명 유효성 확인
# 모델명 유효성 확인 및 대체
supported_models = backup_rembg.get_supported_models()
if model_name not in supported_models:
self.logger.log(f"지원하지 않는 모델명 ({model_name}). birefnet-general-lite으로 대체 사용", level=logging.WARNING)
@ -217,6 +217,7 @@ class Request_AI_Server:
return None
# 내장 rembg로 배경 제거 (지정된 모델 사용)
# 이제 BackgroundRemovalModule이 자동으로 로컬 모델을 우선 사용합니다
result_pil = backup_rembg.remove_background(temp_path, model_name=model_name)
if result_pil is None:
self.logger.log(f"내장 rembg 모듈({model_name}) 처리 실패", level=logging.ERROR)

View File

@ -136,7 +136,8 @@ class PapagoTranslator:
if engine == "google":
self.logger.log("구글 번역 시작", level=logging.INFO)
result = self.google_translate_lines(lines, source_lang, target_lang)
self.logger.log(f"구글 번역 완료: {result}", level=logging.INFO)
self.logger.log(f"구글 번역 완료: {result}", level=logging.DEBUG)
self.logger.log(f"구글 번역 완료", level=logging.INFO)
return result
# engine == "papago" (기본)
@ -145,7 +146,8 @@ class PapagoTranslator:
# 1차 결과가 정상(라인 수 일치 + 한글 포함)일 경우 바로 반환
if papago_result and len(papago_result) == len(lines) and self._is_korean(papago_result):
self.logger.log(f"파파고 번역 완료: {papago_result}", level=logging.INFO)
self.logger.log(f"파파고 번역 완료: {papago_result}", level=logging.DEBUG)
self.logger.log(f"파파고 번역 완료", level=logging.INFO)
return papago_result
# 한글이 포함되지 않은 결과일 경우, 지정된 횟수만큼 Papago 재시도
@ -155,13 +157,15 @@ class PapagoTranslator:
self.logger.log(f"Papago 재시도 {retry}/{self.retry_wrong_lang}", level=logging.INFO)
papago_result = await self.papago_translate(text, source_lang, target_lang)
if papago_result and len(papago_result) == len(lines) and self._is_korean(papago_result):
self.logger.log(f"Papago 재시도 번역 성공: {papago_result}", level=logging.INFO)
self.logger.log(f"Papago 재시도 번역 성공", level=logging.INFO)
self.logger.log(f"파파고 번역 완료: {papago_result}", level=logging.DEBUG)
return papago_result
if fallback:
self.logger.log("Papago 번역 실패 → Google 번역으로 폴백", level=logging.WARNING)
google_result = self.google_translate_lines(lines, source_lang, target_lang)
self.logger.log(f"구글 번역 완료: {google_result}", level=logging.INFO)
self.logger.log(f"구글 번역 완료: {google_result}", level=logging.DEBUG)
self.logger.log(f"구글 번역 완료", level=logging.INFO)
return google_result
else:
self.logger.log(f"번역 실패 → 원본 그대로 반환 : {lines}", level=logging.WARNING)

View File

@ -10,6 +10,8 @@
- 그룹선택시 상품갯수가 제대로 처리되지 않는 문제 개선
### 기능추가
- 가격범위 초과 버튼 별도 동작 분리
- 이미지번역 처리 후 즉시 파일제거
- 프리미엄 이상 사용자가 '자체서버'를 이용할수 있었던 프로모션 종료
- CPU모드 외 GPU 모드 추가
- 베이직 : CPU