This commit is contained in:
R5600U_PC 2024-10-15 13:01:36 +09:00
parent 87fd855ef6
commit a4963acd2c
30 changed files with 13034 additions and 138 deletions

3
.gitignore vendored
View File

@ -1,7 +1,8 @@
Include/
include/
Lib/
Scripts/
__pycache__/
__pycache__/
build/
pyvenv.cfg
*.log

View File

@ -1,6 +1,7 @@
import ctypes
import time
from ctypes import wintypes
wintypes.ULONG_PTR = wintypes.WPARAM
hllDll = ctypes.WinDLL("User32.dll", use_last_error=True)
VK_HANGUEL = 0x15

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,6 @@ class BrowserController:
self.source_button_locator = self.locator_manager.get_locator('BrowserControl', 'source_button_locator')
self.ck_source_editing_area_locator = self.locator_manager.get_locator('BrowserControl', 'ck_source_editing_area_locator')
self.option_input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator')
self.text_templates = self.locator_manager.selectors.get('DetailPageTextTemplates', {})
self.title_tab_locator = self.locator_manager.get_locator('BrowserControl', 'title_tab_locator')
self.option_tab_locator = self.locator_manager.get_locator('BrowserControl', 'option_tab_locator')
self.price_tab_locator = self.locator_manager.get_locator('BrowserControl', 'price_tab_locator')
@ -53,6 +52,8 @@ class BrowserController:
self.upload_tab_locator = self.locator_manager.get_locator('BrowserControl', 'upload_tab_locator')
self.save_button_locator = self.locator_manager.get_locator('BrowserControl', 'save_button_locator')
self.text_templates = self.locator_manager.selectors.get('DetailPageTextTemplates', {})
def get_page(self):
return self.page
@ -431,10 +432,12 @@ class BrowserController:
if is_option_data:
self.logger.debug('옵션 데이터 입력 시작')
option_data = optionHandler.get_selected_translated_options()
self.logger.debug('가져온 옵션 데이터')
self.logger.debug(f'{option_data}')
# 옵션 입력 필드 선택
input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator')
input_field = await self.page.wait_for_selector(input_field_locator)
input_field = await self.page.wait_for_selector(self.option_input_field_locator)
await input_field.press('Enter')
# 선두부 텍스트 입력
for key in sorted(self.text_templates.keys()):
@ -442,31 +445,37 @@ class BrowserController:
if 'leading_text' in key and leading_text: # leading_text 항목만 가져오기
await input_field.type(leading_text)
await input_field.press('Enter')
await input_field.press('Enter')
self.logger.debug(f"{key} 텍스트 입력 완료: {leading_text}")
# 각 옵션을 한 줄씩 입력
for i, option in enumerate(option_data, start=1):
if isinstance(option, tuple):
option_text = option[0] # 튜플의 첫 번째 요소를 사용
else:
option_text = option
await input_field.press('Enter')
await input_field.type("# > 옵션 목록")
await input_field.press('Enter')
await input_field.press('Enter')
# 옵션을 A. B. 등으로 표시하며 입력
# option_prefix = f"{chr(64 + i)}. " # A, B, C...
option_prefix = f"- {chr(64 + i)}. " # 마크다운 목록 A, B, C...
# 첫 번째 옵션에만 - 기호를 붙여 목록 시작
await input_field.type(f"- A. {option_data[0]}")
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
# 나머지 옵션들은 - 없이 입력하여 마크다운 목록으로 표시
for i, option in enumerate(option_data[1:], start=2):
option_text = option[0] if isinstance(option, tuple) else option
option_prefix = f"{chr(64 + i)}. "
await input_field.type(option_prefix + option_text)
await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
# 목록 끝을 알리기 위해 엔터 두 번 입력
await input_field.press('Enter')
await input_field.press('Enter')
# 후두부 텍스트 입력
await input_field.type('---')
await input_field.press('Enter')
await input_field.type('나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
await input_field.press('Enter')
await input_field.press('Enter')
self.logger.debug('옵션 데이터 입력 완료 후 엔터 입력')
@ -477,6 +486,7 @@ class BrowserController:
async def paste_image_in_chrome(self, clipboardImageManager, url):
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
self.logger.debug("크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력")
try:
self.switch_to_chrome() # 크롬으로 포커스 이동
await clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리

View File

@ -2,7 +2,7 @@ import base64
import pyperclip
import win32clipboard
from io import BytesIO
from PIL import Image
from PIL import Image, ImageGrab
import requests
import numpy as np
import cv2
@ -22,12 +22,27 @@ class ClipboardImageManager:
self.debug = True
async def get_clipboard_data(self):
"""클립보드의 텍스트 데이터를 가져옵니다."""
def get_clipboard_data(self):
"""클립보드의 텍스트 또는 이미지 데이터를 가져옵니다."""
self.logger.debug("클립보드의 텍스트 또는 이미지 데이터를 가져옵니다")
try:
return pyperclip.paste() # 클립보드의 텍스트 데이터를 가져옴
# 1. 텍스트 데이터 우선 시도
clipboard_text = pyperclip.paste()
if clipboard_text:
return clipboard_text
# 2. 텍스트가 없으면 이미지 확인
self.logger.debug("텍스트 데이터가 없어 이미지 데이터 확인 시도")
image = ImageGrab.grabclipboard()
if isinstance(image, Image.Image): # 이미지 데이터가 있는 경우
self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
return image # PIL 이미지 객체 반환
else:
self.logger.debug("클립보드에 텍스트 또는 이미지 데이터가 없습니다.")
return None
except Exception as e:
self.logger.debug(f"클립보드 데이터를 가져오는 중 오류 발생: {e}", exc_info=True)
self.logger.error(f"클립보드 데이터를 가져오는 중 오류 발생: {e}", exc_info=True)
return None
def set_image_to_clipboard(self, image):
@ -45,7 +60,15 @@ class ClipboardImageManager:
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
win32clipboard.CloseClipboard()
self.logger.debug(f"클립보드 데이터 저장 성공")
# 클립보드가 제대로 설정되었는지 확인하는 로그
time.sleep(0.1) # 아주 짧은 대기 시간
win32clipboard.OpenClipboard()
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
self.logger.debug("클립보드 데이터 저장 성공")
else:
self.logger.error("클립보드 데이터 저장 실패")
win32clipboard.CloseClipboard()
def save_image_to_path(self, image, path):
try:
@ -110,7 +133,7 @@ class ClipboardImageManager:
# self.logger.debug(f"{crop_percentage*100}% 크롭된 이미지가 클립보드에 저장되었습니다.")
async def base64_to_image(self, base64_data):
def base64_to_image(self, base64_data):
"""Base64 데이터를 이미지로 변환하는 함수"""
if base64_data.startswith('data:image'):
header, encoded = base64_data.split(',', 1)
@ -167,69 +190,79 @@ class ClipboardImageManager:
async def process_clipboard(self, original_url, path=None):
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
clipboard_data = await self.get_clipboard_data()
# 1. 클립보드의 데이터가 Base64 이미지일 경우
if clipboard_data.startswith('data:image'):
self.logger.info("data:image 감지 : 이미지 데이터로 변환")
image = await self.base64_to_image(clipboard_data)
if image:
width, _ = image.size
self.logger.debug(f"Base64 이미지 크기: {width}px")
# 가로 크기가 200픽셀 이상이면 크롭
if width >= 200:
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
cropped_image = self.crop_image(image) # 크롭 메서드 사용
await self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
if path:
self.logger.debug("이미지 저장 시도...")
self.save_image_to_path(path)
try:
clipboard_data = self.get_clipboard_data()
self.logger.debug("clipboard_data")
self.logger.debug(f"{clipboard_data}")
self.logger.debug(f"============================")
# 1. 클립보드의 데이터가 Base64 이미지일 경우
if isinstance(clipboard_data, str) and clipboard_data.startswith('data:image'):
self.logger.info("[process_clipboard] data:image 감지 : 이미지 데이터로 변환")
image = self.base64_to_image(clipboard_data)
if image:
width, _ = image.size
self.logger.debug(f"Base64 이미지 크기: {width}px")
# 가로 크기가 200픽셀 이상이면 크롭
if width >= 200:
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
cropped_image = self.crop_image(image) # 크롭 메서드 사용
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
if path:
self.logger.debug("이미지 저장 시도...")
self.save_image_to_path(path)
else:
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
self.clear_clipboard()
else:
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
await self.clear_clipboard()
else:
self.logger.debug("Base64 이미지 변환 실패.")
self.logger.debug("Base64 이미지 변환 실패.")
# 2. 클립보드에 이미지가 있을 경우
elif self.is_clipboard_image():
self.logger.info("클립보드 이미지 확인")
image = self.get_image_from_clipboard()
if image:
# 2. 클립보드에 이미지가 있을 경우
elif isinstance(clipboard_data, Image.Image):
self.logger.info("[process_clipboard] 클립보드 이미지 확인")
image = clipboard_data
width, _ = image.size
self.logger.debug(f"클립보드에 있는 이미지 크기: {width}px")
if width >= 200:
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
cropped_image = self.crop_image(image) # 크롭 메서드 사용
await self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
if path:
self.logger.debug("이미지 저장 시도...")
self.save_image_to_path(path)
else:
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
await self.clear_clipboard()
self.clear_clipboard()
# 3. html > whale-ocr 처리
elif clipboard_data.strip() == "html > whale-ocr":
self.logger.info("html > whale-ocr 감지 : 이미지 번역 실패 확인")
if original_url:
image = await self.download_image_from_url(original_url)
if image:
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
await self.set_image_to_clipboard(image) # 크롭 없이 저장
if path:
self.logger.debug("이미지 저장 시도...")
self.save_image_to_path(path)
# 3. 클립보드에 데이터가 없거나 html > whale-ocr 처리
elif clipboard_data == "html > whale-ocr" or clipboard_data is None:
if clipboard_data == "html > whale-ocr":
self.logger.info("[process_clipboard] html > whale-ocr 감지 : 이미지 번역 실패 확인")
elif clipboard_data is None:
self.logger.info("[process_clipboard] 클립보드에 이미지 없음")
if original_url:
image = await self.download_image_from_url(original_url)
if image:
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
self.set_image_to_clipboard(image) # 크롭 없이 저장
if path:
self.logger.debug("이미지 저장 시도...")
self.save_image_to_path(path)
else:
self.logger.debug("원본 이미지 다운로드 실패.")
else:
self.logger.debug("원본 이미지 다운로드 실패.")
else:
self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
except Exception as e:
self.logger.error(f"클립보드에서 이미지를 처리하는 중 오류 발생: {e}", exc_info=True)
else:
self.logger.debug("클립보드에 처리할 수 있는 데이터가 없습니다.")
def is_clipboard_image(self):
"""클립보드에 이미지가 있는지 확인하는 함수"""
@ -252,7 +285,7 @@ class ClipboardImageManager:
return None
async def clear_clipboard(self):
def clear_clipboard(self):
"""클립보드를 비우는 함수"""
try:
win32clipboard.OpenClipboard()

View File

@ -8,6 +8,7 @@ option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) >
product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[3]/div/div/div/div[2]/input'
standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[4]/div/div/div[1]/div/div[2]/input'
[OptionLocators]
# 옵션 관련 선택자
option_excluded_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[2]/div/div[3]'
@ -15,7 +16,9 @@ option_input_selector_template = '//*[@id="productMainContentContainerId"]/div[1
single_option_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '단일 상품등록')]'
option_product_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '옵션 상품등록')]'
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate'
; is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate'
is_all_option_checked_selector = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]/input'
ai_option_btn_selector = 'div#productMainContentContainerId div:nth-child(2) > div > div > div.ant-row.ant-row-middle.css-1li46mu > div:nth-child(4) > button[type=\"button\"]'
original_name_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span'
edit_field_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input'
checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
@ -34,12 +37,10 @@ product_image_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/img'
[DetailPageTextTemplates]
leading_text_1 = '---'
leading_text_2 = '# > 안녕하세요 혜리수샵입니다.'
leading_text_3 = ' '
leading_text_4 = ' '
leading_text_5 = '### 마켓정책으로 인해 모든 옵션이 노출되지 않을수도 있습니다.'
leading_text_6 = '**반드시 옵션사진과 옵션이름을 확인하시고 구매하시기 바랍니다.**'
leading_text_7 = '---'
leading_text_2 = '# 안녕하세요 혜리수샵입니다.'
leading_text_3 = '### 마켓정책으로 인해 모든 옵션이 노출되지 않을수도 있습니다.'
leading_text_4 = '### 반드시 옵션사진과 옵션이름을 확인하시고 구매하시기 바랍니다.'
leading_text_5 = '---'
# 필요한 만큼 추가 가능
[TitleLocators]

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

