가격 수정 로직 개선: 가격범위 초과 버튼의 동작을 별도로 분리하고, 이미지 번역 처리 후 즉시 파일을 제거하도록 수정하였습니다. 메모리 모니터링 기능을 추가하여 시스템 메모리 사용량을 실시간으로 확인할 수 있도록 하였으며, 관련 UI 요소를 개선하였습니다. 또한, 이미지 처리 요청 시 임시 파일 정리 로직을 추가하여 안정성을 높였습니다.
This commit is contained in:
parent
e26a7cae64
commit
e34ec7acab
|
|
@ -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()
|
||||
|
|
|
|||
291
mainUI_SP.py
291
mainUI_SP.py
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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초간 작업 없음 - 계속 대기...")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -10,6 +10,8 @@
|
|||
- 그룹선택시 상품갯수가 제대로 처리되지 않는 문제 개선
|
||||
|
||||
### 기능추가
|
||||
- 가격범위 초과 버튼 별도 동작 분리
|
||||
- 이미지번역 처리 후 즉시 파일제거
|
||||
- 프리미엄 이상 사용자가 '자체서버'를 이용할수 있었던 프로모션 종료
|
||||
- CPU모드 외 GPU 모드 추가
|
||||
- 베이직 : CPU
|
||||
|
|
|
|||
Loading…
Reference in New Issue