Compare commits
3 Commits
master
...
celery_wor
| Author | SHA1 | Date |
|---|---|---|
|
|
e3835b3d15 | |
|
|
a927fad068 | |
|
|
3be6c6ee9b |
|
|
@ -13,3 +13,5 @@ pyvenv.cfg
|
|||
*.pyz
|
||||
*.pywz
|
||||
*.pyzw
|
||||
dist/
|
||||
build/
|
||||
|
|
|
|||
537
ITServer.log
537
ITServer.log
|
|
@ -304,110 +304,441 @@ RuntimeError: 이미지 저장 중 오류 발생: The truth value of an array wi
|
|||
[2025-07-04 02:20:54,459] [MainThread] [INFO] [image_processor2.py:process_single_image:114] 텍스트 렌더링 완료
|
||||
[2025-07-04 02:20:54,469] [MainThread] [INFO] [postImageManager.py:save_image_to_path:38] 이미지 저장 완료 : D:\py\IT_Server\temp_images\translated_multi_img_7.png
|
||||
[2025-07-04 02:20:54,506] [MainThread] [INFO] [image_processor2.py:process_single_image:118] 이미지 7 번역 완료: D:\py\IT_Server\temp_images\translated_multi_img_7.png
|
||||
[2025-07-04 09:18:48,438] [MainThread] [ERROR] [image_processor2.py:__init__:34] ❌ PaddleOCR 초기화 실패: No module named 'paddleocr'
|
||||
[2025-07-04 18:18:17,681] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: No module named 'paddleocr'
|
||||
Traceback (most recent call last):
|
||||
File "/home/ckh08045/work/IT_Server/modules/ocr_module.py", line 31, in initialize_ocr
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
ModuleNotFoundError: No module named 'paddleocr'
|
||||
|
||||
[2025-07-04 09:27:49,076] [MainThread] [INFO] [image_processor2.py:__init__:34] PaddleOCR use_gpu: False
|
||||
[2025-07-04 09:27:50,652] [MainThread] [INFO] [main.py:main:38] 마스크 모듈 초기화 완료
|
||||
[2025-07-04 09:27:50,653] [MainThread] [DEBUG] [image_processor2.py:__init__:37] 폰트 로드 성공: /home/ckh08045/work/IT_Server/modules/fonts/HakgyoansimDunggeunmisoTTFB.ttf
|
||||
[2025-07-04 09:28:21,688] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/4.jpg
|
||||
[2025-07-04 09:28:21,689] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,690] [MainThread] [INFO] [events.py:_run:81] 이미지 1 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:28:21,690] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/5.jpg
|
||||
[2025-07-04 09:28:21,690] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,691] [MainThread] [INFO] [events.py:_run:81] 이미지 2 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:28:21,691] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/7.jpg
|
||||
[2025-07-04 09:28:21,691] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,692] [MainThread] [INFO] [events.py:_run:81] 이미지 3 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:28:21,692] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/1.jpg
|
||||
[2025-07-04 09:28:21,692] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,692] [MainThread] [INFO] [events.py:_run:81] 이미지 4 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:28:21,693] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/6.jpg
|
||||
[2025-07-04 09:28:21,693] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,693] [MainThread] [INFO] [events.py:_run:81] 이미지 5 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:28:21,693] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/2.jpg
|
||||
[2025-07-04 09:28:21,694] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,694] [MainThread] [INFO] [events.py:_run:81] 이미지 6 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:28:21,694] [MainThread] [ERROR] [image_translate_server.py:sem_task:81] 이미지 파일을 찾을 수 없습니다: ./img/3.jpg
|
||||
[2025-07-04 09:28:21,695] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 0개 필터링 완료
|
||||
[2025-07-04 09:28:21,695] [MainThread] [INFO] [events.py:_run:81] 이미지 7 중국어 텍스트 없음, 원본 이미지 반환
|
||||
[2025-07-04 09:29:09,454] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:29:16,132] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[431.0, 71.0], [467.0, 71.0], [467.0, 86.0], [431.0, 86.0]], ('85°C', 0.8006035685539246)], [[[516.0, 69.0], [558.0, 69.0], [558.0, 88.0], [516.0, 88.0]], ('185°F', 0.9477267265319824)], [[[430.0, 138.0], [469.0, 138.0], [469.0, 156.0], [430.0, 156.0]], ('80℃', 0.9489307403564453)], [[[510.0, 138.0], [560.0, 136.0], [561.0, 154.0], [511.0, 156.0]], ('176F', 0.9774733781814575)], [[[431.0, 204.0], [487.0, 208.0], [486.0, 226.0], [430.0, 223.0]], ('70°℃', 0.8445404767990112)], [[[492.0, 208.0], [559.0, 204.0], [561.0, 222.0], [494.0, 226.0]], ('158F', 0.9599605798721313)], [[[430.0, 274.0], [469.0, 274.0], [469.0, 292.0], [430.0, 292.0]], ('60℃', 0.9590663313865662)], [[[508.0, 275.0], [560.0, 275.0], [560.0, 292.0], [508.0, 292.0]], ('140°F', 0.9507732391357422)], [[[431.0, 344.0], [467.0, 344.0], [467.0, 359.0], [431.0, 359.0]], ('50°C', 0.8583955764770508)], [[[514.0, 342.0], [560.0, 342.0], [560.0, 360.0], [514.0, 360.0]], ('122°F', 0.859941303730011)], [[[429.0, 412.0], [467.0, 409.0], [468.0, 428.0], [430.0, 431.0]], ('40C', 0.8281939029693604)], [[[515.0, 412.0], [559.0, 412.0], [559.0, 427.0], [515.0, 427.0]], ('104°F', 0.9546246528625488)], [[[24.0, 447.0], [227.0, 449.0], [226.0, 477.0], [24.0, 475.0]], ('精确的温度控制', 0.9948815107345581)], [[[25.0, 484.0], [262.0, 484.0], [262.0, 504.0], [25.0, 504.0]], ('温度范围从30℃至85℃', 0.9833760261535645)], [[[430.0, 480.0], [468.0, 480.0], [468.0, 497.0], [430.0, 497.0]], ('30°C', 0.7743632197380066)], [[[513.0, 480.0], [549.0, 480.0], [549.0, 497.0], [513.0, 497.0]], ('86°F', 0.9255774617195129)], [[[21.0, 514.0], [428.0, 512.0], [428.0, 536.0], [21.0, 538.0]], ('Temperature range from 86 F to 185°F', 0.9300525188446045)], [[[25.0, 542.0], [572.0, 543.0], [572.0, 570.0], [25.0, 569.0]], ('PRECISE TEMPERATURECONTROL', 0.9752768874168396)]]]
|
||||
[2025-07-04 09:29:16,133] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[431.0, 71.0], [467.0, 71.0], [467.0, 86.0], [431.0, 86.0]], ('85°C', 0.8006035685539246)], [[[516.0, 69.0], [558.0, 69.0], [558.0, 88.0], [516.0, 88.0]], ('185°F', 0.9477267265319824)], [[[430.0, 138.0], [469.0, 138.0], [469.0, 156.0], [430.0, 156.0]], ('80℃', 0.9489307403564453)], [[[510.0, 138.0], [560.0, 136.0], [561.0, 154.0], [511.0, 156.0]], ('176F', 0.9774733781814575)], [[[431.0, 204.0], [487.0, 208.0], [486.0, 226.0], [430.0, 223.0]], ('70°℃', 0.8445404767990112)], [[[492.0, 208.0], [559.0, 204.0], [561.0, 222.0], [494.0, 226.0]], ('158F', 0.9599605798721313)], [[[430.0, 274.0], [469.0, 274.0], [469.0, 292.0], [430.0, 292.0]], ('60℃', 0.9590663313865662)], [[[508.0, 275.0], [560.0, 275.0], [560.0, 292.0], [508.0, 292.0]], ('140°F', 0.9507732391357422)], [[[431.0, 344.0], [467.0, 344.0], [467.0, 359.0], [431.0, 359.0]], ('50°C', 0.8583955764770508)], [[[514.0, 342.0], [560.0, 342.0], [560.0, 360.0], [514.0, 360.0]], ('122°F', 0.859941303730011)], [[[429.0, 412.0], [467.0, 409.0], [468.0, 428.0], [430.0, 431.0]], ('40C', 0.8281939029693604)], [[[515.0, 412.0], [559.0, 412.0], [559.0, 427.0], [515.0, 427.0]], ('104°F', 0.9546246528625488)], [[[24.0, 447.0], [227.0, 449.0], [226.0, 477.0], [24.0, 475.0]], ('精确的温度控制', 0.9948815107345581)], [[[25.0, 484.0], [262.0, 484.0], [262.0, 504.0], [25.0, 504.0]], ('温度范围从30℃至85℃', 0.9833760261535645)], [[[430.0, 480.0], [468.0, 480.0], [468.0, 497.0], [430.0, 497.0]], ('30°C', 0.7743632197380066)], [[[513.0, 480.0], [549.0, 480.0], [549.0, 497.0], [513.0, 497.0]], ('86°F', 0.9255774617195129)], [[[21.0, 514.0], [428.0, 512.0], [428.0, 536.0], [21.0, 538.0]], ('Temperature range from 86 F to 185°F', 0.9300525188446045)], [[[25.0, 542.0], [572.0, 543.0], [572.0, 570.0], [25.0, 569.0]], ('PRECISE TEMPERATURECONTROL', 0.9752768874168396)]]
|
||||
[2025-07-04 09:29:16,134] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 2개 필터링 완료
|
||||
[2025-07-04 09:29:20,580] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['85°C', '185°F', '80℃', '176F', '70°℃', '158F', '60℃', '140°F', '50°C', '122°F', '40C', '104°F', '정확한 온도 조절', '온도 범위는 30℃에서 85℃까지입니다', '30°C', '86°F', '온도 범위는 86°F에서 185°F까지입니다', '정확한 온도 조절']
|
||||
[2025-07-04 09:29:20,580] [MainThread] [INFO] [events.py:_run:81] 이미지 1 치환됨
|
||||
[2025-07-04 09:29:20,606] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:29:38,692] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:29:39,048] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:29:39,095] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_1.png
|
||||
[2025-07-04 09:29:39,273] [MainThread] [INFO] [events.py:_run:81] 이미지 1 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_1.png
|
||||
[2025-07-04 09:29:39,295] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:29:41,939] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[339.0, 103.0], [855.0, 103.0], [855.0, 182.0], [339.0, 182.0]], ('现代极简风格', 0.9964838027954102)], [[[235.0, 219.0], [963.0, 219.0], [963.0, 279.0], [235.0, 279.0]], ('更易搭配各种使用场景', 0.9974350929260254)], [[[478.0, 727.0], [649.0, 727.0], [649.0, 762.0], [478.0, 762.0]], ('★WELCOME', 0.8576079607009888)], [[[407.0, 760.0], [724.0, 760.0], [724.0, 855.0], [407.0, 855.0]], ('欢迎光临', 0.999948263168335)], [[[473.0, 856.0], [624.0, 863.0], [623.0, 898.0], [471.0, 891.0]], ('限时促销礼惠全城', 0.9319617748260498)], [[[446.0, 947.0], [640.0, 962.0], [638.0, 995.0], [443.0, 980.0]], ('满499减200/满999减500', 0.927433431148529)], [[[481.0, 980.0], [595.0, 991.0], [592.0, 1016.0], [479.0, 1005.0]], ('动的间167', 0.7950097918510437)]]]
|
||||
[2025-07-04 09:29:41,940] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[339.0, 103.0], [855.0, 103.0], [855.0, 182.0], [339.0, 182.0]], ('现代极简风格', 0.9964838027954102)], [[[235.0, 219.0], [963.0, 219.0], [963.0, 279.0], [235.0, 279.0]], ('更易搭配各种使用场景', 0.9974350929260254)], [[[478.0, 727.0], [649.0, 727.0], [649.0, 762.0], [478.0, 762.0]], ('★WELCOME', 0.8576079607009888)], [[[407.0, 760.0], [724.0, 760.0], [724.0, 855.0], [407.0, 855.0]], ('欢迎光临', 0.999948263168335)], [[[473.0, 856.0], [624.0, 863.0], [623.0, 898.0], [471.0, 891.0]], ('限时促销礼惠全城', 0.9319617748260498)], [[[446.0, 947.0], [640.0, 962.0], [638.0, 995.0], [443.0, 980.0]], ('满499减200/满999减500', 0.927433431148529)], [[[481.0, 980.0], [595.0, 991.0], [592.0, 1016.0], [479.0, 1005.0]], ('动的间167', 0.7950097918510437)]]
|
||||
[2025-07-04 09:29:41,940] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 6개 필터링 완료
|
||||
[2025-07-04 09:29:44,107] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['현대 미니멀 스타일', '다양한 사용 상황에 더 쉽게 어울립니다', '★환영합니다', '환영합니다', '한정 시간 프로모션, 전 도시 할인', '499원 이상 구매 시 200원 할인 / 999원 이상 구매 시 500원 할인', '동의 간167']
|
||||
[2025-07-04 09:29:44,108] [MainThread] [INFO] [events.py:_run:81] 이미지 2 치환됨
|
||||
[2025-07-04 09:29:44,149] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:30:33,009] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:30:33,349] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:30:33,569] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_2.png
|
||||
[2025-07-04 09:30:34,040] [MainThread] [INFO] [events.py:_run:81] 이미지 2 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_2.png
|
||||
[2025-07-04 09:30:34,048] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:30:42,076] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[74.0, 20.0], [157.0, 20.0], [157.0, 49.0], [74.0, 49.0]], ('科尔诺', 0.99479079246521)], [[[243.0, 22.0], [291.0, 22.0], [291.0, 45.0], [243.0, 45.0]], ('MC', 0.6195911169052124)], [[[309.0, 22.0], [350.0, 24.0], [349.0, 43.0], [308.0, 41.0]], ('PA', 0.9959260821342468)], [[[372.0, 25.0], [423.0, 25.0], [423.0, 41.0], [372.0, 41.0]], ('CNEX', 0.9921574592590332)], [[[493.0, 17.0], [542.0, 17.0], [542.0, 48.0], [493.0, 48.0]], ('CE', 0.9579459428787231)], [[[561.0, 19.0], [611.0, 19.0], [611.0, 46.0], [561.0, 46.0]], ('SGS', 0.9938905835151672)], [[[75.0, 53.0], [158.0, 53.0], [158.0, 74.0], [75.0, 74.0]], ('KORNO', 0.9965285062789917)], [[[240.0, 57.0], [291.0, 57.0], [291.0, 71.0], [240.0, 71.0]], ('CMC认证', 0.994641900062561)], [[[306.0, 57.0], [355.0, 57.0], [355.0, 71.0], [306.0, 71.0]], ('CPA认证', 0.997654139995575)], [[[371.0, 57.0], [419.0, 57.0], [419.0, 71.0], [371.0, 71.0]], ('国家防爆', 0.9973001480102539)], [[[436.0, 57.0], [480.0, 57.0], [480.0, 71.0], [436.0, 71.0]], ('ISO认证', 0.9966546297073364)], [[[500.0, 57.0], [539.0, 57.0], [539.0, 71.0], [500.0, 71.0]], ('CE认证', 0.9982120990753174)], [[[562.0, 57.0], [609.0, 57.0], [609.0, 71.0], [562.0, 71.0]], ('SGS认证', 0.99885493516922)], [[[45.0, 101.0], [283.0, 101.0], [283.0, 146.0], [45.0, 146.0]], ('GT-1000', 0.9948693513870239)], [[[45.0, 166.0], [407.0, 166.0], [407.0, 211.0], [45.0, 211.0]], ('激光粉尘检测仪', 0.9980872273445129)], [[[29.0, 239.0], [216.0, 237.0], [216.0, 264.0], [29.0, 266.0]], ('精度≤±5%F.S', 0.9504890441894531)], [[[28.0, 297.0], [162.0, 297.0], [162.0, 324.0], [28.0, 324.0]], ('防护等级:', 0.9984347224235535)], [[[148.0, 295.0], [236.0, 295.0], [236.0, 323.0], [148.0, 323.0]], (':IP65', 0.9789146184921265)], [[[23.0, 356.0], [420.0, 356.0], [420.0, 380.0], [23.0, 380.0]], ('过压保护/声光报警/存储打印', 0.9622878432273865)], [[[21.0, 410.0], [425.0, 413.0], [425.0, 440.0], [21.0, 437.0]], ('PM0.3/0.5/1.0/2.5/5.0/10um', 0.9829199314117432)], [[[29.0, 469.0], [371.0, 469.0], [371.0, 492.0], [29.0, 492.0]], ('可同时监测多种粒径尘埃粒子数', 0.9967647194862366)], [[[29.0, 505.0], [271.0, 505.0], [271.0, 526.0], [29.0, 526.0]], ('适合十万级以上洁净室', 0.9917227625846863)], [[[15.0, 532.0], [119.0, 532.0], [119.0, 589.0], [15.0, 589.0]], ('全国', 0.9994629621505737)], [[[194.0, 544.0], [391.0, 544.0], [391.0, 571.0], [194.0, 571.0]], ('7天无理由退货', 0.9978864789009094)], [[[428.0, 543.0], [542.0, 543.0], [542.0, 571.0], [428.0, 571.0]], ('赠运险费', 0.9983055591583252)], [[[15.0, 585.0], [122.0, 587.0], [121.0, 639.0], [14.0, 637.0]], ('包邮', 0.9907833337783813)], [[[138.0, 585.0], [625.0, 583.0], [625.0, 617.0], [138.0, 619.0]], ('原厂正品/可开发票/质保一年', 0.9910658597946167)]]]
|
||||
[2025-07-04 09:30:42,077] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[74.0, 20.0], [157.0, 20.0], [157.0, 49.0], [74.0, 49.0]], ('科尔诺', 0.99479079246521)], [[[243.0, 22.0], [291.0, 22.0], [291.0, 45.0], [243.0, 45.0]], ('MC', 0.6195911169052124)], [[[309.0, 22.0], [350.0, 24.0], [349.0, 43.0], [308.0, 41.0]], ('PA', 0.9959260821342468)], [[[372.0, 25.0], [423.0, 25.0], [423.0, 41.0], [372.0, 41.0]], ('CNEX', 0.9921574592590332)], [[[493.0, 17.0], [542.0, 17.0], [542.0, 48.0], [493.0, 48.0]], ('CE', 0.9579459428787231)], [[[561.0, 19.0], [611.0, 19.0], [611.0, 46.0], [561.0, 46.0]], ('SGS', 0.9938905835151672)], [[[75.0, 53.0], [158.0, 53.0], [158.0, 74.0], [75.0, 74.0]], ('KORNO', 0.9965285062789917)], [[[240.0, 57.0], [291.0, 57.0], [291.0, 71.0], [240.0, 71.0]], ('CMC认证', 0.994641900062561)], [[[306.0, 57.0], [355.0, 57.0], [355.0, 71.0], [306.0, 71.0]], ('CPA认证', 0.997654139995575)], [[[371.0, 57.0], [419.0, 57.0], [419.0, 71.0], [371.0, 71.0]], ('国家防爆', 0.9973001480102539)], [[[436.0, 57.0], [480.0, 57.0], [480.0, 71.0], [436.0, 71.0]], ('ISO认证', 0.9966546297073364)], [[[500.0, 57.0], [539.0, 57.0], [539.0, 71.0], [500.0, 71.0]], ('CE认证', 0.9982120990753174)], [[[562.0, 57.0], [609.0, 57.0], [609.0, 71.0], [562.0, 71.0]], ('SGS认证', 0.99885493516922)], [[[45.0, 101.0], [283.0, 101.0], [283.0, 146.0], [45.0, 146.0]], ('GT-1000', 0.9948693513870239)], [[[45.0, 166.0], [407.0, 166.0], [407.0, 211.0], [45.0, 211.0]], ('激光粉尘检测仪', 0.9980872273445129)], [[[29.0, 239.0], [216.0, 237.0], [216.0, 264.0], [29.0, 266.0]], ('精度≤±5%F.S', 0.9504890441894531)], [[[28.0, 297.0], [162.0, 297.0], [162.0, 324.0], [28.0, 324.0]], ('防护等级:', 0.9984347224235535)], [[[148.0, 295.0], [236.0, 295.0], [236.0, 323.0], [148.0, 323.0]], (':IP65', 0.9789146184921265)], [[[23.0, 356.0], [420.0, 356.0], [420.0, 380.0], [23.0, 380.0]], ('过压保护/声光报警/存储打印', 0.9622878432273865)], [[[21.0, 410.0], [425.0, 413.0], [425.0, 440.0], [21.0, 437.0]], ('PM0.3/0.5/1.0/2.5/5.0/10um', 0.9829199314117432)], [[[29.0, 469.0], [371.0, 469.0], [371.0, 492.0], [29.0, 492.0]], ('可同时监测多种粒径尘埃粒子数', 0.9967647194862366)], [[[29.0, 505.0], [271.0, 505.0], [271.0, 526.0], [29.0, 526.0]], ('适合十万级以上洁净室', 0.9917227625846863)], [[[15.0, 532.0], [119.0, 532.0], [119.0, 589.0], [15.0, 589.0]], ('全国', 0.9994629621505737)], [[[194.0, 544.0], [391.0, 544.0], [391.0, 571.0], [194.0, 571.0]], ('7天无理由退货', 0.9978864789009094)], [[[428.0, 543.0], [542.0, 543.0], [542.0, 571.0], [428.0, 571.0]], ('赠运险费', 0.9983055591583252)], [[[15.0, 585.0], [122.0, 587.0], [121.0, 639.0], [14.0, 637.0]], ('包邮', 0.9907833337783813)], [[[138.0, 585.0], [625.0, 583.0], [625.0, 617.0], [138.0, 619.0]], ('原厂正品/可开发票/质保一年', 0.9910658597946167)]]
|
||||
[2025-07-04 09:30:42,078] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 18개 필터링 완료
|
||||
[2025-07-04 09:30:48,404] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['코르노', 'MC', 'PA', 'CNEX', 'CE', 'SGS', 'KORNO', 'CMC 인증', 'CPA 인증', '국가 방폭', 'ISO 인증', 'CE 인증', 'SGS 인증', 'GT-1000', '레이저 분진 측정기', '정밀도 ≤ ±5% F.S', '방호 등급:', ':IP65', '과전압 보호/음향 및 광경고/저장 인쇄', 'PM0.3/0.5/1.0/2.5/5.0/10um', '다양한 입자 크기의 먼지 입자 수를 동시에 모니터링 가능', '10만 급 이상의 청정실에 적합', '전국', '7일 무조건 반품', '운송 보험료 무료', '무료 배송', '정품/세금계산서 발행 가능/1년 보증']
|
||||
[2025-07-04 09:30:48,404] [MainThread] [INFO] [events.py:_run:81] 이미지 3 치환됨
|
||||
[2025-07-04 09:30:48,432] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:31:05,907] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:31:06,441] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:31:06,477] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_3.png
|
||||
[2025-07-04 09:31:06,622] [MainThread] [INFO] [events.py:_run:81] 이미지 3 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_3.png
|
||||
[2025-07-04 09:31:06,630] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:31:12,817] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[57.0, 44.0], [708.0, 44.0], [708.0, 122.0], [57.0, 122.0]], ('高质量水晶相纸', 0.9983379244804382)], [[[76.0, 195.0], [373.0, 195.0], [373.0, 225.0], [76.0, 225.0]], ('适合各种喷墨打印机', 0.9938791990280151)], [[[74.0, 263.0], [348.0, 263.0], [348.0, 294.0], [74.0, 294.0]], ('色彩艳丽还原度高', 0.996768593788147)], [[[401.0, 260.0], [464.0, 260.0], [464.0, 277.0], [401.0, 277.0]], ('Colors', 0.9959242343902588)], [[[529.0, 259.0], [718.0, 259.0], [718.0, 283.0], [529.0, 283.0]], ('高质量水晶相纸', 0.9965776801109314)], [[[397.0, 273.0], [514.0, 271.0], [514.0, 292.0], [397.0, 294.0]], ('Beautiful彩丽', 0.8809345364570618)], [[[72.0, 324.0], [346.0, 324.0], [346.0, 355.0], [72.0, 355.0]], ('打印快干多种规格', 0.9959561824798584)], [[[428.0, 382.0], [441.0, 382.0], [441.0, 389.0], [428.0, 389.0]], ('12', 0.7686184644699097)], [[[452.0, 576.0], [499.0, 576.0], [499.0, 613.0], [452.0, 613.0]], ('4R', 0.9986365437507629)], [[[428.0, 599.0], [442.0, 599.0], [442.0, 614.0], [428.0, 614.0]], ('20', 0.9641551971435547)], [[[428.0, 619.0], [448.0, 619.0], [448.0, 636.0], [428.0, 636.0]], ('100', 0.998291015625)], [[[412.0, 640.0], [449.0, 640.0], [449.0, 654.0], [412.0, 654.0]], ('SHEETS', 0.9623568654060364)], [[[463.0, 633.0], [495.0, 633.0], [495.0, 653.0], [463.0, 653.0]], ('230', 0.9994447827339172)], [[[459.0, 654.0], [495.0, 654.0], [495.0, 672.0], [459.0, 672.0]], ('g/m"', 0.7422873377799988)]]]
|
||||
[2025-07-04 09:31:12,818] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[57.0, 44.0], [708.0, 44.0], [708.0, 122.0], [57.0, 122.0]], ('高质量水晶相纸', 0.9983379244804382)], [[[76.0, 195.0], [373.0, 195.0], [373.0, 225.0], [76.0, 225.0]], ('适合各种喷墨打印机', 0.9938791990280151)], [[[74.0, 263.0], [348.0, 263.0], [348.0, 294.0], [74.0, 294.0]], ('色彩艳丽还原度高', 0.996768593788147)], [[[401.0, 260.0], [464.0, 260.0], [464.0, 277.0], [401.0, 277.0]], ('Colors', 0.9959242343902588)], [[[529.0, 259.0], [718.0, 259.0], [718.0, 283.0], [529.0, 283.0]], ('高质量水晶相纸', 0.9965776801109314)], [[[397.0, 273.0], [514.0, 271.0], [514.0, 292.0], [397.0, 294.0]], ('Beautiful彩丽', 0.8809345364570618)], [[[72.0, 324.0], [346.0, 324.0], [346.0, 355.0], [72.0, 355.0]], ('打印快干多种规格', 0.9959561824798584)], [[[428.0, 382.0], [441.0, 382.0], [441.0, 389.0], [428.0, 389.0]], ('12', 0.7686184644699097)], [[[452.0, 576.0], [499.0, 576.0], [499.0, 613.0], [452.0, 613.0]], ('4R', 0.9986365437507629)], [[[428.0, 599.0], [442.0, 599.0], [442.0, 614.0], [428.0, 614.0]], ('20', 0.9641551971435547)], [[[428.0, 619.0], [448.0, 619.0], [448.0, 636.0], [428.0, 636.0]], ('100', 0.998291015625)], [[[412.0, 640.0], [449.0, 640.0], [449.0, 654.0], [412.0, 654.0]], ('SHEETS', 0.9623568654060364)], [[[463.0, 633.0], [495.0, 633.0], [495.0, 653.0], [463.0, 653.0]], ('230', 0.9994447827339172)], [[[459.0, 654.0], [495.0, 654.0], [495.0, 672.0], [459.0, 672.0]], ('g/m"', 0.7422873377799988)]]
|
||||
[2025-07-04 09:31:12,819] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 6개 필터링 완료
|
||||
[2025-07-04 09:31:15,005] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['고품질 크리스탈 사진지', '모든 잉크젯 프린터에 적합', '색상이 선명하고 재현도가 높음', '색상', '고품질 크리스탈 사진지', '아름다운 색상', '빠른 건조, 다양한 규격', '12', '4R', '20', '100', '장', '230', 'g/m²']
|
||||
[2025-07-04 09:31:15,005] [MainThread] [INFO] [events.py:_run:81] 이미지 4 치환됨
|
||||
[2025-07-04 09:31:15,026] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:31:40,489] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:31:40,746] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:31:40,818] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_4.png
|
||||
[2025-07-04 09:31:41,013] [MainThread] [INFO] [events.py:_run:81] 이미지 4 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_4.png
|
||||
[2025-07-04 09:31:41,035] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:31:43,463] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[344.0, 108.0], [855.0, 108.0], [855.0, 182.0], [344.0, 182.0]], ('现代极简风格', 0.9951424598693848)], [[[235.0, 219.0], [964.0, 219.0], [964.0, 279.0], [235.0, 279.0]], ('更易搭配各种使用场景', 0.9972127079963684)], [[[136.0, 447.0], [717.0, 447.0], [717.0, 534.0], [136.0, 534.0]], ('半圆两端设计', 0.998131275177002)], [[[134.0, 571.0], [715.0, 571.0], [715.0, 658.0], [134.0, 658.0]], ('承载各种欢乐', 0.9950398802757263)]]]
|
||||
[2025-07-04 09:31:43,464] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[344.0, 108.0], [855.0, 108.0], [855.0, 182.0], [344.0, 182.0]], ('现代极简风格', 0.9951424598693848)], [[[235.0, 219.0], [964.0, 219.0], [964.0, 279.0], [235.0, 279.0]], ('更易搭配各种使用场景', 0.9972127079963684)], [[[136.0, 447.0], [717.0, 447.0], [717.0, 534.0], [136.0, 534.0]], ('半圆两端设计', 0.998131275177002)], [[[134.0, 571.0], [715.0, 571.0], [715.0, 658.0], [134.0, 658.0]], ('承载各种欢乐', 0.9950398802757263)]]
|
||||
[2025-07-04 09:31:43,465] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 4개 필터링 완료
|
||||
[2025-07-04 09:31:44,783] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['현대 미니멀리즘 스타일', '다양한 사용 상황에 더 쉽게 어울림', '반원 양쪽 끝 디자인', '다양한 즐거움을 담다']
|
||||
[2025-07-04 09:31:44,784] [MainThread] [INFO] [events.py:_run:81] 이미지 5 치환됨
|
||||
[2025-07-04 09:31:44,824] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:32:43,447] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:32:43,689] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:32:43,858] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_5.png
|
||||
[2025-07-04 09:32:44,414] [MainThread] [INFO] [events.py:_run:81] 이미지 5 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_5.png
|
||||
[2025-07-04 09:32:44,432] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:32:46,459] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[60.0, 60.0], [264.0, 63.0], [264.0, 92.0], [60.0, 89.0]], ('WA汉世刘家', 0.7825194001197815)], [[[43.0, 129.0], [541.0, 129.0], [541.0, 182.0], [43.0, 182.0]], ('脱水比洗衣机更干', 0.9943966865539551)], [[[38.0, 205.0], [394.0, 203.0], [394.0, 268.0], [39.0, 270.0]], ('真正免手洗', 0.9823843240737915)]]]
|
||||
[2025-07-04 09:32:46,460] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[60.0, 60.0], [264.0, 63.0], [264.0, 92.0], [60.0, 89.0]], ('WA汉世刘家', 0.7825194001197815)], [[[43.0, 129.0], [541.0, 129.0], [541.0, 182.0], [43.0, 182.0]], ('脱水比洗衣机更干', 0.9943966865539551)], [[[38.0, 205.0], [394.0, 203.0], [394.0, 268.0], [39.0, 270.0]], ('真正免手洗', 0.9823843240737915)]]
|
||||
[2025-07-04 09:32:46,460] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 3개 필터링 완료
|
||||
[2025-07-04 09:32:47,831] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['WA한세유가', '탈수는 세탁기보다 더 건조하다', '진정한 손세탁 면제']
|
||||
[2025-07-04 09:32:47,832] [MainThread] [INFO] [events.py:_run:81] 이미지 6 치환됨
|
||||
[2025-07-04 09:32:47,856] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:33:09,533] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:33:09,637] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:33:09,721] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_6.png
|
||||
[2025-07-04 09:33:09,941] [MainThread] [INFO] [events.py:_run:81] 이미지 6 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_6.png
|
||||
[2025-07-04 09:33:09,952] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 09:33:13,510] [MainThread] [INFO] [image_translate_server.py:sem_task:81] ocr_raw_results: [[[[[196.0, 55.0], [405.0, 55.0], [405.0, 72.0], [196.0, 72.0]], ('MICROCULTIVATOR-', 0.9893487095832825)], [[[183.0, 102.0], [423.0, 102.0], [423.0, 160.0], [183.0, 160.0]], ('轻轻一拉', 0.9972223043441772)], [[[78.0, 179.0], [525.0, 179.0], [525.0, 227.0], [78.0, 227.0]], ('3秒即可快速启动', 0.9966239929199219)], [[[98.0, 266.0], [506.0, 266.0], [506.0, 286.0], [98.0, 286.0]], ('加快机器供油,燃烧,传动流程3秒快速启动', 0.9955464601516724)], [[[178.0, 290.0], [430.0, 290.0], [430.0, 310.0], [178.0, 310.0]], ('让您不用浪费时间在启动上', 0.9967800974845886)]]]
|
||||
[2025-07-04 09:33:13,510] [MainThread] [INFO] [image_translate_server.py:sem_task:81] line: [[[[196.0, 55.0], [405.0, 55.0], [405.0, 72.0], [196.0, 72.0]], ('MICROCULTIVATOR-', 0.9893487095832825)], [[[183.0, 102.0], [423.0, 102.0], [423.0, 160.0], [183.0, 160.0]], ('轻轻一拉', 0.9972223043441772)], [[[78.0, 179.0], [525.0, 179.0], [525.0, 227.0], [78.0, 227.0]], ('3秒即可快速启动', 0.9966239929199219)], [[[98.0, 266.0], [506.0, 266.0], [506.0, 286.0], [98.0, 286.0]], ('加快机器供油,燃烧,传动流程3秒快速启动', 0.9955464601516724)], [[[178.0, 290.0], [430.0, 290.0], [430.0, 310.0], [178.0, 310.0]], ('让您不用浪费时间在启动上', 0.9967800974845886)]]
|
||||
[2025-07-04 09:33:13,511] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 중국어 텍스트 4개 필터링 완료
|
||||
[2025-07-04 09:33:15,757] [MainThread] [INFO] [image_translate_server.py:sem_task:81] 최종 치환 결과: ['마이크로 경작기-', '가볍게 당기기만 하면', '3초 만에 빠르게 시작할 수 있습니다.', '기계의 연료 공급, 연소, 전송 과정을 3초 만에 빠르게 시작합니다.', '시작하는 데 시간을 낭비하지 않도록 해드립니다.']
|
||||
[2025-07-04 09:33:15,758] [MainThread] [INFO] [events.py:_run:81] 이미지 7 치환됨
|
||||
[2025-07-04 09:33:15,782] [MainThread] [INFO] [events.py:_run:81] 마스크 생성 완료
|
||||
[2025-07-04 09:33:42,937] [MainThread] [INFO] [events.py:_run:81] 인페인팅 완료
|
||||
[2025-07-04 09:33:43,057] [MainThread] [INFO] [events.py:_run:81] 텍스트 렌더링 완료
|
||||
[2025-07-04 09:33:43,111] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 이미지 저장 완료 : /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_7.png
|
||||
[2025-07-04 09:33:43,348] [MainThread] [INFO] [events.py:_run:81] 이미지 7 번역 완료: /home/ckh08045/work/IT_Server/temp_images/translated_multi_img_7.png
|
||||
[2025-07-04 18:18:17,703] [MainThread] [INFO] [image_processor2.py:cleanup:50] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 18:19:41,931] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 18:19:42,000] [MainThread] [INFO] [image_processor2.py:cleanup:50] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 19:32:19,168] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: Unknown argument: use_gpu
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 33, in initialize_ocr
|
||||
ocr = PaddleOCR(
|
||||
^^^^^^^^^^
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\_pipelines\ocr.py", line 161, in __init__
|
||||
super().__init__(**base_params)
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\_pipelines\base.py", line 62, in __init__
|
||||
self._common_args = parse_common_args(
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\_common_args.py", line 43, in parse_common_args
|
||||
raise ValueError(f"Unknown argument: {name}")
|
||||
ValueError: Unknown argument: use_gpu
|
||||
|
||||
[2025-07-04 19:32:19,245] [MainThread] [INFO] [image_processor2.py:cleanup:50] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 19:33:10,399] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 19:33:10,483] [MainThread] [INFO] [image_processor2.py:cleanup:50] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 19:39:14,900] [MainThread] [INFO] [mask_module.py:__init__:12] 마스크 모듈 초기화 완료
|
||||
[2025-07-04 19:39:14,901] [MainThread] [DEBUG] [postImageManager.py:font_load:22] 폰트 로드 성공: D:\py\IT_Server\modules\fonts\HakgyoansimDunggeunmisoTTFB.ttf
|
||||
[2025-07-04 19:39:14,967] [MainThread] [INFO] [image_processor2.py:cleanup:50] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 19:43:45,223] [MainThread] [INFO] [mask_module.py:__init__:12] 마스크 모듈 초기화 완료
|
||||
[2025-07-04 19:43:45,224] [MainThread] [DEBUG] [postImageManager.py:font_load:22] 폰트 로드 성공: D:\py\IT_Server\modules\fonts\HakgyoansimDunggeunmisoTTFB.ttf
|
||||
[2025-07-04 19:48:18,222] [MainThread] [INFO] [image_processor2.py:cleanup:50] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:04:18,065] [MainThread] [INFO] [mask_module.py:__init__:12] 마스크 모듈 초기화 완료
|
||||
[2025-07-04 20:04:18,065] [MainThread] [DEBUG] [postImageManager.py:font_load:22] 폰트 로드 성공: D:\py\IT_Server\modules\fonts\HakgyoansimDunggeunmisoTTFB.ttf
|
||||
[2025-07-04 20:04:18,066] [MainThread] [INFO] [iop_Manager.py:_start_instances:73] IOPaint 인스턴스 1 개 시작
|
||||
[2025-07-04 20:04:18,066] [MainThread] [INFO] [iop_Manager.py:_start_instances:78] [7026] 인스턴스 실행 명령: D:\py\IT_Server\modules\iop\iop.exe start --model=migan --device=cpu --port 7026 --model-dir D:\py\IT_Server\modules\iop\models
|
||||
[2025-07-04 20:04:18,132] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:15:10,769] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:15:10,849] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:16:11,790] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:16:11,876] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:18:27,628] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:18:27,697] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:18:35,762] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:18:35,834] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:19:00,214] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:19:00,293] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:27:04,149] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:27:04,231] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:30:08,995] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:30:09,065] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:31:30,942] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:31:31,022] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:32:33,354] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: No module named 'paddle'
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 21, in <module>
|
||||
from paddle.utils import try_import
|
||||
ModuleNotFoundError: No module named 'paddle'
|
||||
|
||||
[2025-07-04 20:32:33,378] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:33:10,059] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:33:10,148] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 20:37:38,096] [MainThread] [ERROR] [ocr_module.py:initialize_ocr:43] ❌ PaddleOCR 초기화 실패: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\IT_Server\modules\ocr_module.py", line 31, in initialize_ocr
|
||||
from paddleocr import PaddleOCR
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\__init__.py", line 14, in <module>
|
||||
from .paddleocr import (
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\paddleocr.py", line 64, in <module>
|
||||
from tools.infer import predict_system
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_system.py", line 33, in <module>
|
||||
import tools.infer.predict_det as predict_det
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\tools\infer\predict_det.py", line 31, in <module>
|
||||
from ppocr.data import create_operators, transform
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\__init__.py", line 35, in <module>
|
||||
from ppocr.data.imaug import transform, create_operators
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\__init__.py", line 19, in <module>
|
||||
from .iaa_augment import IaaAugment
|
||||
File "D:\py\IT_Server\Lib\site-packages\paddleocr\ppocr\data\imaug\iaa_augment.py", line 24, in <module>
|
||||
import albumentations as A
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\__init__.py", line 24, in <module>
|
||||
from .pytorch import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\__init__.py", line 1, in <module>
|
||||
from .transforms import *
|
||||
File "D:\py\IT_Server\Lib\site-packages\albumentations\pytorch\transforms.py", line 15, in <module>
|
||||
import torch
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 270, in <module>
|
||||
_load_dll_libraries()
|
||||
File "D:\py\IT_Server\Lib\site-packages\torch\__init__.py", line 253, in _load_dll_libraries
|
||||
raise err
|
||||
OSError: [WinError 127] 지정된 프로시저를 찾을 수 없습니다. Error loading "D:\py\IT_Server\Lib\site-packages\torch\lib\shm.dll" or one of its dependencies.
|
||||
|
||||
[2025-07-04 20:37:38,184] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 21:14:22,646] [MainThread] [INFO] [mask_module.py:__init__:12] 마스크 모듈 초기화 완료
|
||||
[2025-07-04 21:14:22,647] [MainThread] [DEBUG] [postImageManager.py:font_load:22] 폰트 로드 성공: D:\py\IT_Server\modules\fonts\HakgyoansimDunggeunmisoTTFB.ttf
|
||||
[2025-07-04 21:14:22,917] [MainThread] [INFO] [image_processor2.py:cleanup:53] 임시 폴더 삭제됨: D:\py\IT_Server\temp_images
|
||||
[2025-07-04 21:15:59,637] [MainThread] [INFO] [mask_module.py:__init__:12] 마스크 모듈 초기화 완료
|
||||
[2025-07-04 21:15:59,638] [MainThread] [DEBUG] [postImageManager.py:font_load:22] 폰트 로드 성공: D:\py\IT_Server\modules\fonts\HakgyoansimDunggeunmisoTTFB.ttf
|
||||
[2025-07-04 21:15:59,638] [MainThread] [INFO] [iop_server.py:start:31] [IOPaint] 실행 환경 파이썬: D:\py\IT_Server\scripts\python.exe
|
||||
[2025-07-04 21:15:59,638] [MainThread] [INFO] [iop_server.py:start:32] [IOPaint] 실행 명령: D:\py\IT_Server\scripts\python.exe -m iopaint start --model=migan --device=cpu --port 7024 --model-dir D:\py\IT_Server\iop\models
|
||||
[2025-07-04 21:15:59,639] [MainThread] [INFO] [iop_server.py:start:33] [IOPaint] 모델 디렉토리: D:\py\IT_Server\iop\models
|
||||
[2025-07-04 21:16:00,355] [MainThread] [WARNING] [iop_server.py:start:41] [IOPaint] iopaint 모듈이 현재 환경에 설치되어 있지 않습니다!
|
||||
[2025-07-04 21:16:00,358] [MainThread] [INFO] [iop_server.py:start:47] [IOPaint] 서버 준비 확인 시작 (최대 30초 대기)
|
||||
[2025-07-04 21:16:06,898] [MainThread] [INFO] [iop_server.py:start:68] [IOPaint] 서버가 포트 7024에서 준비됨.
|
||||
[2025-07-04 21:17:17,178] [MainThread] [INFO] [ocr_module.py:detect_text:75] 🔍 OCR 감지 방식: polygon
|
||||
[2025-07-04 21:17:17,599] [MainThread] [INFO] [ocr_module.py:detect_text:81] ocr_raw_results: [[[[[57.0, 44.0], [708.0, 44.0], [708.0, 122.0], [57.0, 122.0]], ('高质量水晶相纸', 0.9983320236206055)], [[[76.0, 195.0], [373.0, 195.0], [373.0, 225.0], [76.0, 225.0]], ('适合各种喷墨打印机', 0.993870735168457)], [[[74.0, 263.0], [348.0, 263.0], [348.0, 294.0], [74.0, 294.0]], ('色彩艳丽还原度高', 0.9967601895332336)], [[[401.0, 260.0], [464.0, 260.0], [464.0, 277.0], [401.0, 277.0]], ('Colors', 0.9959213137626648)], [[[529.0, 259.0], [718.0, 259.0], [718.0, 283.0], [529.0, 283.0]], ('高质量水晶相纸', 0.9965673089027405)], [[[397.0, 273.0], [514.0, 271.0], [514.0, 292.0], [397.0, 294.0]], ('Beautiful彩丽', 0.8809203505516052)], [[[72.0, 324.0], [346.0, 324.0], [346.0, 355.0], [72.0, 355.0]], ('打印快干多种规格', 0.9959524273872375)], [[[428.0, 382.0], [441.0, 382.0], [441.0, 389.0], [428.0, 389.0]], ('12', 0.768598198890686)], [[[452.0, 576.0], [499.0, 576.0], [499.0, 613.0], [452.0, 613.0]], ('4R', 0.9986311793327332)], [[[428.0, 599.0], [442.0, 599.0], [442.0, 614.0], [428.0, 614.0]], ('20', 0.964126706123352)], [[[428.0, 619.0], [448.0, 619.0], [448.0, 636.0], [428.0, 636.0]], ('100', 0.9982807636260986)], [[[412.0, 640.0], [449.0, 640.0], [449.0, 654.0], [412.0, 654.0]], ('SHEETS', 0.9623520970344543)], [[[463.0, 633.0], [495.0, 633.0], [495.0, 653.0], [463.0, 653.0]], ('230', 0.9994370937347412)], [[[459.0, 654.0], [495.0, 654.0], [495.0, 672.0], [459.0, 672.0]], ('g/m"', 0.7422822713851929)]]]
|
||||
[2025-07-04 21:17:17,600] [MainThread] [INFO] [ocr_module.py:detect_text:83] line: [[[[57.0, 44.0], [708.0, 44.0], [708.0, 122.0], [57.0, 122.0]], ('高质量水晶相纸', 0.9983320236206055)], [[[76.0, 195.0], [373.0, 195.0], [373.0, 225.0], [76.0, 225.0]], ('适合各种喷墨打印机', 0.993870735168457)], [[[74.0, 263.0], [348.0, 263.0], [348.0, 294.0], [74.0, 294.0]], ('色彩艳丽还原度高', 0.9967601895332336)], [[[401.0, 260.0], [464.0, 260.0], [464.0, 277.0], [401.0, 277.0]], ('Colors', 0.9959213137626648)], [[[529.0, 259.0], [718.0, 259.0], [718.0, 283.0], [529.0, 283.0]], ('高质量水晶相纸', 0.9965673089027405)], [[[397.0, 273.0], [514.0, 271.0], [514.0, 292.0], [397.0, 294.0]], ('Beautiful彩丽', 0.8809203505516052)], [[[72.0, 324.0], [346.0, 324.0], [346.0, 355.0], [72.0, 355.0]], ('打印快干多种规格', 0.9959524273872375)], [[[428.0, 382.0], [441.0, 382.0], [441.0, 389.0], [428.0, 389.0]], ('12', 0.768598198890686)], [[[452.0, 576.0], [499.0, 576.0], [499.0, 613.0], [452.0, 613.0]], ('4R', 0.9986311793327332)], [[[428.0, 599.0], [442.0, 599.0], [442.0, 614.0], [428.0, 614.0]], ('20', 0.964126706123352)], [[[428.0, 619.0], [448.0, 619.0], [448.0, 636.0], [428.0, 636.0]], ('100', 0.9982807636260986)], [[[412.0, 640.0], [449.0, 640.0], [449.0, 654.0], [412.0, 654.0]], ('SHEETS', 0.9623520970344543)], [[[463.0, 633.0], [495.0, 633.0], [495.0, 653.0], [463.0, 653.0]], ('230', 0.9994370937347412)], [[[459.0, 654.0], [495.0, 654.0], [495.0, 672.0], [459.0, 672.0]], ('g/m"', 0.7422822713851929)]]
|
||||
[2025-07-04 21:17:17,601] [MainThread] [INFO] [ocr_module.py:filter_chinese_text:137] 중국어 텍스트 6개 필터링 완료
|
||||
[2025-07-04 21:17:22,501] [MainThread] [INFO] [image_processor2.py:process_translated_texts:234] 최종 치환 결과: ['고품질 크리스탈 사진지', '모든 잉크젯 프린터에 적합', '색상이 선명하고 재현도가 높음', '색상', '고품질 크리스탈 사진지', '아름다운 화려함', '빠른 건조, 다양한 규격', '12', '4R', '20', '100', '장', '230', 'g/m²']
|
||||
[2025-07-04 21:17:22,502] [MainThread] [INFO] [image_processor2.py:process_single_image:102] 이미지 1 치환됨
|
||||
[2025-07-04 21:17:22,508] [MainThread] [INFO] [image_processor2.py:process_single_image:108] 마스크 생성 완료
|
||||
[2025-07-04 21:17:31,648] [MainThread] [INFO] [image_processor2.py:process_single_image:112] 인페인팅 완료
|
||||
[2025-07-04 21:17:31,714] [MainThread] [INFO] [image_processor2.py:process_single_image:117] 텍스트 렌더링 완료
|
||||
[2025-07-04 21:17:31,725] [MainThread] [INFO] [postImageManager.py:save_image_to_path:38] 이미지 저장 완료 : D:\py\IT_Server\temp_images\translated_test_img_1.png
|
||||
[2025-07-04 21:17:31,806] [MainThread] [INFO] [image_processor2.py:process_single_image:121] 이미지 1 번역 완료: D:\py\IT_Server\temp_images\translated_test_img_1.png
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import paddleocr
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# PaddlePaddle 경로
|
||||
paddle_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'paddle')
|
||||
paddle_libs_path = os.path.join(paddle_path, 'libs')
|
||||
paddle_base_path = os.path.join(paddle_path, 'base')
|
||||
|
||||
# Cython 경로
|
||||
cython_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'Cython')
|
||||
cython_utility_path = os.path.join(cython_path, 'Utility')
|
||||
|
||||
# DLL 파일들 수집
|
||||
binaries = []
|
||||
|
||||
# paddle/libs의 모든 DLL 파일들
|
||||
for dll_file in os.listdir(paddle_libs_path):
|
||||
if dll_file.endswith('.dll'):
|
||||
src_path = os.path.join(paddle_libs_path, dll_file)
|
||||
binaries.append((src_path, '.'))
|
||||
|
||||
# paddle/base의 libpaddle.pyd
|
||||
libpaddle_pyd = os.path.join(paddle_base_path, 'libpaddle.pyd')
|
||||
if os.path.exists(libpaddle_pyd):
|
||||
binaries.append((libpaddle_pyd, '.'))
|
||||
|
||||
# 데이터 파일들 수집
|
||||
datas = [
|
||||
('modules', 'modules'),
|
||||
('modules/PP_Models', 'modules/PP_Models'),
|
||||
('modules/fonts', 'modules/fonts'),
|
||||
('modules/iop/models', 'modules/iop/models'),
|
||||
]
|
||||
|
||||
# Cython Utility 폴더의 모든 파일을 datas에 추가
|
||||
if os.path.exists(cython_utility_path):
|
||||
for f in glob.glob(os.path.join(cython_utility_path, '*')):
|
||||
datas.append((f, os.path.join('Cython', 'Utility')))
|
||||
|
||||
# paddleocr 전체 소스 폴더 datas에 추가
|
||||
paddleocr_path = os.path.dirname(paddleocr.__file__)
|
||||
datas.append((paddleocr_path, 'paddleocr'))
|
||||
# paddleocr/tools 폴더를 dist 루트에도 추가
|
||||
datas.append((os.path.join(paddleocr_path, 'tools'), 'tools'))
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=[
|
||||
'paddle',
|
||||
'paddleocr',
|
||||
'paddle.base',
|
||||
'paddle.base.core',
|
||||
'paddle.base.framework',
|
||||
'paddle.nn',
|
||||
'paddle.nn.functional',
|
||||
'numpy',
|
||||
'cv2',
|
||||
'PIL',
|
||||
'requests',
|
||||
'easyocr',
|
||||
'matplotlib',
|
||||
'scipy',
|
||||
'skimage',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[
|
||||
'Cython',
|
||||
'Cython.Compiler',
|
||||
'Cython.Utility',
|
||||
],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='ImageTranslateServer',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False, # 🚫 콘솔 창 숨김 (노콘솔)
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='ImageTranslateServer', # 📁 폴더 형태로 배포 (노윈도우)
|
||||
)
|
||||
Binary file not shown.
31
main.py
31
main.py
|
|
@ -3,24 +3,33 @@ from modules.image_translate_server import run_server
|
|||
from modules.image_processor2 import ImageProcessor
|
||||
from modules.loggerModule import Logger1
|
||||
from modules.gpt_client import GPTClient
|
||||
from modules.iop_server import IOPaint_Server
|
||||
import sys, os
|
||||
|
||||
|
||||
def get_base_dir():
|
||||
"""
|
||||
실행 환경에 따라 base_dir을 설정하는 메서드.
|
||||
cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
|
||||
PyInstaller/cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
|
||||
"""
|
||||
if getattr(sys, 'frozen', False): # 패키징된 경우
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
internal_dir = os.path.join(base_dir, 'lib') # lib 디렉토리 포함
|
||||
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
|
||||
return internal_dir
|
||||
|
||||
if hasattr(sys, '_MEIPASS'): # PyInstaller
|
||||
# PyInstaller의 임시 폴더 경로
|
||||
return sys._MEIPASS
|
||||
else: # cx_Freeze
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
internal_dir = os.path.join(base_dir, 'lib') # lib 디렉토리 포함
|
||||
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
|
||||
return internal_dir
|
||||
return base_dir # lib 디렉토리가 없으면 base_dir 반환
|
||||
else: # 일반 Python 실행 환경
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
debug_dir = os.path.join(base_dir) # lib 디렉토리 포함
|
||||
return debug_dir
|
||||
return base_dir
|
||||
|
||||
def run_iop_server(logger, base_dir):
|
||||
iop_server = IOPaint_Server(logger=logger, base_dir=base_dir)
|
||||
iop_port = iop_server.start()
|
||||
return iop_port
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="이미지 번역 FastAPI 서버 실행")
|
||||
|
|
@ -37,8 +46,12 @@ def main():
|
|||
|
||||
image_processor = ImageProcessor(logger, gpt_client, base_dir, font_path)
|
||||
|
||||
iop_port = run_iop_server(logger, base_dir)
|
||||
image_processor.update_iop_port(iop_port)
|
||||
|
||||
port = run_server(image_processor, max_workers)
|
||||
print(f"서버가 127.0.0.1:{port} 에서 실행 중입니다.")
|
||||
print(f"이미지번역서버가 127.0.0.1:{port} 에서 실행 중입니다.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,764 @@
|
|||
import base64
|
||||
import pyperclip
|
||||
import win32clipboard
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageGrab, ImageFont, ImageDraw
|
||||
import requests
|
||||
import numpy as np
|
||||
import cv2
|
||||
import time
|
||||
import os, sys
|
||||
from datetime import datetime
|
||||
import random
|
||||
import pywinauto
|
||||
import io
|
||||
import logging
|
||||
|
||||
class ClipboardImageManager:
|
||||
def __init__(self, logger, watermark_font_size=36, debug_flag=False):
|
||||
self.logger = logger
|
||||
self.debug = debug_flag # 디버그 플래그를 클래스 변수로 사용
|
||||
|
||||
# 프로그램이 위치한 경로 기준으로 폰트 경로 설정
|
||||
self.base_path = self.get_base_dir()
|
||||
# 먼저 현재 모듈과 같은 디렉토리에서 폰트 파일 찾기
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.font_path = os.path.join(current_dir, 'HakgyoansimDunggeunmisoTTFB.ttf')
|
||||
|
||||
# 폰트 파일이 없으면 다른 경로들을 시도
|
||||
if not os.path.exists(self.font_path):
|
||||
alternative_paths = [
|
||||
os.path.join(self.base_path, 'HakgyoansimDunggeunmisoTTFB.ttf'),
|
||||
os.path.join(self.base_path, 'src', 'modules', 'HakgyoansimDunggeunmisoTTFB.ttf'),
|
||||
os.path.join(os.path.dirname(self.base_path), 'src', 'modules', 'HakgyoansimDunggeunmisoTTFB.ttf')
|
||||
]
|
||||
|
||||
for alt_path in alternative_paths:
|
||||
if os.path.exists(alt_path):
|
||||
self.font_path = alt_path
|
||||
break
|
||||
|
||||
# 폰트 로드 (예외 처리 추가)
|
||||
try:
|
||||
self.font = ImageFont.truetype(self.font_path, watermark_font_size)
|
||||
self.logger.log(f"폰트 로드 성공: {self.font_path}", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
self.logger.log(f"커스텀 폰트 로드 실패 ({self.font_path}): {e}", level=logging.WARNING)
|
||||
try:
|
||||
# 기본 폰트 사용
|
||||
self.font = ImageFont.load_default()
|
||||
self.logger.log("기본 폰트를 사용합니다.", level=logging.INFO)
|
||||
except Exception as e2:
|
||||
self.logger.log(f"기본 폰트 로드도 실패: {e2}", level=logging.ERROR)
|
||||
# 최후의 수단으로 None 설정
|
||||
self.font = None
|
||||
|
||||
# self.debug = True
|
||||
|
||||
def reset_state(self):
|
||||
"""클립보드 이미지 관리자의 상태를 초기화합니다."""
|
||||
self.logger.log("ClipboardImageManager 상태 초기화", level=logging.DEBUG)
|
||||
# 클립보드 비우기
|
||||
self.clear_clipboard()
|
||||
|
||||
def get_base_dir(self):
|
||||
"""
|
||||
실행 환경에 따라 base_dir을 설정하는 메서드.
|
||||
cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
|
||||
"""
|
||||
if getattr(sys, 'frozen', False): # 패키징된 경우
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
internal_dir = os.path.join(base_dir, 'lib', 'src') # lib 디렉토리 포함
|
||||
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
|
||||
return internal_dir
|
||||
|
||||
else: # 일반 Python 실행 환경
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
debug_dir = os.path.join(base_dir, 'src') # lib 디렉토리 포함
|
||||
|
||||
return debug_dir
|
||||
|
||||
|
||||
def get_clipboard_data(self):
|
||||
"""클립보드의 텍스트 또는 이미지 데이터를 가져옵니다."""
|
||||
self.logger.log("클립보드의 텍스트 또는 이미지 데이터를 가져옵니다", level=logging.DEBUG)
|
||||
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
# 1. 텍스트 데이터 우선 시도
|
||||
clipboard_text = pyperclip.paste()
|
||||
if clipboard_text:
|
||||
return clipboard_text
|
||||
|
||||
# 2. 텍스트가 없으면 이미지 확인
|
||||
self.logger.log("텍스트 데이터가 없어 이미지 데이터 확인 시도", level=logging.DEBUG)
|
||||
image = ImageGrab.grabclipboard()
|
||||
if isinstance(image, Image.Image): # 이미지 데이터가 있는 경우
|
||||
self.logger.log("클립보드에 이미지 데이터가 확인되었습니다.", level=logging.DEBUG)
|
||||
return image # PIL 이미지 객체 반환
|
||||
else:
|
||||
self.logger.log("클립보드에 텍스트 또는 이미지 데이터가 없습니다.", level=logging.DEBUG)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
self.logger.log(f"클립보드 데이터를 가져오는 중 오류 발생 (시도 {attempt}/{max_attempts}): {e}", level=logging.WARNING)
|
||||
if attempt < max_attempts:
|
||||
time.sleep(0.5) # 0.5초 대기 후 재시도
|
||||
else:
|
||||
self.logger.log(f"클립보드 데이터를 가져오는 중 최대 시도 횟수 초과: {e}", level=logging.ERROR, exc_info=True)
|
||||
return None
|
||||
|
||||
def set_image_to_clipboard(self, image):
|
||||
"""이미지를 클립보드에 넣는 함수 (Windows 전용)"""
|
||||
output = BytesIO()
|
||||
image.save(output, "BMP")
|
||||
self.logger.log(f"이미지 데이터 BMP 변환", level=logging.DEBUG)
|
||||
|
||||
data = output.getvalue()[14:] # BMP 헤더 제거
|
||||
output.close()
|
||||
self.logger.log(f"이미지 BMP 헤더 제거", level=logging.DEBUG)
|
||||
|
||||
# 클립보드 접근 재시도 로직
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
success = False
|
||||
|
||||
while attempt < max_attempts and not success:
|
||||
try:
|
||||
# 클립보드에 이미지 데이터 넣기
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
win32clipboard.CloseClipboard()
|
||||
success = True
|
||||
self.logger.log(f"클립보드 데이터 저장 성공 (시도 {attempt+1}/{max_attempts})", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
self.logger.log(f"클립보드 데이터 저장 실패 (시도 {attempt}/{max_attempts}): {e}", level=logging.WARNING)
|
||||
if attempt < max_attempts:
|
||||
time.sleep(0.5) # 0.5초 대기 후 재시도
|
||||
|
||||
# 클립보드가 제대로 설정되었는지 확인하는 로그
|
||||
if success:
|
||||
try:
|
||||
time.sleep(0.1) # 아주 짧은 대기 시간
|
||||
win32clipboard.OpenClipboard()
|
||||
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
|
||||
self.logger.log("클립보드 데이터 확인 성공", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log("클립보드 데이터 확인 실패", level=logging.ERROR)
|
||||
win32clipboard.CloseClipboard()
|
||||
except Exception as e:
|
||||
self.logger.log(f"클립보드 데이터 확인 중 오류: {e}", level=logging.ERROR)
|
||||
|
||||
def save_image_to_path(self, image, path):
|
||||
try:
|
||||
if image:
|
||||
# 이미지를 저장 경로에 저장
|
||||
self.logger.log(f"이미지 저장 완료 : {path}", level=logging.INFO)
|
||||
image.save(path, format='PNG')
|
||||
return path
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"이미지 저장 중 오류 발생: {e}")
|
||||
|
||||
def add_watermark(self, image, watermark_text="Watermark", opacity_percent=30, angle=30, font_size=36):
|
||||
"""
|
||||
이미지에 텍스트 워터마크를 이미지 전체에 걸쳐서 추가하는 함수
|
||||
:param image: PIL 이미지 객체
|
||||
:param watermark_text: 워터마크로 추가할 텍스트
|
||||
:param opacity_percent: 워터마크의 투명도 (0~100)
|
||||
:param angle: 워터마크 텍스트 회전 각도 (기본 30도)
|
||||
:param font_size: 워터마크 텍스트의 폰트 크기 (기본 36)
|
||||
:return: 워터마크가 추가된 이미지
|
||||
"""
|
||||
# 폰트가 로드되지 않은 경우 원본 이미지 반환
|
||||
if self.font is None:
|
||||
self.logger.log("폰트가 로드되지 않아 워터마크를 추가할 수 없습니다. 원본 이미지를 반환합니다.", level=logging.WARNING)
|
||||
return image
|
||||
|
||||
# 이미지 복사본 생성
|
||||
watermark_image = image.copy()
|
||||
|
||||
# 폰트 설정 (안전한 폰트 로딩)
|
||||
try:
|
||||
# self.font가 있으면 크기만 조정해서 새 폰트 생성
|
||||
if hasattr(self, 'font_path') and os.path.exists(self.font_path):
|
||||
font = ImageFont.truetype(self.font_path, font_size)
|
||||
else:
|
||||
# 크기를 조정할 수 없으면 기존 폰트 사용
|
||||
font = self.font
|
||||
except Exception as e:
|
||||
self.logger.log(f"폰트 크기 조정 실패: {e}. 기본 폰트를 사용합니다.", level=logging.WARNING)
|
||||
font = self.font
|
||||
|
||||
# 텍스트 투명도를 0~255로 변환
|
||||
opacity = int(255 * (opacity_percent / 100))
|
||||
|
||||
# 텍스트 크기 측정 (textbbox 사용)
|
||||
draw = ImageDraw.Draw(watermark_image)
|
||||
bbox = draw.textbbox((0, 0), watermark_text, font=font)
|
||||
text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
|
||||
# 이미지 크기
|
||||
width, height = image.size
|
||||
|
||||
# 워터마크 레이어 생성
|
||||
watermark_layer = Image.new("RGBA", (width, height)) # RGBA 이미지 생성
|
||||
|
||||
# 지그재그 간격 설정
|
||||
zigzag_step = int(text_height * 2) # Y축의 지그재그 간격
|
||||
|
||||
|
||||
# 이미지 전체에 반복적으로 워터마크 텍스트 그리기 (지그재그 형태)
|
||||
for y in range(0, height, zigzag_step):
|
||||
for x in range(0, width, int(text_width * 3)): # 3배 너비 간격으로 반복
|
||||
# 텍스트가 한 줄씩 지그재그 형태로 X축을 교차하여 이동
|
||||
x_offset = (y // zigzag_step) % 2 * int(text_width * 1.5) # 짝수 행에서는 X축을 약간 이동
|
||||
|
||||
# 텍스트 레이어 생성
|
||||
text_layer = Image.new("RGBA", (text_width, text_height), (255, 255, 255, 0))
|
||||
text_draw = ImageDraw.Draw(text_layer)
|
||||
|
||||
# 텍스트 그리기
|
||||
text_draw.text((0, 0), watermark_text, fill=(255, 255, 255, opacity), font=font)
|
||||
|
||||
# 텍스트 회전
|
||||
rotated_text_layer = text_layer.rotate(angle, expand=1)
|
||||
|
||||
# 회전된 텍스트를 워터마크 레이어에 추가
|
||||
watermark_layer.paste(rotated_text_layer, (x + x_offset, y), rotated_text_layer)
|
||||
|
||||
# 원본 이미지와 워터마크 레이어 합성
|
||||
watermark_image = Image.alpha_composite(watermark_image.convert("RGBA"), watermark_layer)
|
||||
|
||||
# 최종적으로 RGB 형식으로 변환 후 반환
|
||||
return watermark_image.convert("RGB")
|
||||
|
||||
def base64_to_image(self, base64_data):
|
||||
"""Base64 데이터를 이미지로 변환하는 함수"""
|
||||
if base64_data.startswith('data:image'):
|
||||
header, encoded = base64_data.split(',', 1)
|
||||
img_data = base64.b64decode(encoded)
|
||||
image = Image.open(BytesIO(img_data))
|
||||
return image
|
||||
else:
|
||||
self.logger.log("유효하지 않은 Base64 이미지 데이터입니다.", level=logging.DEBUG)
|
||||
return None
|
||||
|
||||
def image_to_base64(self, image):
|
||||
# 이미지 Base64로 변환
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format="PNG")
|
||||
base64_image = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
return base64_image
|
||||
|
||||
def download_image_from_url(self, url, max_retries=3):
|
||||
"""URL에서 이미지를 다운로드하고 PIL 이미지 객체로 반환"""
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"DNT": "1", # Do Not Track 요청 헤더
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Cache-Control": "max-age=0"
|
||||
}
|
||||
|
||||
retries = 0
|
||||
while retries < max_retries:
|
||||
try:
|
||||
self.logger.log(f"이미지 URL 다운로드 중: {url}", level=logging.DEBUG)
|
||||
response = requests.get(url, headers=headers, stream=True)
|
||||
|
||||
# 상태 코드가 200이 아니면 재시도
|
||||
if response.status_code == 200:
|
||||
# OpenCV로 이미지를 로드하여 변환
|
||||
image = np.asarray(bytearray(response.content), dtype="uint8")
|
||||
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
|
||||
|
||||
# OpenCV에서 이미지를 PIL로 변환
|
||||
if image is not None:
|
||||
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
return pil_image
|
||||
else:
|
||||
self.logger.log(f"이미지 파일 형식이 올바르지 않습니다. 대상 URL: {url}", level=logging.DEBUG)
|
||||
return None
|
||||
else:
|
||||
self.logger.log(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}", level=logging.DEBUG)
|
||||
retries += 1
|
||||
# await asyncio.sleep(random.randint(2, 5)) # 2~5초 대기 후 재시도
|
||||
time.sleep(random.randint(2, 5))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}", level=logging.DEBUG)
|
||||
retries += 1
|
||||
# await asyncio.sleep(random.randint(2, 5)) # 예외 발생 시 대기 후 재시도
|
||||
time.sleep(random.randint(2, 5))
|
||||
|
||||
self.logger.log("이미지 다운로드 최대 재시도 횟수를 초과했습니다.", level=logging.DEBUG)
|
||||
return None
|
||||
|
||||
def process_clipboard(self, original_url, is_success_translated, toggle_states, path=None, is_thumb=False):
|
||||
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
|
||||
|
||||
try:
|
||||
is_watermark = toggle_states.get('watermark')
|
||||
self.logger.log(f"is_watermark : {is_watermark}", level=logging.DEBUG)
|
||||
|
||||
watermark_text = toggle_states.get('watermark_text')
|
||||
self.logger.log(f"watermark_text : {watermark_text}", level=logging.DEBUG)
|
||||
|
||||
opacity_percent = toggle_states.get('opacity_percent')
|
||||
self.logger.log(f"opacity_percent : {opacity_percent}", level=logging.DEBUG)
|
||||
|
||||
clipboard_data = self.get_clipboard_data()
|
||||
|
||||
self.logger.log("clipboard_data", level=logging.DEBUG)
|
||||
self.logger.log(f"{clipboard_data}", level=logging.DEBUG)
|
||||
self.logger.log(f"============================", level=logging.DEBUG)
|
||||
|
||||
# 1. 클립보드의 데이터가 Base64 이미지일 경우
|
||||
if isinstance(clipboard_data, str) and clipboard_data.startswith('data:image'):
|
||||
self.logger.log("[process_clipboard] data:image 감지 : 이미지 데이터로 변환", level=logging.INFO)
|
||||
image = self.base64_to_image(clipboard_data)
|
||||
if image:
|
||||
width, _ = image.size
|
||||
self.logger.log(f"Base64 이미지 크기: {width}px", level=logging.DEBUG)
|
||||
|
||||
# 가로 크기가 200픽셀 이상이면 크롭
|
||||
if width >= 200:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...", level=logging.DEBUG)
|
||||
cropped_image = self.crop_image(image, is_thumb) # 크롭 메서드 사용
|
||||
|
||||
# 워터마크 추가
|
||||
if is_watermark and not is_thumb: # is_thumb가 True라면 워터마크 추가를 건너뜁니다
|
||||
self.logger.log("워터마크 추가 중...", level=logging.DEBUG)
|
||||
cropped_watermark_image = self.add_watermark(cropped_image, watermark_text, opacity_percent) # 워터마크 추가
|
||||
cropped_image = cropped_watermark_image
|
||||
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
self.save_image_to_path(cropped_image, path)
|
||||
else:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이하: 클립보드 비움.", level=logging.DEBUG)
|
||||
self.clear_clipboard()
|
||||
else:
|
||||
self.logger.log("Base64 이미지 변환 실패.", level=logging.DEBUG)
|
||||
|
||||
# 2. 클립보드에 이미지가 있을 경우
|
||||
elif isinstance(clipboard_data, Image.Image):
|
||||
self.logger.log("[process_clipboard] 클립보드 이미지 확인", level=logging.INFO)
|
||||
|
||||
image = clipboard_data
|
||||
width, _ = image.size
|
||||
self.logger.log(f"클립보드에 있는 이미지 크기: {width}px", level=logging.DEBUG)
|
||||
|
||||
if width >= 200:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...", level=logging.DEBUG)
|
||||
cropped_image = self.crop_image(image, is_thumb) # 크롭 메서드 사용
|
||||
|
||||
# 워터마크 추가
|
||||
if is_watermark and not is_thumb: # is_thumb가 True라면 워터마크 추가를 건너뜁니다
|
||||
self.logger.log("워터마크 추가 중...", level=logging.DEBUG)
|
||||
cropped_watermark_image = self.add_watermark(cropped_image, watermark_text, opacity_percent) # 워터마크 추가
|
||||
cropped_image = cropped_watermark_image
|
||||
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
self.save_image_to_path(cropped_image, path)
|
||||
|
||||
else:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이하: 클립보드 비움.", level=logging.DEBUG)
|
||||
self.clear_clipboard()
|
||||
|
||||
# 3. 클립보드에 데이터가 없거나 html > whale-ocr 처리
|
||||
elif clipboard_data == "html > whale-ocr" or clipboard_data is None or not is_success_translated:
|
||||
if clipboard_data == "html > whale-ocr":
|
||||
self.logger.log("[process_clipboard] html > whale-ocr 감지 : 이미지 번역 실패 확인", level=logging.INFO)
|
||||
elif clipboard_data is None:
|
||||
self.logger.log("[process_clipboard] 클립보드에 이미지 없음", level=logging.INFO)
|
||||
elif is_success_translated is None:
|
||||
self.logger.log("[process_clipboard] 번역 실패로 인한 원본이미지 다운로드", level=logging.INFO)
|
||||
|
||||
if original_url:
|
||||
image = self.download_image_from_url(original_url)
|
||||
if image:
|
||||
self.logger.log("원본 이미지 다운로드 성공!", level=logging.DEBUG)
|
||||
|
||||
self.set_image_to_clipboard(image) # 크롭 없이 저장
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
self.save_image_to_path(image, path)
|
||||
else:
|
||||
self.logger.log("원본 이미지 다운로드 실패.", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log("원본 이미지 URL을 찾을 수 없습니다.", level=logging.DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"클립보드에서 이미지를 처리하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
|
||||
def process_clipboard_to_save_path(self, original_url, is_success_translated, toggle_states, path=None, is_thumb=False):
|
||||
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
|
||||
|
||||
try:
|
||||
is_watermark = toggle_states.get('watermark')
|
||||
self.logger.log(f"is_watermark : {is_watermark}", level=logging.DEBUG)
|
||||
|
||||
watermark_text = toggle_states.get('watermark_text')
|
||||
self.logger.log(f"watermark_text : {watermark_text}", level=logging.DEBUG)
|
||||
|
||||
opacity_percent = toggle_states.get('opacity_percent')
|
||||
self.logger.log(f"opacity_percent : {opacity_percent}", level=logging.DEBUG)
|
||||
|
||||
clipboard_data = self.get_clipboard_data()
|
||||
|
||||
self.logger.log("clipboard_data", level=logging.DEBUG)
|
||||
self.logger.log(f"{clipboard_data}", level=logging.DEBUG)
|
||||
self.logger.log(f"============================", level=logging.DEBUG)
|
||||
|
||||
# 1. 클립보드의 데이터가 Base64 이미지일 경우
|
||||
if isinstance(clipboard_data, str) and clipboard_data.startswith('data:image'):
|
||||
self.logger.log("[process_clipboard] data:image 감지 : 이미지 데이터로 변환", level=logging.INFO)
|
||||
image = self.base64_to_image(clipboard_data)
|
||||
if image:
|
||||
width, _ = image.size
|
||||
self.logger.log(f"Base64 이미지 크기: {width}px", level=logging.DEBUG)
|
||||
|
||||
# 가로 크기가 200픽셀 이상이면 크롭
|
||||
if width >= 200:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...", level=logging.DEBUG)
|
||||
cropped_image = self.crop_image(image, is_thumb) # 크롭 메서드 사용
|
||||
|
||||
# 워터마크 추가
|
||||
if is_watermark and not is_thumb: # is_thumb가 True라면 워터마크 추가를 건너뜁니다
|
||||
self.logger.log("워터마크 추가 중...", level=logging.DEBUG)
|
||||
cropped_watermark_image = self.add_watermark(cropped_image, watermark_text, opacity_percent) # 워터마크 추가
|
||||
cropped_image = cropped_watermark_image
|
||||
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
return self.save_image_to_path(cropped_image, path)
|
||||
else:
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
|
||||
else:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이하로 처리불가: 클립보드 비움.", level=logging.DEBUG)
|
||||
self.clear_clipboard()
|
||||
else:
|
||||
self.logger.log("Base64 이미지 변환 실패.", level=logging.DEBUG)
|
||||
|
||||
# 2. 클립보드에 이미지가 있을 경우
|
||||
elif isinstance(clipboard_data, Image.Image):
|
||||
self.logger.log("[process_clipboard] 클립보드 이미지 확인", level=logging.INFO)
|
||||
|
||||
image = clipboard_data
|
||||
width, _ = image.size
|
||||
self.logger.log(f"클립보드에 있는 이미지 크기: {width}px", level=logging.DEBUG)
|
||||
|
||||
if width >= 200:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...", level=logging.DEBUG)
|
||||
cropped_image = self.crop_image(image, is_thumb) # 크롭 메서드 사용
|
||||
|
||||
# 워터마크 추가
|
||||
if is_watermark and not is_thumb: # is_thumb가 True라면 워터마크 추가를 건너뜁니다
|
||||
self.logger.log("워터마크 추가 중...", level=logging.DEBUG)
|
||||
cropped_watermark_image = self.add_watermark(cropped_image, watermark_text, opacity_percent) # 워터마크 추가
|
||||
cropped_image = cropped_watermark_image
|
||||
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
return self.save_image_to_path(cropped_image, path)
|
||||
else:
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
|
||||
else:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이하로 처리불가: 클립보드 비움.", level=logging.DEBUG)
|
||||
self.clear_clipboard()
|
||||
|
||||
# 3. 클립보드에 데이터가 없거나 html > whale-ocr 처리
|
||||
elif clipboard_data == "html > whale-ocr" or clipboard_data is None or not is_success_translated or clipboard_data.startswith("https://") or clipboard_data.startswith("http://"):
|
||||
if clipboard_data == "html > whale-ocr":
|
||||
self.logger.log("[process_clipboard] html > whale-ocr 감지 : 이미지 번역 실패 확인", level=logging.INFO)
|
||||
elif clipboard_data is None:
|
||||
self.logger.log("[process_clipboard] 클립보드에 이미지 없음", level=logging.INFO)
|
||||
elif is_success_translated is None:
|
||||
self.logger.log("[process_clipboard] 번역 실패로 인한 원본이미지 다운로드", level=logging.INFO)
|
||||
elif clipboard_data.startswith("https://") or clipboard_data.startswith("http://"):
|
||||
self.logger.log("[process_clipboard] 타임아웃으로 인한 번역 실패 - 원본이미지 다운로드", level=logging.INFO)
|
||||
|
||||
if original_url:
|
||||
image = self.download_image_from_url(original_url)
|
||||
if image:
|
||||
self.logger.log("원본 이미지 다운로드 성공!", level=logging.DEBUG)
|
||||
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
return self.save_image_to_path(image, path)
|
||||
else:
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
else:
|
||||
self.logger.log("원본 이미지 다운로드 실패.", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log("원본 이미지 URL을 찾을 수 없습니다.", level=logging.DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"클립보드에서 이미지를 처리하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
def process_clipboard_to_save_path_with_local_hosted_image(self, local_image_path, is_success_translated, toggle_states, path=None, is_thumb=False):
|
||||
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기
|
||||
|
||||
Returns:
|
||||
str: 처리된 이미지 파일 경로 (성공 시)
|
||||
str: 원본 이미지 파일 경로 (실패 시)
|
||||
"""
|
||||
|
||||
# 매개변수 유효성 검사
|
||||
if not local_image_path or not os.path.exists(local_image_path):
|
||||
self.logger.log(f"유효하지 않은 로컬 이미지 경로: {local_image_path}", level=logging.ERROR)
|
||||
return local_image_path if local_image_path else None
|
||||
|
||||
if not toggle_states:
|
||||
self.logger.log("toggle_states가 제공되지 않았습니다", level=logging.WARNING)
|
||||
toggle_states = {}
|
||||
|
||||
try:
|
||||
is_watermark = toggle_states.get('watermark', False)
|
||||
self.logger.log(f"is_watermark : {is_watermark}", level=logging.DEBUG)
|
||||
|
||||
watermark_text = toggle_states.get('watermark_text', '')
|
||||
self.logger.log(f"watermark_text : {watermark_text}", level=logging.DEBUG)
|
||||
|
||||
opacity_percent = toggle_states.get('opacity_percent', 20)
|
||||
self.logger.log(f"opacity_percent : {opacity_percent}", level=logging.DEBUG)
|
||||
|
||||
clipboard_data = self.get_clipboard_data()
|
||||
|
||||
self.logger.log(f"type(clipboard_data) : {type(clipboard_data)}", level=logging.DEBUG)
|
||||
self.logger.log(f"============================", level=logging.DEBUG)
|
||||
|
||||
# 1. 클립보드의 데이터가 Base64 이미지일 경우
|
||||
if isinstance(clipboard_data, str) and clipboard_data.startswith('data:image'):
|
||||
self.logger.log("[process_clipboard] data:image 감지 : 이미지 데이터로 변환", level=logging.INFO)
|
||||
image = self.base64_to_image(clipboard_data)
|
||||
if image:
|
||||
width, _ = image.size
|
||||
self.logger.log(f"Base64 이미지 크기: {width}px", level=logging.DEBUG)
|
||||
|
||||
# 가로 크기가 200픽셀 이상이면 크롭
|
||||
if width >= 200:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...", level=logging.DEBUG)
|
||||
cropped_image = self.crop_image(image, is_thumb) # 크롭 메서드 사용
|
||||
|
||||
# 워터마크 추가
|
||||
if is_watermark and not is_thumb: # is_thumb가 True라면 워터마크 추가를 건너뜁니다
|
||||
self.logger.log("워터마크 추가 중...", level=logging.DEBUG)
|
||||
cropped_watermark_image = self.add_watermark(cropped_image, watermark_text, opacity_percent) # 워터마크 추가
|
||||
cropped_image = cropped_watermark_image
|
||||
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
saved_path = self.save_image_to_path(cropped_image, path)
|
||||
return saved_path if saved_path else local_image_path
|
||||
else:
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
return local_image_path # path가 없으면 원본 경로 반환
|
||||
|
||||
else:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이하로 처리불가: 클립보드 비움.", level=logging.DEBUG)
|
||||
self.clear_clipboard()
|
||||
return local_image_path
|
||||
else:
|
||||
self.logger.log("Base64 이미지 변환 실패.", level=logging.DEBUG)
|
||||
return local_image_path
|
||||
|
||||
# 2. 클립보드에 이미지가 있을 경우
|
||||
elif isinstance(clipboard_data, Image.Image):
|
||||
self.logger.log("[process_clipboard] 클립보드 이미지 확인", level=logging.INFO)
|
||||
|
||||
image = clipboard_data
|
||||
width, _ = image.size
|
||||
self.logger.log(f"클립보드에 있는 이미지 크기: {width}px", level=logging.DEBUG)
|
||||
|
||||
if width >= 200:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...", level=logging.DEBUG)
|
||||
cropped_image = self.crop_image(image, is_thumb) # 크롭 메서드 사용
|
||||
|
||||
# 워터마크 추가
|
||||
if is_watermark and not is_thumb: # is_thumb가 True라면 워터마크 추가를 건너뜁니다
|
||||
self.logger.log("워터마크 추가 중...", level=logging.DEBUG)
|
||||
cropped_watermark_image = self.add_watermark(cropped_image, watermark_text, opacity_percent) # 워터마크 추가
|
||||
cropped_image = cropped_watermark_image
|
||||
|
||||
if path:
|
||||
self.logger.log("이미지 저장 시도...", level=logging.DEBUG)
|
||||
saved_path = self.save_image_to_path(cropped_image, path)
|
||||
return saved_path if saved_path else local_image_path
|
||||
else:
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
return local_image_path # path가 없으면 원본 경로 반환
|
||||
|
||||
else:
|
||||
self.logger.log("이미지 가로 크기 200픽셀 이하로 처리불가: 클립보드 비움.", level=logging.DEBUG)
|
||||
self.clear_clipboard()
|
||||
return local_image_path
|
||||
|
||||
# 3. 클립보드에 데이터가 없거나 html > whale-ocr 처리
|
||||
elif clipboard_data == "html > whale-ocr" or clipboard_data is None or not is_success_translated or clipboard_data.startswith("https://") or clipboard_data.startswith("http://"):
|
||||
if clipboard_data == "html > whale-ocr":
|
||||
self.logger.log("[process_clipboard] html > whale-ocr 감지 : 이미지 번역 실패 확인", level=logging.INFO)
|
||||
elif clipboard_data is None:
|
||||
self.logger.log("[process_clipboard] 클립보드에 이미지 없음", level=logging.INFO)
|
||||
elif not is_success_translated:
|
||||
self.logger.log("[process_clipboard] 번역 실패로 인한 원본이미지 사용", level=logging.INFO)
|
||||
elif clipboard_data.startswith("https://") or clipboard_data.startswith("http://"):
|
||||
self.logger.log("[process_clipboard] 타임아웃으로 인한 번역 실패 - 원본이미지 사용", level=logging.INFO)
|
||||
|
||||
return local_image_path
|
||||
|
||||
# 4. 기타 예상하지 못한 클립보드 데이터
|
||||
else:
|
||||
self.logger.log(f"[process_clipboard] 예상하지 못한 클립보드 데이터 타입: {type(clipboard_data)}", level=logging.WARNING)
|
||||
return local_image_path
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"클립보드에서 이미지를 처리하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
return local_image_path # 오류 시 원본 경로 반환
|
||||
|
||||
def is_clipboard_image(self):
|
||||
"""클립보드에 이미지가 있는지 확인하는 함수"""
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
is_clipboard_image_flag = win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB)
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
if is_clipboard_image_flag:
|
||||
self.logger.log("클립보드에 이미지가 존재합니다.", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log("클립보드에 이미지가 없습니다.", level=logging.DEBUG)
|
||||
|
||||
return is_clipboard_image_flag
|
||||
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
# 클립보드가 열려있으면 닫기 시도
|
||||
try:
|
||||
win32clipboard.CloseClipboard()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.logger.log(f"클립보드 이미지 확인 중 오류 발생 (시도 {attempt}/{max_attempts}): {e}", level=logging.WARNING)
|
||||
if attempt < max_attempts:
|
||||
time.sleep(0.5) # 0.5초 대기 후 재시도
|
||||
else:
|
||||
self.logger.log(f"클립보드 이미지 확인 중 최대 시도 횟수 초과: {e}", level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
|
||||
def get_image_from_clipboard(self):
|
||||
"""클립보드에서 이미지를 가져오는 함수"""
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
if self.is_clipboard_image():
|
||||
dib_data = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
|
||||
win32clipboard.CloseClipboard()
|
||||
image = Image.open(BytesIO(dib_data))
|
||||
return image
|
||||
else:
|
||||
win32clipboard.CloseClipboard()
|
||||
self.logger.log("클립보드에 이미지가 없습니다.", level=logging.DEBUG)
|
||||
return None
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
# 클립보드가 열려있으면 닫기 시도
|
||||
try:
|
||||
win32clipboard.CloseClipboard()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.logger.log(f"클립보드에서 이미지를 가져오는 중 오류 발생 (시도 {attempt}/{max_attempts}): {e}", level=logging.WARNING)
|
||||
if attempt < max_attempts:
|
||||
time.sleep(0.5) # 0.5초 대기 후 재시도
|
||||
else:
|
||||
self.logger.log(f"클립보드에서 이미지를 가져오는 중 최대 시도 횟수 초과: {e}", level=logging.ERROR, exc_info=True)
|
||||
return None
|
||||
|
||||
def clear_clipboard(self):
|
||||
"""클립보드를 비우는 함수"""
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
success = False
|
||||
|
||||
while attempt < max_attempts and not success:
|
||||
try:
|
||||
# 먼저 pywinauto로 시도
|
||||
try:
|
||||
pywinauto.clipboard.EmptyClipboard()
|
||||
success = True
|
||||
except:
|
||||
# pywinauto 실패 시 win32clipboard로 시도
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.CloseClipboard()
|
||||
success = True
|
||||
|
||||
self.logger.log(f"클립보드가 비워졌습니다. (시도 {attempt+1}/{max_attempts})", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
attempt += 1
|
||||
self.logger.log(f"클립보드를 비우는 중 오류 발생 (시도 {attempt}/{max_attempts}): {e}", level=logging.WARNING)
|
||||
if attempt < max_attempts:
|
||||
time.sleep(0.5) # 0.5초 대기 후 재시도
|
||||
|
||||
if not success:
|
||||
self.logger.log("최대 시도 횟수를 초과하여 클립보드를 비우지 못했습니다.", level=logging.ERROR)
|
||||
|
||||
def crop_image(self, image, is_thumb=False, crop_percentage=0.01):
|
||||
"""이미지를 주어진 퍼센트만큼 크롭하는 함수"""
|
||||
if is_thumb:
|
||||
crop_percentage = 0.03
|
||||
self.logger.log(f"썸네일 이미지 이므로 크롭 3%로 조정", level=logging.DEBUG)
|
||||
|
||||
width, height = image.size
|
||||
left = width * crop_percentage
|
||||
top = height * crop_percentage
|
||||
right = width * (1 - crop_percentage)
|
||||
bottom = height * (1 - crop_percentage)
|
||||
|
||||
cropped_image = image.crop((left, top, right, bottom))
|
||||
|
||||
if self.debug:
|
||||
# 디버그 모드일 경우 크롭 전후 다양한 비율로 이미지 저장
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
original_image_path = os.path.join(os.getcwd(), f"original_image_{timestamp}.png")
|
||||
image.save(original_image_path) # 크롭 전 이미지 저장
|
||||
self.logger.log(f"크롭 전 이미지 저장됨: {original_image_path}", level=logging.DEBUG)
|
||||
|
||||
# 1%, 2%, 3% 크롭 이미지 저장
|
||||
crop_alternatives = [0.01, 0.02, 0.03]
|
||||
for crop in crop_alternatives:
|
||||
left_alt = width * crop
|
||||
top_alt = height * crop
|
||||
right_alt = width * (1 - crop)
|
||||
bottom_alt = height * (1 - crop)
|
||||
|
||||
cropped_alt_image = image.crop((left_alt, top_alt, right_alt, bottom_alt))
|
||||
cropped_image_path = os.path.join(os.getcwd(), f"cropped_image_{int(crop*100)}_{timestamp}.png")
|
||||
cropped_alt_image.save(cropped_image_path)
|
||||
self.logger.log(f"{int(crop*100)}% 크롭된 이미지 저장됨: {cropped_image_path}", level=logging.DEBUG)
|
||||
|
||||
return cropped_image
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import logging
|
||||
from openai import OpenAI
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
|
||||
|
||||
class GPTClient:
|
||||
def __init__(self, model="gpt-4o-mini", temperature=0.2):
|
||||
self.client = None
|
||||
self.model = model
|
||||
self.temperature = temperature
|
||||
|
||||
self.set_client(api_key='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')
|
||||
|
||||
def set_client(self, api_key):
|
||||
self.client = OpenAI(api_key=api_key)
|
||||
|
||||
def ask(self, prompt: str) -> dict:
|
||||
"""프롬프트를 이용하여 GPT 모델로부터 응답을 받습니다. 항상 JSON 형식으로 반환."""
|
||||
try:
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
temperature=self.temperature,
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
# GPT 응답 내용 가져오기
|
||||
content = response.choices[0].message.content.strip()
|
||||
print(f'GPT 응답: {content}')
|
||||
# 불필요한 포맷팅 제거 (```json```)
|
||||
cleaned_content = re.sub(r"^```json|```$", "", content).strip()
|
||||
|
||||
# JSON 변환 시도
|
||||
return json.loads(cleaned_content)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'JSON 디코딩 실패: {e}. 원본 응답: {content}')
|
||||
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f'GPT 통신 오류: {e}')
|
||||
return {}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import os
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
import shutil
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import re
|
||||
import cv2
|
||||
import base64
|
||||
import requests
|
||||
import numpy as np
|
||||
from modules.ocr_module import OCRModule
|
||||
from modules.mask_module import MaskModule
|
||||
from modules.text_rendering_module import TextRenderingModule
|
||||
from modules.postImageManager import PostImageManager
|
||||
|
||||
class ImageProcessor:
|
||||
"""이미지 다운로드, OCR, 번역 처리를 담당하는 클래스"""
|
||||
|
||||
def __init__(self, logger, gpt_client, base_dir, font_path):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
self.gpt_client = gpt_client
|
||||
|
||||
# 인페인트 포트
|
||||
self.inpaint_sv_port = None
|
||||
|
||||
self.font_path = font_path
|
||||
self.TEMP_IMAGE_DIR = os.path.join(self.base_dir, "temp_images")
|
||||
os.makedirs(self.TEMP_IMAGE_DIR, exist_ok=True)
|
||||
|
||||
self.ocr_module = OCRModule(logger=self.logger, base_dir=self.base_dir)
|
||||
self.mask_module = MaskModule(logger=self.logger, base_dir=self.base_dir)
|
||||
self.text_rendering_module = TextRenderingModule(logger=self.logger, font_path=self.font_path)
|
||||
self.postImageManager = PostImageManager(logger=self.logger, font_path=self.font_path)
|
||||
|
||||
def __del__(self):
|
||||
"""소멸자에서 리소스 정리"""
|
||||
self.cleanup()
|
||||
|
||||
def update_iop_port(self, port):
|
||||
self.inpaint_sv_port = port
|
||||
|
||||
def cleanup(self):
|
||||
"""리소스 정리"""
|
||||
try:
|
||||
|
||||
# 임시 폴더 삭제
|
||||
if hasattr(self, 'TEMP_IMAGE_DIR') and os.path.exists(self.TEMP_IMAGE_DIR):
|
||||
shutil.rmtree(self.TEMP_IMAGE_DIR)
|
||||
self.logger.log(f"임시 폴더 삭제됨: {self.TEMP_IMAGE_DIR}", level=logging.INFO)
|
||||
except Exception as e:
|
||||
self.logger.log(f"리소스 정리 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
def is_valid_image_path(self, path):
|
||||
# http/https 또는 로컬 파일(.jpg, .png 등) 모두 허용
|
||||
if re.match(r'^(http|https)://.*\\.(jpg|jpeg|png|bmp|gif|webp|tiff?)(\\?.*)?$', path, re.IGNORECASE):
|
||||
return True
|
||||
if os.path.isfile(path) and path.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tif', '.tiff')):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def process_single_image(self, toggle_states, unwanted_texts, local_image_path, index, file_prefix=""):
|
||||
"""
|
||||
단일 이미지를 처리합니다 (다운로드 -> OCR -> 인페인팅)
|
||||
|
||||
Args:
|
||||
toggle_states: 토글 상태 딕셔너리
|
||||
local_image_path (str): 처리할 이미지 경로
|
||||
index (int): 이미지 인덱스
|
||||
unwanted_texts: 치환할 텍스트 딕셔너리
|
||||
file_prefix (str): 파일명에 추가할 접두사 (예: "detail", "option")
|
||||
|
||||
Returns:
|
||||
dict: 처리 결과를 포함한 딕셔너리
|
||||
- status: 'inpainted', 'original', 'exclude', 'error' 중 하나
|
||||
- path: 처리된 이미지 파일 경로 또는 원본 이미지 파일 경로
|
||||
- error: 오류 메시지 (status가 'error'인 경우에만 포함)
|
||||
"""
|
||||
ocr_enabled = toggle_states.get('ocr', False)
|
||||
unwanted_texts = unwanted_texts
|
||||
|
||||
try:
|
||||
ocr_results = self.ocr_module.detect_text(local_image_path)
|
||||
|
||||
# 3. 중국어 텍스트 없는 경우 원본 이미지 반환
|
||||
if not self.ocr_module.filter_chinese_text(ocr_results):
|
||||
self.logger.log(f"이미지 {index+1} 중국어 텍스트 없음, 원본 이미지 반환", level=logging.INFO)
|
||||
return local_image_path
|
||||
|
||||
# 4. 텍스트 번역 (GPT)
|
||||
translated_texts = self.gpt_translate_texts(ocr_results, self.gpt_client)
|
||||
|
||||
if ocr_enabled:
|
||||
filtered_translated_texts = self.process_translated_texts(translated_texts, unwanted_texts, local_image_path, index)
|
||||
if not filtered_translated_texts:
|
||||
self.logger.log(f"이미지 {index+1} 제외됨", level=logging.INFO)
|
||||
return None
|
||||
else:
|
||||
self.logger.log(f"이미지 {index+1} 치환됨", level=logging.INFO)
|
||||
|
||||
# 마스크 생성 (basic 방식만 사용)
|
||||
masks = self.mask_module.create_masks(
|
||||
image_path=local_image_path, ocr_results=ocr_results, mask_option="basic"
|
||||
)
|
||||
self.logger.log(f"마스크 생성 완료", level=logging.INFO)
|
||||
|
||||
# 인페인팅
|
||||
inpainted_image = self.call_inpaint_api(local_image_path, masks)
|
||||
self.logger.log(f"인페인팅 완료", level=logging.INFO)
|
||||
|
||||
# 텍스트 렌더링
|
||||
text_rendered_image = self.text_rendering_module.render_text(
|
||||
inpainted_image, ocr_results, filtered_translated_texts, font_path=self.font_path)
|
||||
self.logger.log(f"텍스트 렌더링 완료", level=logging.INFO)
|
||||
|
||||
# 결과 저장
|
||||
translated_img_path = await self.postProcess_and_save_image(local_image_path, text_rendered_image, index, file_prefix, toggle_states)
|
||||
self.logger.log(f"이미지 {index+1} 번역 완료: {translated_img_path}", level=logging.INFO)
|
||||
return translated_img_path
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"이미지 {index+1} 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
return {'status': 'failed', 'path': local_image_path, 'error': str(e)}
|
||||
|
||||
async def postProcess_and_save_image(self, local_image_path, text_rendered_image, index, file_prefix, toggle_states):
|
||||
"""로컬 서버 URL을 사용해 이미지를 번역하고 로컬에 저장합니다"""
|
||||
try:
|
||||
# 파일명에 접두사 포함
|
||||
if file_prefix:
|
||||
img_path = os.path.join(self.TEMP_IMAGE_DIR, f"translated_{file_prefix}_img_{index+1}.png")
|
||||
else:
|
||||
img_path = os.path.join(self.TEMP_IMAGE_DIR, f"translated_img_{index+1}.png")
|
||||
|
||||
watermarked_image_data = self.postImageManager.add_watermark(image_data=text_rendered_image, watermark_text=toggle_states.get("watermark_text", "워터마크"))
|
||||
final_image_path = self.postImageManager.save_image_to_path(watermarked_image_data, img_path)
|
||||
|
||||
return final_image_path
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"이미지 {index+1} 번역 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
return local_image_path
|
||||
|
||||
def is_valid_image_data(self, image_data):
|
||||
"""이미지 데이터가 유효한지 확인합니다"""
|
||||
if not image_data or len(image_data) < 100:
|
||||
return False
|
||||
|
||||
# 일반적인 이미지 파일 시그니처 확인
|
||||
image_signatures = [
|
||||
b'\xFF\xD8\xFF', # JPEG
|
||||
b'\x89PNG\r\n\x1a\n', # PNG
|
||||
b'GIF87a', # GIF87a
|
||||
b'GIF89a', # GIF89a
|
||||
b'RIFF', # WebP (RIFF 컨테이너)
|
||||
]
|
||||
|
||||
return any(image_data.startswith(sig) for sig in image_signatures)
|
||||
|
||||
def call_inpaint_api(self, image, mask):
|
||||
"""
|
||||
인페인팅 API를 호출하여 이미지를 인페인팅합니다.
|
||||
"""
|
||||
try:
|
||||
# 이미지 처리
|
||||
if isinstance(image, str):
|
||||
image_np = cv2.imread(image)
|
||||
if image_np is None:
|
||||
self.logger.log(f"이미지 로딩 실패: {image}", level=logging.ERROR)
|
||||
return None
|
||||
else:
|
||||
image_np = image
|
||||
|
||||
# 마스크 처리
|
||||
if isinstance(mask, str):
|
||||
mask_np = cv2.imread(mask, cv2.IMREAD_GRAYSCALE)
|
||||
if mask_np is None:
|
||||
self.logger.log(f"마스크 로딩 실패: {mask}", level=logging.ERROR)
|
||||
return None
|
||||
else:
|
||||
mask_np = mask
|
||||
|
||||
api_url = f"http://127.0.0.1:{self.inpaint_sv_port}/api/v1/inpaint"
|
||||
_, img_encoded = cv2.imencode('.png', image_np)
|
||||
_, mask_encoded = cv2.imencode('.png', mask_np)
|
||||
img_b64 = base64.b64encode(img_encoded).decode('utf-8')
|
||||
mask_b64 = base64.b64encode(mask_encoded).decode('utf-8')
|
||||
payload = {
|
||||
"image": img_b64,
|
||||
"mask": mask_b64
|
||||
}
|
||||
response = requests.post(api_url, json=payload)
|
||||
if response.status_code != 200:
|
||||
self.logger.log(f"IOPaint 서버 에러: {response.text}", level=logging.ERROR)
|
||||
return None
|
||||
nparr = np.frombuffer(response.content, np.uint8)
|
||||
result = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||
return result
|
||||
except Exception as e:
|
||||
self.logger.log(f"인페인팅 API 호출 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def process_translated_texts(self, translated_texts, unwanted_texts, local_image_path, index):
|
||||
"""
|
||||
번역된 단어 리스트(translated_texts)에서 unwanted_texts의 원본값이
|
||||
앞이나 뒤에 포함되면 치환값으로 바꿉니다.
|
||||
치환값이 '이미지삭제'라면 None 반환(이미지 제외)
|
||||
"""
|
||||
|
||||
new_texts = []
|
||||
for text in translated_texts:
|
||||
replaced = False
|
||||
for origin, replace in unwanted_texts.items():
|
||||
# 앞/뒤에 원본값이 있는지 확인
|
||||
if text.startswith(origin) or text.endswith(origin):
|
||||
self.logger.log(f"[{text}] -> [{replace}] (치환)", level=logging.INFO)
|
||||
if replace == "이미지삭제":
|
||||
self.logger.log(f"이미지 {index+1} 제외됨: {local_image_path}", level=logging.INFO)
|
||||
return None
|
||||
# 앞/뒤 원본값만 치환
|
||||
if text.startswith(origin):
|
||||
new = replace + text[len(origin):]
|
||||
elif text.endswith(origin):
|
||||
new = text[:-len(origin)] + replace
|
||||
new_texts.append(new)
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
new_texts.append(text)
|
||||
|
||||
self.logger.log(f"최종 치환 결과: {new_texts}", level=logging.INFO)
|
||||
return new_texts
|
||||
|
||||
|
||||
async def process_image_list(self, image_urls, delay=1.0, file_prefix="", use_inpainting=False):
|
||||
"""
|
||||
이미지 리스트를 순차적으로 처리합니다.
|
||||
"""
|
||||
if not image_urls:
|
||||
self.logger.log("처리할 이미지가 없습니다.", level=logging.INFO)
|
||||
return []
|
||||
|
||||
processing_mode = "인페인팅" if use_inpainting else "웨일 번역"
|
||||
self.logger.log(f"이미지 {len(image_urls)}개를 {processing_mode} 모드로 처리 시작", level=logging.INFO)
|
||||
|
||||
processed_images = []
|
||||
|
||||
for i, url in enumerate(image_urls):
|
||||
self.logger.log(f"이미지 {i+1}/{len(image_urls)} 처리 중... ({processing_mode} 모드)", level=logging.INFO)
|
||||
|
||||
result = await self.process_single_image(
|
||||
url, i, delay, file_prefix, use_inpainting
|
||||
)
|
||||
|
||||
# 결과 처리
|
||||
if isinstance(result, dict):
|
||||
status = result.get('status')
|
||||
path = result.get('path')
|
||||
|
||||
if status == 'inpainted':
|
||||
processed_images.append(path)
|
||||
self.logger.log(f"이미지 {i+1} 인페인팅 처리 완료", level=logging.INFO)
|
||||
elif status == 'original':
|
||||
processed_images.append(path)
|
||||
self.logger.log(f"이미지 {i+1} 원본 사용", level=logging.INFO)
|
||||
elif status == 'exclude':
|
||||
self.logger.log(f"이미지 {i+1} 제외됨", level=logging.INFO)
|
||||
# 제외된 이미지는 리스트에 추가하지 않음
|
||||
else: # failed
|
||||
self.logger.log(f"이미지 {i+1} 처리 실패: {result.get('error', '알 수 없는 오류')}", level=logging.WARNING)
|
||||
# 실패한 이미지는 원본 경로 추가
|
||||
processed_images.append(path)
|
||||
else:
|
||||
# 이전 버전과의 호환성을 위한 처리
|
||||
if result:
|
||||
processed_images.append(result)
|
||||
|
||||
self.logger.log(f"이미지 처리 완료: 총 {len(processed_images)}개 ({processing_mode} 모드)", level=logging.INFO)
|
||||
return processed_images
|
||||
|
||||
|
||||
def gpt_translate_texts(self, ocr_results, gpt_client):
|
||||
texts = [result['text'] for result in ocr_results]
|
||||
if not texts:
|
||||
return []
|
||||
prompt = (
|
||||
"다음 중국어 문장들을 한국어로 자연스럽고 의미가 잘 전달되게 번역해줘. "
|
||||
"순서와 개수는 반드시 그대로 유지하고, 결과는 JSON 배열(리스트)로만 반환해. "
|
||||
"중국어 리스트:\n" +
|
||||
str(texts)
|
||||
)
|
||||
response = gpt_client.ask(prompt)
|
||||
if isinstance(response, list):
|
||||
return response
|
||||
elif isinstance(response, dict) and 'result' in response:
|
||||
return response['result']
|
||||
else:
|
||||
print("GPT 번역 결과 파싱 실패, 원본 반환")
|
||||
return texts
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import random
|
||||
import socket
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Query, Body
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from modules.image_processor2 import ImageProcessor
|
||||
|
||||
# 포트 범위 설정
|
||||
PORT_RANGE = (7000, 7010)
|
||||
|
||||
# 사용 가능한 포트 찾기
|
||||
def find_free_port():
|
||||
for _ in range(20):
|
||||
port = random.randint(*PORT_RANGE)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
s.bind(("127.0.0.1", port))
|
||||
return port
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError("사용 가능한 포트를 찾을 수 없습니다.")
|
||||
|
||||
# 요청 모델 정의
|
||||
class ImageRequest(BaseModel):
|
||||
local_image_path: str
|
||||
file_prefix: Optional[str] = ""
|
||||
use_inpainting: Optional[bool] = False
|
||||
toggle_states: Optional[dict] = None
|
||||
unwanted_texts: Optional[dict] = None
|
||||
watermark_text: Optional[str] = None
|
||||
watermark_opacity: Optional[float] = None
|
||||
|
||||
class ImagesRequest(BaseModel):
|
||||
local_image_paths: List[str]
|
||||
file_prefix: Optional[str] = ""
|
||||
use_inpainting: Optional[bool] = False
|
||||
toggle_states: Optional[dict] = None
|
||||
unwanted_texts: Optional[dict] = None
|
||||
watermark_text: Optional[str] = None
|
||||
watermark_opacity: Optional[float] = None
|
||||
|
||||
# FastAPI 앱 생성
|
||||
def create_app(image_processor: ImageProcessor, max_workers: int = 2):
|
||||
app = FastAPI()
|
||||
executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
|
||||
@app.post("/translate_image")
|
||||
async def translate_image(req: ImageRequest):
|
||||
# 워터마크 관련 옵션을 toggle_states에 병합
|
||||
toggle_states = req.toggle_states.copy() if req.toggle_states else {}
|
||||
if req.watermark_text is not None:
|
||||
toggle_states["watermark_text"] = req.watermark_text
|
||||
if req.watermark_opacity is not None:
|
||||
toggle_states["watermark_opacity"] = req.watermark_opacity
|
||||
# 단일 이미지 번역
|
||||
result = await image_processor.process_single_image(
|
||||
toggle_states, req.unwanted_texts or {}, req.local_image_path, 0, req.file_prefix
|
||||
)
|
||||
# 경로만 반환
|
||||
if isinstance(result, dict):
|
||||
return {"result": result.get("path", None)}
|
||||
return {"result": result}
|
||||
|
||||
@app.post("/translate_images")
|
||||
async def translate_images(req: ImagesRequest):
|
||||
# 워터마크 관련 옵션을 toggle_states에 병합
|
||||
toggle_states = req.toggle_states.copy() if req.toggle_states else {}
|
||||
if req.watermark_text is not None:
|
||||
toggle_states["watermark_text"] = req.watermark_text
|
||||
if req.watermark_opacity is not None:
|
||||
toggle_states["watermark_opacity"] = req.watermark_opacity
|
||||
# 여러 이미지 병렬 번역
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = []
|
||||
sem = asyncio.Semaphore(max_workers)
|
||||
async def sem_task(idx, path):
|
||||
async with sem:
|
||||
return await image_processor.process_single_image(
|
||||
toggle_states, req.unwanted_texts or {}, path, idx, req.file_prefix
|
||||
)
|
||||
for idx, path in enumerate(req.local_image_paths):
|
||||
tasks.append(sem_task(idx, path))
|
||||
results = await asyncio.gather(*tasks)
|
||||
# 경로만 리스트로 반환
|
||||
def extract_path(res):
|
||||
if isinstance(res, dict):
|
||||
return res.get("path", None)
|
||||
return res
|
||||
return {"results": [extract_path(r) for r in results]}
|
||||
|
||||
return app
|
||||
|
||||
# 서버 실행 함수
|
||||
def run_server(image_processor, max_workers=2):
|
||||
port = find_free_port()
|
||||
app = create_app(image_processor, max_workers)
|
||||
uvicorn.run(app, host="127.0.0.1", port=port, workers=1)
|
||||
# FastAPI의 workers는 프로세스 수이므로, 내부 병렬은 ThreadPoolExecutor로 제어
|
||||
# 실제 워커 수는 process_single_image 병렬 호출로 제한
|
||||
# 서버 실행 후 포트 정보 반환 가능
|
||||
return port
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import numpy as np
|
||||
import requests
|
||||
import cv2
|
||||
import base64
|
||||
|
||||
class IOPaintInpainting:
|
||||
"""IOPaint 서버 연동 인페인팅 모델 (REST API /api/v1/inpaint 사용, 바이너리 PNG 반환)"""
|
||||
def __init__(self, server_url="http://localhost:8080"):
|
||||
self.api_url = f"http://localhost:8080/api/v1/inpaint"
|
||||
def inpaint(self, image: np.ndarray, mask: np.ndarray, api_url:str = 'http://localhost:8080/api/v1/inpaint', ) -> np.ndarray:
|
||||
# 이미지를 base64로 인코딩
|
||||
_, img_encoded = cv2.imencode('.png', image)
|
||||
_, mask_encoded = cv2.imencode('.png', mask)
|
||||
img_b64 = base64.b64encode(img_encoded).decode('utf-8')
|
||||
mask_b64 = base64.b64encode(mask_encoded).decode('utf-8')
|
||||
payload = {
|
||||
"image": img_b64,
|
||||
"mask": mask_b64
|
||||
}
|
||||
response = requests.post(api_url, json=payload)
|
||||
if response.status_code != 200:
|
||||
print("IOPaint 서버 에러:", response.text)
|
||||
return None
|
||||
# 응답이 바이너리 PNG 이미지이므로 바로 디코딩
|
||||
nparr = np.frombuffer(response.content, np.uint8)
|
||||
result = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||
return result
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
import numpy as np
|
||||
import requests
|
||||
import cv2
|
||||
import base64
|
||||
import subprocess
|
||||
import random
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import logging
|
||||
|
||||
class IOPaintManager:
|
||||
"""IOPaint 서버 인스턴스 및 인페인팅 요청을 통합 관리하는 매니저"""
|
||||
class ServerInstance:
|
||||
def __init__(self, port, process):
|
||||
self.port = port
|
||||
self.process = process
|
||||
self.busy = False
|
||||
self.last_used = time.time()
|
||||
def mark_busy(self):
|
||||
self.busy = True
|
||||
self.last_used = time.time()
|
||||
def mark_idle(self):
|
||||
self.busy = False
|
||||
self.last_used = time.time()
|
||||
def is_alive(self):
|
||||
return self.process.poll() is None
|
||||
|
||||
def __init__(self, logger, base_dir, num_instances=1, port_range=(7020, 7030), wait_ready=30, model_dir=None):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
self.iop_port = None
|
||||
self.instances = []
|
||||
self.port_range = port_range
|
||||
self.lock = threading.Lock()
|
||||
self.model_dir = model_dir or os.path.join(self.base_dir, 'modules', 'iop', 'models')
|
||||
self.exe_path = os.path.join(self.base_dir, 'modules', 'iop', 'iop.exe')
|
||||
self._start_instances(num_instances, wait_ready)
|
||||
|
||||
def _get_random_port(self):
|
||||
used_ports = {inst.port for inst in self.instances}
|
||||
candidates = [p for p in range(self.port_range[0], self.port_range[1]+1) if p not in used_ports]
|
||||
if not candidates:
|
||||
self.logger.log("사용 가능한 포트가 없습니다.", level=logging.ERROR)
|
||||
raise RuntimeError("사용 가능한 포트가 없습니다.")
|
||||
return random.choice(candidates)
|
||||
|
||||
def wait_for_server_ready(self, port, timeout=30):
|
||||
url = f"http://localhost:{port}/api/v1/server-config"
|
||||
start = time.time()
|
||||
last_error = None
|
||||
self.logger.log(f"[{port}] 서버 준비 체크 시작 (최대 {timeout}초 대기)", level=logging.INFO)
|
||||
tries = 0
|
||||
while time.time() - start < timeout:
|
||||
tries += 1
|
||||
try:
|
||||
r = requests.get(url, timeout=2)
|
||||
self.logger.log(f"응답 : {r}", level=logging.INFO)
|
||||
if r.status_code == 200:
|
||||
elapsed = time.time() - start
|
||||
self.logger.log(f"[{port}] 서버 준비 완료! (시도 {tries}회, {elapsed:.1f}초 소요)", level=logging.INFO)
|
||||
return True
|
||||
else:
|
||||
self.logger.log(f"[{port}] 응답 코드: {r.status_code}", level=logging.INFO)
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
self.logger.log(f"[{port}] 준비 체크 실패 (시도 {tries}회): {last_error}", level=logging.ERROR, exc_info=True)
|
||||
time.sleep(0.5)
|
||||
self.logger.log(f"[{port}] 서버 준비 실패 (총 {tries}회 시도, 마지막 에러: {last_error})", level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
|
||||
def _start_instances(self, num, wait_ready):
|
||||
self.logger.log(f"IOPaint 인스턴스 {num} 개 시작", level=logging.INFO)
|
||||
for _ in range(num):
|
||||
port = self._get_random_port()
|
||||
self.iop_port = port
|
||||
cmd = [self.exe_path, 'start', '--model=migan', '--device=cpu', '--port', str(port), '--model-dir', self.model_dir]
|
||||
self.logger.log(f"[{port}] 인스턴스 실행 명령: {' '.join(cmd)}", level=logging.INFO)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
instance = self.ServerInstance(port, proc)
|
||||
self.instances.append(instance)
|
||||
|
||||
start_wait = 8
|
||||
self.logger.log(f"[{port}] 인스턴스 실행 명시대기: {start_wait}초", level=logging.INFO)
|
||||
time.sleep(start_wait)
|
||||
|
||||
if self.wait_for_server_ready(port, timeout=wait_ready):
|
||||
self.logger.log(f"IOPaint 인스턴스 {instance.port} 준비됨", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log(f"IOPaint 인스턴스 {instance.port} 시작 실패", level=logging.ERROR)
|
||||
# 에러 메시지 출력
|
||||
try:
|
||||
out, err = proc.communicate(timeout=3)
|
||||
self.logger.log(f"[{port}] 표준출력:\n{out.decode(errors='ignore')}", level=logging.INFO)
|
||||
self.logger.log(f"[{port}] 표준에러:\n{err.decode(errors='ignore')}", level=logging.INFO)
|
||||
except Exception as e:
|
||||
self.logger.log(f"[{port}] 에러 메시지 읽기 실패: {e}", level=logging.ERROR)
|
||||
|
||||
def get_instance_info(self):
|
||||
"""모든 인스턴스의 정보를 반환"""
|
||||
info = []
|
||||
for inst in self.instances:
|
||||
info.append({
|
||||
"port": inst.port,
|
||||
"busy": inst.busy,
|
||||
"alive": inst.is_alive(),
|
||||
"last_used": inst.last_used
|
||||
})
|
||||
return info
|
||||
|
||||
def get_idle_instance(self):
|
||||
"""놀고 있는(사용 가능한) 인스턴스 반환 (없으면 None)"""
|
||||
with self.lock:
|
||||
for inst in self.instances:
|
||||
if not inst.busy and inst.is_alive():
|
||||
inst.mark_busy()
|
||||
self.logger.log(f"IOPaint 인스턴스 {inst.port} 사용 중", level=logging.INFO)
|
||||
return inst
|
||||
return None
|
||||
|
||||
def mark_instance_idle(self, port):
|
||||
"""작업이 끝난 인스턴스를 idle로 표시"""
|
||||
for inst in self.instances:
|
||||
if inst.port == port:
|
||||
inst.mark_idle()
|
||||
self.logger.log(f"IOPaint 인스턴스 {inst.port} 유휴", level=logging.INFO)
|
||||
break
|
||||
|
||||
def shutdown_all(self):
|
||||
"""모든 서버 인스턴스 종료"""
|
||||
for inst in self.instances:
|
||||
if inst.is_alive():
|
||||
inst.process.terminate()
|
||||
self.logger.log(f"IOPaint 인스턴스 {inst.port} 종료", level=logging.INFO)
|
||||
self.instances = []
|
||||
self.logger.log("모든 IOPaint 인스턴스 종료", level=logging.INFO)
|
||||
|
||||
def inpaint(self, image, mask, instance=None) -> np.ndarray:
|
||||
"""image와 mask를 경로나 np.ndarray 모두 지원"""
|
||||
# 이미지 처리
|
||||
if isinstance(image, str):
|
||||
image_np = cv2.imread(image)
|
||||
if image_np is None:
|
||||
self.logger.log(f"이미지 로딩 실패: {image}", level=logging.ERROR)
|
||||
return None
|
||||
else:
|
||||
image_np = image
|
||||
|
||||
# 마스크 처리
|
||||
if isinstance(mask, str):
|
||||
mask_np = cv2.imread(mask, cv2.IMREAD_GRAYSCALE)
|
||||
if mask_np is None:
|
||||
self.logger.log(f"마스크 로딩 실패: {mask}", level=logging.ERROR)
|
||||
return None
|
||||
else:
|
||||
mask_np = mask
|
||||
|
||||
|
||||
if instance is None:
|
||||
instance = self.get_idle_instance()
|
||||
if instance is None:
|
||||
self.logger.log("사용 가능한 IOPaint 인스턴스가 없습니다.", level=logging.ERROR)
|
||||
return None
|
||||
api_url = f"http://localhost:{instance.port}/api/v1/inpaint"
|
||||
self.logger.log(f"IOPaint 인스턴스 {instance.port} 사용", level=logging.INFO)
|
||||
try:
|
||||
_, img_encoded = cv2.imencode('.png', image_np)
|
||||
_, mask_encoded = cv2.imencode('.png', mask_np)
|
||||
img_b64 = base64.b64encode(img_encoded).decode('utf-8')
|
||||
mask_b64 = base64.b64encode(mask_encoded).decode('utf-8')
|
||||
payload = {
|
||||
"image": img_b64,
|
||||
"mask": mask_b64
|
||||
}
|
||||
response = requests.post(api_url, json=payload)
|
||||
if response.status_code != 200:
|
||||
self.logger.log(f"IOPaint 서버 에러: {response.text}", level=logging.ERROR)
|
||||
return None
|
||||
nparr = np.frombuffer(response.content, np.uint8)
|
||||
result = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||
return result
|
||||
finally:
|
||||
self.mark_instance_idle(instance.port)
|
||||
|
||||
def add_instance(self, wait_ready=30):
|
||||
port = self._get_random_port()
|
||||
cmd = [self.exe_path, 'start', '--model=lama', '--device=cpu', '--port', str(port), '--model-dir', self.model_dir]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
instance = self.ServerInstance(port, proc)
|
||||
self.instances.append(instance)
|
||||
if self.wait_for_server_ready(port, timeout=wait_ready):
|
||||
self.logger.log(f"IOPaint 인스턴스 {instance.port} 시작", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log(f"IOPaint 인스턴스 {instance.port} 시작 실패", level=logging.ERROR)
|
||||
return instance
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# manager = IOPaintManager(num_instances=1)
|
||||
# result = manager.inpaint(image, mask) # 자동으로 idle 인스턴스에 요청
|
||||
# print(manager.get_instance_info())
|
||||
# manager.shutdown_all()
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import subprocess
|
||||
import logging
|
||||
import socket
|
||||
import random
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
class IOPaint_Server:
|
||||
def __init__(self, logger, base_dir):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
|
||||
def find_free_port(self, port_range=(7322, 7322)):
|
||||
"""포트 범위 내에서 사용 가능한 포트 반환"""
|
||||
for _ in range(20):
|
||||
port = random.randint(*port_range)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
s.bind(("0.0.0.0", port))
|
||||
return port
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError("사용 가능한 포트를 찾을 수 없습니다.")
|
||||
|
||||
def start(self, wait_ready=30):
|
||||
"""iopaint 서버를 실행하고, 정상 실행시 포트 반환"""
|
||||
port = self.find_free_port()
|
||||
model_dir = os.path.join(self.base_dir, 'modules', 'iop', 'models')
|
||||
cmd = [sys.executable, '-m', 'iopaint', 'start', '--model=migan', '--device=cpu', '--port', str(port), '--model-dir', model_dir]
|
||||
self.logger.log(f"[IOPaint] 실행 환경 파이썬: {sys.executable}", level=logging.INFO)
|
||||
self.logger.log(f"[IOPaint] 실행 명령: {' '.join(cmd)}", level=logging.INFO)
|
||||
self.logger.log(f"[IOPaint] 모델 디렉토리: {model_dir}", level=logging.INFO)
|
||||
# pip list로 iopaint 설치여부 확인
|
||||
try:
|
||||
pip_list = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], text=True)
|
||||
found = any('iopaint' in line for line in pip_list.splitlines())
|
||||
if found:
|
||||
self.logger.log("[IOPaint] iopaint 모듈이 현재 환경에 설치되어 있습니다.", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log("[IOPaint] iopaint 모듈이 현재 환경에 설치되어 있지 않습니다!", level=logging.WARNING)
|
||||
except Exception as e:
|
||||
self.logger.log(f"[IOPaint] pip list 실행 실패: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
# subprocess를 실시간 출력으로 실행
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
||||
self.logger.log(f"[IOPaint] 서버 준비 확인 시작 (최대 {wait_ready}초 대기)", level=logging.INFO)
|
||||
url = f"http://localhost:{port}/api/v1/server-config"
|
||||
start_time = time.time()
|
||||
stdout_lines = []
|
||||
stderr_lines = []
|
||||
import threading
|
||||
def read_stream(stream, lines, stream_name):
|
||||
for line in iter(stream.readline, ''):
|
||||
lines.append(line)
|
||||
print(f"[{stream_name}] {line}", end='')
|
||||
stream.close()
|
||||
t_out = threading.Thread(target=read_stream, args=(proc.stdout, stdout_lines, 'STDOUT'))
|
||||
t_err = threading.Thread(target=read_stream, args=(proc.stderr, stderr_lines, 'STDERR'))
|
||||
t_out.start()
|
||||
t_err.start()
|
||||
ready = False
|
||||
while time.time() - start_time < wait_ready:
|
||||
try:
|
||||
import requests
|
||||
r = requests.get(url, timeout=2)
|
||||
if r.status_code == 200:
|
||||
self.logger.log(f"[IOPaint] 서버가 포트 {port}에서 준비됨.")
|
||||
ready = True
|
||||
break
|
||||
except Exception as e:
|
||||
time.sleep(0.5)
|
||||
t_out.join(timeout=2)
|
||||
t_err.join(timeout=2)
|
||||
if ready:
|
||||
return port
|
||||
# 실패 시 로그 및 예외
|
||||
self.logger.log(f"[IOPaint] 서버 실행 실패.\nstdout:\n{''.join(stdout_lines)}\nstderr:\n{''.join(stderr_lines)}", level=logging.ERROR, exc_info=True)
|
||||
print("[IOPaint] 서버 실행 실패. 전체 STDOUT:")
|
||||
print(''.join(stdout_lines))
|
||||
print("[IOPaint] 서버 실행 실패. 전체 STDERR:")
|
||||
print(''.join(stderr_lines))
|
||||
raise RuntimeError(f"IOPaint 서버가 {wait_ready}초 내에 준비되지 않았습니다.")
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
# logger = logging.getLogger(__name__)
|
||||
# base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# iop = IOPaint(logger, base_dir)
|
||||
# iop.start()
|
||||
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import logging
|
||||
from logging.handlers import RotatingFileHandler, BaseRotatingHandler
|
||||
import os
|
||||
import glob
|
||||
import traceback
|
||||
import inspect
|
||||
|
||||
|
||||
class Logger1():
|
||||
|
||||
def __init__(self, log_file="ITServer.log", logger_name="MainLogger",
|
||||
file_log_level=logging.DEBUG):
|
||||
"""
|
||||
Logger 초기화
|
||||
:param log_file: 로그 파일 이름
|
||||
:param logger_name: 로거 이름
|
||||
:param file_log_level: 파일 로거의 로그 레벨
|
||||
"""
|
||||
super().__init__()
|
||||
self.file_log_level = file_log_level
|
||||
|
||||
# 로그 설정
|
||||
self.logger = logging.getLogger(logger_name)
|
||||
self.logger.setLevel(file_log_level) # 파일 로거 레벨 설정
|
||||
|
||||
# 포맷 설정
|
||||
self.simple_format = "[%(asctime)s] [%(levelname)s] %(message)s"
|
||||
self.detailed_format = (
|
||||
"[%(asctime)s] [%(threadName)s] [%(levelname)s] "
|
||||
"[%(filename)s:%(funcName)s:%(lineno)d] %(message)s"
|
||||
)
|
||||
|
||||
# 핸들러 추가
|
||||
self._add_console_handler(file_log_level)
|
||||
self._add_file_handler(log_file, file_log_level)
|
||||
|
||||
def _add_console_handler(self, level):
|
||||
"""콘솔 핸들러 추가"""
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(level)
|
||||
formatter = logging.Formatter(
|
||||
self.detailed_format if level <= logging.DEBUG else self.simple_format
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
def _add_file_handler(self, log_file, level):
|
||||
"""파일 핸들러 추가"""
|
||||
# 확장자가 .log가 아니면 .log로 변경
|
||||
if not log_file.endswith('.log'):
|
||||
base_name, _ = os.path.splitext(log_file)
|
||||
log_file = base_name + '.log'
|
||||
|
||||
# 커스텀 로테이팅 핸들러 사용
|
||||
file_handler = CustomRotatingFileHandler(
|
||||
log_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8"
|
||||
)
|
||||
file_handler.setLevel(level)
|
||||
formatter = logging.Formatter(
|
||||
self.detailed_format if level <= logging.DEBUG else self.simple_format
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
def log(self, message, level=logging.INFO, exc_info=False):
|
||||
"""로그 메시지 기록"""
|
||||
if exc_info:
|
||||
message = f"{message}\n{traceback.format_exc()}"
|
||||
|
||||
# 호출 위치 정보를 동적으로 추출
|
||||
caller_frame = logging.currentframe().f_back
|
||||
record = self.logger.makeRecord(
|
||||
self.logger.name, level, caller_frame.f_code.co_filename,
|
||||
caller_frame.f_lineno, message, None, None, caller_frame.f_code.co_name
|
||||
)
|
||||
|
||||
# 파일 로거에 메시지 전달
|
||||
if level >= self.file_log_level:
|
||||
self.logger.handle(record)
|
||||
|
||||
class CustomRotatingFileHandler(BaseRotatingHandler):
|
||||
"""로그 파일을 모두 .log 확장자로 생성하는 커스텀 핸들러"""
|
||||
|
||||
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None):
|
||||
super().__init__(filename, mode, encoding)
|
||||
self.maxBytes = maxBytes
|
||||
self.backupCount = backupCount
|
||||
# 기존 로그 파일 확인 및 인덱스 설정
|
||||
self._base_filename = filename
|
||||
self._extension = '.log'
|
||||
|
||||
def doRollover(self):
|
||||
"""롤오버 수행 - 파일이 최대 크기에 도달하면 새 파일 생성"""
|
||||
if self.stream:
|
||||
self.stream.close()
|
||||
self.stream = None
|
||||
|
||||
# 기존 로그 파일 이름을 기반으로 백업 파일 생성
|
||||
base_name, ext = os.path.splitext(self._base_filename)
|
||||
|
||||
# 현재 디렉토리의 모든 로그 파일 확인
|
||||
log_dir = os.path.dirname(self._base_filename) or '.'
|
||||
existing_logs = glob.glob(f"{base_name}*.log")
|
||||
existing_logs.sort()
|
||||
|
||||
# 최대 백업 수 초과하는 파일 제거
|
||||
while len(existing_logs) >= self.backupCount:
|
||||
try:
|
||||
oldest_file = existing_logs.pop(0)
|
||||
os.remove(oldest_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 새 로그 파일 이름 생성 (주 로그 파일 이름은 그대로 유지)
|
||||
# 예: log.log, log_1.log, log_2.log, ...
|
||||
if os.path.exists(self._base_filename):
|
||||
# 인덱스가 있는 로그 파일들 찾기
|
||||
indexed_logs = [f for f in existing_logs if f != self._base_filename]
|
||||
max_index = 0
|
||||
|
||||
for log_file in indexed_logs:
|
||||
try:
|
||||
# 파일 이름에서 인덱스 부분 추출
|
||||
name_part = os.path.basename(log_file)
|
||||
name_without_ext = os.path.splitext(name_part)[0]
|
||||
if '_' in name_without_ext:
|
||||
idx_str = name_without_ext.split('_')[-1]
|
||||
if idx_str.isdigit():
|
||||
max_index = max(max_index, int(idx_str))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 새 인덱스로 파일 이름 설정
|
||||
new_index = max_index + 1
|
||||
new_log_file = f"{base_name}_{new_index}.log"
|
||||
|
||||
# 기존 파일 이름 변경
|
||||
try:
|
||||
os.rename(self._base_filename, new_log_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 스트림 다시 열기
|
||||
self.mode = 'w'
|
||||
self.stream = self._open()
|
||||
|
||||
def shouldRollover(self, record):
|
||||
"""롤오버가 필요한지 확인"""
|
||||
if self.stream is None: # 첫 번째 로그 쓰기 시도
|
||||
self.stream = self._open()
|
||||
|
||||
if self.maxBytes > 0: # 최대 크기가 지정된 경우만 검사
|
||||
self.stream.seek(0, 2) # 파일 끝으로 이동
|
||||
if self.stream.tell() >= self.maxBytes:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import argparse
|
||||
from modules.image_translate_server import run_server
|
||||
from modules.image_processor2 import ImageProcessor
|
||||
from modules.loggerModule import Logger1
|
||||
from modules.gpt_client import GPTClient
|
||||
from modules.iop_server import IOPaint_Server
|
||||
import sys, os
|
||||
|
||||
|
||||
def get_base_dir():
|
||||
"""
|
||||
실행 환경에 따라 base_dir을 설정하는 메서드.
|
||||
cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
|
||||
"""
|
||||
if getattr(sys, 'frozen', False): # 패키징된 경우
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
internal_dir = os.path.join(base_dir, 'lib') # lib 디렉토리 포함
|
||||
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
|
||||
return internal_dir
|
||||
|
||||
else: # 일반 Python 실행 환경
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
debug_dir = os.path.join(base_dir) # lib 디렉토리 포함
|
||||
return debug_dir
|
||||
|
||||
def run_iop_server(logger, base_dir):
|
||||
iop_server = IOPaint_Server(logger=logger, base_dir=base_dir)
|
||||
iop_port = iop_server.start()
|
||||
return iop_port
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="이미지 번역 FastAPI 서버 실행")
|
||||
parser.add_argument('--workers', type=int, default=2, help='최대 동시 워커 수 (2~8)')
|
||||
args = parser.parse_args()
|
||||
max_workers = max(2, min(args.workers, 8))
|
||||
|
||||
# 실제 환경에 맞게 객체 생성
|
||||
logger = Logger1()
|
||||
gpt_client = GPTClient()
|
||||
base_dir = get_base_dir()
|
||||
font_path = os.path.join(base_dir, "modules", "fonts", "HakgyoansimDunggeunmisoTTFB.ttf")
|
||||
print(f"font_path: {font_path}")
|
||||
|
||||
image_processor = ImageProcessor(logger, gpt_client, base_dir, font_path)
|
||||
|
||||
iop_port = run_iop_server(logger, base_dir)
|
||||
image_processor.update_iop_port(iop_port)
|
||||
|
||||
port = run_server(image_processor, max_workers)
|
||||
print(f"이미지번역서버가 127.0.0.1:{port} 에서 실행 중입니다.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from typing import List, Dict, Any
|
||||
from shapely.geometry import Polygon
|
||||
import logging
|
||||
|
||||
class MaskModule:
|
||||
def __init__(self, logger, base_dir):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
self.logger.log("마스크 모듈 초기화 완료", level=logging.INFO)
|
||||
|
||||
def create_masks(self, image_path: str, ocr_results: List[Dict], expansion_size: int = 10, blur_size: int = 15, mask_option: str = "basic") -> np.ndarray:
|
||||
image = cv2.imread(image_path)
|
||||
if image is None:
|
||||
self.logger.log(f"이미지를 읽을 수 없습니다: {image_path}", level=logging.ERROR)
|
||||
return None
|
||||
height, width = image.shape[:2]
|
||||
mask = np.zeros((height, width), dtype=np.uint8)
|
||||
for i, result in enumerate(ocr_results, 1):
|
||||
polygon = result['polygon']
|
||||
expanded_poly = self.expand_polygon(polygon, offset=5)
|
||||
cv2.fillPoly(mask, [expanded_poly], 255)
|
||||
processed_mask = self.process_mask(mask, expansion_size, blur_size)
|
||||
return processed_mask
|
||||
|
||||
def expand_polygon(self, polygon, offset=15):
|
||||
poly = Polygon(polygon)
|
||||
expanded = poly.buffer(offset)
|
||||
if expanded.is_empty:
|
||||
return np.array(polygon, dtype=np.int32)
|
||||
return np.array(expanded.exterior.coords, dtype=np.int32)
|
||||
|
||||
def process_mask(self, mask: np.ndarray, expansion_size: int = 5, blur_size: int = 3) -> np.ndarray:
|
||||
processed_mask = mask.copy()
|
||||
if expansion_size > 0:
|
||||
kernel = np.ones((expansion_size, expansion_size), np.uint8)
|
||||
processed_mask = cv2.dilate(processed_mask, kernel, iterations=1)
|
||||
if blur_size > 0:
|
||||
blur_size = blur_size if blur_size % 2 == 1 else blur_size + 1
|
||||
processed_mask = cv2.GaussianBlur(processed_mask, (blur_size, blur_size), 0)
|
||||
return processed_mask
|
||||
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class OCRModule:
|
||||
def __init__(self, logger=None, base_dir=None):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
|
||||
# CPU만 사용하도록 환경 변수 설정
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||
|
||||
self.ocr = None
|
||||
|
||||
self.ocr = self.initialize_ocr()
|
||||
if self.ocr is None:
|
||||
raise Exception("PaddleOCR 초기화 실패")
|
||||
|
||||
def initialize_ocr(self):
|
||||
"""
|
||||
PaddleOCR 초기화. det_enabled 옵션에 따라 Detection 모델 사용 여부 결정.
|
||||
"""
|
||||
# 모델 디렉토리 설정
|
||||
self.rec_model_dir = os.path.join(self.base_dir, "modules", "PP_Models", "rec")
|
||||
self.det_model_dir = os.path.join(self.base_dir, "modules", "PP_Models", "det")
|
||||
self.cls_model_dir = os.path.join(self.base_dir, "modules", "PP_Models", "cls")
|
||||
|
||||
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
|
||||
)
|
||||
return ocr
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ PaddleOCR 초기화 실패: {e}", level=logging.ERROR, exc_info=True)
|
||||
# raise e # 에러 발생시 프로그램 종료
|
||||
return None
|
||||
|
||||
|
||||
def detect_text(self, image_path: str, method: str = 'polygon') -> List[Dict[str, Any]]:
|
||||
"""
|
||||
이미지에서 텍스트를 감지하고 다양한 방식으로 영역 반환
|
||||
|
||||
Args:
|
||||
image_path (str): 이미지 파일 경로
|
||||
method (str): 감지 방식 ('polygon', 'bbox', 'expanded_bbox', 'rotated_bbox', 'contour')
|
||||
|
||||
Returns:
|
||||
List[Dict]: 감지된 텍스트 정보 리스트
|
||||
- text: 감지된 텍스트
|
||||
- confidence: 신뢰도
|
||||
- polygon: 폴리곤 좌표 (4개 점)
|
||||
- bbox: 바운딩 박스 좌표 (x, y, w, h)
|
||||
- method: 사용된 감지 방식
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
self.logger.log(f"이미지 파일을 찾을 수 없습니다: {image_path}", level=logging.ERROR)
|
||||
return []
|
||||
|
||||
try:
|
||||
# 이미지 읽기
|
||||
image = cv2.imread(image_path)
|
||||
if image is None:
|
||||
self.logger.log(f"이미지를 읽을 수 없습니다: {image_path}", level=logging.ERROR)
|
||||
return []
|
||||
|
||||
self.logger.log(f"🔍 OCR 감지 방식: {method}", level=logging.INFO)
|
||||
|
||||
# 실제 OCR 실행
|
||||
# ocr_raw_results = self.ocr.predict(image)
|
||||
ocr_raw_results = self.ocr.ocr(image)
|
||||
|
||||
self.logger.log(f"ocr_raw_results: {ocr_raw_results}", level=logging.INFO)
|
||||
for line in ocr_raw_results:
|
||||
self.logger.log(f"line: {line}", level=logging.INFO)
|
||||
|
||||
if not ocr_raw_results or len(ocr_raw_results) == 0:
|
||||
self.logger.log("⚠️ OCR 결과가 비어있습니다.", level=logging.WARNING)
|
||||
return []
|
||||
|
||||
# paddleocr 2.x 결과 파싱
|
||||
converted_results = []
|
||||
for page in ocr_raw_results: # page는 텍스트별 결과 리스트
|
||||
for line in page:
|
||||
poly = line[0]
|
||||
text = line[1][0]
|
||||
score = line[1][1]
|
||||
converted_results.append([poly, [text, score]])
|
||||
|
||||
# 감지 방식에 따라 결과 처리
|
||||
if method == 'polygon':
|
||||
ocr_results = self._detect_with_polygon(image, converted_results)
|
||||
elif method == 'bbox':
|
||||
ocr_results = self._detect_with_bbox(image, converted_results)
|
||||
elif method == 'expanded_bbox':
|
||||
ocr_results = self._detect_with_expanded_bbox(image, converted_results)
|
||||
elif method == 'rotated_bbox':
|
||||
ocr_results = self._detect_with_rotated_bbox(image, converted_results)
|
||||
elif method == 'contour':
|
||||
ocr_results = self._detect_with_contour(image, converted_results)
|
||||
else:
|
||||
self.logger.log(f"⚠️ 지원하지 않는 감지 방식: {method}, 기본 polygon 방식 사용", level=logging.WARNING)
|
||||
ocr_results = self._detect_with_polygon(image, converted_results)
|
||||
|
||||
return ocr_results
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ OCR 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
return []
|
||||
|
||||
def filter_chinese_text(self, ocr_results: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
중국어 텍스트만 필터링
|
||||
|
||||
Args:
|
||||
ocr_results (List[Dict]): OCR 결과
|
||||
|
||||
Returns:
|
||||
List[Dict]: 중국어 텍스트만 포함된 결과
|
||||
"""
|
||||
chinese_results = []
|
||||
|
||||
for result in ocr_results:
|
||||
text = result['text']
|
||||
# 중국어 문자 범위 확인 (간체/번체 포함)
|
||||
if any('\u4e00' <= char <= '\u9fff' for char in text):
|
||||
chinese_results.append(result)
|
||||
|
||||
self.logger.log(f"중국어 텍스트 {len(chinese_results)}개 필터링 완료", level=logging.INFO)
|
||||
return chinese_results
|
||||
|
||||
|
||||
def _detect_with_polygon(self, image: np.ndarray, ocr_raw_results: List) -> List[Dict[str, Any]]:
|
||||
"""폴리곤 방식으로 텍스트 영역 감지 (기본 방식)"""
|
||||
ocr_results = []
|
||||
|
||||
for line in ocr_raw_results:
|
||||
if len(line) >= 2:
|
||||
polygon = line[0] # 폴리곤 좌표 (4개 점)
|
||||
text_info = line[1] # (텍스트, 신뢰도)
|
||||
|
||||
if len(text_info) >= 2:
|
||||
text = text_info[0]
|
||||
confidence = text_info[1]
|
||||
|
||||
# 폴리곤을 바운딩 박스로 변환
|
||||
polygon_np = np.array(polygon, dtype=np.int32)
|
||||
x, y, w, h = cv2.boundingRect(polygon_np)
|
||||
|
||||
ocr_result = {
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'polygon': polygon,
|
||||
'bbox': (x, y, w, h),
|
||||
'method': 'polygon'
|
||||
}
|
||||
ocr_results.append(ocr_result)
|
||||
|
||||
return ocr_results
|
||||
|
||||
def _detect_with_bbox(self, image: np.ndarray, ocr_raw_results: List) -> List[Dict[str, Any]]:
|
||||
"""바운딩 박스 방식으로 텍스트 영역 감지"""
|
||||
ocr_results = []
|
||||
|
||||
for line in ocr_raw_results:
|
||||
if len(line) >= 2:
|
||||
polygon = line[0]
|
||||
text_info = line[1]
|
||||
|
||||
if len(text_info) >= 2:
|
||||
text = text_info[0]
|
||||
confidence = text_info[1]
|
||||
|
||||
# 바운딩 박스 계산
|
||||
polygon_np = np.array(polygon, dtype=np.int32)
|
||||
x, y, w, h = cv2.boundingRect(polygon_np)
|
||||
|
||||
# 바운딩 박스를 폴리곤으로 변환
|
||||
bbox_polygon = [
|
||||
[x, y],
|
||||
[x + w, y],
|
||||
[x + w, y + h],
|
||||
[x, y + h]
|
||||
]
|
||||
|
||||
ocr_result = {
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'polygon': bbox_polygon,
|
||||
'bbox': (x, y, w, h),
|
||||
'method': 'bbox'
|
||||
}
|
||||
ocr_results.append(ocr_result)
|
||||
|
||||
return ocr_results
|
||||
|
||||
def _detect_with_expanded_bbox(self, image: np.ndarray, ocr_raw_results: List) -> List[Dict[str, Any]]:
|
||||
"""확장된 바운딩 박스 방식으로 텍스트 영역 감지"""
|
||||
ocr_results = []
|
||||
h_img, w_img = image.shape[:2]
|
||||
|
||||
for line in ocr_raw_results:
|
||||
if len(line) >= 2:
|
||||
polygon = line[0]
|
||||
text_info = line[1]
|
||||
|
||||
if len(text_info) >= 2:
|
||||
text = text_info[0]
|
||||
confidence = text_info[1]
|
||||
|
||||
# 기본 바운딩 박스
|
||||
polygon_np = np.array(polygon, dtype=np.int32)
|
||||
x, y, w, h = cv2.boundingRect(polygon_np)
|
||||
|
||||
# 확장 크기 계산 (텍스트 크기의 20%)
|
||||
expand_x = max(1, int(w * 0.2))
|
||||
expand_y = max(1, int(h * 0.2))
|
||||
|
||||
# 확장된 바운딩 박스
|
||||
x_exp = max(0, x - expand_x)
|
||||
y_exp = max(0, y - expand_y)
|
||||
w_exp = min(w_img - x_exp, w + 2 * expand_x)
|
||||
h_exp = min(h_img - y_exp, h + 2 * expand_y)
|
||||
|
||||
# 확장된 바운딩 박스를 폴리곤으로 변환
|
||||
expanded_polygon = [
|
||||
[x_exp, y_exp],
|
||||
[x_exp + w_exp, y_exp],
|
||||
[x_exp + w_exp, y_exp + h_exp],
|
||||
[x_exp, y_exp + h_exp]
|
||||
]
|
||||
|
||||
ocr_result = {
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'polygon': expanded_polygon,
|
||||
'bbox': (x_exp, y_exp, w_exp, h_exp),
|
||||
'method': 'expanded_bbox'
|
||||
}
|
||||
ocr_results.append(ocr_result)
|
||||
|
||||
return ocr_results
|
||||
|
||||
def _detect_with_rotated_bbox(self, image: np.ndarray, ocr_raw_results: List) -> List[Dict[str, Any]]:
|
||||
"""회전된 바운딩 박스 방식으로 텍스트 영역 감지"""
|
||||
ocr_results = []
|
||||
|
||||
for line in ocr_raw_results:
|
||||
if len(line) >= 2:
|
||||
polygon = line[0]
|
||||
text_info = line[1]
|
||||
|
||||
if len(text_info) >= 2:
|
||||
text = text_info[0]
|
||||
confidence = text_info[1]
|
||||
|
||||
# 회전된 바운딩 박스 계산
|
||||
polygon_np = np.array(polygon, dtype=np.float32)
|
||||
rect = cv2.minAreaRect(polygon_np)
|
||||
box = cv2.boxPoints(rect)
|
||||
box = np.int32(box)
|
||||
|
||||
# 일반 바운딩 박스도 계산
|
||||
x, y, w, h = cv2.boundingRect(polygon_np.astype(np.int32))
|
||||
|
||||
ocr_result = {
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'polygon': box.tolist(),
|
||||
'bbox': (x, y, w, h),
|
||||
'method': 'rotated_bbox',
|
||||
'rotation_info': {
|
||||
'center': rect[0],
|
||||
'size': rect[1],
|
||||
'angle': rect[2]
|
||||
}
|
||||
}
|
||||
ocr_results.append(ocr_result)
|
||||
|
||||
return ocr_results
|
||||
|
||||
def _detect_with_contour(self, image: np.ndarray, ocr_raw_results: List) -> List[Dict[str, Any]]:
|
||||
"""컨투어 방식으로 텍스트 영역 감지"""
|
||||
ocr_results = []
|
||||
|
||||
for line in ocr_raw_results:
|
||||
if len(line) >= 2:
|
||||
polygon = line[0]
|
||||
text_info = line[1]
|
||||
|
||||
if len(text_info) >= 2:
|
||||
text = text_info[0]
|
||||
confidence = text_info[1]
|
||||
|
||||
# 폴리곤을 컨투어로 변환
|
||||
polygon_np = np.array(polygon, dtype=np.int32)
|
||||
|
||||
# 컨투어 근사화
|
||||
epsilon = 0.02 * cv2.arcLength(polygon_np, True)
|
||||
approx_contour = cv2.approxPolyDP(polygon_np, epsilon, True)
|
||||
|
||||
# 컨투어를 다시 폴리곤으로 변환
|
||||
contour_polygon = approx_contour.reshape(-1, 2).tolist()
|
||||
|
||||
# 바운딩 박스 계산
|
||||
x, y, w, h = cv2.boundingRect(polygon_np)
|
||||
|
||||
ocr_result = {
|
||||
'text': text,
|
||||
'confidence': confidence,
|
||||
'polygon': contour_polygon,
|
||||
'bbox': (x, y, w, h),
|
||||
'method': 'contour',
|
||||
'contour_points': len(contour_polygon)
|
||||
}
|
||||
ocr_results.append(ocr_result)
|
||||
|
||||
return ocr_results
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from mmocr.apis import TextDetInferencer, TextRecogInferencer
|
||||
|
||||
class OCRModule:
|
||||
def __init__(self, det_config: str, det_checkpoint: str,
|
||||
rec_config: str, rec_checkpoint: str,
|
||||
logger=None):
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||
|
||||
# MMOCR detection 및 recognition inferencer 초기화
|
||||
self.det_infer = TextDetInferencer(model=dict(config=det_config, ckpt=det_checkpoint), device='cpu')
|
||||
self.rec_infer = TextRecogInferencer(model=dict(config=rec_config, ckpt=rec_checkpoint), device='cpu')
|
||||
self.logger.info("✅ MMOCR detection 및 recognition 모델 초기화 완료")
|
||||
|
||||
def detect_text(self, image_path: str, method: str = 'polygon') -> List[Dict[str, Any]]:
|
||||
if not os.path.exists(image_path):
|
||||
self.logger.error(f"이미지 파일을 찾을 수 없습니다: {image_path}")
|
||||
return []
|
||||
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
self.logger.error(f"이미지를 읽을 수 없습니다: {image_path}")
|
||||
return []
|
||||
self.logger.info(f"❇️ OCR 감지(method={method}) 시작")
|
||||
|
||||
# 1) 텍스트 영역 감지
|
||||
det_res = self.det_infer(image_path)
|
||||
polys = det_res[0]['boundary_result']
|
||||
self.logger.info(f"👉 감지된 텍스트 영역 수: {len(polys)}")
|
||||
|
||||
# 2) 영역 crop 후 recognition
|
||||
crops = [self._crop_poly(img, poly) for poly in polys]
|
||||
rec_res = self.rec_infer(crops)
|
||||
self.logger.info("📖 텍스트 인식 완료")
|
||||
|
||||
ocr_results = []
|
||||
for poly, rec in zip(polys, rec_res):
|
||||
text, score = rec
|
||||
x, y, w, h = cv2.boundingRect(np.array(poly, dtype=np.int32))
|
||||
ocr_results.append({
|
||||
'text': text,
|
||||
'confidence': float(score),
|
||||
'polygon': poly,
|
||||
'bbox': (int(x), int(y), int(w), int(h)),
|
||||
'method': method
|
||||
})
|
||||
|
||||
return ocr_results
|
||||
|
||||
def filter_chinese_text(self, ocr_results: List[Dict]) -> List[Dict]:
|
||||
chinese = [r for r in ocr_results if any('\u4e00' <= c <= '\u9fff' for c in r['text'])]
|
||||
self.logger.info(f"중국어 텍스트 {len(chinese)}개 필터링 완료")
|
||||
return chinese
|
||||
|
||||
def _crop_poly(self, img: np.ndarray, poly: List[List[int]]) -> np.ndarray:
|
||||
mask = np.zeros(img.shape[:2], dtype=np.uint8)
|
||||
cv2.fillPoly(mask, [np.array(poly, dtype=np.int32)], 255)
|
||||
x, y, w, h = cv2.boundingRect(np.array(poly, dtype=np.int32))
|
||||
return cv2.bitwise_and(img[y:y+h, x:x+w], img[y:y+h, x:x+w], mask=mask[y:y+h, x:x+w])
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
from PIL import Image, ImageFont, ImageDraw
|
||||
import requests
|
||||
import numpy as np
|
||||
import cv2
|
||||
import os
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
class PostImageManager:
|
||||
def __init__(self, logger, font_path):
|
||||
self.logger = logger
|
||||
self.font_path = font_path
|
||||
|
||||
# 폰트 로드
|
||||
self.font_load()
|
||||
|
||||
|
||||
def font_load(self):
|
||||
# 폰트 로드
|
||||
try:
|
||||
self.font = ImageFont.truetype(self.font_path, 36)
|
||||
self.logger.log(f"폰트 로드 성공: {self.font_path}", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
self.logger.log(f"커스텀 폰트 로드 실패 ({self.font_path}): {e}", level=logging.WARNING)
|
||||
try:
|
||||
# 기본 폰트 사용
|
||||
self.font = ImageFont.load_default()
|
||||
self.logger.log("기본 폰트를 사용합니다.", level=logging.INFO)
|
||||
except Exception as e2:
|
||||
self.logger.log(f"기본 폰트 로드도 실패: {e2}", level=logging.ERROR)
|
||||
# 최후의 수단으로 None 설정
|
||||
self.font = None
|
||||
|
||||
def save_image_to_path(self, image, path):
|
||||
try:
|
||||
if image:
|
||||
# 이미지를 저장 경로에 저장
|
||||
self.logger.log(f"이미지 저장 완료 : {path}", level=logging.INFO)
|
||||
image.save(path, format='PNG')
|
||||
return path
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"이미지 저장 중 오류 발생: {e}")
|
||||
|
||||
def add_watermark(self, image_data, watermark_text="Watermark", opacity_percent=30, angle=30, font_size=36):
|
||||
"""
|
||||
이미지에 텍스트 워터마크를 이미지 전체에 걸쳐서 추가하는 함수
|
||||
:param image_data: PIL 이미지 객체
|
||||
:param watermark_text: 워터마크로 추가할 텍스트
|
||||
:param opacity_percent: 워터마크의 투명도 (0~100)
|
||||
:param angle: 워터마크 텍스트 회전 각도 (기본 30도)
|
||||
:param font_size: 워터마크 텍스트의 폰트 크기 (기본 36)
|
||||
:return: 워터마크가 추가된 이미지
|
||||
"""
|
||||
try:
|
||||
if isinstance(image_data, np.ndarray):
|
||||
image_data = Image.fromarray(cv2.cvtColor(image_data, cv2.COLOR_BGR2RGB))
|
||||
|
||||
# 폰트가 로드되지 않은 경우 원본 이미지 반환
|
||||
if self.font is None:
|
||||
self.logger.log("폰트가 로드되지 않아 워터마크를 추가할 수 없습니다. 원본 이미지를 반환합니다.", level=logging.WARNING)
|
||||
return image_data
|
||||
|
||||
# 이미지 복사본 생성
|
||||
watermark_image = image_data.copy()
|
||||
|
||||
# 폰트 설정 (안전한 폰트 로딩)
|
||||
try:
|
||||
# self.font가 있으면 크기만 조정해서 새 폰트 생성
|
||||
if hasattr(self, 'font_path') and os.path.exists(self.font_path):
|
||||
font = ImageFont.truetype(self.font_path, font_size)
|
||||
else:
|
||||
# 크기를 조정할 수 없으면 기존 폰트 사용
|
||||
font = self.font
|
||||
except Exception as e:
|
||||
self.logger.log(f"폰트 크기 조정 실패: {e}. 기본 폰트를 사용합니다.", level=logging.WARNING)
|
||||
font = self.font
|
||||
|
||||
# 텍스트 투명도를 0~255로 변환
|
||||
opacity = int(255 * (opacity_percent / 100))
|
||||
|
||||
# 텍스트 크기 측정 (textbbox 사용)
|
||||
draw = ImageDraw.Draw(watermark_image)
|
||||
bbox = draw.textbbox((0, 0), watermark_text, font=font)
|
||||
text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
|
||||
# 이미지 크기
|
||||
width, height = image_data.size
|
||||
|
||||
# 워터마크 레이어 생성
|
||||
watermark_layer = Image.new("RGBA", (width, height)) # RGBA 이미지 생성
|
||||
|
||||
# 지그재그 간격 설정
|
||||
zigzag_step = int(text_height * 2) # Y축의 지그재그 간격
|
||||
|
||||
|
||||
# 이미지 전체에 반복적으로 워터마크 텍스트 그리기 (지그재그 형태)
|
||||
for y in range(0, height, zigzag_step):
|
||||
for x in range(0, width, int(text_width * 3)): # 3배 너비 간격으로 반복
|
||||
# 텍스트가 한 줄씩 지그재그 형태로 X축을 교차하여 이동
|
||||
x_offset = (y // zigzag_step) % 2 * int(text_width * 1.5) # 짝수 행에서는 X축을 약간 이동
|
||||
|
||||
# 텍스트 레이어 생성
|
||||
text_layer = Image.new("RGBA", (text_width, text_height), (255, 255, 255, 0))
|
||||
text_draw = ImageDraw.Draw(text_layer)
|
||||
|
||||
# 텍스트 그리기
|
||||
text_draw.text((0, 0), watermark_text, fill=(255, 255, 255, opacity), font=font)
|
||||
|
||||
# 텍스트 회전
|
||||
rotated_text_layer = text_layer.rotate(angle, expand=1)
|
||||
|
||||
# 회전된 텍스트를 워터마크 레이어에 추가
|
||||
watermark_layer.paste(rotated_text_layer, (x + x_offset, y), rotated_text_layer)
|
||||
|
||||
# 원본 이미지와 워터마크 레이어 합성
|
||||
watermark_image = Image.alpha_composite(watermark_image.convert("RGBA"), watermark_layer)
|
||||
|
||||
# 최종적으로 RGB 형식으로 변환 후 반환
|
||||
return watermark_image.convert("RGB")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"워터마크 추가 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
return image_data
|
||||
|
||||
def crop_image(self, image, is_thumb=False, crop_percentage=0.01):
|
||||
"""이미지를 주어진 퍼센트만큼 크롭하는 함수"""
|
||||
if is_thumb:
|
||||
crop_percentage = 0.03
|
||||
self.logger.log(f"썸네일 이미지 이므로 크롭 3%로 조정", level=logging.DEBUG)
|
||||
|
||||
width, height = image.size
|
||||
left = width * crop_percentage
|
||||
top = height * crop_percentage
|
||||
right = width * (1 - crop_percentage)
|
||||
bottom = height * (1 - crop_percentage)
|
||||
|
||||
cropped_image = image.crop((left, top, right, bottom))
|
||||
|
||||
if self.debug:
|
||||
# 디버그 모드일 경우 크롭 전후 다양한 비율로 이미지 저장
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
original_image_path = os.path.join(os.getcwd(), f"original_image_{timestamp}.png")
|
||||
image.save(original_image_path) # 크롭 전 이미지 저장
|
||||
self.logger.log(f"크롭 전 이미지 저장됨: {original_image_path}", level=logging.DEBUG)
|
||||
|
||||
# 1%, 2%, 3% 크롭 이미지 저장
|
||||
crop_alternatives = [0.01, 0.02, 0.03]
|
||||
for crop in crop_alternatives:
|
||||
left_alt = width * crop
|
||||
top_alt = height * crop
|
||||
right_alt = width * (1 - crop)
|
||||
bottom_alt = height * (1 - crop)
|
||||
|
||||
cropped_alt_image = image.crop((left_alt, top_alt, right_alt, bottom_alt))
|
||||
cropped_image_path = os.path.join(os.getcwd(), f"cropped_image_{int(crop*100)}_{timestamp}.png")
|
||||
cropped_alt_image.save(cropped_image_path)
|
||||
self.logger.log(f"{int(crop*100)}% 크롭된 이미지 저장됨: {cropped_image_path}", level=logging.DEBUG)
|
||||
|
||||
return cropped_image
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# torch==2.2.2+cpu --index-url https://download.pytorch.org/whl/cpu
|
||||
# torchvision==0.17.2+cpu --index-url https://download.pytorch.org/whl/cpu
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
aiofiles
|
||||
opencv-python
|
||||
numpy
|
||||
requests
|
||||
pillow
|
||||
openai
|
||||
shapely
|
||||
paddleocr==2.10.0
|
||||
paddlepaddle==2.6.2
|
||||
iopaint==1.6.0
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
from PySide6.QtCore import QSettings
|
||||
import logging
|
||||
|
||||
class SettingsManager:
|
||||
"""
|
||||
사용자 설정값(토글, 스핀, 텍스트 등)을 저장/불러오고,
|
||||
각종 위젯에 따라 UI 상태까지 자동 적용하는 매니저 클래스입니다.
|
||||
"""
|
||||
|
||||
def __init__(self, logger=None, organization="WhenRideMycar", application="EditPartTimer3"):
|
||||
"""
|
||||
QSettings 기반 설정 매니저 초기화
|
||||
|
||||
:param logger: logging.Logger 또는 print 대체용 함수
|
||||
:param organization: QSettings 구분용 회사명
|
||||
:param application: QSettings 구분용 앱명
|
||||
"""
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.settings = QSettings(organization, application)
|
||||
self.widget_map = {} # 위젯명 → 저장키 매핑
|
||||
self.toggle_dependencies = {} # 토글명 → 종속 위젯 매핑
|
||||
|
||||
def _log(self, message, level=logging.INFO):
|
||||
"""
|
||||
커스텀/표준 로거 모두 지원하는 내부 로깅 함수
|
||||
"""
|
||||
if hasattr(self.logger, "log") and "level" in self.logger.log.__code__.co_varnames:
|
||||
# 커스텀 로거: self.logger.log(msg, level=logging.INFO)
|
||||
self.logger.log(message, level=level)
|
||||
else:
|
||||
# 표준 로거
|
||||
if level == logging.DEBUG:
|
||||
self.logger.debug(message)
|
||||
elif level == logging.INFO:
|
||||
self.logger.info(message)
|
||||
elif level == logging.WARNING:
|
||||
self.logger.warning(message)
|
||||
elif level == logging.ERROR:
|
||||
self.logger.error(message)
|
||||
elif level == logging.CRITICAL:
|
||||
self.logger.critical(message)
|
||||
else:
|
||||
self.logger.info(message)
|
||||
|
||||
def bind_widgets(self, widget_map, toggle_dependencies):
|
||||
"""
|
||||
위젯-키, 토글-종속 딕셔너리를 등록합니다.
|
||||
:param widget_map: { "widget명": "저장키" }
|
||||
:param toggle_dependencies: { "toggle명": { "dependents": [...], "visible": [...] } }
|
||||
"""
|
||||
self.widget_map = widget_map
|
||||
self.toggle_dependencies = toggle_dependencies
|
||||
|
||||
def save_settings(self, widget_obj):
|
||||
"""
|
||||
현재 UI에 연결된 각종 위젯의 값을 QSettings에 저장합니다.
|
||||
|
||||
:param widget_obj: 실제 위젯 객체(self 등)
|
||||
|
||||
config 예시:
|
||||
{
|
||||
'discord_notify_toggle': {
|
||||
'dependents': ['webhook_input'],
|
||||
'visible': ['webhook_input'],
|
||||
},
|
||||
'ocr_toggle': {
|
||||
'dependents': ['unwanted_words_button']
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
for widget_name, key in self.widget_map.items():
|
||||
widget = getattr(widget_obj, widget_name, None)
|
||||
if widget is None:
|
||||
self._log(f"[SettingsManager] '{widget_name}' 위젯을 찾을 수 없습니다.", logging.WARNING)
|
||||
|
||||
continue
|
||||
|
||||
try:
|
||||
# 체크박스, 토글, 커스텀토글 등
|
||||
if hasattr(widget, 'isChecked'):
|
||||
value = bool(widget.isChecked())
|
||||
self.logger.log(f"[SettingsManager] bool 타입 저장: {key} 값 저장: {value}", level=logging.DEBUG)
|
||||
self.settings.setValue(key, value)
|
||||
# SpinBox/DoubleSpinBox 등
|
||||
elif hasattr(widget, 'value'):
|
||||
self.settings.setValue(key, widget.value())
|
||||
self.logger.log(f"[SettingsManager] int/float 타입 저장: {key} 값 저장: {widget.value()}", level=logging.DEBUG)
|
||||
# QLineEdit 등 (단일줄 텍스트)
|
||||
elif hasattr(widget, 'text'):
|
||||
self.settings.setValue(key, widget.text())
|
||||
self.logger.log(f"[SettingsManager] str 타입 저장: {key} 값 저장: {widget.text()}", level=logging.DEBUG)
|
||||
# QTextEdit 등 (여러줄 텍스트)
|
||||
elif hasattr(widget, 'toPlainText'):
|
||||
self.settings.setValue(key, widget.toPlainText())
|
||||
self.logger.log(f"[SettingsManager] str 타입 저장: {key} 값 저장: {widget.toPlainText()}", level=logging.DEBUG)
|
||||
else:
|
||||
self._log(f"[SettingsManager] '{widget_name}'의 값을 저장하는 방법을 알 수 없습니다.", logging.WARNING)
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] '{widget_name}' 저장 중 오류: {e}", logging.ERROR, exc_info=True)
|
||||
|
||||
def save_value(self, key, value):
|
||||
"""특정 키로 단일 값을 설정에 저장합니다. 타입별로 정확히 저장."""
|
||||
try:
|
||||
# bool
|
||||
if isinstance(value, bool):
|
||||
self.settings.setValue(key, value)
|
||||
self.logger.log(f"[SettingsManager] bool 타입 저장: {key} 값 저장: {value}", level=logging.DEBUG)
|
||||
# int/float
|
||||
elif isinstance(value, (int, float)):
|
||||
self.settings.setValue(key, value)
|
||||
self.logger.log(f"[SettingsManager] int/float 타입 저장: {key} 값 저장: {value}", level=logging.DEBUG)
|
||||
# 그 외(특히 str)
|
||||
else:
|
||||
self.settings.setValue(key, str(value))
|
||||
self.logger.log(f"[SettingsManager] str 타입 저장: {key} 값 저장: {value}", level=logging.DEBUG)
|
||||
self.settings.sync()
|
||||
self._log(f"[SettingsManager] {key} 값을 저장했습니다: {value}")
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] {key} 값 저장 중 오류: {e}", level=logging.WARNING)
|
||||
|
||||
# 기타 값 직접 접근용
|
||||
def get_value(self, key, default=None):
|
||||
return self.settings.value(key, default)
|
||||
|
||||
def load_settings(self, widget_obj):
|
||||
"""
|
||||
QSettings에서 저장된 값을 각 위젯에 복원합니다.
|
||||
|
||||
:param widget_obj: 실제 위젯 객체(self 등)
|
||||
"""
|
||||
for widget_name, key in self.widget_map.items():
|
||||
widget = getattr(widget_obj, widget_name, None)
|
||||
if widget is None:
|
||||
self._log(f"[SettingsManager] '{widget_name}' 위젯을 찾을 수 없습니다.", logging.WARNING)
|
||||
continue
|
||||
|
||||
val = self.settings.value(key, None)
|
||||
if val is None:
|
||||
continue # 미저장 값
|
||||
|
||||
try:
|
||||
# 체크박스/토글 등 (bool 변환)
|
||||
if hasattr(widget, 'setChecked'):
|
||||
# QSettings는 bool을 str로 저장할 수 있어 안전하게 변환
|
||||
if isinstance(val, bool):
|
||||
widget.setChecked(val)
|
||||
elif isinstance(val, (int, float)):
|
||||
widget.setChecked(bool(val))
|
||||
elif isinstance(val, str):
|
||||
widget.setChecked(val.lower() in ['true', '1', 'yes'])
|
||||
# SpinBox류
|
||||
elif hasattr(widget, 'setValue'):
|
||||
if isinstance(val, (int, float)):
|
||||
widget.setValue(val)
|
||||
elif isinstance(val, str):
|
||||
if '.' in val:
|
||||
widget.setValue(float(val))
|
||||
else:
|
||||
widget.setValue(int(val))
|
||||
# QLineEdit 등
|
||||
elif hasattr(widget, 'setText'):
|
||||
widget.setText(str(val))
|
||||
# QTextEdit 등
|
||||
elif hasattr(widget, 'setPlainText'):
|
||||
widget.setPlainText(str(val))
|
||||
else:
|
||||
self._log(f"[SettingsManager] '{widget_name}'에 값을 복원할 수 없습니다.", logging.WARNING)
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] '{widget_name}' 불러오기 오류: {e}", logging.ERROR, exc_info=True)
|
||||
|
||||
def reset_settings(self):
|
||||
"""
|
||||
전체 QSettings 값을 초기화(삭제)합니다.
|
||||
"""
|
||||
try:
|
||||
self.settings.clear()
|
||||
self._log("[SettingsManager] 모든 설정이 초기화되었습니다.", logging.INFO)
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] 설정 초기화 실패: {e}", logging.ERROR, exc_info=True)
|
||||
|
||||
def remove_setting(self, key):
|
||||
"""
|
||||
특정 키의 설정만 삭제합니다.
|
||||
|
||||
:param key: 삭제할 설정 키(str)
|
||||
"""
|
||||
try:
|
||||
self.settings.remove(key)
|
||||
self._log(f"[SettingsManager] '{key}' 설정이 삭제되었습니다.", logging.INFO)
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] '{key}' 삭제 실패: {e}", logging.ERROR, exc_info=True)
|
||||
|
||||
def debug_print(self):
|
||||
"""
|
||||
QSettings에 저장된 모든 값을 로그로 출력합니다.
|
||||
"""
|
||||
try:
|
||||
self.settings.sync()
|
||||
all_keys = self.settings.allKeys()
|
||||
self.logger.info("[SettingsManager] 저장된 모든 설정값:")
|
||||
for key in all_keys:
|
||||
value = self.settings.value(key)
|
||||
self.logger.info(f" {key}: {value}")
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] 설정값 출력 실패: {e}", logging.ERROR, exc_info=True)
|
||||
|
||||
def apply_settings_to_ui(self, widget_obj):
|
||||
"""
|
||||
종속 딕셔너리(toggle_dependencies)에 따라,
|
||||
토글/체크박스 등 상태에 따라 dependents/visible 위젯을 자동으로 활성/비활성, 표시/숨김 처리합니다.
|
||||
|
||||
:param widget_obj: 실제 위젯 객체(self 등)
|
||||
"""
|
||||
for toggle_name, deps in self.toggle_dependencies.items():
|
||||
toggle_widget = getattr(widget_obj, toggle_name, None)
|
||||
if toggle_widget is None or not hasattr(toggle_widget, "isChecked"):
|
||||
continue
|
||||
checked = toggle_widget.isChecked()
|
||||
# 종속 위젯 enable/disable
|
||||
for dep in deps.get("dependents", []):
|
||||
dep_widget = getattr(widget_obj, dep, None)
|
||||
if dep_widget and hasattr(dep_widget, "setEnabled"):
|
||||
try:
|
||||
dep_widget.setEnabled(checked)
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] {dep} setEnabled 실패: {e}", logging.ERROR, exc_info=True)
|
||||
# 종속 위젯 visible
|
||||
for vis in deps.get("visible", []):
|
||||
vis_widget = getattr(widget_obj, vis, None)
|
||||
if vis_widget and hasattr(vis_widget, "setVisible"):
|
||||
try:
|
||||
vis_widget.setVisible(checked)
|
||||
except Exception as e:
|
||||
self._log(f"[SettingsManager] {vis} setVisible 실패: {e}", logging.ERROR, exc_info=True)
|
||||
|
||||
# (필요 시) 확장: 토글에 따라 특정 값을 리셋, 콜백 트리거 등
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def save_user_info(self, user_info: dict):
|
||||
for key, value in user_info.items():
|
||||
self.settings.setValue(f"user/{key}", value)
|
||||
self.settings.sync()
|
||||
|
||||
def load_user_info(self) -> dict:
|
||||
info = {}
|
||||
for key in ["email", "password", "id", "membership_level", "name"]:
|
||||
info[key] = self.settings.value(f"user/{key}", "")
|
||||
return info
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import shutil
|
||||
from modules.image_processor2 import ImageProcessor
|
||||
from modules.loggerModule import Logger1
|
||||
from modules.gpt_client import GPTClient
|
||||
import logging
|
||||
import cv2
|
||||
|
||||
# 더미 Logger
|
||||
class DummyLogger:
|
||||
def log(self, msg, level=logging.INFO, exc_info=None):
|
||||
print(f"[{logging.getLevelName(level)}] {msg}")
|
||||
|
||||
|
||||
# 테스트용 치환단어
|
||||
unwanted_texts = {
|
||||
'크리스탈': '이미지삭제',
|
||||
'세탁기': '세탁기는개뿔',
|
||||
}
|
||||
|
||||
def get_image_list(img_dir):
|
||||
files = [f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.webp'))]
|
||||
files.sort()
|
||||
return [os.path.join(img_dir, f) for f in files]
|
||||
|
||||
def ensure_dir(path):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
def save_image(image, path):
|
||||
cv2.imwrite(path, image)
|
||||
|
||||
async def sequential_process(image_paths, processor, output_dir):
|
||||
print("[순차처리] 시작")
|
||||
results = []
|
||||
for idx, img_path in enumerate(image_paths):
|
||||
print(f"[{idx+1}] {img_path} 처리 중...")
|
||||
# OCR, 번역, 치환, 인페인팅 등 전체 파이프라인 실행
|
||||
# process_single_image는 내부적으로 모든 로직을 처리함
|
||||
result = await processor.process_single_image(
|
||||
page=None, # 실제 Playwright 객체 대신 None
|
||||
original_image_url=img_path,
|
||||
index=idx,
|
||||
is_localServer=True,
|
||||
delay=0.1,
|
||||
file_prefix="seq",
|
||||
use_inpainting=True
|
||||
)
|
||||
# 결과 파일명 결정
|
||||
if isinstance(result, dict):
|
||||
status = result.get('status', 'unknown')
|
||||
path = result.get('path', img_path)
|
||||
if status == 'failed':
|
||||
out_name = f"{idx+1}_failed_{os.path.basename(img_path)}"
|
||||
shutil.copy(img_path, os.path.join(output_dir, out_name))
|
||||
elif status == 'exclude':
|
||||
# 이미지삭제: 파일을 output에 저장하지 않음
|
||||
print(f"[{idx+1}] 이미지삭제로 제외됨: {img_path}")
|
||||
continue
|
||||
else:
|
||||
out_name = f"{idx+1}_{status}_{os.path.basename(img_path)}"
|
||||
shutil.copy(path, os.path.join(output_dir, out_name))
|
||||
else:
|
||||
# result가 경로(str)라면 원본/번역된 이미지로 간주
|
||||
out_name = f"{idx+1}_original_{os.path.basename(img_path)}"
|
||||
shutil.copy(result, os.path.join(output_dir, out_name))
|
||||
results.append(out_name)
|
||||
print("[순차처리] 완료")
|
||||
return results
|
||||
|
||||
async def parallel_process(image_paths, processor, output_dir):
|
||||
print("[동시처리] 시작")
|
||||
tasks = []
|
||||
for idx, img_path in enumerate(image_paths):
|
||||
tasks.append(processor.process_single_image(
|
||||
page=None,
|
||||
original_image_url=img_path,
|
||||
index=idx,
|
||||
is_localServer=True,
|
||||
delay=0.1,
|
||||
file_prefix="par",
|
||||
use_inpainting=True
|
||||
))
|
||||
results = await asyncio.gather(*tasks)
|
||||
for idx, (img_path, result) in enumerate(zip(image_paths, results)):
|
||||
if isinstance(result, dict):
|
||||
status = result.get('status', 'unknown')
|
||||
path = result.get('path', img_path)
|
||||
if status == 'failed':
|
||||
out_name = f"{idx+1}_failed_{os.path.basename(img_path)}"
|
||||
shutil.copy(img_path, os.path.join(output_dir, out_name))
|
||||
elif status == 'exclude':
|
||||
print(f"[{idx+1}] 이미지삭제로 제외됨: {img_path}")
|
||||
continue
|
||||
else:
|
||||
out_name = f"{idx+1}_{status}_{os.path.basename(img_path)}"
|
||||
shutil.copy(path, os.path.join(output_dir, out_name))
|
||||
else:
|
||||
out_name = f"{idx+1}_original_{os.path.basename(img_path)}"
|
||||
shutil.copy(result, os.path.join(output_dir, out_name))
|
||||
print("[동시처리] 완료")
|
||||
return results
|
||||
|
||||
async def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
img_dir = os.path.join(base_dir, 'img')
|
||||
output_dir = os.path.join(base_dir, 'output')
|
||||
ensure_dir(output_dir)
|
||||
image_paths = get_image_list(img_dir)
|
||||
print(f"테스트 이미지: {image_paths}")
|
||||
|
||||
# 더미 logger, gpt_client, toggle_states
|
||||
logger = DummyLogger()
|
||||
set_log = Logger1()
|
||||
gpt_client = GPTClient()
|
||||
toggle_states = {
|
||||
'image_font_path': os.path.join(base_dir, "HakgyoansimDunggeunmisoTTFB.ttf"),
|
||||
'TEMP_IMAGE_DIR': output_dir,
|
||||
'ocr': True,
|
||||
'watermark_text': '테스트워터마크',
|
||||
}
|
||||
processor = ImageProcessor(set_log, None, toggle_states, gpt_client, base_dir)
|
||||
processor.update_unwanted_texts(unwanted_texts)
|
||||
|
||||
print("1. 순차처리 테스트")
|
||||
await sequential_process(image_paths, processor, output_dir)
|
||||
|
||||
print("2. 동시처리 테스트")
|
||||
await parallel_process(image_paths, processor, output_dir)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
# API_URL = "http://192.168.0.150:7000/translate_image"
|
||||
API_URL = "http://127.0.0.1:7002/translate_image"
|
||||
|
||||
payload = {
|
||||
"local_image_path": "d:/py/IT_Server/modules/img/1.jpg",
|
||||
"file_prefix": "test",
|
||||
"toggle_states": {"ocr": True},
|
||||
"unwanted_texts": {"크리스탈": "크리미", "미니멀": "확화곽"},
|
||||
"watermark_text": "테스트 워터마크",
|
||||
"watermark_opacity": 0.5,
|
||||
"watermark_font_size": 32
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
response = requests.post(API_URL, data=json.dumps(payload), headers=headers)
|
||||
|
||||
print("응답 결과:")
|
||||
print(response.status_code)
|
||||
print(response.json())
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import requests
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
|
||||
API_URL = "http://192.168.0.150:7000/translate_image"
|
||||
# API_URL = "http://127.0.0.1:7000/translate_image"
|
||||
|
||||
# 이미지 파일을 base64로 변환하는 함수
|
||||
def image_to_base64(image_path):
|
||||
"""이미지 파일을 base64 문자열로 변환"""
|
||||
if not os.path.exists(image_path):
|
||||
raise FileNotFoundError(f"이미지 파일을 찾을 수 없습니다: {image_path}")
|
||||
|
||||
with open(image_path, "rb") as image_file:
|
||||
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
||||
return encoded_string
|
||||
|
||||
# base64 데이터를 이미지 파일로 저장하는 함수
|
||||
def base64_to_image(base64_data, output_path):
|
||||
"""base64 문자열을 이미지 파일로 저장"""
|
||||
try:
|
||||
image_data = base64.b64decode(base64_data)
|
||||
with open(output_path, "wb") as image_file:
|
||||
image_file.write(image_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"이미지 저장 중 오류: {e}")
|
||||
return False
|
||||
|
||||
# 이미지 파일 경로
|
||||
image_path = "d:/py/IT_Server/modules/img/6.jpg"
|
||||
|
||||
print("=== 이미지 번역 API 테스트 시작 ===")
|
||||
start_total_time = time.time()
|
||||
|
||||
# 이미지를 base64로 변환
|
||||
print("\n1. 이미지 base64 변환 중...")
|
||||
start_encode_time = time.time()
|
||||
try:
|
||||
image_base64 = image_to_base64(image_path)
|
||||
encode_time = time.time() - start_encode_time
|
||||
print(f" ✓ 이미지 파일 '{image_path}' 를 base64로 변환 완료")
|
||||
print(f" ✓ Base64 길이: {len(image_base64):,} 문자")
|
||||
print(f" ✓ 인코딩 시간: {encode_time:.3f}초")
|
||||
except FileNotFoundError as e:
|
||||
print(f" ✗ 오류: {e}")
|
||||
exit(1)
|
||||
|
||||
payload = {
|
||||
"image_data": image_base64, # 필드명을 image_data로 수정
|
||||
"file_prefix": "test",
|
||||
"toggle_states": {"ocr": True},
|
||||
"unwanted_texts": {"크리스탈": "크리미"},
|
||||
"watermark_text": "테스트 워터마크",
|
||||
"watermark_opacity": 0.5,
|
||||
"watermark_font_size": 32
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
print(f"\n2. API 요청 전송 중... (URL: {API_URL})")
|
||||
start_api_time = time.time()
|
||||
response = requests.post(API_URL, data=json.dumps(payload), headers=headers)
|
||||
api_time = time.time() - start_api_time
|
||||
|
||||
total_time = time.time() - start_total_time
|
||||
|
||||
print(f"\n=== 처리 결과 ===")
|
||||
print(f"상태 코드: {response.status_code}")
|
||||
print(f"API 처리 시간: {api_time:.3f}초")
|
||||
print(f"전체 처리 시간: {total_time:.3f}초")
|
||||
|
||||
try:
|
||||
response_data = response.json()
|
||||
print(f"\n응답 내용:")
|
||||
print(json.dumps(response_data, indent=2, ensure_ascii=False))
|
||||
|
||||
# 성공적으로 처리된 경우 추가 정보 출력
|
||||
if response.status_code == 200:
|
||||
print(f"\n=== 성능 요약 ===")
|
||||
print(f"• Base64 인코딩: {encode_time:.3f}초")
|
||||
print(f"• API 서버 처리: {api_time:.3f}초")
|
||||
print(f"• 전체 소요 시간: {total_time:.3f}초")
|
||||
if api_time > 10:
|
||||
print("⚠️ API 처리 시간이 10초를 초과했습니다.")
|
||||
elif api_time > 5:
|
||||
print("⚠️ API 처리 시간이 5초를 초과했습니다.")
|
||||
else:
|
||||
print("✓ 처리 시간이 양호합니다.")
|
||||
|
||||
# 결과 이미지를 파일로 저장
|
||||
output_path = "d:/py/IT_Server/modules/translated_result.png"
|
||||
if base64_to_image(response_data["result"], output_path):
|
||||
print(f"처리된 이미지가 저장되었습니다: {output_path}")
|
||||
else:
|
||||
print("이미지 저장에 실패했습니다.")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"\nJSON 응답이 아닙니다:")
|
||||
print(response.text)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import requests
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
API_URL = "http://127.0.0.1:7000/translate_images"
|
||||
IMG_DIR = "d:/py/IT_Server/modules/img"
|
||||
|
||||
# img 폴더의 모든 이미지 파일 리스트업
|
||||
image_files = [os.path.join(IMG_DIR, f) for f in os.listdir(IMG_DIR)
|
||||
if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp", ".tif", ".tiff"))]
|
||||
|
||||
payload = {
|
||||
"local_image_paths": image_files,
|
||||
"file_prefix": "multi",
|
||||
"toggle_states": {"ocr": True},
|
||||
"unwanted_texts": {"크리스탈": "크리미", "세탁기": "이미지삭제"},
|
||||
"watermark_text": "테스트 워터마크",
|
||||
"watermark_opacity": 0.5,
|
||||
"watermark_font_size": 32
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
start = time.time()
|
||||
response = requests.post(API_URL, data=json.dumps(payload), headers=headers)
|
||||
elapsed = time.time() - start
|
||||
|
||||
print("응답 결과:")
|
||||
print(response.status_code)
|
||||
print(response.json())
|
||||
print(f"총 소요 시간: {elapsed:.2f}초")
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
import os
|
||||
import math
|
||||
import logging
|
||||
|
||||
class TextRenderingModule:
|
||||
def __init__(self, logger, font_path: Optional[str] = None):
|
||||
self.logger = logger
|
||||
self.font_path = font_path
|
||||
self.default_font_size = 20
|
||||
self.font_cache = {}
|
||||
|
||||
def get_font(self, size: int, font_path: Optional[str] = None) -> ImageFont.FreeTypeFont:
|
||||
font_path = font_path or self.font_path
|
||||
cache_key = f"{font_path}_{size}"
|
||||
if cache_key not in self.font_cache:
|
||||
try:
|
||||
if font_path and os.path.exists(font_path):
|
||||
font = ImageFont.truetype(font_path, size)
|
||||
else:
|
||||
font = ImageFont.load_default()
|
||||
self.font_cache[cache_key] = font
|
||||
except Exception as e:
|
||||
print(f"폰트 로드 오류: {e}")
|
||||
font = ImageFont.load_default()
|
||||
self.font_cache[cache_key] = font
|
||||
return self.font_cache[cache_key]
|
||||
|
||||
def estimate_text_size(self, text: str, font_size: int, font_path: Optional[str] = None) -> Tuple[int, int]:
|
||||
font = self.get_font(font_size, font_path)
|
||||
try:
|
||||
bbox = font.getbbox(text)
|
||||
width = bbox[2] - bbox[0]
|
||||
height = bbox[3] - bbox[1]
|
||||
except AttributeError:
|
||||
width, height = font.getsize(text)
|
||||
return width, height
|
||||
|
||||
def calculate_optimal_font_size(self, text: str, target_width: int, target_height: int, min_size: int = 8, max_size: int = 100, font_path: Optional[str] = None) -> int:
|
||||
best_size = min_size
|
||||
for size in range(min_size, max_size + 1):
|
||||
width, height = self.estimate_text_size(text, size, font_path)
|
||||
if width <= target_width and height <= target_height:
|
||||
best_size = size
|
||||
else:
|
||||
break
|
||||
return best_size
|
||||
|
||||
def _estimate_background_color(self, image: np.ndarray, x1: int, y1: int, x2: int, y2: int) -> Tuple[int, int, int]:
|
||||
margin = 5
|
||||
y1_exp = max(0, y1 - margin)
|
||||
y2_exp = min(image.shape[0], y2 + margin)
|
||||
x1_exp = max(0, x1 - margin)
|
||||
x2_exp = min(image.shape[1], x2 + margin)
|
||||
region = image[y1_exp:y2_exp, x1_exp:x2_exp]
|
||||
mean_color = np.mean(region, axis=(0, 1))
|
||||
return (int(mean_color[2]), int(mean_color[1]), int(mean_color[0]))
|
||||
|
||||
def _get_contrasting_color(self, bg_color: Tuple[int, int, int]) -> Tuple[int, int, int]:
|
||||
brightness = (bg_color[0] * 0.299 + bg_color[1] * 0.587 + bg_color[2] * 0.114)
|
||||
if brightness > 128:
|
||||
return (0, 0, 0)
|
||||
else:
|
||||
return (255, 255, 255)
|
||||
|
||||
def render_text(self, image: np.ndarray, ocr_results: List[Dict], translated_texts: List[str], font_path: Optional[str] = None) -> np.ndarray:
|
||||
result_image = image.copy()
|
||||
for i, (ocr_result, translated_text) in enumerate(zip(ocr_results, translated_texts)):
|
||||
polygon = ocr_result['polygon']
|
||||
polygon_array = np.array(polygon)
|
||||
x_coords = polygon_array[:, 0]
|
||||
y_coords = polygon_array[:, 1]
|
||||
x_min, x_max = int(np.min(x_coords)), int(np.max(x_coords))
|
||||
y_min, y_max = int(np.min(y_coords)), int(np.max(y_coords))
|
||||
width = x_max - x_min
|
||||
height = y_max - y_min
|
||||
optimal_font_size = self.calculate_optimal_font_size(translated_text, width, height, font_path=font_path)
|
||||
text_width, text_height = self.estimate_text_size(translated_text, optimal_font_size, font_path)
|
||||
center_x = (x_min + x_max) // 2
|
||||
center_y = (y_min + y_max) // 2
|
||||
text_x = center_x - text_width // 2
|
||||
text_y = center_y - text_height // 2
|
||||
angle = 0
|
||||
if len(polygon_array) >= 2:
|
||||
dx = polygon_array[1][0] - polygon_array[0][0]
|
||||
dy = polygon_array[1][1] - polygon_array[0][1]
|
||||
angle = math.degrees(math.atan2(dy, dx))
|
||||
bg_color = self._estimate_background_color(image, x_min, y_min, x_max, y_max)
|
||||
text_color = self._get_contrasting_color(bg_color)
|
||||
result_image = self.render_text_on_image(
|
||||
result_image, translated_text, (text_x, text_y),
|
||||
font_size=optimal_font_size,
|
||||
font_path=font_path,
|
||||
text_color=text_color,
|
||||
background_color=None,
|
||||
angle=angle
|
||||
)
|
||||
return result_image
|
||||
|
||||
def render_text_on_image(self, image: np.ndarray, text: str, position: Tuple[int, int], font_size: Optional[int] = None, font_path: Optional[str] = None, text_color: Tuple[int, int, int] = (0, 0, 0), background_color: Optional[Tuple[int, int, int]] = None, angle: float = 0) -> np.ndarray:
|
||||
if font_size is None:
|
||||
font_size = self.default_font_size
|
||||
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
draw = ImageDraw.Draw(pil_image)
|
||||
font = self.get_font(font_size, font_path)
|
||||
print(f"render_text_on_image font: {font}")
|
||||
text_width, text_height = self.estimate_text_size(text, font_size, font_path)
|
||||
if background_color is not None:
|
||||
bg_x1 = position[0] - 2
|
||||
bg_y1 = position[1] - 2
|
||||
bg_x2 = position[0] + text_width + 2
|
||||
bg_y2 = position[1] + text_height + 2
|
||||
draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=background_color)
|
||||
if angle != 0:
|
||||
text_image = Image.new('RGBA', (text_width + 10, text_height + 10), (255, 255, 255, 0))
|
||||
text_draw = ImageDraw.Draw(text_image)
|
||||
text_draw.text((5, 5), text, font=font, fill=text_color + (255,))
|
||||
rotated_text = text_image.rotate(angle, expand=True)
|
||||
pil_image.paste(rotated_text, position, rotated_text)
|
||||
else:
|
||||
draw.text(position, text, font=font, fill=text_color)
|
||||
result_image = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
|
||||
return result_image
|
||||
|
||||
def create_text_styles(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""다양한 텍스트 스타일 정의"""
|
||||
styles = {
|
||||
'default': {
|
||||
'color': (0, 0, 0),
|
||||
'bg_color': None,
|
||||
'outline': True,
|
||||
'outline_color': (255, 255, 255),
|
||||
'outline_width': 1
|
||||
},
|
||||
'bold': {
|
||||
'color': (0, 0, 0),
|
||||
'bg_color': (255, 255, 255),
|
||||
'outline': True,
|
||||
'outline_color': (128, 128, 128),
|
||||
'outline_width': 2
|
||||
},
|
||||
'highlight': {
|
||||
'color': (255, 255, 255),
|
||||
'bg_color': (255, 0, 0),
|
||||
'outline': False,
|
||||
'outline_color': None,
|
||||
'outline_width': 0
|
||||
},
|
||||
'subtle': {
|
||||
'color': (128, 128, 128),
|
||||
'bg_color': None,
|
||||
'outline': True,
|
||||
'outline_color': (255, 255, 255),
|
||||
'outline_width': 1
|
||||
}
|
||||
}
|
||||
|
||||
return styles
|
||||
|
||||
def render_with_style(self, image: np.ndarray, ocr_results: List[Dict],
|
||||
translated_texts: List[str], style_name: str = 'default') -> np.ndarray:
|
||||
"""스타일을 적용한 텍스트 렌더링"""
|
||||
styles = self.create_text_styles()
|
||||
|
||||
if style_name not in styles:
|
||||
print(f"알 수 없는 스타일: {style_name}")
|
||||
style_name = 'default'
|
||||
|
||||
style = styles[style_name]
|
||||
|
||||
# 기본 렌더링 후 스타일 적용
|
||||
result = self.render_text(image, ocr_results, translated_texts)
|
||||
|
||||
# 추가 스타일 처리는 여기서 구현
|
||||
# (예: 그림자, 글로우 효과 등)
|
||||
|
||||
return result
|
||||
|
||||
def adjust_text_for_space(self, text: str, max_width: int, max_height: int,
|
||||
font_size: int) -> Tuple[str, int]:
|
||||
"""
|
||||
공간에 맞게 텍스트 조정
|
||||
|
||||
Args:
|
||||
text (str): 원본 텍스트
|
||||
max_width (int): 최대 너비
|
||||
max_height (int): 최대 높이
|
||||
font_size (int): 폰트 크기
|
||||
|
||||
Returns:
|
||||
Tuple[str, int]: 조정된 텍스트와 폰트 크기
|
||||
"""
|
||||
# 텍스트가 너무 길면 줄바꿈 또는 생략
|
||||
if len(text) > 20:
|
||||
# 긴 텍스트는 줄바꿈
|
||||
words = text.split(' ')
|
||||
if len(words) > 1:
|
||||
mid = len(words) // 2
|
||||
text = ' '.join(words[:mid]) + '\n' + ' '.join(words[mid:])
|
||||
else:
|
||||
# 단어가 하나면 생략
|
||||
text = text[:15] + '...'
|
||||
|
||||
# 폰트 크기 조정
|
||||
adjusted_font_size = font_size
|
||||
while adjusted_font_size > 8:
|
||||
# 실제로는 텍스트 크기를 측정해서 비교
|
||||
estimated_width = len(text) * adjusted_font_size * 0.6
|
||||
if estimated_width <= max_width:
|
||||
break
|
||||
adjusted_font_size -= 2
|
||||
|
||||
return text, adjusted_font_size
|
||||
|
||||
def _create_style_comparison(self, images: List[np.ndarray], style_names: List[str]):
|
||||
"""스타일 비교 이미지 생성"""
|
||||
if not images:
|
||||
return
|
||||
|
||||
# 이미지 크기 조정
|
||||
target_width = 200
|
||||
target_height = int(images[0].shape[0] * target_width / images[0].shape[1])
|
||||
|
||||
resized_images = []
|
||||
for img in images:
|
||||
resized = cv2.resize(img, (target_width, target_height))
|
||||
resized_images.append(resized)
|
||||
|
||||
# 비교 이미지 생성
|
||||
num_images = len(resized_images)
|
||||
comparison_width = target_width * num_images
|
||||
comparison_height = target_height + 30
|
||||
|
||||
comparison = np.ones((comparison_height, comparison_width, 3), dtype=np.uint8) * 255
|
||||
|
||||
# 원본 이미지
|
||||
comparison[30:30+target_height, 0:target_width] = resized_images[0]
|
||||
cv2.putText(comparison, "Original", (10, 20),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
|
||||
|
||||
# 스타일 이미지들
|
||||
for i, (img, style_name) in enumerate(zip(resized_images[1:], style_names)):
|
||||
x_offset = target_width * (i + 1)
|
||||
comparison[30:30+target_height, x_offset:x_offset+target_width] = img
|
||||
cv2.putText(comparison, style_name, (x_offset + 10, 20),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
|
||||
|
||||
cv2.imwrite("test_output/text_style_comparison.jpg", comparison)
|
||||
self.logger.log("스타일 비교 이미지 저장 완료", level=logging.INFO)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -24,8 +24,8 @@ class ImageProcessor:
|
|||
self.base_dir = base_dir
|
||||
self.gpt_client = gpt_client
|
||||
|
||||
# OCR 관련
|
||||
self.inpaint_sv_port = 8080
|
||||
# 인페인트 포트
|
||||
self.inpaint_sv_port = None
|
||||
|
||||
self.font_path = font_path
|
||||
self.TEMP_IMAGE_DIR = os.path.join(self.base_dir, "temp_images")
|
||||
|
|
@ -39,6 +39,9 @@ class ImageProcessor:
|
|||
def __del__(self):
|
||||
"""소멸자에서 리소스 정리"""
|
||||
self.cleanup()
|
||||
|
||||
def update_iop_port(self, port):
|
||||
self.inpaint_sv_port = port
|
||||
|
||||
def cleanup(self):
|
||||
"""리소스 정리"""
|
||||
|
|
@ -179,7 +182,7 @@ class ImageProcessor:
|
|||
else:
|
||||
mask_np = mask
|
||||
|
||||
api_url = f"http://localhost:{self.inpaint_sv_port}/api/v1/inpaint"
|
||||
api_url = f"http://127.0.0.1:{self.inpaint_sv_port}/api/v1/inpaint"
|
||||
_, img_encoded = cv2.imencode('.png', image_np)
|
||||
_, mask_encoded = cv2.imencode('.png', mask_np)
|
||||
img_b64 = base64.b64encode(img_encoded).decode('utf-8')
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||
from modules.image_processor2 import ImageProcessor
|
||||
|
||||
# 포트 범위 설정
|
||||
PORT_RANGE = (7000, 7000)
|
||||
PORT_RANGE = (7321, 7321)
|
||||
|
||||
# 사용 가능한 포트 찾기
|
||||
def find_free_port():
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
1
|
||||
|
|
@ -0,0 +1 @@
|
|||
1
|
||||
Binary file not shown.
|
|
@ -26,14 +26,15 @@ class IOPaintManager:
|
|||
def is_alive(self):
|
||||
return self.process.poll() is None
|
||||
|
||||
def __init__(self, logger, num_instances=1, port_range=(8099, 8199), base_dir=None, wait_ready=30, model_dir=None):
|
||||
def __init__(self, logger, base_dir, num_instances=1, port_range=(7020, 7030), wait_ready=30, model_dir=None):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
self.iop_port = None
|
||||
self.instances = []
|
||||
self.port_range = port_range
|
||||
self.lock = threading.Lock()
|
||||
self.base_dir = base_dir or os.getcwd()
|
||||
self.model_dir = model_dir or os.path.join(self.base_dir, 'iop', 'models')
|
||||
self.exe_path = os.path.join(self.base_dir, 'iop', 'iop.exe')
|
||||
self.model_dir = model_dir or os.path.join(self.base_dir, 'modules', 'iop', 'models')
|
||||
self.exe_path = os.path.join(self.base_dir, 'modules', 'iop', 'iop.exe')
|
||||
self._start_instances(num_instances, wait_ready)
|
||||
|
||||
def _get_random_port(self):
|
||||
|
|
@ -70,22 +71,19 @@ class IOPaintManager:
|
|||
|
||||
def _start_instances(self, num, wait_ready):
|
||||
self.logger.log(f"IOPaint 인스턴스 {num} 개 시작", level=logging.INFO)
|
||||
try:
|
||||
import torch
|
||||
device_type = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
except Exception as e:
|
||||
self.logger.log(f"torch import 또는 GPU 체크 실패: {e}", level=logging.WARNING)
|
||||
device_type = "cpu"
|
||||
for _ in range(num):
|
||||
port = self._get_random_port()
|
||||
cmd = [self.exe_path, 'start', '--model=lama', f'--device={device_type}', '--port', str(port), '--model-dir', self.model_dir]
|
||||
self.iop_port = port
|
||||
cmd = [self.exe_path, 'start', '--model=migan', '--device=cpu', '--port', str(port), '--model-dir', self.model_dir]
|
||||
self.logger.log(f"[{port}] 인스턴스 실행 명령: {' '.join(cmd)}", level=logging.INFO)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
instance = self.ServerInstance(port, proc)
|
||||
self.instances.append(instance)
|
||||
|
||||
start_wait = 8
|
||||
time.sleep(start_wait)
|
||||
self.logger.log(f"[{port}] 인스턴스 실행 명시대기: {start_wait}초", level=logging.INFO)
|
||||
time.sleep(start_wait)
|
||||
|
||||
if self.wait_for_server_ready(port, timeout=wait_ready):
|
||||
self.logger.log(f"IOPaint 인스턴스 {instance.port} 준비됨", level=logging.INFO)
|
||||
else:
|
||||
|
|
@ -185,14 +183,8 @@ class IOPaintManager:
|
|||
self.mark_instance_idle(instance.port)
|
||||
|
||||
def add_instance(self, wait_ready=30):
|
||||
try:
|
||||
import torch
|
||||
device_type = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
except Exception as e:
|
||||
self.logger.log(f"torch import 또는 GPU 체크 실패: {e}", level=logging.WARNING)
|
||||
device_type = "cpu"
|
||||
port = self._get_random_port()
|
||||
cmd = [self.exe_path, 'start', '--model=lama', f'--device={device_type}', '--port', str(port), '--model-dir', self.model_dir]
|
||||
cmd = [self.exe_path, 'start', '--model=lama', '--device=cpu', '--port', str(port), '--model-dir', self.model_dir]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
instance = self.ServerInstance(port, proc)
|
||||
self.instances.append(instance)
|
||||
|
|
@ -203,9 +195,8 @@ class IOPaintManager:
|
|||
return instance
|
||||
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# manager = IOPaintManager(num_instances=1)
|
||||
# # result = manager.inpaint(image, mask) # 자동으로 idle 인스턴스에 요청
|
||||
# print(manager.get_instance_info())
|
||||
# manager.shutdown_all()
|
||||
# manager = IOPaintManager(num_instances=1)
|
||||
# result = manager.inpaint(image, mask) # 자동으로 idle 인스턴스에 요청
|
||||
# print(manager.get_instance_info())
|
||||
# manager.shutdown_all()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import subprocess
|
||||
import logging
|
||||
import socket
|
||||
import random
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
class IOPaint_Server:
|
||||
def __init__(self, logger, base_dir):
|
||||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
|
||||
def find_free_port(self, port_range=(7322, 7322)):
|
||||
"""포트 범위 내에서 사용 가능한 포트 반환"""
|
||||
for _ in range(20):
|
||||
port = random.randint(*port_range)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
try:
|
||||
s.bind(("0.0.0.0", port))
|
||||
return port
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError("사용 가능한 포트를 찾을 수 없습니다.")
|
||||
|
||||
def start(self, wait_ready=30):
|
||||
"""iopaint 서버를 실행하고, 정상 실행시 포트 반환"""
|
||||
port = self.find_free_port()
|
||||
model_dir = os.path.join(self.base_dir, 'modules', 'iop', 'models')
|
||||
cmd = [sys.executable, '-m', 'iopaint', 'start', '--model=migan', '--device=cpu', '--port', str(port), '--model-dir', model_dir]
|
||||
self.logger.log(f"[IOPaint] 실행 환경 파이썬: {sys.executable}", level=logging.INFO)
|
||||
self.logger.log(f"[IOPaint] 실행 명령: {' '.join(cmd)}", level=logging.INFO)
|
||||
self.logger.log(f"[IOPaint] 모델 디렉토리: {model_dir}", level=logging.INFO)
|
||||
# pip list로 iopaint 설치여부 확인
|
||||
try:
|
||||
pip_list = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], text=True)
|
||||
found = any('iopaint' in line for line in pip_list.splitlines())
|
||||
if found:
|
||||
self.logger.log("[IOPaint] iopaint 모듈이 현재 환경에 설치되어 있습니다.", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log("[IOPaint] iopaint 모듈이 현재 환경에 설치되어 있지 않습니다!", level=logging.WARNING)
|
||||
except Exception as e:
|
||||
self.logger.log(f"[IOPaint] pip list 실행 실패: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
# subprocess를 실시간 출력으로 실행
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
||||
self.logger.log(f"[IOPaint] 서버 준비 확인 시작 (최대 {wait_ready}초 대기)", level=logging.INFO)
|
||||
url = f"http://localhost:{port}/api/v1/server-config"
|
||||
start_time = time.time()
|
||||
stdout_lines = []
|
||||
stderr_lines = []
|
||||
import threading
|
||||
def read_stream(stream, lines, stream_name):
|
||||
for line in iter(stream.readline, ''):
|
||||
lines.append(line)
|
||||
print(f"[{stream_name}] {line}", end='')
|
||||
stream.close()
|
||||
t_out = threading.Thread(target=read_stream, args=(proc.stdout, stdout_lines, 'STDOUT'))
|
||||
t_err = threading.Thread(target=read_stream, args=(proc.stderr, stderr_lines, 'STDERR'))
|
||||
t_out.start()
|
||||
t_err.start()
|
||||
ready = False
|
||||
while time.time() - start_time < wait_ready:
|
||||
try:
|
||||
import requests
|
||||
r = requests.get(url, timeout=2)
|
||||
if r.status_code == 200:
|
||||
self.logger.log(f"[IOPaint] 서버가 포트 {port}에서 준비됨.")
|
||||
ready = True
|
||||
break
|
||||
except Exception as e:
|
||||
time.sleep(0.5)
|
||||
t_out.join(timeout=2)
|
||||
t_err.join(timeout=2)
|
||||
if ready:
|
||||
return port
|
||||
# 실패 시 로그 및 예외
|
||||
self.logger.log(f"[IOPaint] 서버 실행 실패.\nstdout:\n{''.join(stdout_lines)}\nstderr:\n{''.join(stderr_lines)}", level=logging.ERROR, exc_info=True)
|
||||
print("[IOPaint] 서버 실행 실패. 전체 STDOUT:")
|
||||
print(''.join(stdout_lines))
|
||||
print("[IOPaint] 서버 실행 실패. 전체 STDERR:")
|
||||
print(''.join(stderr_lines))
|
||||
raise RuntimeError(f"IOPaint 서버가 {wait_ready}초 내에 준비되지 않았습니다.")
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# logging.basicConfig(level=logging.INFO)
|
||||
# logger = logging.getLogger(__name__)
|
||||
# base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# iop = IOPaint(logger, base_dir)
|
||||
# iop.start()
|
||||
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import os
|
||||
import socket
|
||||
import threading
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
import logging
|
||||
|
||||
|
||||
class LocalImageServer:
|
||||
"""로컬 이미지 파일을 웹에서 접근 가능하도록 하는 HTTP 서버"""
|
||||
|
||||
def __init__(self, logger, image_dir, port=8000):
|
||||
self.logger = logger
|
||||
self.image_dir = os.path.abspath(image_dir) # 절대 경로로 변환
|
||||
self.original_cwd = os.getcwd() # 원래 작업 디렉토리 저장
|
||||
self.port = self.find_available_port(port)
|
||||
self.server = None
|
||||
self.server_thread = None
|
||||
|
||||
def find_available_port(self, start_port=8000, max_port=8100):
|
||||
"""사용 가능한 포트를 찾습니다"""
|
||||
for port in range(start_port, max_port + 1):
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(('localhost', port))
|
||||
return port
|
||||
except OSError:
|
||||
continue
|
||||
raise RuntimeError(f"포트 {start_port}-{max_port} 범위에서 사용 가능한 포트를 찾을 수 없습니다.")
|
||||
|
||||
def start_server(self):
|
||||
"""HTTP 서버를 시작합니다"""
|
||||
if self.server_thread and self.server_thread.is_alive():
|
||||
self.logger.log(f"로컬 이미지 서버가 이미 포트 {self.port}에서 실행 중입니다.", level=logging.DEBUG)
|
||||
return
|
||||
|
||||
# 이미지 디렉토리 존재 확인
|
||||
if not os.path.exists(self.image_dir):
|
||||
try:
|
||||
os.makedirs(self.image_dir, exist_ok=True)
|
||||
self.logger.log(f"이미지 디렉토리 생성: {self.image_dir}", level=logging.INFO)
|
||||
except Exception as e:
|
||||
self.logger.log(f"이미지 디렉토리 생성 실패: {e}", level=logging.ERROR)
|
||||
raise
|
||||
|
||||
try:
|
||||
# 작업 디렉토리 변경 없이 CustomHandler에서 직접 경로 처리
|
||||
class CustomHandler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, image_directory=None, **kwargs):
|
||||
self.image_directory = image_directory
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def translate_path(self, path):
|
||||
"""요청 경로를 이미지 디렉토리 내의 실제 파일 경로로 변환"""
|
||||
# 기본 translate_path 호출하여 상대 경로 얻기
|
||||
path = super().translate_path(path)
|
||||
# 현재 작업 디렉토리 대신 이미지 디렉토리 사용
|
||||
rel_path = os.path.relpath(path, os.getcwd())
|
||||
return os.path.join(self.image_directory, rel_path)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# 로그 출력을 비활성화 (너무 많은 로그 방지)
|
||||
pass
|
||||
|
||||
def end_headers(self):
|
||||
# CORS 헤더 추가
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
super().end_headers()
|
||||
|
||||
# 핸들러에 이미지 디렉토리 전달
|
||||
def handler_factory(*args, **kwargs):
|
||||
return CustomHandler(*args, image_directory=self.image_dir, **kwargs)
|
||||
|
||||
self.server = HTTPServer(('localhost', self.port), handler_factory)
|
||||
self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||
self.server_thread.start()
|
||||
|
||||
self.logger.log(f"로컬 이미지 서버가 포트 {self.port}에서 시작되었습니다. (디렉토리: {self.image_dir})", level=logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"로컬 웹서버 시작 실패: {e}", level=logging.ERROR)
|
||||
# 실패 시 상태 정리
|
||||
self.server = None
|
||||
self.server_thread = None
|
||||
raise
|
||||
|
||||
def stop_server(self):
|
||||
"""HTTP 서버를 중지합니다"""
|
||||
if self.server:
|
||||
try:
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
if self.server_thread and self.server_thread.is_alive():
|
||||
self.server_thread.join(timeout=5)
|
||||
self.logger.log("로컬 이미지 서버가 중지되었습니다.", level=logging.INFO)
|
||||
except Exception as e:
|
||||
self.logger.log(f"로컬 웹서버 중지 중 오류 발생: {e}", level=logging.ERROR)
|
||||
finally:
|
||||
self.server = None
|
||||
self.server_thread = None
|
||||
|
||||
def restart_server(self):
|
||||
"""서버를 재시작합니다"""
|
||||
self.logger.log("로컬 이미지 서버 재시작 중...", level=logging.INFO)
|
||||
self.stop_server()
|
||||
# 새로운 포트 찾기
|
||||
self.port = self.find_available_port(self.port)
|
||||
self.start_server()
|
||||
|
||||
def get_base_url(self):
|
||||
"""서버의 기본 URL을 반환합니다"""
|
||||
return f"http://localhost:{self.port}"
|
||||
|
||||
def is_running(self):
|
||||
"""서버가 실행 중인지 확인합니다"""
|
||||
return self.server is not None and self.server_thread and self.server_thread.is_alive()
|
||||
|
||||
def get_file_url(self, filename):
|
||||
"""특정 파일의 URL을 반환합니다"""
|
||||
if not self.is_running():
|
||||
self.logger.log("서버가 실행되지 않았습니다.", level=logging.WARNING)
|
||||
return None
|
||||
return f"{self.get_base_url()}/{filename}"
|
||||
|
||||
def __del__(self):
|
||||
"""소멸자에서 서버 정리"""
|
||||
try:
|
||||
self.stop_server()
|
||||
except:
|
||||
pass
|
||||
|
|
@ -9,8 +9,8 @@ class OCRModule:
|
|||
self.logger = logger
|
||||
self.base_dir = base_dir
|
||||
|
||||
# CUDA 사용 가능하도록 환경 변수 설정 제거
|
||||
# os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||
# CPU만 사용하도록 환경 변수 설정
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||
|
||||
self.ocr = None
|
||||
|
||||
|
|
@ -29,16 +29,9 @@ class OCRModule:
|
|||
|
||||
try:
|
||||
from paddleocr import PaddleOCR
|
||||
import paddle
|
||||
use_gpu = False
|
||||
try:
|
||||
use_gpu = paddle.is_compiled_with_cuda() and paddle.device.is_compiled_with_cuda()
|
||||
except Exception as e:
|
||||
self.logger.log(f"GPU 사용 가능 여부 확인 중 오류: {e}", level=logging.WARNING)
|
||||
use_gpu = False
|
||||
self.logger.log(f"PaddleOCR use_gpu: {use_gpu}", level=logging.INFO)
|
||||
|
||||
ocr = PaddleOCR(
|
||||
use_gpu=use_gpu, # GPU 사용 가능하면 활성화
|
||||
use_gpu=False,
|
||||
use_angle_cls=True, # 텍스트 방향 분류 활성화
|
||||
lang="ch",
|
||||
det_model_dir=self.det_model_dir,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from mmocr.apis import TextDetInferencer, TextRecogInferencer
|
||||
|
||||
class OCRModule:
|
||||
def __init__(self, det_config: str, det_checkpoint: str,
|
||||
rec_config: str, rec_checkpoint: str,
|
||||
logger=None):
|
||||
self.logger = logger or logging.getLogger(__name__)
|
||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||
|
||||
# MMOCR detection 및 recognition inferencer 초기화
|
||||
self.det_infer = TextDetInferencer(model=dict(config=det_config, ckpt=det_checkpoint), device='cpu')
|
||||
self.rec_infer = TextRecogInferencer(model=dict(config=rec_config, ckpt=rec_checkpoint), device='cpu')
|
||||
self.logger.info("✅ MMOCR detection 및 recognition 모델 초기화 완료")
|
||||
|
||||
def detect_text(self, image_path: str, method: str = 'polygon') -> List[Dict[str, Any]]:
|
||||
if not os.path.exists(image_path):
|
||||
self.logger.error(f"이미지 파일을 찾을 수 없습니다: {image_path}")
|
||||
return []
|
||||
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
self.logger.error(f"이미지를 읽을 수 없습니다: {image_path}")
|
||||
return []
|
||||
self.logger.info(f"❇️ OCR 감지(method={method}) 시작")
|
||||
|
||||
# 1) 텍스트 영역 감지
|
||||
det_res = self.det_infer(image_path)
|
||||
polys = det_res[0]['boundary_result']
|
||||
self.logger.info(f"👉 감지된 텍스트 영역 수: {len(polys)}")
|
||||
|
||||
# 2) 영역 crop 후 recognition
|
||||
crops = [self._crop_poly(img, poly) for poly in polys]
|
||||
rec_res = self.rec_infer(crops)
|
||||
self.logger.info("📖 텍스트 인식 완료")
|
||||
|
||||
ocr_results = []
|
||||
for poly, rec in zip(polys, rec_res):
|
||||
text, score = rec
|
||||
x, y, w, h = cv2.boundingRect(np.array(poly, dtype=np.int32))
|
||||
ocr_results.append({
|
||||
'text': text,
|
||||
'confidence': float(score),
|
||||
'polygon': poly,
|
||||
'bbox': (int(x), int(y), int(w), int(h)),
|
||||
'method': method
|
||||
})
|
||||
|
||||
return ocr_results
|
||||
|
||||
def filter_chinese_text(self, ocr_results: List[Dict]) -> List[Dict]:
|
||||
chinese = [r for r in ocr_results if any('\u4e00' <= c <= '\u9fff' for c in r['text'])]
|
||||
self.logger.info(f"중국어 텍스트 {len(chinese)}개 필터링 완료")
|
||||
return chinese
|
||||
|
||||
def _crop_poly(self, img: np.ndarray, poly: List[List[int]]) -> np.ndarray:
|
||||
mask = np.zeros(img.shape[:2], dtype=np.uint8)
|
||||
cv2.fillPoly(mask, [np.array(poly, dtype=np.int32)], 255)
|
||||
x, y, w, h = cv2.boundingRect(np.array(poly, dtype=np.int32))
|
||||
return cv2.bitwise_and(img[y:y+h, x:x+w], img[y:y+h, x:x+w], mask=mask[y:y+h, x:x+w])
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
API_URL = "http://127.0.0.1:7000/translate_image"
|
||||
# API_URL = "http://192.168.0.150:7000/translate_image"
|
||||
API_URL = "http://127.0.0.1:7002/translate_image"
|
||||
|
||||
payload = {
|
||||
"local_image_path": "d:/py/IT_Server/modules/img/6.jpg",
|
||||
"local_image_path": "d:/py/IT_Server/modules/img/1.jpg",
|
||||
"file_prefix": "test",
|
||||
"toggle_states": {"ocr": True},
|
||||
"unwanted_texts": {"크리스탈": "크리미"},
|
||||
"unwanted_texts": {"크리스탈": "크리미", "미니멀": "확화곽"},
|
||||
"watermark_text": "테스트 워터마크",
|
||||
"watermark_opacity": 0.5,
|
||||
"watermark_font_size": 32
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import requests
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
|
||||
API_URL = "http://192.168.0.150:7000/translate_image"
|
||||
# API_URL = "http://127.0.0.1:7000/translate_image"
|
||||
|
||||
# 이미지 파일을 base64로 변환하는 함수
|
||||
def image_to_base64(image_path):
|
||||
"""이미지 파일을 base64 문자열로 변환"""
|
||||
if not os.path.exists(image_path):
|
||||
raise FileNotFoundError(f"이미지 파일을 찾을 수 없습니다: {image_path}")
|
||||
|
||||
with open(image_path, "rb") as image_file:
|
||||
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
||||
return encoded_string
|
||||
|
||||
# base64 데이터를 이미지 파일로 저장하는 함수
|
||||
def base64_to_image(base64_data, output_path):
|
||||
"""base64 문자열을 이미지 파일로 저장"""
|
||||
try:
|
||||
image_data = base64.b64decode(base64_data)
|
||||
with open(output_path, "wb") as image_file:
|
||||
image_file.write(image_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"이미지 저장 중 오류: {e}")
|
||||
return False
|
||||
|
||||
# 이미지 파일 경로
|
||||
image_path = "d:/py/IT_Server/modules/img/6.jpg"
|
||||
|
||||
print("=== 이미지 번역 API 테스트 시작 ===")
|
||||
start_total_time = time.time()
|
||||
|
||||
# 이미지를 base64로 변환
|
||||
print("\n1. 이미지 base64 변환 중...")
|
||||
start_encode_time = time.time()
|
||||
try:
|
||||
image_base64 = image_to_base64(image_path)
|
||||
encode_time = time.time() - start_encode_time
|
||||
print(f" ✓ 이미지 파일 '{image_path}' 를 base64로 변환 완료")
|
||||
print(f" ✓ Base64 길이: {len(image_base64):,} 문자")
|
||||
print(f" ✓ 인코딩 시간: {encode_time:.3f}초")
|
||||
except FileNotFoundError as e:
|
||||
print(f" ✗ 오류: {e}")
|
||||
exit(1)
|
||||
|
||||
payload = {
|
||||
"image_data": image_base64, # 필드명을 image_data로 수정
|
||||
"file_prefix": "test",
|
||||
"toggle_states": {"ocr": True},
|
||||
"unwanted_texts": {"크리스탈": "크리미"},
|
||||
"watermark_text": "테스트 워터마크",
|
||||
"watermark_opacity": 0.5,
|
||||
"watermark_font_size": 32
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
print(f"\n2. API 요청 전송 중... (URL: {API_URL})")
|
||||
start_api_time = time.time()
|
||||
response = requests.post(API_URL, data=json.dumps(payload), headers=headers)
|
||||
api_time = time.time() - start_api_time
|
||||
|
||||
total_time = time.time() - start_total_time
|
||||
|
||||
print(f"\n=== 처리 결과 ===")
|
||||
print(f"상태 코드: {response.status_code}")
|
||||
print(f"API 처리 시간: {api_time:.3f}초")
|
||||
print(f"전체 처리 시간: {total_time:.3f}초")
|
||||
|
||||
try:
|
||||
response_data = response.json()
|
||||
print(f"\n응답 내용:")
|
||||
print(json.dumps(response_data, indent=2, ensure_ascii=False))
|
||||
|
||||
# 성공적으로 처리된 경우 추가 정보 출력
|
||||
if response.status_code == 200:
|
||||
print(f"\n=== 성능 요약 ===")
|
||||
print(f"• Base64 인코딩: {encode_time:.3f}초")
|
||||
print(f"• API 서버 처리: {api_time:.3f}초")
|
||||
print(f"• 전체 소요 시간: {total_time:.3f}초")
|
||||
if api_time > 10:
|
||||
print("⚠️ API 처리 시간이 10초를 초과했습니다.")
|
||||
elif api_time > 5:
|
||||
print("⚠️ API 처리 시간이 5초를 초과했습니다.")
|
||||
else:
|
||||
print("✓ 처리 시간이 양호합니다.")
|
||||
|
||||
# 결과 이미지를 파일로 저장
|
||||
output_path = "d:/py/IT_Server/modules/translated_result.png"
|
||||
if base64_to_image(response_data["result"], output_path):
|
||||
print(f"처리된 이미지가 저장되었습니다: {output_path}")
|
||||
else:
|
||||
print("이미지 저장에 실패했습니다.")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"\nJSON 응답이 아닙니다:")
|
||||
print(response.text)
|
||||
|
|
@ -4,7 +4,7 @@ import os
|
|||
import time
|
||||
|
||||
API_URL = "http://127.0.0.1:7000/translate_images"
|
||||
IMG_DIR = "/home/ckh08045/work/IT_Server/modules/img"
|
||||
IMG_DIR = "d:/py/IT_Server/modules/img"
|
||||
|
||||
# img 폴더의 모든 이미지 파일 리스트업
|
||||
image_files = [os.path.join(IMG_DIR, f) for f in os.listdir(IMG_DIR)
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -1,3 +1,5 @@
|
|||
# torch==2.2.2+cpu --index-url https://download.pytorch.org/whl/cpu
|
||||
# torchvision==0.17.2+cpu --index-url https://download.pytorch.org/whl/cpu
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
|
|
@ -9,4 +11,5 @@ pillow
|
|||
openai
|
||||
shapely
|
||||
paddleocr==2.10.0
|
||||
paddlepaddle
|
||||
paddlepaddle==2.6.2
|
||||
iopaint==1.6.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
from cx_Freeze import setup, Executable
|
||||
import sys
|
||||
import os
|
||||
|
||||
# base 옵션: 콘솔/GUI 구분 (여기서는 콘솔)
|
||||
base = None
|
||||
if sys.platform == "win32":
|
||||
base = "Console"
|
||||
|
||||
# # requirements.txt에서 패키지 목록 읽기 (참고용, 실제로는 pip로 설치 필요)
|
||||
# with open("requirements.txt", "r", encoding="utf-8") as f:
|
||||
# install_requires = [
|
||||
# line.strip().split("#")[0]
|
||||
# for line in f
|
||||
# if line.strip() and not line.startswith("#")
|
||||
# ]
|
||||
|
||||
# 추가로 포함할 파일/폴더 지정
|
||||
include_files = [
|
||||
("modules", "modules"), # modules 폴더 전체 포함
|
||||
]
|
||||
|
||||
setup(
|
||||
name="ImageTranslateServer",
|
||||
version="1.0",
|
||||
description="이미지 번역 FastAPI 서버",
|
||||
options={
|
||||
"build_exe": {
|
||||
# 문제가 되는 대형 패키지들 제외하고 최소한만
|
||||
"include_files": include_files,
|
||||
"excludes": [
|
||||
"tkinter", "matplotlib", "paddle", "torch", "torchvision",
|
||||
"paddleocr", "iopaint", "test", "unittest", "pdb"
|
||||
],
|
||||
"include_msvcr": True,
|
||||
"optimize": 2,
|
||||
}
|
||||
},
|
||||
executables=[
|
||||
Executable("main.py", base=base, target_name="ImageTranslateServer.exe")
|
||||
]
|
||||
)
|
||||
Loading…
Reference in New Issue