15
gui.py
View File

@ -22,7 +22,7 @@ class TranslationApp(QWidget):
self.initUI()
self.logger = logger
self.debug = False
key_path = 'leensoo1nt.json'
# key_path = 'leensoo1nt.json'
self.settings = QSettings("WhenRideMycar", "TranslationApp") # QSettings 초기화
self.locator_manager = LocatorManager()
self.browser_controller = BrowserController(self, self.logger, self.locator_manager)
@ -31,7 +31,7 @@ class TranslationApp(QWidget):
self.whale_translator = None
self.app = app
self.vertexAI = VertexAITranslator(self.logger, key_path)
self.vertexAI = VertexAITranslator(self.logger)
self.optionHandler = None
# DB 파일 경로 설정
@ -542,11 +542,12 @@ class TranslationApp(QWidget):
self.logger.debug('크롬 실행 버튼 클릭됨')
self.logger.debug(f'self.browser_controller.page : {self.browser_controller.page}')
# 비동기 함수 실행을 위해 asyncio.create_task 사용
optionIMGTrans_status = self.toggle_states['vd_mode']
detail_IMGTrans_status = self.toggle_states['vd_mode']
optionIMGTrans_status = self.toggle_states['optionIMGTrans']
detail_IMGTrans_status = self.toggle_states['detail_IMGTrans']
vd_mode_status = self.toggle_states['vd_mode']
if optionIMGTrans_status or detail_IMGTrans_status:
self.logger.debug(f"optionIMGTrans_status : {optionIMGTrans_status}, detail_IMGTrans_status : {detail_IMGTrans_status}")
self.whale_translator = WhaleTranslator(self.app, self.logger, secret_mode=True, vd_mode=vd_mode_status) # 모드 켜기
self.whale_translator.start_whale_browser()
@ -779,7 +780,8 @@ class TranslationApp(QWidget):
self.logger.debug('프로그램을 종료합니다...')
self.save_settings()
await self.browser_controller.close_browser() # 브라우저 종료
self.whale_translator.close_all_virtual_desktops()
if self.toggle_states['vd_mode']:
self.whale_translator.close_all_virtual_desktops()
super().close()
async def detail_trans(self):
@ -807,8 +809,11 @@ class TranslationApp(QWidget):
self.logger.debug('번역 작업이 중단되었습니다.')
break
self.logger.debug(f"이미지 번역 프로세스")
self.whale_translator.translate_image(url)
self.logger.debug(f"이미지 붙여넣기")
await self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
self.logger.debug(f"Progress Update")
self.update_detail_progress(i,total_images)
current_image_count += 1

