브라우저 컨트롤러의 팝업 처리 로직에서 대기 시간을 단축하고, MIGAN ONNX 파이프라인을 설정하는 코드를 추가하였습니다. 또한, 이미지 처리 요청 시 내장 rembg 모듈 사용 조건을 개선하여 서버 오류 발생 시 백업 모듈을 사용할 수 있도록 하였습니다. 버전 3.11.2로 업데이트하며, 업데이트 로그를 수정하여 변경 사항을 반영하였습니다.

This commit is contained in:
9700X_PC 2025-08-22 00:05:26 +09:00
parent 9f8908b3fb
commit e622f4125a
12 changed files with 771 additions and 42 deletions

View File

@ -0,0 +1,328 @@
; AutoPercenty3 Inno Setup Script
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
; 20250821_233810에 생성됨
#define AppId "autopercenty"
#define MyAppName "Edit_PartTimer"
#define MyAppVersion "3.11.1"
#define MyAppPublisher "WhenRideMyCar"
#define MyAppProgramName "편집알바생"
#define MyAppDescription "편집알바생"
#define MyAppCopyright "Copyright 2024"
#define MyAppExeName "Edit_PartTimer3"
#define MySetupName "Edit_PartTimer Setup"
#define MySetupIcon "src/Edit_PartTimer3.ico"
#define MySetupOutputDir "dist/installer"
[Setup]
; 기본 설정
AppId={#AppId}
AppName={#MyAppProgramName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppPublisher}
OutputDir={#MySetupOutputDir}
OutputBaseFilename={#MySetupName}
SetupIconFile={#MySetupIcon}
Compression=lzma
SolidCompression=yes
; 업데이트 관련 설정 - 권한 최적화
PrivilegesRequired=admin
PrivilegesRequiredOverridesAllowed=dialog
UpdateUninstallLogAppName=yes
AppMutex={#MyAppName}
CloseApplications=yes
RestartApplications=no
; 보안 및 호환성 설정
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
AllowNoIcons=yes
; 버전 정보
VersionInfoVersion={#MyAppVersion}
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription={#MyAppDescription}
VersionInfoCopyright={#MyAppCopyright}
VersionInfoProductName={#MyAppProgramName}
VersionInfoProductVersion={#MyAppVersion}
[Languages]
Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
[Dirs]
; 설치 시 {app}\logs 폴더를 생성하고,
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
Name: "{app}\logs"; Permissions: users-modify
; 설치 시 {app}\user_data 폴더를 생성하고,
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
Name: "{app}\user_data"; Permissions: users-modify
; Playwright 브라우저 폴더를 Program Files 내부에 생성
Name: "{app}\lib\src\browsers\chromium-1155"; Permissions: users-modify
; Playwright 브라우저 사용자폴더를 Program Files 내부에 생성
Name: "{app}\lib\src\browsers\user_data"; Permissions: users-modify
[Files]
; 프로그램 파일만 설치 (항상 덮어쓰기)
Source: "build\exe.win-amd64-3.11\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; VC++ 재배포 패키지 파일을 임시 폴더({tmp})에 복사
Source: "VC_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
[Registry]
; Playwright 브라우저 경로를 Program Files 내부로 설정
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "PLAYWRIGHT_BROWSERS_PATH"; ValueData: "{app}\lib\src\browsers"; Flags: preservestringtype
[Icons]
; 시작 메뉴 바로가기
Name: "{group}\{#MyAppProgramName}"; Filename: "{app}\{#MyAppExeName}.exe"
; 바탕화면 바로가기
Name: "{autodesktop}\{#MyAppProgramName}"; Filename: "{app}\{#MyAppExeName}.exe"; Tasks: desktopicon
; 프로그램 제거 바로가기
Name: "{group}\{#MyAppProgramName} 제거"; Filename: "{uninstallexe}"
[Run]
; VC++ 재배포 패키지 설치 (필요할 경우)
Filename: "{tmp}\VC_redist.x64.exe"; Parameters: "/install /passive /norestart"; StatusMsg: "VC++ 재배포 패키지 설치 중..."; Check: NeedsVCredist
; 설치 후 프로그램 실행 (원할 경우)
Filename: "{app}\{#MyAppExeName}.exe"; Description: "{cm:LaunchProgram,{#MyAppProgramName}}"; Flags: nowait postinstall skipifsilent
[Code]
function CompareVersion(V1, V2: string): Integer;
var
P1, P2, N1, N2: Integer;
begin
P1 := 1;
P2 := 1;
Result := 0;
while (Result = 0) and ((P1 <= Length(V1)) or (P2 <= Length(V2))) do begin
while (P1 <= Length(V1)) and (V1[P1] = '.') do Inc(P1);
while (P2 <= Length(V2)) and (V2[P2] = '.') do Inc(P2);
if (P1 <= Length(V1)) and (P2 <= Length(V2)) then begin
N1 := 0; while (P1 <= Length(V1)) and (V1[P1] >= '0') and (V1[P1] <= '9') do begin N1 := N1 * 10 + Ord(V1[P1]) - Ord('0'); Inc(P1); end;
N2 := 0; while (P2 <= Length(V2)) and (V2[P2] >= '0') and (V2[P2] <= '9') do begin N2 := N2 * 10 + Ord(V2[P2]) - Ord('0'); Inc(P2); end;
if N1 < N2 then Result := -1 else if N1 > N2 then Result := 1;
end else begin
if P1 <= Length(V1) then Result := 1 else if P2 <= Length(V2) then Result := -1;
end;
while (P1 <= Length(V1)) and (V1[P1] <> '.') do Inc(P1);
while (P2 <= Length(V2)) and (V2[P2] <> '.') do Inc(P2);
end;
end;
// 파일 또는 폴더 복사 함수
procedure CopyDir(const SourcePath, DestPath: string);
var
FindRec: TFindRec;
SourceFilePath: string;
DestFilePath: string;
begin
ForceDirectories(DestPath);
if FindFirst(SourcePath + '\*', FindRec) then
begin
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
begin
SourceFilePath := SourcePath + '\' + FindRec.Name;
DestFilePath := DestPath + '\' + FindRec.Name;
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
begin
if FileCopy(SourceFilePath, DestFilePath, False) then
Log('파일 복사 성공: ' + SourceFilePath + ' -> ' + DestFilePath)
else
Log('파일 복사 실패: ' + SourceFilePath);
end
else
CopyDir(SourceFilePath, DestFilePath);
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
// 디렉토리 삭제 함수
procedure DeleteDir(const DirPath: string);
var
FindRec: TFindRec;
FilePath: string;
begin
if not DirExists(DirPath) then Exit;
if FindFirst(DirPath + '\*', FindRec) then
begin
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
begin
FilePath := DirPath + '\' + FindRec.Name;
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
begin
if DeleteFile(FilePath) then
Log('파일 삭제 성공: ' + FilePath)
else
Log('파일 삭제 실패: ' + FilePath);
end
else
DeleteDir(FilePath);
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
if RemoveDir(DirPath) then
Log('디렉토리 삭제 성공: ' + DirPath)
else
Log('디렉토리 삭제 실패: ' + DirPath);
end;
// 프로그램 실행 여부 확인
function IsAppRunning(const FileName: string): Boolean;
var
Handle: THandle;
begin
Handle := FindWindowByWindowName('{#MyAppProgramName}'); // 프로그램의 윈도우 타이틀로 찾기
Result := (Handle <> 0);
end;
// 프로그램 종료
procedure CloseApplication(const FileName: string);
var
Handle: THandle;
begin
Handle := FindWindowByWindowName('{#MyAppProgramName}');
if Handle <> 0 then
begin
PostMessage(Handle, 18, 0, 0); // WM_QUIT
Sleep(1000); // 종료 대기
end;
end;
// VC++ 재배포 패키지 필요 여부 확인
function NeedsVCredist: Boolean;
begin
if RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64') then
Result := False // 이미 설치됨
else
Result := True; // 미설치 -> 설치 필요
end;
// 설치 완료 후 실행 여부 확인
function InitializeFinish(): Boolean;
var
ResultCode: Integer;
begin
Result := True;
if MsgBox('설치가 완료되었습니다. 프로그램을 실행하시겠습니까?' + #13#10 +
'(실행 시 서버와 동기화하여 설정이 업데이트됩니다)',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(ExpandConstant('{app}\{#MyAppExeName}.exe'), '', '', SW_SHOW, ewNoWait, ResultCode);
end;
end;
function InitializeSetup(): Boolean;
var
OldVersion: String;
NewVersion: String;
OldAppPath: String;
UserDataSourcePath, UserDataBackupPath: String;
ResultCode: Integer;
begin
Result := True;
NewVersion := '{#MyAppVersion}';
UserDataBackupPath := ExpandConstant('{tmp}\user_data_backup');
// 현재 프로그램 버전 확인
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppName}_is1',
'DisplayVersion', OldVersion) then
begin
// 같은 버전이거나 더 높은 버전이 설치되어 있는 경우
if CompareVersion(OldVersion, NewVersion) >= 0 then
begin
MsgBox('현재 설치된 버전(' + OldVersion + ')이 이 설치 프로그램의 버전(' +
NewVersion + ')과 같거나 더 높습니다.' + #13#10 +
'설치를 계속할 수 없습니다.', mbInformation, MB_OK);
Result := False;
exit;
end;
// 이전 버전이 설치되어 있는 경우 업데이트 진행
if CompareVersion(OldVersion, NewVersion) < 0 then
begin
Log('업데이트 설치 진행: ' + OldVersion + ' -> ' + NewVersion);
// 프로그램이 실행 중인지 확인하고 종료 요청
if IsAppRunning('{#MyAppExeName}.exe') then
begin
if MsgBox('프로그램을 업데이트하기 위해 실행 중인 프로그램을 종료해야 합니다.' + #13#10 +
'계속하시겠습니까?', mbConfirmation, MB_YESNO) = IDNO then
begin
Result := False;
exit;
end;
CloseApplication('{#MyAppExeName}.exe');
Sleep(2000); // 프로세스 종료 대기
end;
// 레지스트리에서 설치 경로 확인
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppName}_is1',
'InstallLocation', OldAppPath) then
begin
Log('기존 설치 경로: ' + OldAppPath);
// lib/src/user_data 폴더 백업
UserDataSourcePath := OldAppPath + '\lib\src\user_data';
if DirExists(UserDataSourcePath) then
begin
Log('사용자 데이터 백업 중: ' + UserDataSourcePath + ' -> ' + UserDataBackupPath);
ForceDirectories(UserDataBackupPath);
CopyDir(UserDataSourcePath, UserDataBackupPath);
end
else
begin
Log('사용자 데이터 폴더가 존재하지 않음: ' + UserDataSourcePath);
end;
// 기존 설치 폴더 완전 삭제
if DirExists(OldAppPath) then
begin
Log('기존 설치 폴더 삭제 중: ' + OldAppPath);
DeleteDir(OldAppPath);
end;
end;
end;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
UserDataBackupPath, UserDataDestPath: String;
begin
// 설치 완료 후
if CurStep = ssPostInstall then
begin
UserDataBackupPath := ExpandConstant('{tmp}\user_data_backup');
UserDataDestPath := ExpandConstant('{app}\lib\src\user_data');
// 백업한 사용자 데이터 폴더가 있으면 복원
if DirExists(UserDataBackupPath) then
begin
Log('사용자 데이터 복원 중: ' + UserDataBackupPath + ' -> ' + UserDataDestPath);
ForceDirectories(UserDataDestPath);
CopyDir(UserDataBackupPath, UserDataDestPath);
Log('사용자 데이터 복원 완료');
end;
end;
end;

View File

@ -469,14 +469,14 @@ class BrowserController(QThread):
# return debug_dir # return debug_dir
async def close_welcome_popup_if_exists(self, page, timeout_sec: int = 3): async def close_welcome_popup_if_exists(self, page, timeout_sec: int = 1):
""" """
로그인 직후 간헐적으로 뜨는 '환영합니다' 팝업이 있을 경우 닫아준다. 로그인 직후 간헐적으로 뜨는 '환영합니다' 팝업이 있을 경우 닫아준다.
일정 시간 동안 팝업이 감지되지 않으면 아무것도 하지 않고 종료한다. 일정 시간 동안 팝업이 감지되지 않으면 아무것도 하지 않고 종료한다.
Args: Args:
page (Page): Playwright의 page 객체 page (Page): Playwright의 page 객체
timeout_sec (int): 팝업을 최대 동안 기다릴지 (기본값 3) timeout_sec (int): 팝업을 최대 동안 기다릴지 (기본값 1)
""" """
# shadow-root 내부의 닫기 버튼을 가리키는 선택자 # shadow-root 내부의 닫기 버튼을 가리키는 선택자
# self.welcome_popup_closeBTN_selector = '#ch-plugin >>> div:has(span.a11y-hidden:has-text("닫기")) button' # self.welcome_popup_closeBTN_selector = '#ch-plugin >>> div:has(span.a11y-hidden:has-text("닫기")) button'
@ -493,7 +493,7 @@ class BrowserController(QThread):
finally: finally:
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await asyncio.sleep(0.53) await asyncio.sleep(0.37)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
async def start_browser_async(self): async def start_browser_async(self):
@ -2031,7 +2031,7 @@ class BrowserController(QThread):
# self.logger.log(f"이미지 붙여넣기 중 오류: {e}", level=logging.ERROR, exc_info=True) # self.logger.log(f"이미지 붙여넣기 중 오류: {e}", level=logging.ERROR, exc_info=True)
# return False # return False
async def handle_exist_productname_popup(self, timeout_sec: int = 3) -> bool: async def handle_exist_productname_popup(self, timeout_sec: int = 1) -> bool:
""" """
이미 존재하는 상품명 알림(다이얼로그/모달) 처리 이미 존재하는 상품명 알림(다이얼로그/모달) 처리
@ -2048,11 +2048,11 @@ class BrowserController(QThread):
await ok_btn.click() await ok_btn.click()
self.logger.log("다이얼로그 확인버튼 클릭 완료.", level=logging.INFO) self.logger.log("다이얼로그 확인버튼 클릭 완료.", level=logging.INFO)
try: try:
await self.page.locator(self.dialog_selector).wait_for(state="detached", timeout=2000) await self.page.locator(self.dialog_selector).wait_for(state="detached", timeout=1000)
return True return True
except TimeoutError: except TimeoutError:
await ok_btn.click() await ok_btn.click()
await self.page.locator(self.dialog_selector).wait_for(state="detached", timeout=2000) await self.page.locator(self.dialog_selector).wait_for(state="detached", timeout=1000)
return True return True
except TimeoutError: except TimeoutError:
self.logger.log("다이얼로그 팝업 감지 안됨.", level=logging.WARNING) self.logger.log("다이얼로그 팝업 감지 안됨.", level=logging.WARNING)
@ -2078,7 +2078,7 @@ class BrowserController(QThread):
# ──────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────
async def _resolve_duplication(step_desc: str) -> bool: async def _resolve_duplication(step_desc: str) -> bool:
"""저장/닫기 단계에서 중복 팝업이 뜨면 상품명 재설정 후 재저장한다.""" """저장/닫기 단계에서 중복 팝업이 뜨면 상품명 재설정 후 재저장한다."""
await asyncio.sleep(0.5) # 팝업 렌더링 대기 await asyncio.sleep(0.2) # 팝업 렌더링 대기
rename_need = await self.handle_exist_productname_popup() rename_need = await self.handle_exist_productname_popup()
if not rename_need: if not rename_need:
return False # 중복 없음 return False # 중복 없음
@ -2096,7 +2096,7 @@ class BrowserController(QThread):
# 상품명 탭 이동 → 제목 재설정 # 상품명 탭 이동 → 제목 재설정
await self.edit_product_name() await self.edit_product_name()
await asyncio.sleep(0.5) await asyncio.sleep(0.2)
is_set = await self.titleGenerator.set_product_name(new_title, duplication_suffix) is_set = await self.titleGenerator.set_product_name(new_title, duplication_suffix)
if not is_set: if not is_set:
@ -2638,13 +2638,13 @@ class BrowserController(QThread):
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.logger.log(f'스크롤 중 타임아웃 발생 (시도 {retry_count + 1}/{max_retries})', level=logging.WARNING) self.logger.log(f'스크롤 중 타임아웃 발생 (시도 {retry_count + 1}/{max_retries})', level=logging.WARNING)
retry_count += 1 retry_count += 1
await asyncio.sleep(1.0) # 타임아웃 후 잠시 대기 await asyncio.sleep(0.2) # 타임아웃 후 잠시 대기
# 페이지 상태 재확인 # 페이지 상태 재확인
try: try:
previous_height = await asyncio.wait_for( previous_height = await asyncio.wait_for(
self.page.evaluate("() => window.pageYOffset"), self.page.evaluate("() => window.pageYOffset"),
timeout=2.0 timeout=1.0
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.logger.log('페이지 상태 확인 실패, 스크롤 중단', level=logging.WARNING) self.logger.log('페이지 상태 확인 실패, 스크롤 중단', level=logging.WARNING)
@ -3056,7 +3056,7 @@ class BrowserController(QThread):
try: try:
await self.page.wait_for_selector(self.memo_input_locator, await self.page.wait_for_selector(self.memo_input_locator,
state="detached", # hidden 으로 바꿔도 됨 state="detached", # hidden 으로 바꿔도 됨
timeout=3000) timeout=1000)
except TimeoutError: except TimeoutError:
# 남아있어도 다음 재시도 전에 old-element 가 선택되지 않도록 # 남아있어도 다음 재시도 전에 old-element 가 선택되지 않도록
self.logger.log("메모 팝업이 닫히지 않았습니다 selector 재생성", level=logging.WARNING) self.logger.log("메모 팝업이 닫히지 않았습니다 selector 재생성", level=logging.WARNING)

View File

@ -149,6 +149,9 @@ class MAIN_GUI(QMainWindow):
self.base_dir = self.get_base_dir() self.base_dir = self.get_base_dir()
self.toggle_states['base_dir'] = self.base_dir self.toggle_states['base_dir'] = self.base_dir
# MIGAN ONNX 경로 설정
self.toggle_states['migan_onnx_path'] = os.path.join(self.base_dir, "modules", "migan_onnx", "migan_pipeline_v2.onnx")
# # 폰트 경로 설정 # # 폰트 경로 설정
# self.toggle_states['image_font_path'] = os.path.join(self.base_dir, "fonts", "HakgyoansimDunggeunmisoTTFB.ttf") # self.toggle_states['image_font_path'] = os.path.join(self.base_dir, "fonts", "HakgyoansimDunggeunmisoTTFB.ttf")
# self.toggle_states['watermark_font_path'] = os.path.join(self.base_dir, 'fonts', 'HakgyoansimDunggeunmisoTTFB.ttf') # self.toggle_states['watermark_font_path'] = os.path.join(self.base_dir, 'fonts', 'HakgyoansimDunggeunmisoTTFB.ttf')
@ -1202,7 +1205,14 @@ class MAIN_GUI(QMainWindow):
"blend_mode": "simple", # 단순 블렌딩 "blend_mode": "simple", # 단순 블렌딩
"performance_mode": True, # 빠른 경로 사용 "performance_mode": True, # 빠른 경로 사용
"max_image_size": 1280, # 더 작은 크기 제한 "max_image_size": 1280, # 더 작은 크기 제한
"roi_area_high": 0.0 # 기본값: 0.60 → 0.0으로 변경 # 풀프레임 인페인팅 강제 "roi_area_high": 0.0, # 기본값: 0.60 → 0.0으로 변경 # 풀프레임 인페인팅 강제
"local_inpaint_method": "migan",
"migan_onnx_path": "",
"migan_use_cuda": False, # 3050 4GB면 True 권장(실패시 자동 CPU 폴백)
"migan_intra_threads": 0,
"migan_inter_threads": 0,
} }

View File

@ -156,6 +156,8 @@ include_files = dll_include_files + paddle_includes + [
('src/modules/fonts/NanumSquareRoundR.ttf', 'lib/src/modules/fonts/NanumSquareRoundR.ttf'), ('src/modules/fonts/NanumSquareRoundR.ttf', 'lib/src/modules/fonts/NanumSquareRoundR.ttf'),
('src/modules/fonts/HakgyoansimDunggeunmisoTTFB.ttf', 'lib/src/modules/fonts/HakgyoansimDunggeunmisoTTFB.ttf'), ('src/modules/fonts/HakgyoansimDunggeunmisoTTFB.ttf', 'lib/src/modules/fonts/HakgyoansimDunggeunmisoTTFB.ttf'),
('src/modules/migan_onnx/migan_pipeline_v2.onnx', 'lib/src/modules/migan_onnx/migan_onnx/migan_pipeline_v2.onnx'),
('퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx', '퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx'), ('퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx', '퍼센티 다양한 카테고리 엑셀 수집(스스 기준).xlsx'),
('src/Percenty_SS_Code.json', 'lib/src/Percenty_SS_Code.json'), ('src/Percenty_SS_Code.json', 'lib/src/Percenty_SS_Code.json'),
('src/browsers/chromium-1155', 'lib/src/browsers/chromium-1155'), ('src/browsers/chromium-1155', 'lib/src/browsers/chromium-1155'),
@ -177,17 +179,20 @@ build_exe_options = {
'packages': [ 'packages': [
'ctypes', 'asyncio', 'ctypes', 'asyncio',
'subprocess', 'pyperclip', 'numpy', 'subprocess', 'pyperclip', 'numpy',
# "numba", "rembg", "pymatting", 'numba', 'rembg', # rembg 의존성 추가
'cv2', 'requests', 'pyclipper', 'skimage', 'cv2', 'requests', 'pyclipper', 'skimage',
'PIL', 'bs4', 'PySide6', 'psutil', 'PIL', 'bs4', 'PySide6', 'psutil',
# 'win32api', 'win32file', 'win32pipe', 'win32event', 'pywintypes', 'win32con', 'win32process', 'win32clipboard', 'win32gui', # 'win32api', 'win32file', 'win32pipe', 'win32event', 'pywintypes', 'win32con', 'win32process', 'win32clipboard', 'win32gui',
'pandas', 'supabase', 'translatepy', 'markdown', 'pandas', 'supabase', 'translatepy', 'markdown',
'paddle', 'paddleocr', # paddle 관련 모듈 포함 'paddle', 'paddleocr', # paddle 관련 모듈 포함
'jsonschema', # rembg 추가 의존성
], ],
'includes': [ 'includes': [
# 'PySide6.QtWidgets', 'PySide6.QtCore', 'PySide6.QtGui', # 'PySide6.QtWidgets', 'PySide6.QtCore', 'PySide6.QtGui',
'shiboken6','playwright','comtypes.stream', 'win32com.client', 'win32com.server', 'pythoncom', 'shiboken6','playwright','comtypes.stream', 'win32com.client', 'win32com.server', 'pythoncom',
'loggerModule', 'toggleSwitch', 'src.modules.request_inpaint', 'onnxruntime', 'rembg', 'loggerModule', 'toggleSwitch', 'src.modules.request_inpaint', 'onnxruntime', 'rembg', 'numba', 'numba.core.types.old_scalars',
'jsonschema', # rembg 관련 의존성 추가
'numba.core', 'numba.core.types', 'numba.core.types.scalars', 'numba.typed', 'numba.experimental', # numba 내부 모듈들
'browser_control', 'locatorManager', 'src.cmdDiag', 'src.inputDiag', 'src.keyword', 'src.priceSetDiag', 'src.modules.image_processor3', 'browser_control', 'locatorManager', 'src.cmdDiag', 'src.inputDiag', 'src.keyword', 'src.priceSetDiag', 'src.modules.image_processor3',
'skimage', 'skimage.morphology', 'skimage.measure', 'skimage.filters', 'skimage.color', 'skimage.util', 'imghdr', 'imgaug', 'rapidfuzz', 'skimage', 'skimage.morphology', 'skimage.measure', 'skimage.filters', 'skimage.color', 'skimage.util', 'imghdr', 'imgaug', 'rapidfuzz',
'albumentations', 'albumentations', 'cython', 'fire', 'lmdb', 'PIL', 'docx', 'yaml', 'shapely', 'tqdm', 'albumentations', 'albumentations', 'cython', 'fire', 'lmdb', 'PIL', 'docx', 'yaml', 'shapely', 'tqdm',

View File

@ -69,6 +69,8 @@ class ImageProcessor3:
self.logger.log(f"self.unwanted_texts: {self.unwanted_texts}", level=logging.DEBUG) self.logger.log(f"self.unwanted_texts: {self.unwanted_texts}", level=logging.DEBUG)
self.logger.log(f"self.inpaint_method: {self.inpaint_method}", level=logging.DEBUG)
# ----------------------------- 메모리 파편화 완화 ----------------------------- # ----------------------------- 메모리 파편화 완화 -----------------------------
# Pillow 가 거대 이미지를 열 때 과도한 메모리를 점유하지 않도록 최대 픽셀 수 제한 # Pillow 가 거대 이미지를 열 때 과도한 메모리를 점유하지 않도록 최대 픽셀 수 제한
max_px = self.toggle_states.get("max_image_pixels", 20_000_000) # 약 20MP, 필요 시 조정 (20MP = 4500x4500, 50MP=8000x6000) max_px = self.toggle_states.get("max_image_pixels", 20_000_000) # 약 20MP, 필요 시 조정 (20MP = 4500x4500, 50MP=8000x6000)
@ -95,6 +97,19 @@ class ImageProcessor3:
self.request_ai_server = Request_AI_Server(logger=self.logger, inpaint_server_url=self.request_inpainting_server_url, rembg_server_url=self.request_rembg_server_url) self.request_ai_server = Request_AI_Server(logger=self.logger, inpaint_server_url=self.request_inpainting_server_url, rembg_server_url=self.request_rembg_server_url)
self.gtranslate = GoogleTranslate() self.gtranslate = GoogleTranslate()
# MIGAN ONNX 파이프라인 준비(옵션 토글 기반)
try:
from src.modules.migan_module import build_migan_from_toggle
if self.toggle_states.get("migan_onnx_path"):
self.migan = build_migan_from_toggle(self.toggle_states, logger=self.logger)
else:
self.migan = None
self.logger.log("migan_onnx_path 미설정: MIGAN 비활성", level=logging.INFO)
except Exception as e:
self.migan = None
self.logger.log(f"MIGAN 초기화 실패: {e}", level=logging.ERROR, exc_info=True)
except Exception as e: except Exception as e:
self.logger.log(f"ImageProcessor3 초기화 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) self.logger.log(f"ImageProcessor3 초기화 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
@ -289,16 +304,49 @@ class ImageProcessor3:
inpainted_image = self.request_ai_server.request_inpaint(local_image_path, masks) inpainted_image = self.request_ai_server.request_inpaint(local_image_path, masks)
if inpainted_image is None: if inpainted_image is None:
self.logger.log(f"Request 인페인팅 실패, opencv 인페인팅으로 대체", level=logging.WARNING) self.logger.log(f"Request 인페인팅 실패, opencv 인페인팅으로 대체", level=logging.WARNING)
# inpainted_image = self.opencv_inpaint(local_image_path, masks, method='telea', radius=3)
# # inpainted_image = self.lama_inpaint(local_image_path, masks)
inpainted_image = None
if getattr(self, "migan", None) is not None:
try:
self.logger.log(f"MIGAN 인페인팅 시작", level=logging.DEBUG)
inpainted_image = self.migan.inpaint(local_image_path, masks)
self.logger.log(f"MIGAN 인페인팅 완료", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"MIGAN 인페인팅 실패, OpenCV로 폴백: {e}", level=logging.WARNING, exc_info=True)
if inpainted_image is None:
inpainted_image = self.opencv_inpaint(local_image_path, masks, method='telea', radius=3) inpainted_image = self.opencv_inpaint(local_image_path, masks, method='telea', radius=3)
# inpainted_image = self.lama_inpaint(local_image_path, masks)
else: else:
self.logger.log(f"자체 인페인팅 실행", level=logging.DEBUG) self.logger.log(f"자체 인페인팅 실행", level=logging.DEBUG)
inpainted_image = None
if getattr(self, "migan", None) is not None:
try:
self.logger.log(f"MIGAN 인페인팅 시작", level=logging.DEBUG)
inpainted_image = self.migan.inpaint(local_image_path, masks)
self.logger.log(f"MIGAN 인페인팅 완료", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"MIGAN 인페인팅 실패, OpenCV로 폴백: {e}", level=logging.WARNING, exc_info=True)
if inpainted_image is None:
inpainted_image = self.opencv_inpaint(local_image_path, masks, method='telea', radius=3) inpainted_image = self.opencv_inpaint(local_image_path, masks, method='telea', radius=3)
# inpainted_image = self.lama_inpaint(local_image_path, masks)
self.logger.log(f"인페인팅 완료", level=logging.DEBUG) self.logger.log(f"인페인팅 완료", level=logging.DEBUG)
# # migan 테스트 코드
# inpainted_image = None
# if getattr(self, "migan", None) is not None:
# try:
# self.logger.log(f"MIGAN TEST 인페인팅 시작", level=logging.DEBUG)
# inpainted_image = self.migan.inpaint(local_image_path, masks)
# self.logger.log(f"MIGAN TEST 인페인팅 완료", level=logging.DEBUG)
# except Exception as e:
# self.logger.log(f"MIGAN Test 인페인팅 실패, OpenCV로 폴백: {e}", level=logging.WARNING, exc_info=True)
# 인페인팅 실패 시 원본 이미지 사용 # 인페인팅 실패 시 원본 이미지 사용
if inpainted_image is None: if inpainted_image is None:
self.logger.log(f"인페인팅 실패, 원본 이미지 사용", level=logging.WARNING) self.logger.log(f"인페인팅 실패, 원본 이미지 사용", level=logging.WARNING)

243
src/modules/migan_module.py Normal file
View File

@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
"""
src/modules/migan_module.py
- MI-GAN ONNX 파이프라인(/후처리 포함) 기존 파이프라인과 100% 호환되게 감싼 래퍼.
- 핵심 호환 포인트:
1) 입력은 네가 쓰던 방식대로: image_path(str), mask(np.ndarray, 0~255 그레이, 텍스트영역=255)
2) 마스크는 내부에서 자동으로 (이진화 -> 반전) 하여 MI-GAN 규칙(255=known, 0=hole) 맞춤
3) CUDA/CPU 자동 선택, 실패 CPU 폴백
4) 로거 인터페이스는 코드(logger.log(msg, level=...)) 동일하게 사용
의존성: onnxruntime, opencv-python, numpy
설정: toggle_states에 다음 사용(선택):
- "migan_onnx_path": 파이프라인 ONNX 경로(필수)
- "migan_use_cuda": True/False (기본 False)
- "migan_intra_threads": int (기본 0 = onnxruntime 기본)
- "migan_inter_threads": int (기본 0)
"""
import os
import sys
import time
import logging
from typing import Optional
import cv2
import numpy as np
import onnxruntime as ort
# OpenCV 내부 최적화 off (네 기존 코드와 동일 정책)
cv2.setUseOptimized(False)
def _np_uint8_2d(arr, name="mask"):
if arr is None:
raise ValueError(f"{name} is None")
if not isinstance(arr, np.ndarray):
raise TypeError(f"{name} must be np.ndarray, got {type(arr)}")
if arr.ndim != 2:
raise ValueError(f"{name} must be 2D, got shape={arr.shape}")
if arr.dtype != np.uint8:
# 안전 변환
arr = arr.astype(np.uint8, copy=False)
return arr
def _ensure_logger(logger: Optional[object]) -> logging.Logger:
"""네 logger( .log(msg, level=) )가 없을 때를 대비한 기본 로거"""
if logger and hasattr(logger, "log"):
return logger
pylogger = logging.getLogger("MIGAN")
if not pylogger.handlers:
pylogger.setLevel(logging.DEBUG)
h = logging.StreamHandler(stream=sys.stdout)
h.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s] %(message)s"))
pylogger.addHandler(h)
# .log 호환 어댑터
class _Adapter:
def __init__(self, _lg): self._lg = _lg
def log(self, msg, level=logging.INFO, **kwargs):
self._lg.log(level, msg)
return _Adapter(pylogger)
class MIGANPipelineONNXCompat:
"""
MI-GAN ONNX 파이프라인 래퍼( 프로젝트 호환용)
- 입력: image_path(str), mask(gray uint8 HxW) 텍스트영역=255(너의 MaskModule 출력 가정)
- 내부에서 mask를 (이진화반전)하여 MI-GAN 규칙(255=known, 0=hole)으로 맞춤
- 출력: BGR uint8(H,W,3)
"""
_SESSION_CACHE = {} # onnx_path -> InferenceSession 캐시
def __init__(self,
onnx_path: str,
logger: Optional[object] = None,
use_cuda: bool = False,
intra_threads: int = 0,
inter_threads: int = 0):
self.logger = _ensure_logger(logger)
self.onnx_path = onnx_path
self.use_cuda = bool(use_cuda)
self.intra_threads = int(intra_threads or 0)
self.inter_threads = int(inter_threads or 0)
if not os.path.exists(self.onnx_path):
self.logger.log(f"[MIGAN] ONNX 파일을 찾을 수 없습니다: {self.onnx_path}", level=logging.ERROR)
raise FileNotFoundError(self.onnx_path)
self.session = self._get_or_create_session()
ins = self.session.get_inputs()
outs = self.session.get_outputs()
self.in_image = ins[0].name
self.in_mask = ins[1].name
self.out_name = outs[0].name
# 입력/출력 형태 정보 로깅 (디버깅용)
for i, inp in enumerate(ins):
self.logger.log(f"[MIGAN] 입력 {i}: {inp.name}, 형태: {inp.shape}, 타입: {inp.type}", level=logging.INFO)
for i, out in enumerate(outs):
self.logger.log(f"[MIGAN] 출력 {i}: {out.name}, 형태: {out.shape}, 타입: {out.type}", level=logging.INFO)
self.logger.log(f"[MIGAN] 세션 준비 완료. providers={self.session.get_providers()}", level=logging.INFO)
def _get_or_create_session(self) -> ort.InferenceSession:
key = (self.onnx_path, self.use_cuda, self.intra_threads, self.inter_threads)
if key in self._SESSION_CACHE:
return self._SESSION_CACHE[key]
so = ort.SessionOptions()
if self.intra_threads > 0:
so.intra_op_num_threads = self.intra_threads
if self.inter_threads > 0:
so.inter_op_num_threads = self.inter_threads
providers = []
if self.use_cuda:
providers.append("CUDAExecutionProvider")
providers.append("CPUExecutionProvider")
try:
sess = ort.InferenceSession(self.onnx_path, sess_options=so, providers=providers)
except Exception as e:
# CUDA 실패 시 CPU 폴백
self.logger.log(f"[MIGAN] CUDA EP 초기화 실패, CPU로 폴백: {e}", level=logging.WARNING)
sess = ort.InferenceSession(self.onnx_path, sess_options=so, providers=["CPUExecutionProvider"])
self._SESSION_CACHE[key] = sess
return sess
# ───────────────────────────────────────────────────────────────
# 퍼블릭 API: 네 파이프라인에서 바로 호출
# ───────────────────────────────────────────────────────────────
def inpaint(self, image_path: str, mask_gray_255_text: np.ndarray) -> Optional[np.ndarray]:
"""
Args
image_path: 원본 이미지 경로 (BGR 로딩)
mask_gray_255_text: 0~255 그레이, '텍스트영역=255' 형태( MaskModule 출력)
(GaussianBlur 포함되어 있을 있음)
Return
inpainted BGR 이미지 (np.ndarray) or None
"""
try:
# 1) 입력 이미지 로드
bgr = cv2.imread(image_path, cv2.IMREAD_COLOR)
if bgr is None:
self.logger.log(f"[MIGAN] 이미지 로드 실패: {image_path}", level=logging.ERROR)
return None
H, W = bgr.shape[:2]
# 2) 마스크 정규화: (이진화 → 반전) 해서 255=known, 0=hole 맞추기
mask = _np_uint8_2d(mask_gray_255_text, name="mask")
if mask.shape != (H, W):
self.logger.log(f"[MIGAN] 마스크 크기 불일치: mask={mask.shape}, img={(H,W)}", level=logging.ERROR)
return None
# 이진화: 128 스레시hold 기준
_, mask_bin = cv2.threshold(mask, 128, 255, cv2.THRESH_BINARY)
# 너의 마스크는 “텍스트=255”, MI-GAN은 “hole=0”이므로 반전
# (텍스트영역 255 -> 0), (배경 0 -> 255)
mask_known255 = 255 - mask_bin
# 3) RGB 변환 (파이프라인 입력은 RGB uint8)
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
# 4) ONNX 추론 - 배치 차원 추가 및 차원 순서 변경
start = time.time()
# ONNX 모델 입력 형태:
# - image: (1, 3, H, W) - 배치, 채널, 높이, 너비 순서
# - mask: (1, 1, H, W) - 배치, 채널(1), 높이, 너비 순서
# 이미지: (H, W, 3) -> (1, 3, H, W)
rgb_batch = np.expand_dims(rgb, 0).transpose(0, 3, 1, 2)
# 마스크: (H, W) -> (1, 1, H, W)
mask_batch = np.expand_dims(mask_known255, (0, 1))
self.logger.log(f"[MIGAN] 입력 형태 - 이미지: {rgb_batch.shape}, 마스크: {mask_batch.shape}", level=logging.DEBUG)
out = self.session.run(
[self.out_name],
{self.in_image: rgb_batch, self.in_mask: mask_batch}
)[0] # expect RGB uint8(1,3,H,W)
# 출력 차원 처리: (1,3,H,W) -> (H,W,3)
if out.ndim == 4 and out.shape[0] == 1:
out = out[0].transpose(1, 2, 0) # (1,3,H,W) -> (3,H,W) -> (H,W,3)
elif out.ndim == 3 and out.shape[0] == 3: # (3,H,W) -> (H,W,3)
out = out.transpose(1, 2, 0)
self.logger.log(f"[MIGAN] 출력 형태: {out.shape}, dtype: {out.dtype}", level=logging.DEBUG)
if not isinstance(out, np.ndarray) or out.ndim != 3 or out.dtype != np.uint8:
self.logger.log(f"[MIGAN] ONNX 출력 형식 오류: type={type(out)}, shape={getattr(out,'shape',None)}, dtype={getattr(out,'dtype',None)}",
level=logging.ERROR)
return None
elapsed = (time.time() - start) * 1000.0
self.logger.log(f"[MIGAN] 추론 완료: {elapsed:.2f} ms", level=logging.DEBUG)
# 5) BGR로 되돌려 반환
bgr_out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
return bgr_out
except Exception as e:
error_msg = str(e).lower()
if "invalid rank" in error_msg or "invalid argument" in error_msg:
self.logger.log(f"[MIGAN] ONNX 입력 차원 오류: {e}", level=logging.ERROR)
self.logger.log(f"[MIGAN] 입력 이미지 형태: {rgb_batch.shape if 'rgb_batch' in locals() else 'N/A'}", level=logging.ERROR)
self.logger.log(f"[MIGAN] 입력 마스크 형태: {mask_batch.shape if 'mask_batch' in locals() else 'N/A'}", level=logging.ERROR)
else:
self.logger.log(f"[MIGAN] inpaint 예외: {e}", level=logging.ERROR, exc_info=True)
return None
# ───────────────────────────────────────────────────────────────
# 네 ImageProcessor3에서 바로 부를 수 있게 하는 편의 함수
# ───────────────────────────────────────────────────────────────
def build_migan_from_toggle(toggle_states: dict, logger: Optional[object] = None) -> MIGANPipelineONNXCompat:
"""
toggle_states로부터 설정을 읽어 MIGANPipelineONNXCompat 인스턴스를 생성.
필수 :
- migan_onnx_path
선택 :
- migan_use_cuda (bool)
- migan_intra_threads (int)
- migan_inter_threads (int)
"""
onnx_path = toggle_states.get("migan_onnx_path", "")
if not onnx_path:
raise ValueError("toggle_states['migan_onnx_path'] 가 필요합니다.")
use_cuda = bool(toggle_states.get("migan_use_cuda", False))
intra = int(toggle_states.get("migan_intra_threads", 0) or 0)
inter = int(toggle_states.get("migan_inter_threads", 0) or 0)
return MIGANPipelineONNXCompat(
onnx_path=onnx_path,
logger=logger,
use_cuda=use_cuda,
intra_threads=intra,
inter_threads=inter,
)

Binary file not shown.

View File

@ -109,7 +109,7 @@ class Request_AI_Server:
Args: Args:
image: 입력 이미지 (np.ndarray 또는 파일 경로) image: 입력 이미지 (np.ndarray 또는 파일 경로)
use_local_rembg: True면 서버 상태와 관계없이 내장 rembg 모듈 직접 use_local_rembg: True면 서버 실패 백업으로 내장 rembg 모듈 사용
local_model_name: 내장 rembg에서 사용할 모델명 (기본값: "birefnet-general-lite") local_model_name: 내장 rembg에서 사용할 모델명 (기본값: "birefnet-general-lite")
""" """
try: try:
@ -123,24 +123,18 @@ class Request_AI_Server:
self.logger.log(f"이미지 파일을 읽을 수 없습니다: {image}", level=logging.ERROR) self.logger.log(f"이미지 파일을 읽을 수 없습니다: {image}", level=logging.ERROR)
return None return None
try: # # 🚀 테스트를 위해 무조건 내장 rembg 모듈 사용
# 내장 모듈 직접 사용 요청시 # self.logger.log("테스트 모드: 무조건 내장 rembg 모듈 사용", level=logging.INFO)
if use_local_rembg: # return self._use_backup_rembg(image_data, local_model_name)
self.logger.log(f"외부 인자에 의해 내장 rembg 모듈({local_model_name})을 직접 사용합니다.", level=logging.INFO)
result = self._use_backup_rembg(image_data, local_model_name)
if result is None:
self.logger.log("내장 rembg 모듈 사용이 요청되었지만 실행할 수 없습니다. rembg 관련 의존성을 확인하세요.", level=logging.ERROR)
return result
except Exception as e:
self.logger.log(f"내장 rembg 모듈 사용 실패: {e}", level=logging.ERROR, exc_info=True)
if use_local_rembg:
# 내장 모듈만 사용하도록 요청된 경우 서버 대안 시도하지 않음
return None
# 서버 상태 먼저 확인 # 서버 상태 먼저 확인
if not self.is_server_alive(self.rembg_base_url): if not self.is_server_alive(self.rembg_base_url):
if use_local_rembg:
self.logger.log("rembg 서버가 비정상입니다. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING) self.logger.log("rembg 서버가 비정상입니다. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING)
return self._use_backup_rembg(image_data, local_model_name) return self._use_backup_rembg(image_data, local_model_name)
else:
self.logger.log("rembg 서버가 비정상이지만 백업 모듈 사용이 비활성화되어 있습니다.", level=logging.ERROR)
return None
# base64 인코딩 (data URL) # base64 인코딩 (data URL)
_, img_encoded = cv2.imencode('.png', image_data) _, img_encoded = cv2.imencode('.png', image_data)
@ -153,22 +147,30 @@ class Request_AI_Server:
response = requests.post(self.rembg_api_url, json=payload) response = requests.post(self.rembg_api_url, json=payload)
if response.status_code != 200: if response.status_code != 200:
if use_local_rembg:
self.logger.log(f"rembg 서버 에러: {response.text}. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING) self.logger.log(f"rembg 서버 에러: {response.text}. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING)
return self._use_backup_rembg(image_data, local_model_name) return self._use_backup_rembg(image_data, local_model_name)
else:
self.logger.log(f"rembg 서버 에러: {response.text}. 백업 모듈 사용이 비활성화되어 있습니다.", level=logging.ERROR)
return None
# PNG 바이너리 → numpy # PNG 바이너리 → numpy
nparr = np.frombuffer(response.content, np.uint8) nparr = np.frombuffer(response.content, np.uint8)
result_img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) result_img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
if result_img is None or result_img.ndim != 3: if result_img is None or result_img.ndim != 3:
if use_local_rembg:
self.logger.log("서버 응답 이미지 디코딩 실패. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING) self.logger.log("서버 응답 이미지 디코딩 실패. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING)
return self._use_backup_rembg(image_data, local_model_name) return self._use_backup_rembg(image_data, local_model_name)
else:
self.logger.log("서버 응답 이미지 디코딩 실패. 백업 모듈 사용이 비활성화되어 있습니다.", level=logging.ERROR)
return None
# ---- 후처리: 마스크 정제 및 중앙 배치 ---- # ---- 후처리: 마스크 정제 및 중앙 배치 ----
return self._postprocess_rembg_result(result_img) return self._postprocess_rembg_result(result_img)
except Exception as e: except Exception as e:
self.logger.log(f"rembg 서버 에러: {e}. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING, exc_info=True) self.logger.log(f"request_rembg 실행 중 오류: {e}. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING, exc_info=True)
return self._use_backup_rembg(image_data if 'image_data' in locals() else image, local_model_name) return self._use_backup_rembg(image_data if 'image_data' in locals() else image, local_model_name)
def _use_backup_rembg(self, image_data: np.ndarray, model_name: str = "birefnet-general-lite") -> np.ndarray: def _use_backup_rembg(self, image_data: np.ndarray, model_name: str = "birefnet-general-lite") -> np.ndarray:

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""
tests/test_migan_module.py
- 로컬 이미지와 마스크로 MIGAN 파이프라인을 단독 테스트
- 너의 MaskModule 없이도 간단 폴리곤으로 마스크 생성 가능
"""
import os
import cv2
import numpy as np
import logging
from src.modules.migan_module import MIGANPipelineONNXCompat
def get_logger():
lg = logging.getLogger("MIGAN_TEST")
if not lg.handlers:
lg.setLevel(logging.DEBUG)
h = logging.StreamHandler()
h.setFormatter(logging.Formatter("[%(asctime)s][%(levelname)s] %(message)s"))
lg.addHandler(h)
# 네 프로젝트와 호환(.log 인터페이스)
class _Adapter:
def __init__(self, _lg): self._lg = _lg
def log(self, msg, level=logging.INFO, **kwargs): self._lg.log(level, msg)
return _Adapter(lg)
def make_demo_mask_like_yours(img_path: str):
"""네 MaskModule 스타일(텍스트영역=255) 흉내내기: 사각형 2개를 255로 채움 + 블러"""
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
h,w = img.shape[:2]
mask = np.zeros((h,w), np.uint8)
cv2.rectangle(mask, (int(0.1*w), int(0.2*h)), (int(0.4*w), int(0.3*h)), 255, -1)
cv2.rectangle(mask, (int(0.6*w), int(0.55*h)), (int(0.9*w), int(0.65*h)), 255, -1)
mask = cv2.GaussianBlur(mask, (15,15), 0)
return mask
def main():
logger = get_logger()
# 경로 설정
onnx_path = os.environ.get("MIGAN_ONNX", "migan_pipeline_v2.onnx")
img_path = os.environ.get("MIGAN_IMG", "examples/input.png")
out_path = os.environ.get("MIGAN_OUT", "examples/output_migan.png")
if not os.path.exists(onnx_path):
logger.log(f"ONNX 파일 없음: {onnx_path}", level=logging.ERROR); return
if not os.path.exists(img_path):
logger.log(f"입력 이미지 없음: {img_path}", level=logging.ERROR); return
migan = MIGANPipelineONNXCompat(onnx_path, logger=logger, use_cuda=True)
mask = make_demo_mask_like_yours(img_path)
out = migan.inpaint(img_path, mask)
if out is None:
logger.log("인페인팅 실패", level=logging.ERROR); return
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
cv2.imwrite(out_path, out)
logger.log(f"저장 완료: {out_path}")
if __name__ == "__main__":
main()

View File

@ -7,7 +7,7 @@ import logging
""" 프로그램 기본 정보 """ """ 프로그램 기본 정보 """
__title__ = "Edit_PartTimer" __title__ = "Edit_PartTimer"
__description__ = "편집알바생" __description__ = "편집알바생"
__version__ = "3.11.1" __version__ = "3.11.2"
__build__ = "1" # 빌드 번호 __build__ = "1" # 빌드 번호
__author__ = "WhenRideMyCar" __author__ = "WhenRideMyCar"
__author_email__ = "abc@gmail.com" __author_email__ = "abc@gmail.com"

View File

@ -3,6 +3,36 @@
# 3.11.2 업데이트 로그
### 패치 2 오류수정
- 누끼서버 폴백기능 오류 수정
### 패치 2 기능추가
- 이미지번역 폴백2 추가
### 패치 1 오류수정
- 폰트누락 수정 (죄송합니다. 기본폰트 이외의 폰트가 누락되었었습니다)
- 이미지워커 재시작 오류 수정
- 네이버검색 API 오류 수정
### 패치 1 기능추가
- 누끼서버 폴백기능 추가(Self수행)
### 3.10 마이너 업데이트 오류수정
- 기존버전 사용자 중 상세페이지 번역 누락 수정
- 이미지워커 중복큐 제거
- 이미지폰트가 저장되지 않던 문제 수정
### 3.10 마이너 업데이트 기능 추가
- gpt5의 특수문자 처리 로직 추가
- gpt5 폴백 개선
- 이미지 처리방식 webp 모드 추가(기본적용)
- 로거모듈 개선
# 3.11.1 업데이트 로그 # 3.11.1 업데이트 로그
### 패치 1 오류수정 ### 패치 1 오류수정