버전 3.11.4로 업데이트하며, 메모리 상황에 따른 동적 재시작 기능을 추가하고, GPT5 응답 처리 로직을 개선하였습니다. DLL 누락 문제를 해결하였으며, 업데이트 로그를 수정하여 변경 사항을 반영하였습니다.
This commit is contained in:
parent
6b9ac3321f
commit
672c5a3e4f
|
|
@ -0,0 +1,328 @@
|
|||
; AutoPercenty3 Inno Setup Script
|
||||
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
|
||||
; 20250823_090708에 생성됨
|
||||
|
||||
#define AppId "autopercenty"
|
||||
#define MyAppName "Edit_PartTimer"
|
||||
#define MyAppVersion "3.11.3"
|
||||
#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;
|
||||
|
|
@ -4083,6 +4083,13 @@ class ImageWorkerManager:
|
|||
self.restart_count = 0
|
||||
self.max_restart = 5
|
||||
self.image_worker_fatal = image_worker_fatal
|
||||
# 정책 로드(토글 기반)
|
||||
self._success_count = 0
|
||||
self._consecutive_mem_errors = 0
|
||||
self._periodic_restart_every = 0
|
||||
self._mem_restart_threshold = 0
|
||||
self._mem_error_escalate_after = 2
|
||||
self._load_worker_policies()
|
||||
# ─── 프로세스/큐 생성 ───────────────────────────────────
|
||||
self.task_q = Queue()
|
||||
self.result_q = Queue()
|
||||
|
|
@ -4143,6 +4150,30 @@ class ImageWorkerManager:
|
|||
timeout = 120 if self.first_call else 60
|
||||
result = await self._call_worker("process_single_image", timeout=timeout, **kwargs)
|
||||
self.first_call = False
|
||||
# 성공 시 연속 메모리 오류 카운터 초기화
|
||||
try:
|
||||
self._consecutive_mem_errors = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 메모리 임계치 기반 재시작
|
||||
try:
|
||||
if self._mem_restart_threshold and self._mem_restart_threshold > 0:
|
||||
vm = psutil.virtual_memory()
|
||||
if vm.percent >= self._mem_restart_threshold:
|
||||
self.logger.log(f"메모리 임계치 초과({vm.percent}% >= {self._mem_restart_threshold}%) → 워커 재시작", level=logging.WARNING)
|
||||
self.restart(cause="error")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 주기적 재시작 트리거: 성공으로 간주되는 경우에만 카운트
|
||||
try:
|
||||
if self._periodic_restart_every and isinstance(result, dict) and result.get("status") in {"translated", "original", "exclude", "success"}:
|
||||
self._success_count += 1
|
||||
if self._success_count % self._periodic_restart_every == 0:
|
||||
self.restart(cause="periodic")
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
async def safe_process_single_image(self, **kwargs):
|
||||
|
|
@ -4197,6 +4228,20 @@ class ImageWorkerManager:
|
|||
self.restart(cause="error")
|
||||
raise # 상위에서 필요하다면 재시도 로직을 추가
|
||||
except Exception as e: # ← RuntimeError 포함
|
||||
# primitive/메모리 오류 에스컬레이션 처리
|
||||
msg = str(e).lower()
|
||||
if any(s in msg for s in ["memory", "primitive", "out of memory", "unable to allocate", "cv::outofmemoryerror"]):
|
||||
try:
|
||||
self._consecutive_mem_errors += 1
|
||||
except Exception:
|
||||
self._consecutive_mem_errors = 1
|
||||
self.logger.log(f"메모리/primitive 오류 {self._consecutive_mem_errors}회: {e}", level=logging.WARNING)
|
||||
if self._consecutive_mem_errors >= max(1, self._mem_error_escalate_after):
|
||||
self.logger.log("연속 메모리 오류 기준 충족 → 워커 재시작", level=logging.WARNING)
|
||||
self.restart(cause="error")
|
||||
self._consecutive_mem_errors = 0
|
||||
else:
|
||||
# 일반 오류도 재시작
|
||||
self.logger.log(f"워커 내부 오류 → 재시작: {e}", level=logging.WARNING)
|
||||
self.restart(cause="error")
|
||||
raise
|
||||
|
|
@ -4274,8 +4319,25 @@ class ImageWorkerManager:
|
|||
if toggle_states is not None:
|
||||
self.toggle_states = toggle_states
|
||||
self.logger.log("ImageWorker toggle_states 업데이트됨", level=logging.DEBUG)
|
||||
self._load_worker_policies()
|
||||
|
||||
if unwanted_words is not None:
|
||||
self.unwanted_words = unwanted_words
|
||||
self.logger.log("ImageWorker unwanted_words 업데이트됨", level=logging.DEBUG)
|
||||
|
||||
def _load_worker_policies(self):
|
||||
"""toggle_states 로부터 워커 재시작 정책을 로드한다."""
|
||||
try:
|
||||
every = int(self.toggle_states.get("image_worker_periodic_restart_every", 0) or 0)
|
||||
mem_thr = int(self.toggle_states.get("image_worker_mem_restart_threshold", 0) or 0)
|
||||
esc_after = int(self.toggle_states.get("image_worker_mem_error_escalate_after", 2) or 2)
|
||||
self._periodic_restart_every = max(0, every)
|
||||
self._mem_restart_threshold = max(0, mem_thr)
|
||||
self._mem_error_escalate_after = max(1, esc_after)
|
||||
self.logger.log(
|
||||
f"워커 정책 로드: periodic={self._periodic_restart_every}, mem_thr={self._mem_restart_threshold}%, escalate_after={self._mem_error_escalate_after}",
|
||||
level=logging.INFO,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.log(f"워커 정책 로드 실패: {e}", level=logging.WARNING)
|
||||
|
||||
|
|
|
|||
|
|
@ -1212,7 +1212,9 @@ class MAIN_GUI(QMainWindow):
|
|||
"migan_use_cuda": False, # 3050 4GB면 True 권장(실패시 자동 CPU 폴백)
|
||||
"migan_intra_threads": 0,
|
||||
"migan_inter_threads": 0,
|
||||
|
||||
"migan_use_tensorrt": True,
|
||||
"migan_trt_fp16_enable": True,
|
||||
"migan_max_image_size": 2048
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
21
setup.py
21
setup.py
|
|
@ -113,6 +113,8 @@ dll_files = [
|
|||
system32_path = "C:/Windows/System32/downlevel"
|
||||
dll_include_files = [(os.path.join(system32_path, dll), dll) for dll in dll_files]
|
||||
|
||||
dll_include_files = [] # 충돌 가능성으로 인해 빈 리스트로 초기화
|
||||
|
||||
# 패들 라이브러리 경로 설정
|
||||
# 경로 수정: scripts/Lib -> Lib로 변경
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
@ -124,6 +126,7 @@ paddleocr_path = os.path.join(site_packages, "paddleocr")
|
|||
|
||||
paddle_includes = []
|
||||
|
||||
|
||||
# 경로가 존재하는 경우에만 포함
|
||||
if os.path.exists(paddle_path):
|
||||
paddle_includes.append((paddle_path, 'lib/paddle'))
|
||||
|
|
@ -133,8 +136,23 @@ if os.path.exists(paddleocr_path):
|
|||
paddle_includes.append((paddleocr_path, 'lib/paddleocr'))
|
||||
print(f"paddleocr 경로 추가: {paddleocr_path}")
|
||||
|
||||
onnxruntime_capi = os.path.join(site_packages, 'onnxruntime', 'capi')
|
||||
if os.path.exists(onnxruntime_capi):
|
||||
onnxruntime_includes.append((onnxruntime_capi, 'lib/onnxruntime/capi'))
|
||||
|
||||
vc_runtime_files = [
|
||||
('C:/Windows/System32/vcruntime140.dll', 'vcruntime140.dll'),
|
||||
('C:/Windows/System32/vcruntime140_1.dll', 'vcruntime140_1.dll'),
|
||||
('C:/Windows/System32/msvcp140.dll', 'msvcp140.dll'),
|
||||
('C:/Windows/System32/msvcp140_1.dll', 'msvcp140_1.dll'),
|
||||
('C:/Windows/System32/msvcp140_2.dll', 'msvcp140_2.dll'),
|
||||
('C:/Windows/System32/concrt140.dll', 'concrt140.dll'),
|
||||
('C:/Windows/System32/vcomp140.dll', 'vcomp140.dll'),
|
||||
]
|
||||
include_files.extend([f for f in vc_runtime_files if os.path.exists(f[0])])
|
||||
|
||||
# ✅ 기존 포함 파일 + DLL 추가
|
||||
include_files = dll_include_files + paddle_includes + [
|
||||
include_files = dll_include_files + paddle_includes + onnxruntime_includes + vc_runtime_files + [
|
||||
# include_files = dll_include_files + [
|
||||
# 나머지 파일들
|
||||
('src/ppocr/PP_Models', 'lib/src/ppocr/PP_Models'),
|
||||
|
|
@ -176,6 +194,7 @@ for src, dest in include_files:
|
|||
|
||||
# 사용된 패키지 정의
|
||||
build_exe_options = {
|
||||
'include_msvcr': True, # VC++ 런타임 자동 포함
|
||||
'packages': [
|
||||
'ctypes', 'asyncio',
|
||||
'subprocess', 'pyperclip', 'numpy',
|
||||
|
|
|
|||
|
|
@ -1131,52 +1131,63 @@ class OptionHandler:
|
|||
async def adjust_options_by_index(self, filtered, options_items, max_option_count):
|
||||
"""
|
||||
인덱스 기반 옵션 체크/해제. 상태 span을 재조회해서 실제 반영 확인.
|
||||
filtered: [{'index':i, ...}, ...] (선택대상)
|
||||
filtered: [{'index':i, 'original_name': str, ...}, ...] (선택대상)
|
||||
options_items: option_type_1의 items 리스트
|
||||
"""
|
||||
selected_count = 0
|
||||
filtered_idx_set = set([opt["index"] for opt in filtered])
|
||||
# 재정렬되는 환경 대응: 인덱스 대신 원본 옵션명으로 대상으로 판단
|
||||
filtered_name_set = set([opt.get("original_name") for opt in filtered if opt.get("original_name")])
|
||||
|
||||
# option_type_1 컨테이너 Locator (첫 번째 콜랩스 박스)
|
||||
option_type_1_container = self.page.locator("div#productMainContentContainerId div.ant-collapse-content-box").first
|
||||
|
||||
for idx, item in enumerate(options_items):
|
||||
element = item.get("checkbox_elem")
|
||||
original_name = item.get("original_name")
|
||||
if not original_name:
|
||||
continue
|
||||
|
||||
# 현재 상태 확인 (제외된 옵션 span)
|
||||
span_elems = await element.query_selector_all("span")
|
||||
# 현재 상태 재조회: 원본 옵션명으로 해당 아이템을 찾아 검사
|
||||
item_locator = option_type_1_container.locator(
|
||||
"li.ant-list-item[aria-roledescription='sortable']"
|
||||
).filter(has_text=original_name)
|
||||
|
||||
# '제외된 옵션' 여부 확인
|
||||
try:
|
||||
excluded_count = await item_locator.locator("span:has-text('제외된 옵션')").count()
|
||||
is_excluded = excluded_count > 0
|
||||
except Exception:
|
||||
is_excluded = False
|
||||
for span in span_elems:
|
||||
text = (await span.inner_text()).strip()
|
||||
if text == "제외된 옵션":
|
||||
is_excluded = True
|
||||
break
|
||||
item["is_excluded"] = is_excluded
|
||||
|
||||
should_checked = idx in filtered_idx_set and selected_count < max_option_count
|
||||
# 목표 상태 산정 (이름 기준)
|
||||
within_limit = (max_option_count == 0) or (selected_count < max_option_count)
|
||||
should_checked = (original_name in filtered_name_set) and within_limit
|
||||
should_excluded = not should_checked
|
||||
|
||||
self.logger.log(
|
||||
f"[{idx}] {item.get('original_name')} | 현재상태: {'제외됨' if is_excluded else '체크됨'} | 목표: {'체크' if should_checked else '제외'}",
|
||||
f"[{idx}] {original_name} | 현재상태: {'제외됨' if is_excluded else '체크됨'} | 목표: {'체크' if should_checked else '제외'}",
|
||||
level=logging.DEBUG,
|
||||
)
|
||||
|
||||
if should_checked and is_excluded:
|
||||
label_elem = await element.query_selector("label.ant-checkbox-wrapper")
|
||||
if label_elem:
|
||||
await label_elem.click()
|
||||
await self.page.wait_for_timeout(50) # 50~100ms
|
||||
# 클릭 대상 라벨 Locator (Locator는 재해결되어 분리 오류를 피함)
|
||||
label_locator = item_locator.locator("label.ant-checkbox-wrapper")
|
||||
|
||||
# 상태 변화 기다리며, 최대 1초 대기
|
||||
if should_checked and is_excluded:
|
||||
try:
|
||||
await label_locator.click()
|
||||
await self.page.wait_for_timeout(50)
|
||||
except Exception as e:
|
||||
self.logger.log(f"옵션[{idx}] 체크 시도 클릭 오류: {e}", level=logging.WARNING)
|
||||
|
||||
# 상태 변화 대기 및 재확인 (재정렬 대비 매번 재조회)
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0.2)
|
||||
span_elems = await element.query_selector_all("span")
|
||||
after_is_excluded = False
|
||||
for span in span_elems:
|
||||
text = (await span.inner_text()).strip()
|
||||
if text == "제외된 옵션":
|
||||
after_is_excluded = True
|
||||
break
|
||||
item_locator = option_type_1_container.locator(
|
||||
"li.ant-list-item[aria-roledescription='sortable']"
|
||||
).filter(has_text=original_name)
|
||||
excluded_count = await item_locator.locator("span:has-text('제외된 옵션')").count()
|
||||
after_is_excluded = excluded_count > 0
|
||||
if not after_is_excluded:
|
||||
break
|
||||
item["is_excluded"] = after_is_excluded
|
||||
|
||||
if after_is_excluded:
|
||||
self.logger.log(f"옵션[{idx}] 체크 시도 후에도 '제외된 옵션' 상태임! 재시도 필요", level=logging.ERROR)
|
||||
else:
|
||||
|
|
@ -1184,25 +1195,22 @@ class OptionHandler:
|
|||
self.logger.log(f"옵션[{idx}] 체크 성공", level=logging.INFO)
|
||||
|
||||
elif should_excluded and not is_excluded:
|
||||
label_elem = await element.query_selector("label.ant-checkbox-wrapper")
|
||||
if label_elem:
|
||||
await label_elem.click()
|
||||
await self.page.wait_for_timeout(50) # 50~100ms
|
||||
try:
|
||||
await label_locator.click()
|
||||
await self.page.wait_for_timeout(50)
|
||||
except Exception as e:
|
||||
self.logger.log(f"옵션[{idx}] 체크해제 시도 클릭 오류: {e}", level=logging.WARNING)
|
||||
|
||||
# 상태 변화 기다리며, 최대 1초 대기
|
||||
# 상태 변화 대기 및 재확인 (재정렬 대비 매번 재조회)
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0.2)
|
||||
span_elems = await element.query_selector_all("span")
|
||||
after_is_excluded = False
|
||||
for span in span_elems:
|
||||
text = (await span.inner_text()).strip()
|
||||
if text == "제외된 옵션":
|
||||
after_is_excluded = True
|
||||
break
|
||||
item_locator = option_type_1_container.locator(
|
||||
"li.ant-list-item[aria-roledescription='sortable']"
|
||||
).filter(has_text=original_name)
|
||||
excluded_count = await item_locator.locator("span:has-text('제외된 옵션')").count()
|
||||
after_is_excluded = excluded_count > 0
|
||||
if after_is_excluded:
|
||||
break
|
||||
item["is_excluded"] = after_is_excluded
|
||||
|
||||
if not after_is_excluded:
|
||||
self.logger.log(f"옵션[{idx}] 체크해제 시도 후에도 체크상태임! 재시도 필요", level=logging.ERROR)
|
||||
else:
|
||||
|
|
@ -1212,7 +1220,7 @@ class OptionHandler:
|
|||
if should_checked:
|
||||
selected_count += 1
|
||||
|
||||
if selected_count >= max_option_count:
|
||||
if max_option_count and selected_count >= max_option_count:
|
||||
self.logger.log(f"최대 선택 옵션 수 {max_option_count} 도달, 이후 옵션은 제외처리", level=logging.DEBUG)
|
||||
continue
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
|
|
@ -0,0 +1,276 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
GPU 유틸리티 모듈 - CUDA 지원 및 GPU 상태 관리
|
||||
|
||||
기능:
|
||||
- GPU 사용 가능성 검사
|
||||
- CUDA 지원 여부 확인
|
||||
- 전역 GPU 상태 관리
|
||||
- CPU 폴백 처리
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import platform
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class GPUManager:
|
||||
"""GPU 상태 관리 및 CUDA 지원 확인"""
|
||||
|
||||
def __init__(self, logger: Optional[object] = None):
|
||||
self.logger = logger or self._create_dummy_logger()
|
||||
|
||||
# GPU 상태 전역 변수들
|
||||
self.can_use_cuda = False
|
||||
self.cuda_available = False
|
||||
self.gpu_info = {}
|
||||
self.initialization_attempted = False
|
||||
|
||||
def _create_dummy_logger(self):
|
||||
"""로거가 없을 때 사용할 더미 로거"""
|
||||
class DummyLogger:
|
||||
def log(self, msg, level=logging.INFO, exc_info=False):
|
||||
print(f"[GPU] {msg}")
|
||||
return DummyLogger()
|
||||
|
||||
def initialize_gpu_state(self, toggle_states: Dict[str, Any]) -> None:
|
||||
"""
|
||||
GPU 상태를 초기화하고 전역 변수에 저장
|
||||
|
||||
Args:
|
||||
toggle_states: 설정 딕셔너리
|
||||
"""
|
||||
if self.initialization_attempted:
|
||||
return # 이미 초기화됨
|
||||
|
||||
self.initialization_attempted = True
|
||||
|
||||
# 사용자가 CUDA 사용을 원하는지 확인
|
||||
use_cuda_requested = toggle_states.get("use_cuda", False)
|
||||
|
||||
self.logger.log("=== GPU 상태 초기화 시작 ===", level=logging.INFO)
|
||||
self.logger.log(f"사용자 CUDA 사용 요청: {use_cuda_requested}", level=logging.INFO)
|
||||
|
||||
if not use_cuda_requested:
|
||||
self.logger.log("CUDA 사용이 비활성화됨 (toggle_states['use_cuda'] = False)", level=logging.INFO)
|
||||
self.can_use_cuda = False
|
||||
return
|
||||
|
||||
# GPU 하드웨어 검사
|
||||
gpu_detected = self._detect_gpu_hardware()
|
||||
|
||||
if not gpu_detected:
|
||||
self.logger.log("GPU 하드웨어가 감지되지 않음 - CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
return
|
||||
|
||||
# CUDA 설치 및 작동 상태 확인
|
||||
cuda_working = self._check_cuda_installation()
|
||||
|
||||
if not cuda_working:
|
||||
self.logger.log("CUDA가 정상적으로 작동하지 않음 - CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
return
|
||||
|
||||
# PyTorch/ONNXRuntime CUDA 지원 확인
|
||||
framework_cuda_support = self._check_framework_cuda_support()
|
||||
|
||||
if not framework_cuda_support:
|
||||
self.logger.log("ML 프레임워크에서 CUDA를 지원하지 않음 - CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
return
|
||||
|
||||
# 모든 검사 통과
|
||||
self.can_use_cuda = True
|
||||
self.cuda_available = True
|
||||
|
||||
self.logger.log("✅ CUDA 사용 가능 - GPU 가속 모드로 동작", level=logging.INFO)
|
||||
self.logger.log(f"GPU 정보: {self.gpu_info}", level=logging.INFO)
|
||||
self.logger.log("=== GPU 상태 초기화 완료 ===", level=logging.INFO)
|
||||
|
||||
def _detect_gpu_hardware(self) -> bool:
|
||||
"""GPU 하드웨어 감지"""
|
||||
try:
|
||||
if platform.system() != "Windows":
|
||||
self.logger.log("현재 Windows만 지원됨", level=logging.WARNING)
|
||||
return False
|
||||
|
||||
# nvidia-smi 명령어로 GPU 확인
|
||||
result = subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader,nounits"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
gpu_lines = result.stdout.strip().split('\n')
|
||||
for i, line in enumerate(gpu_lines):
|
||||
if line.strip():
|
||||
parts = [p.strip() for p in line.split(',')]
|
||||
if len(parts) >= 3:
|
||||
self.gpu_info[f'gpu_{i}'] = {
|
||||
'name': parts[0],
|
||||
'memory_mb': parts[1],
|
||||
'driver_version': parts[2]
|
||||
}
|
||||
|
||||
self.logger.log(f"GPU 하드웨어 감지됨: {len(self.gpu_info)}개", level=logging.INFO)
|
||||
for gpu_id, info in self.gpu_info.items():
|
||||
self.logger.log(f" {gpu_id}: {info['name']} ({info['memory_mb']}MB, 드라이버 {info['driver_version']})", level=logging.INFO)
|
||||
return True
|
||||
else:
|
||||
self.logger.log(f"nvidia-smi 실행 실패: {result.stderr}", level=logging.WARNING)
|
||||
return False
|
||||
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError) as e:
|
||||
self.logger.log(f"GPU 하드웨어 감지 실패: {e}", level=logging.WARNING)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(f"GPU 하드웨어 감지 중 예외: {e}", level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
|
||||
def _check_cuda_installation(self) -> bool:
|
||||
"""CUDA 설치 및 작동 상태 확인"""
|
||||
try:
|
||||
# nvcc 버전 확인
|
||||
result = subprocess.run(
|
||||
["nvcc", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
version_output = result.stdout
|
||||
self.logger.log(f"CUDA 컴파일러 감지됨", level=logging.INFO)
|
||||
|
||||
# 버전 정보 추출
|
||||
for line in version_output.split('\n'):
|
||||
if 'release' in line.lower():
|
||||
self.logger.log(f"CUDA 버전: {line.strip()}", level=logging.INFO)
|
||||
break
|
||||
|
||||
return True
|
||||
else:
|
||||
self.logger.log("CUDA 컴파일러(nvcc)를 찾을 수 없음", level=logging.WARNING)
|
||||
return False
|
||||
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
||||
self.logger.log(f"CUDA 설치 확인 실패: {e}", level=logging.WARNING)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(f"CUDA 설치 확인 중 예외: {e}", level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
|
||||
def _check_framework_cuda_support(self) -> bool:
|
||||
"""ML 프레임워크의 CUDA 지원 확인"""
|
||||
support_count = 0
|
||||
|
||||
# ONNXRuntime CUDA 지원 확인
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
providers = ort.get_available_providers()
|
||||
if "CUDAExecutionProvider" in providers:
|
||||
self.logger.log("✅ ONNXRuntime CUDA 지원 확인됨", level=logging.INFO)
|
||||
support_count += 1
|
||||
else:
|
||||
self.logger.log("❌ ONNXRuntime CUDA 지원 없음", level=logging.WARNING)
|
||||
self.logger.log(f"사용 가능한 providers: {providers}", level=logging.DEBUG)
|
||||
except ImportError:
|
||||
self.logger.log("ONNXRuntime가 설치되지 않음", level=logging.WARNING)
|
||||
except Exception as e:
|
||||
self.logger.log(f"ONNXRuntime CUDA 지원 확인 실패: {e}", level=logging.WARNING)
|
||||
|
||||
# PyTorch CUDA 지원 확인 (선택적)
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
device_count = torch.cuda.device_count()
|
||||
current_device = torch.cuda.current_device()
|
||||
device_name = torch.cuda.get_device_name(current_device)
|
||||
self.logger.log(f"✅ PyTorch CUDA 지원 확인됨 ({device_count}개 디바이스, 현재: {device_name})", level=logging.INFO)
|
||||
support_count += 1
|
||||
else:
|
||||
self.logger.log("❌ PyTorch CUDA 지원 없음", level=logging.WARNING)
|
||||
except ImportError:
|
||||
self.logger.log("PyTorch가 설치되지 않음 (선택적)", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
self.logger.log(f"PyTorch CUDA 지원 확인 실패: {e}", level=logging.WARNING)
|
||||
|
||||
# 최소 하나의 프레임워크에서 CUDA를 지원해야 함
|
||||
if support_count > 0:
|
||||
self.logger.log(f"ML 프레임워크 CUDA 지원: {support_count}개 확인됨", level=logging.INFO)
|
||||
return True
|
||||
else:
|
||||
self.logger.log("ML 프레임워크에서 CUDA를 지원하지 않음", level=logging.WARNING)
|
||||
return False
|
||||
|
||||
def get_cuda_status(self) -> Dict[str, Any]:
|
||||
"""현재 CUDA 상태 정보 반환"""
|
||||
return {
|
||||
"can_use_cuda": self.can_use_cuda,
|
||||
"cuda_available": self.cuda_available,
|
||||
"gpu_info": self.gpu_info.copy(),
|
||||
"initialization_attempted": self.initialization_attempted
|
||||
}
|
||||
|
||||
def force_cpu_mode(self) -> None:
|
||||
"""강제로 CPU 모드로 전환"""
|
||||
self.can_use_cuda = False
|
||||
self.logger.log("강제로 CPU 모드로 전환됨", level=logging.WARNING)
|
||||
|
||||
def log_gpu_memory_usage(self) -> None:
|
||||
"""현재 GPU 메모리 사용량 로깅"""
|
||||
if not self.can_use_cuda:
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=memory.used,memory.total", "--format=csv,noheader,nounits"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip():
|
||||
parts = [p.strip() for p in line.split(',')]
|
||||
if len(parts) >= 2:
|
||||
used_mb = int(parts[0])
|
||||
total_mb = int(parts[1])
|
||||
usage_percent = (used_mb / total_mb) * 100
|
||||
self.logger.log(
|
||||
f"GPU {i} 메모리 사용량: {used_mb}MB/{total_mb}MB ({usage_percent:.1f}%)",
|
||||
level=logging.INFO
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.log(f"GPU 메모리 사용량 확인 실패: {e}", level=logging.DEBUG)
|
||||
|
||||
|
||||
# 전역 GPU 관리자 인스턴스 (선택적 사용)
|
||||
_global_gpu_manager = None
|
||||
|
||||
def get_global_gpu_manager(logger=None) -> GPUManager:
|
||||
"""전역 GPU 관리자 인스턴스 반환"""
|
||||
global _global_gpu_manager
|
||||
if _global_gpu_manager is None:
|
||||
_global_gpu_manager = GPUManager(logger)
|
||||
return _global_gpu_manager
|
||||
|
||||
|
||||
def check_cuda_simple() -> bool:
|
||||
"""간단한 CUDA 사용 가능성 확인 (캐시 없음)"""
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
providers = ort.get_available_providers()
|
||||
return "CUDAExecutionProvider" in providers
|
||||
except:
|
||||
return False
|
||||
|
|
@ -25,6 +25,7 @@ from translatepy.translators.google import GoogleTranslate
|
|||
# from src.modules.background_removal_module_pp import PPMattingBackgroundRemovalModule # (변경)
|
||||
|
||||
from src.modules.request_inpaint import Request_AI_Server
|
||||
from src.modules.gpu_utils import GPUManager
|
||||
|
||||
class ImageProcessor3:
|
||||
"""이미지 다운로드, OCR, 번역 처리를 담당하는 클래스"""
|
||||
|
|
@ -37,6 +38,10 @@ class ImageProcessor3:
|
|||
self.unwanted_texts = unwanted_words
|
||||
self.authenticated_by_admin = authenticated_by_admin
|
||||
|
||||
# GPU 관리자 초기화
|
||||
self.gpu_manager = GPUManager(logger=logger)
|
||||
self.gpu_manager.initialize_gpu_state(toggle_states)
|
||||
|
||||
self.logger.log(f"ImageProcessor3 Init toggle_states: {self.toggle_states}", level=logging.DEBUG)
|
||||
|
||||
self.papago_translator = papago_translator
|
||||
|
|
@ -94,7 +99,13 @@ class ImageProcessor3:
|
|||
if not self.is_frozen():
|
||||
self.request_rembg_server_url = self.toggle_states.get("request_rembg_server_url_local", None)
|
||||
|
||||
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)
|
||||
# Request_AI_Server에도 GPU 상태 전달
|
||||
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,
|
||||
gpu_manager=self.gpu_manager
|
||||
)
|
||||
|
||||
self.gtranslate = GoogleTranslate()
|
||||
|
||||
|
|
@ -102,7 +113,16 @@ class ImageProcessor3:
|
|||
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)
|
||||
# GPU 상태에 따라 CUDA 사용 여부 결정
|
||||
enhanced_toggle_states = self.toggle_states.copy()
|
||||
if self.gpu_manager.can_use_cuda:
|
||||
enhanced_toggle_states["migan_use_cuda"] = enhanced_toggle_states.get("use_cuda", False)
|
||||
self.logger.log(f"MIGAN CUDA 사용 설정: {enhanced_toggle_states['migan_use_cuda']}", level=logging.INFO)
|
||||
else:
|
||||
enhanced_toggle_states["migan_use_cuda"] = False
|
||||
self.logger.log("MIGAN CUDA 사용 불가 - CPU 모드로 설정", level=logging.INFO)
|
||||
|
||||
self.migan = build_migan_from_toggle(enhanced_toggle_states, logger=self.logger)
|
||||
else:
|
||||
self.migan = None
|
||||
self.logger.log("migan_onnx_path 미설정: MIGAN 비활성", level=logging.INFO)
|
||||
|
|
@ -1026,10 +1046,11 @@ class ImageProcessor3:
|
|||
except Exception as e:
|
||||
msg = str(e).lower()
|
||||
# 메모리 / primitive 관련 오류 → OCR 모듈 재초기화 후 1회 재시도
|
||||
if any(err in msg for err in ["create a primitive", "memory object", "unable to allocate"]):
|
||||
if any(err in msg for err in ["create a primitive", "memory object", "unable to allocate", "out of memory", "cv::outofmemoryerror"]):
|
||||
ok = self.reset_ocr_module()
|
||||
if ok:
|
||||
return self.ocr_module.detect_text(img_path)
|
||||
# 재시도 시 실패하면 MemoryError를 재전파하여 상위에서 워커 재시작 트리거
|
||||
return self.ocr_module.detect_text(img_path, raise_on_memory_error=True)
|
||||
# 그 외 예외는 그대로 상위로 전달
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ class OCRModule:
|
|||
|
||||
# CPU만 사용하도록 환경 변수 설정
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||
# OpenCV OpenCL 비활성화 (메모리 primitive 오류 예방)
|
||||
try:
|
||||
import cv2 as _cv2
|
||||
_cv2.ocl.setUseOpenCL(False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 멀티 스레드 사용 시 오류 방지
|
||||
os.environ["OMP_NUM_THREADS"] = "1"
|
||||
|
|
@ -45,13 +51,16 @@ class OCRModule:
|
|||
try:
|
||||
from paddleocr import PaddleOCR
|
||||
|
||||
# 메모리 사용량을 줄이기 위해 배치/스레드 수를 제한
|
||||
ocr = PaddleOCR(
|
||||
use_gpu=False,
|
||||
use_angle_cls=True, # 텍스트 방향 분류 활성화
|
||||
lang="ch",
|
||||
det_model_dir=self.det_model_dir,
|
||||
rec_model_dir=self.rec_model_dir,
|
||||
cls_model_dir=self.cls_model_dir
|
||||
cls_model_dir=self.cls_model_dir,
|
||||
rec_batch_num=4, # 기본(6)보다 낮춰 메모리 사용 축소
|
||||
cpu_threads=2 # 스레드 수 제한으로 피크 메모리 완화
|
||||
)
|
||||
return ocr
|
||||
except Exception as e:
|
||||
|
|
@ -60,7 +69,7 @@ class OCRModule:
|
|||
return None
|
||||
|
||||
|
||||
def detect_text(self, image_path: str, method: str = 'polygon') -> List[Dict[str, Any]]:
|
||||
def detect_text(self, image_path: str, method: str = 'polygon', raise_on_memory_error: bool = False) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
이미지에서 텍스트를 감지하고 다양한 방식으로 영역 반환
|
||||
|
||||
|
|
@ -110,20 +119,37 @@ class OCRModule:
|
|||
try:
|
||||
ocr_raw_results = self.ocr.ocr(image)
|
||||
except Exception as e:
|
||||
# 메모리 오류나 기타 예외가 발생할 경우 1/2로 추가 축소 후 재시도
|
||||
if 'unable to allocate' in str(e) or isinstance(e, MemoryError):
|
||||
# 메모리 오류나 기타 예외가 발생할 경우 1/2 → 1/4로 단계적 축소 재시도
|
||||
err_msg = str(e).lower()
|
||||
mem_signals = [
|
||||
'unable to allocate',
|
||||
'out of memory',
|
||||
'could not create a memory object',
|
||||
'create a primitive',
|
||||
'cv::outofmemoryerror',
|
||||
]
|
||||
if isinstance(e, MemoryError) or any(s in err_msg for s in mem_signals):
|
||||
try:
|
||||
h, w = image.shape[:2]
|
||||
self.logger.log(
|
||||
f"🔁 OCR 메모리 오류 재시도: 이미지 1/2 축소 ({w}x{h}) → ({w//2}x{h//2})",
|
||||
level=logging.WARNING,
|
||||
)
|
||||
image_small = cv2.resize(image, (w // 2, h // 2), interpolation=cv2.INTER_AREA)
|
||||
image_small = cv2.resize(image, (max(1, w // 2), max(1, h // 2)), interpolation=cv2.INTER_AREA)
|
||||
ocr_raw_results = self.ocr.ocr(image_small)
|
||||
except Exception as e2:
|
||||
try:
|
||||
self.logger.log(
|
||||
f"❌ OCR 재시도 실패: {e2}", level=logging.ERROR, exc_info=True,
|
||||
f"🔁 2차 재시도(1/4 축소): {e2}", level=logging.WARNING,
|
||||
)
|
||||
image_small2 = cv2.resize(image, (max(1, w // 4), max(1, h // 4)), interpolation=cv2.INTER_AREA)
|
||||
ocr_raw_results = self.ocr.ocr(image_small2)
|
||||
except Exception as e3:
|
||||
self.logger.log(
|
||||
f"❌ OCR 재시도 실패: {e3}", level=logging.ERROR, exc_info=True,
|
||||
)
|
||||
if raise_on_memory_error:
|
||||
raise MemoryError(f"OCR memory/primitive failure after retries: {e3}")
|
||||
return []
|
||||
else:
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
from PIL import ImageFont
|
||||
font_path = r"D:\py\AutoPercenty3_311\src\modules\fonts\HakgyoansimDunggeunmisoTTFB.ttf"
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, 24)
|
||||
print("폰트 로드 성공")
|
||||
except Exception as e:
|
||||
print("폰트 로드 실패:", e)
|
||||
|
|
@ -52,14 +52,31 @@ class GPTClient:
|
|||
"【": "[",
|
||||
"】": "]",
|
||||
"(": "(",
|
||||
")": ")"
|
||||
# 주의: 콤마(,)나 기타 JSON 구조 문자는 변환하지 않음
|
||||
")": ")",
|
||||
"“": '"',
|
||||
"”": '"',
|
||||
"„": '"',
|
||||
"‘": "'",
|
||||
"’": "'",
|
||||
"\ufeff": "", # BOM 제거
|
||||
"\u200b": "", # zero width space
|
||||
"\u200c": "",
|
||||
"\u200d": "",
|
||||
}
|
||||
|
||||
cleaned = content
|
||||
for old_char, new_char in json_replacements.items():
|
||||
cleaned = cleaned.replace(old_char, new_char)
|
||||
|
||||
# 키의 닫는 따옴표 앞에 잘못 삽입된 백슬래시를 제거: "1\": → "1":
|
||||
cleaned = re.sub(r'\\"(?=\s*:)', '"', cleaned)
|
||||
|
||||
# 객체/배열 종료 직전의 트레일링 콤마 제거
|
||||
cleaned = re.sub(r',\s*([}\]])', r'\1', cleaned)
|
||||
|
||||
# 앞뒤 공백 및 개행 정리
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
return cleaned
|
||||
|
||||
def _create_gpt5_response(self, prompt: str) -> dict:
|
||||
|
|
@ -90,7 +107,16 @@ class GPTClient:
|
|||
# 추가 로깅으로 디버깅 개선
|
||||
self.logger.log(f'JSON 파싱 시도 전 정리된 내용: {cleaned_content}', level=logging.DEBUG)
|
||||
|
||||
try:
|
||||
return json.loads(cleaned_content)
|
||||
except json.JSONDecodeError:
|
||||
# 추가 보정 1차: 키 끝의 이스케이프 제거 및 트레일링 콤마 제거 후 재시도
|
||||
repaired_once = re.sub(r'\\"(?=\s*:)', '"', cleaned_content)
|
||||
repaired_once = re.sub(r',\s*([}\]])', r'\1', repaired_once)
|
||||
if repaired_once != cleaned_content:
|
||||
self.logger.log(f'JSON 1차 보정 후 재시도: {repaired_once}', level=logging.DEBUG)
|
||||
return json.loads(repaired_once)
|
||||
raise
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.log(f'JSON 디코딩 실패: {e}. 원본 응답: {content}', level=logging.ERROR, exc_info=True)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -7,7 +7,7 @@ import logging
|
|||
""" 프로그램 기본 정보 """
|
||||
__title__ = "Edit_PartTimer"
|
||||
__description__ = "편집알바생"
|
||||
__version__ = "3.11.2"
|
||||
__version__ = "3.11.4"
|
||||
__build__ = "1" # 빌드 번호
|
||||
__author__ = "WhenRideMyCar"
|
||||
__author_email__ = "abc@gmail.com"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,19 @@
|
|||
|
||||
|
||||
|
||||
# 3.11.2 업데이트 로그
|
||||
# 3.11.4 업데이트 로그
|
||||
|
||||
### 패치 4 오류수정
|
||||
- GPT5 응답처리 변경
|
||||
- DLL 누락 추가
|
||||
### 패치 4 기능변경
|
||||
- 메모리 상황에 따른 동적재시작 추가
|
||||
|
||||
### 패치 3 기능변경
|
||||
- 옵션 선택방식 변경(업데이트 대응)
|
||||
|
||||
### 패치 2 오류수정
|
||||
- 누끼서버 폴백기능 오류 수정
|
||||
|
||||
### 패치 2 기능추가
|
||||
<!-- - CPU 이미지번역 개선(품질/속도개선) -->
|
||||
- 이미지번역 폴백2 추가
|
||||
|
|
@ -17,16 +24,15 @@
|
|||
- 폰트누락 수정 (죄송합니다. 기본폰트 이외의 폰트가 누락되었었습니다)
|
||||
- 이미지워커 재시작 오류 수정
|
||||
- 네이버검색 API 오류 수정
|
||||
|
||||
### 패치 1 기능추가
|
||||
- 누끼서버 폴백기능 추가(Self수행)
|
||||
|
||||
### 3.10 마이너 업데이트 오류수정
|
||||
### 3.11 마이너 업데이트 오류수정
|
||||
- 기존버전 사용자 중 상세페이지 번역 누락 수정
|
||||
- 이미지워커 중복큐 제거
|
||||
- 이미지폰트가 저장되지 않던 문제 수정
|
||||
|
||||
### 3.10 마이너 업데이트 기능 추가
|
||||
### 3.11 마이너 업데이트 기능 추가
|
||||
- gpt5의 특수문자 처리 로직 추가
|
||||
- gpt5 폴백 개선
|
||||
- 이미지 처리방식 webp 모드 추가(기본적용)
|
||||
|
|
|
|||
Loading…
Reference in New Issue