import time import os, sys import pyperclip from pywinauto import Application, findwindows, clipboard, timings from pywinauto.controls.hwndwrapper import HwndWrapper from PIL import ImageGrab from pywinauto.findwindows import ElementNotFoundError from pywinauto.timings import wait_until import re class WhaleTranslator: def __init__(self, logger): self.logger = logger self.whale_app = None self.whale_window = None self.translation_success_flag = False # 번역 성공 플래그 self.failure_count = 0 # 실패 횟수 self.min_image_width = 200 self.min_image_height = 150 def get_base_dir(self): """ 실행 환경에 따라 base_dir을 설정하는 메서드. cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정. """ if getattr(sys, 'frozen', False): # 패키징된 경우 base_dir = os.path.dirname(sys.executable) else: # 일반 Python 실행 환경 base_dir = os.path.dirname(os.path.abspath(__file__)) return base_dir def start_whale_browser(self): base_path = self.get_base_dir() whale_exe_path = os.path.join(base_path, "browsers", "whale", "whale.exe") user_data_dir = os.path.join(base_path, "browsers", "whale", "user_data") cache_dir = os.path.join(base_path, "browsers", "whale", "cache") self.whale_app = Application(backend="uia").start( f'"{whale_exe_path}" --incognito --user-data-dir="{user_data_dir}" --disk-cache-dir="{cache_dir}"' ) # 창이 완전히 생성될 때까지 대기 self.whale_window = self.find_whale_window() if self.whale_window: self.logger.info("웨일 시크릿 모드로 시작 완료.") else: self.logger.warning("웨일 창을 찾을 수 없습니다.") def find_whale_window(self): try: # 최대 10초 동안 '새 시크릿 탭 - Whale' 창이 나타나기를 기다림 timings.wait_until(10, 0.5, lambda: any(window.name == '새 시크릿 탭 - Whale' for window in findwindows.find_elements())) windows = findwindows.find_elements() for window in windows: if window.name == '새 시크릿 탭 - Whale': whale_pid = window.process_id self.whale_app = Application(backend="uia").connect(process=whale_pid) self.whale_window = self.whale_app.top_window() # 위치 및 크기 조절 self.hwnd_wrapper = HwndWrapper(self.whale_window.handle) self.hwnd_wrapper.move_window(x=1, y=1, width=1280, height=720) self.whale_window.set_focus() self.logger.info("웨일 창을 성공적으로 찾았습니다.") return self.whale_window self.logger.error("'새 시크릿 탭 - Whale' 창을 찾을 수 없습니다.") except Exception as e: self.logger.error(f"웨일 창 탐색 중 오류 발생: {e}", exc_info=True) return None def translate_image(self, url, path=None): if url.startswith("https://assets.alicdn.com"): self.logger.info("assets.alicdn.com 확인 : 번역작업을 패스합니다.") return False if self.whale_app: try: self.navigate_to_url(url) if not self.check_image_size(): self.logger.info("해상도가 기준보다 낮아 작업을 패스합니다.") return False # 해상도 기준 미달로 작업 종료 self.translation_success_flag = self.right_click_on_image_and_inspect() self.click_back_button() # 경로를 인자로 받을경우 해당경로에 파일 저장 if path: try: # 클립보드에서 이미지 가져오기 clipboard_image = ImageGrab.grabclipboard() if clipboard_image: clipboard_image.save(path, format='PNG') self.logger.info(f"번역된 이미지가 {path}에 저장되었습니다.") else: self.logger.error("클립보드에 이미지가 존재하지 않아 파일로 저장할 수 없습니다.") return False except Exception as e: self.logger.error(f"이미지 저장 중 오류 발생: {e}", exc_info=True) return False return True if self.translation_success_flag else False except Exception as e: self.logger.error(f"번역 중 오류 발생: {e}", exc_info=True) return False # self.handle_translation_failure() else: self.logger.error('웨일 창을 찾을 수 없습니다.') return False def navigate_to_url(self, url): """주소창에 URL을 입력하고 페이지 로딩을 대기""" retry_count = 5 # 주소창 찾기 재시도 횟수 for attempt in range(retry_count): try: # URL을 클립보드에 복사 pyperclip.copy(url) # 주소창을 클릭하여 URL 붙여넣기 address_bar = self.whale_window.child_window(title="주소창 및 검색창", control_type="Edit") address_bar.click_input() # Ctrl + V로 URL 붙여넣기 후 Enter 키 입력 address_bar.type_keys("^v{ENTER}", with_spaces=True) self.logger.debug(f"{url}로 이동 중...") # 10초 동안 0.5초 간격으로 이미지 요소가 나타나는지 검사 start_time = time.time() while time.time() - start_time < 10: try: # 이미지 요소를 찾으면 즉시 반환 image_element = self.whale_window.child_window( title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.", control_type="Image" ) if image_element.exists(timeout=1): self.logger.debug("페이지 로딩 완료: 이미지 요소가 나타났습니다.") return except Exception as e: self.logger.debug(f"이미지 요소 확인 중 오류 발생: {e}") # 안전장치 1: 현재 창 제목 확인 window_title = self.whale_window.window_text() if re.match(r".+\.(jpg|png|jpeg) \(\d+x\d+\)", window_title, re.IGNORECASE): self.logger.info(f"창 제목에 이미지 파일과 해상도가 감지됨: {window_title}") return # 안전장치 2: 현재 창의 컨트롤 핸들 요소 출력 self.logger.error("지정된 시간 내에 이미지 요소를 찾지 못했습니다. 현재 창의 컨트롤 요소:") with open("debug_controls.txt", "w", encoding="utf-8") as f: original_stdout = os.sys.stdout os.sys.stdout = f self.whale_window.print_control_identifiers() os.sys.stdout = original_stdout self.logger.info("컨트롤 식별자가 debug_controls.txt에 저장되었습니다.") except ElementNotFoundError as e: self.logger.error("주소창 요소를 찾을 수 없습니다. 창을 다시 검색합니다.", exc_info=True) self.whale_window = self.find_whale_window() # 창을 다시 찾음 if not self.whale_window: self.logger.error("웨일 창을 다시 찾을 수 없습니다. 작업 중단.") return except Exception as e: self.logger.error(f"주소창 접근 중 오류 발생: {e}", exc_info=True) self.logger.error("주소창 찾기 및 URL 이동 시도 횟수를 초과했습니다.") def navigate_to_url_for_typing(self, url): """주소창에 URL을 입력하고 페이지 로딩을 대기""" try: address_bar = self.whale_window.child_window(title="주소창 및 검색창", control_type="Edit") address_bar.click_input() address_bar.type_keys(f"{url}{{ENTER}}", with_spaces=True) self.logger.debug(f"{url}로 이동 중...") # 5초 동안 0.1초 간격으로 이미지 요소가 나타나는지 검사 start_time = time.time() while time.time() - start_time < 5: try: # 특정 이미지 요소를 찾으면 즉시 반환 image_element = self.whale_window.child_window(title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.", control_type="Image") if image_element.exists(timeout=0.5): self.logger.debug("페이지 로딩 완료: 이미지 요소가 나타났습니다.") return except Exception: pass # 요소가 아직 나타나지 않은 경우 대기 self.logger.error("지정된 시간 내에 이미지 요소를 찾지 못했습니다.") except Exception as e: self.logger.error(f"주소창에 접근할 수 없습니다: {e}", exc_info=True) def check_translation_status(self, max_wait_time=10, check_interval=1): start_time = time.time() while time.time() - start_time < max_wait_time: try: fail_indicator = self.whale_window.child_window(title="번역할 영역을 선택하세요.", control_type="Text") if fail_indicator.exists(): self.logger.info("번역할 문구가 없는 이미지 입니다.") return "fail" image_element = self.whale_window.child_window(title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.", control_type="Image") if image_element.exists(): image_element.right_click_input() success_indicator = self.whale_window.child_window(title="이미지 복사(C)", control_type="MenuItem") if success_indicator.wait('visible', timeout=5): success_indicator.click_input() time.sleep(0.5) formats = clipboard.GetClipboardFormats() # logger.debug(f"클립보드에 있는 형식 목록: {formats}") for format_id in formats: format_name = clipboard.GetFormatName(format_id) # logger.debug(f"형식 ID {format_id}: {format_name}") if format_name in ("CF_BITMAP", "CF_DIB"): image_data = clipboard.GetData(format_id=format_id) if isinstance(image_data, bytes): self.logger.info("번역 성공: 이미지 데이터 복사 완료.") return "success" self.logger.info("번역이 아직 완료되지 않았습니다. 다시 시도 중...") except Exception as e: self.logger.error("클립보드 접근 중 오류 발생", exc_info=True) return "error" time.sleep(check_interval) self.logger.warning("번역 확인 시간 초과.") return "timeout" def right_click_on_image_and_inspect(self): try: image = self.whale_window.child_window(title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.", control_type="Image") if image.exists(): image.right_click_input() self.logger.debug("이미지 요소에서 우클릭을 수행했습니다.") else: self.logger.error("이미지 요소를 찾을 수 없습니다.") return translate_menu_item = self.whale_window.child_window(title="이미지 번역 (R)", control_type="MenuItem") translate_menu_item.click_input() self.logger.debug("이미지 번역 명령이 실행되었습니다.") time.sleep(0.5) status = self.check_translation_status() if status == "success": self.logger.info("번역이 성공적으로 완료되었습니다.") return True elif status == "fail": self.logger.warning("번역에 실패했습니다.") return False else: self.logger.error("번역 상태를 확인할 수 없습니다.") return False except Exception as e: self.logger.error(f"이미지 요소에서 우클릭 중 오류 발생: {e}", exc_info=True) return False def check_image_size(self): """창 제목에서 이미지의 너비와 높이를 추출하여 반환합니다.""" try: window_title = self.whale_window.window_text() self.logger.debug(f"Window Title: {window_title}") # 창 제목에서 해상도 추출 시도 match = re.search(r"\((\d+)×(\d+)\)", window_title) if match: # 해상도가 있는 경우, 너비와 높이를 기준으로 작업 처리 width = int(match.group(1)) height = int(match.group(2)) self.logger.info(f"이미지 해상도: {width}×{height}") # 최소 해상도 조건 검사 if width < self.min_image_width or height < self.min_image_height: self.logger.info(f"이미지 해상도가 기준 이하 [{self.min_image_width} x {self.min_image_height}]입니다. 작업을 패스합니다.") return False # 작업을 수행하지 않음 return True # 해상도 조건을 만족하면 작업을 진행 # 해상도가 없다면, 파일 확장자를 확인하여 번역 여부 결정 elif ".jpg" in window_title or ".png" in window_title or ".jpeg" in window_title: self.logger.info("이미지 해상도가 없지만, 파일 확장자가 .jpg 또는 .jpeg 또는 .png입니다. 번역 작업을 진행합니다.") return True # 해상도가 없어도 번역 작업을 수행 # 해상도도 없고, 파일 확장자도 매칭되지 않는 경우 self.logger.warning("이미지 해상도가 존재하지 않고 파일 확장자도 일치하지 않습니다. 작업을 패스합니다.") return False except Exception as e: self.logger.error(f"이미지 사이즈 측정 중 오류 발생: {e}", exc_info=True) return False def click_back_button(self): """'뒤로' 버튼을 클릭합니다.""" try: # '뒤로' 버튼 찾기 back_button = self.whale_window.child_window(title="뒤로", control_type="Button") if back_button.exists(): back_button.click_input() self.logger.debug("'뒤로' 버튼을 클릭했습니다.") else: self.logger.warning("'뒤로' 버튼을 찾을 수 없습니다.") except Exception as e: self.logger.error(f"'뒤로' 버튼 클릭 중 오류 발생: {e}", exc_info=True) def close_whale_window(self): """웨일 창을 종료하는 메서드.""" try: if self.whale_app: self.whale_app.kill() self.logger.info("웨일 창을 성공적으로 종료했습니다.") else: self.logger.warning("웨일 애플리케이션이 시작되지 않았습니다.") except Exception as e: self.logger.error("웨일 창을 종료하는 중 오류 발생", exc_info=True)