|
|
@ -1,7 +1,8 @@
|
|||
Include/
|
||||
include/
|
||||
Lib/
|
||||
Scripts/
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
build/
|
||||
pyvenv.cfg
|
||||
*.log
|
||||
1
KO_EN.py
|
|
@ -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
|
||||
|
|
|
|||
12717
appTranslator.log
|
|
@ -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) # 클립보드 내용을 처리
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
15
config.ini
|
|
@ -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]
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 648 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 635 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 621 KiB |
|
After Width: | Height: | Size: 265 KiB |
15
gui.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("'"),
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()으로 실행
|
||||
|
|
|
|||
66
option.py
|
|
@ -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']}")
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 660 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
|
@ -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
|
||||
)
|
||||
24
vertexAI.py
|
|
@ -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):
|
||||
"""
|
||||
텍스트에서 허용되지 않는 특수 문자를 제거하고,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||