View File

@ -46,6 +46,7 @@ class LocatorManager:
'next_page_button_template': self.config.get('BrowserControl', 'next_page_button_template').strip("'"),
'source_button_locator': self.config.get('BrowserControl', 'source_button_locator').strip("'"),
'ck_source_editing_area_locator': self.config.get('BrowserControl', 'ck_source_editing_area_locator').strip("'"),
'option_input_field_locator': self.config.get('BrowserControl', 'option_input_field_locator').strip("'"),
'title_tab_locator': self.config.get('BrowserControl', 'title_tab_locator').strip("'"),
'option_tab_locator': self.config.get('BrowserControl', 'option_tab_locator').strip("'"),
'price_tab_locator': self.config.get('BrowserControl', 'price_tab_locator').strip("'"),
@ -56,6 +57,12 @@ class LocatorManager:
'save_button_locator': self.config.get('BrowserControl', 'save_button_locator').strip("'"),
}
# DetailPageTextTemplates 섹션
self.selectors['DetailPageTextTemplates'] = {
key: value.strip("'") for key, value in self.config.items('DetailPageTextTemplates')
}
# OptionLocators 섹션
self.selectors['OptionLocators'] = {
'option_excluded_selector_template': self.config.get('OptionLocators', 'option_excluded_selector_template').strip("'"),
@ -64,6 +71,7 @@ class LocatorManager:
'option_product_locator': self.config.get('OptionLocators', 'option_product_locator').strip("'"),
'total_options_selector': self.config.get('OptionLocators', 'total_options_selector').strip("'"),
'is_all_option_checked_selector': self.config.get('OptionLocators', 'is_all_option_checked_selector').strip("'"),
'ai_option_btn_selector': self.config.get('OptionLocators', 'ai_option_btn_selector').strip("'"),
'original_name_selector_template': self.config.get('OptionLocators', 'original_name_selector_template').strip("'"),
'edit_field_selector_template': self.config.get('OptionLocators', 'edit_field_selector_template').strip("'"),
'checkbox_selector_template': self.config.get('OptionLocators', 'checkbox_selector_template').strip("'"),

View File

@ -1,6 +1,7 @@
import logging
import os
from logging.handlers import RotatingFileHandler
# from PySide6.QtCore import Signal, QObject
from PyQt5.QtCore import pyqtSignal, QObject
def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=10*1024*1024, backup_count=5):
@ -24,15 +25,15 @@ def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=10*1024*1024, ba
return logger
class QTextEditLogger(logging.Handler, QObject):
class QTextEditLogger(QObject, logging.Handler):
appendHtml = pyqtSignal(str) # HTML 메시지를 전달할 시그널 정의
scrollToBottom = pyqtSignal() # 스크롤을 최하단으로 이동시키는 시그널
def __init__(self):
logging.Handler.__init__(self)
QObject.__init__(self)
logging.Handler.__init__(self)
def emit(self, record):
def log_message(self, record):
msg = self.format(record) # 로그 레코드를 문자열로 포매팅
color = {
@ -47,10 +48,14 @@ class QTextEditLogger(logging.Handler, QObject):
message = f"<span style=\"color:{color};\">{msg}</span><br/>"
self.appendHtml.emit(message) # HTML 메시지로 변경
self.scrollToBottom.emit() # 스크롤 시그널 발생
# emit 대신 log_message를 호출하도록 수정
def emit(self, record):
self.log_message(record)
def close(self):
self.flush()
logging.Handler.close(self)
def flush(self):
pass # 필요 시 정리 작업 수행
pass

44
main.py
View File

@ -21,9 +21,13 @@ def allow_sleep():
async def process_qt_events(app, stop_event):
"""PySide6의 이벤트를 처리하는 비동기 함수"""
while not stop_event.done():
app.processEvents()
await asyncio.sleep(0.01) # 10ms마다 Qt 이벤트 처리
try:
while not stop_event.done():
app.processEvents()
await asyncio.sleep(0.01) # 10ms마다 Qt 이벤트 처리
except asyncio.CancelledError:
# 취소 시 안전하게 종료
pass
async def main():
# 로깅 설정
@ -38,38 +42,38 @@ async def main():
stop_event = asyncio.Future() # 종료 이벤트 생성
try:
# PySide6 앱 실행
app = QApplication([])
# DPI 설정
try:
# DPI 인식 설정을 위한 환경 변수
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
os.environ["QT_SCALE_FACTOR"] = "1"
# DPI 인식 설정
ctypes.windll.shcore.SetProcessDpiAwareness(1) # 시스템 DPI 인식 대신 개별 모니터 인식으로 변경
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except Exception as e:
logger.error(f"DPI 인식 설정 실패: {e}")
# 기존 TranslationApp UI 사용
# whale_translator = WhaleTranslator(app, logger, secret_mode=True, vd_mode=True) # 모드 켜기
# await whale_translator.start_whale_browser()
window = TranslationApp(logger, app) # PySide6 UI
window = TranslationApp(logger, app)
window.show()
# asyncio와 PySide6 이벤트 루프를 통합
# QApplication.exec_()을 사용하여 Qt 이벤트 루프 시작
await asyncio.gather(
process_qt_events(app, stop_event), # PySide6 이벤트 처리, stop_event 추가
window.run_async_tasks(), # 비동기 작업
process_qt_events(app, stop_event),
window.run_async_tasks(),
return_exceptions=True
)
# 이 부분은 exec_()를 호출했기 때문에 도달하지 않습니다.
# app.exec_()
except Exception as e:
logger.exception(f"Main function error: {e}")
finally:
# 앱 종료 시 절전모드 방지 해제
allow_sleep()
stop_event.set_result(True) # 종료 이벤트 설정 (process_qt_events 종료)
if window: # window가 생성되었을 경우에만 close() 호출
await window.close() # window.close()를 finally 블록으로 이동
if not stop_event.done():
stop_event.set_result(True)
if window:
await window.close() # await 추가
if app:
app.quit() #QApplication을 명시적으로 종료
if __name__ == '__main__':
asyncio.run(main()) # 비동기 함수는 asyncio.run()으로 실행

View File

@ -22,6 +22,7 @@ class OptionHandler:
self.option_product_locator = self.locator_manager.get_locator('OptionLocators', 'option_product_locator')
self.total_options_selector = self.locator_manager.get_locator('OptionLocators', 'total_options_selector')
self.is_all_option_checked_selector = self.locator_manager.get_locator('OptionLocators', 'is_all_option_checked_selector')
self.ai_option_btn_selector = self.locator_manager.get_locator('OptionLocators', 'ai_option_btn_selector')
self.original_name_selector_template = self.locator_manager.get_locator('OptionLocators', 'original_name_selector_template')
self.edit_field_selector_template = self.locator_manager.get_locator('OptionLocators', 'edit_field_selector_template')
self.checkbox_selector_template = self.locator_manager.get_locator('OptionLocators', 'checkbox_selector_template')
@ -131,7 +132,8 @@ class OptionHandler:
if option_input_element:
option_name_value = (await option_input_element.get_attribute('value')).strip()
selected_translated_options.append(
(option_name_value, self.option_info['prices'].get(option_name_value, {}).get('low_price', 0))
# (option_name_value, self.option_info['prices'].get(option_name_value, {}).get('low_price', 0))
(option_name_value)
)
self.option_info['selected_translated_options'] = selected_translated_options
@ -180,32 +182,56 @@ class OptionHandler:
await self.low_order_click()
# 4. 옵션 정보 수집 및 번역
if toggle_states['optionTrnas']:
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
self.option_info = await self.collect_options_info()
try:
if toggle_states['optionTrnas']:
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
self.option_info = await self.collect_options_info()
translation_success = False # 성공/실패 플래그
translation_success = False # 성공/실패 플래그
try:
# Vertex AI를 통한 번역 시도
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
self.logger.debug(f"번역된 옵션 입력")
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
try:
# Vertex AI를 통한 번역 시도
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
self.logger.debug(f"번역된 옵션 입력")
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
translation_success = True # 번역 성공
translation_success = True # 번역 성공
except Exception as ve:
if "SAFETY" in str(ve):
self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}")
self.logger.debug(f"퍼센티 자체 AI번역 사용 시도")
pyautogui.hotkey('alt', 'q')
self.logger.debug(f"번역을 위한 5초간 대기")
# await asyncio.sleep(5)
time.sleep(5)
except ValueError as ve:
# 안전 필터 예외 처리
if "SAFETY" in str(ve):
self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}")
self.logger.debug("퍼센티 자체 AI번역 사용 시도")
await self.page.click(self.ai_option_btn_selector)
self.logger.debug("번역을 위한 5초간 대기")
await asyncio.sleep(5)
translation_success = False # 번역 실패
# except google.api_core.exceptions.ResourceExhausted as re:
# # 할당량 초과 예외 처리
# self.logger.error(f"Vertex AI 할당량 초과: {re}")
# self.logger.debug("퍼센티 자체 AI번역 사용 시도")
# pyautogui.hotkey('alt', 'q')
# self.logger.debug("번역을 위한 5초간 대기")
# time.sleep(5)
# translation_success = False # 번역 실패
except Exception as e:
# 기타 예외 처리
self.logger.error(f"번역 처리 중 알 수 없는 오류 발생: {e}", exc_info=True)
self.logger.debug("퍼센티 자체 AI번역 사용 시도")
await self.page.click(self.ai_option_btn_selector)
self.logger.debug("번역을 위한 5초간 대기")
await asyncio.sleep(5)
translation_success = False # 번역 실패
self.logger.debug(f"[{'VertexAI' if translation_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
# 번역 성공 여부에 따른 로그
self.logger.debug(f"[{'VertexAI' if translation_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
except Exception as e:
# 옵션 처리 중 오류 발생 시 전체 로그 출력
self.logger.error(f"옵션 처리 중 오류 발생: {e}", exc_info=True)
# 5. 옵션 필터링 및 조정
if toggle_states['optionAutoSelect']:
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

53
setup.py Normal file
View File

@ -0,0 +1,53 @@
# setup.py
import sys
from cx_Freeze import setup, Executable
# 필요한 파일 정의
include_files = [
'config.ini',
'leensoo1nt.json',
'prompt.json',
'userDB.db',
('src/initialDB.db', 'src/initialDB.db'),
('src/Percenty_SS_Code.json', 'src/Percenty_SS_Code.json')
]
# 사용된 패키지 정의
build_exe_options = {
'packages': [
'ctypes', 'asyncio', 'os', 're', 'time', 'math', 'json', 'logging', 'shutil', 'random', 'base64',
'subprocess', 'configparser', 'pyperclip', 'numpy', 'cv2', 'requests', 'win32clipboard', 'win32gui',
'win32con', 'win32process', 'PIL', 'bs4', 'pyautogui', 'pyvda', 'sqlalchemy', 'sqlalchemy.orm',
'sqlalchemy.exc', 'collections', 'pandas'
],
'includes': [
'PySide6.QtWidgets', 'PySide6.QtCore', 'PySide6.QtGui',
'whale_translator', 'gui', 'logger_module', 'toggleSwitch',
'browser_control', 'clipboardImageManager', 'vertexAI', 'option',
'price', 'title', 'locatorManager', 'src.cmb_diag', 'src.DatabaseManager',
'vertexai.generative_models'
],
'include_files': include_files,
'excludes': [
'tkinter', 'PyQt4', 'PyQt6', 'AppKit', 'Foundation', 'IPython', 'OpenSSL', 'asyncpg',
'curses', 'pydantic', 'ssl', 'test', 'unittest', 'matplotlib', 'tensorflow', 'torch', 'scipy'
]
}
# 애플리케이션 메인 파일 및 설정
base = None
if sys.platform == 'win32':
base = 'Win32GUI'
executables = [
Executable('main.py', base=base, target_name='AutoPercenty2.exe')
]
# Setup 설정
setup(
name='AutoPercenty2',
version='1.1',
description='자동화도구',
options={'build_exe': build_exe_options},
executables=executables
)

View File

@ -1,18 +1,18 @@
import os
import json
import asyncio
import sys
from vertexai.generative_models import GenerativeModel
class VertexAITranslator:
def __init__(self, logger, key_path):
def __init__(self, logger):
"""
VertexAITranslator 클래스 초기화 메서드.
:param logger: 로깅을 위한 로거 객체.
:param key_path: Google Application Credentials의 파일 경로.
"""
self.logger = logger
key_path = self.get_key_path('leensoo1nt.json')
# GOOGLE_APPLICATION_CREDENTIALS 환경 변수 설정
self.logger.debug(f"GOOGLE_APPLICATION_CREDENTIALS 환경 변수를 설정: {key_path}")
@ -36,12 +36,20 @@ class VertexAITranslator:
:return: 파싱된 JSON 데이터.
"""
try:
prompt_path = os.path.join(os.path.dirname(__file__), 'prompt.json')
# cx_Freeze로 패키징된 경우 실행 파일의 경로로 설정
if getattr(sys, 'frozen', False):
prompt_path = os.path.join(os.path.dirname(sys.executable), 'prompt.json')
else:
# 일반 Python 실행 환경일 경우
prompt_path = os.path.join(os.path.dirname(__file__), 'prompt.json')
self.logger.debug(f"프롬프트 파일 경로: {prompt_path}")
with open(prompt_path, 'r', encoding='utf-8') as file:
prompt_data = json.load(file)
self.logger.debug("prompt.json 파일이 성공적으로 로드되었습니다.")
return prompt_data
except FileNotFoundError as e:
self.logger.error(f"prompt.json 파일을 찾을 수 없습니다: {e}", exc_info=True)
raise e
@ -49,6 +57,14 @@ class VertexAITranslator:
self.logger.error(f"prompt.json 파일 파싱 중 오류 발생: {e}", exc_info=True)
raise e
def get_key_path(self, key_path):
# cx_Freeze로 패키징된 경우 실행 파일 경로로 설정
if getattr(sys, 'frozen', False):
return os.path.join(os.path.dirname(sys.executable), key_path)
else:
# 일반 Python 실행 환경일 경우
return os.path.join(os.path.dirname(__file__), key_path)
def clean_special_chars(self, text):
"""
텍스트에서 허용되지 않는 특수 문자를 제거하고,

View File

@ -7,6 +7,7 @@ import subprocess
import asyncio
import KO_EN
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
from PIL import ImageGrab
class WhaleTranslator:
def __init__(self, app, logger, secret_mode=True, vd_mode=False, max_failures=5):
@ -65,8 +66,8 @@ class WhaleTranslator:
# 주소창으로 이동 후 URL 입력
pyautogui.hotkey('ctrl', 'l')
time.sleep(0.4)
# pyautogui.typewrite('https://daum.net')
# self.change_lang()
pyautogui.typewrite('https://daum.net')
self.change_lang()
self.enter_url("about:newtab", change=True)
self.logger.debug("URL 입력 완료")
@ -91,7 +92,7 @@ class WhaleTranslator:
asyncio.run(self.start_whale_browser()) # 브라우저 재시작
self.reset_failures() # 실패 횟수 초기화
def is_image_in_clipboard(self):
def is_image_in_clipboard_with_text(self):
"""클립보드에 이미지 데이터 또는 base64로 인코딩된 이미지 데이터가 있는지 확인"""
clipboard_content = pyperclip.paste()
if clipboard_content.startswith("data:image") or isinstance(clipboard_content, bytes):
@ -100,6 +101,21 @@ class WhaleTranslator:
else:
self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
return False
def is_image_in_clipboard(self):
"""클립보드에 이미지 데이터가 있는지 확인"""
try:
# 클립보드에서 이미지를 가져오고 None이 아니면 이미지가 있는 것으로 간주
image = ImageGrab.grabclipboard()
if isinstance(image, bytes) or image is not None:
self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
return True
else:
self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
return False
except Exception as e:
self.logger.error(f"클립보드에서 이미지 확인 중 오류 발생: {e}", exc_info=True)
return False
def find_whale_window(self):
"""웨일 창 핸들을 찾는 메서드"""
@ -223,19 +239,19 @@ class WhaleTranslator:
self.enter_url(url)
pyautogui.press('enter')
# await asyncio.sleep(1) # 페이지 로딩 대기
time.sleep(2)
time.sleep(1)
pyautogui.rightClick()
# await asyncio.sleep(0.2) # 페이지 로딩 대기
time.sleep(2)
time.sleep(1)
pyautogui.press('r') # 번역 클릭
# await asyncio.sleep(7) # 페이지 로딩 대기
time.sleep(7)
time.sleep(5)
pyautogui.rightClick()
# await asyncio.sleep(0.2) # 페이지 로딩 대기
time.sleep(0.2)
time.sleep(1)
pyautogui.press('c') # 번역된 이미지 클립보드에 복사
@ -344,7 +360,7 @@ class WhaleTranslator:
self.logger.debug("전환 성공")
def enter_url_for_typing(self, url, change=False):
def enter_url(self, url, change=False):
# 언어 전환이 완료되면 주소창으로 이동 후 URL 입력
pyautogui.hotkey('ctrl', 'l') # 주소창으로 이동
@ -353,7 +369,7 @@ class WhaleTranslator:
pyautogui.press('enter') # Enter 키 입력
time.sleep(1) # 페이지 로딩 대기
def enter_url(self, url, change=False):
def enter_url_for_clipboard(self, url, change=False):
# URL을 클립보드에 복사
pyperclip.copy(url)