784 lines
31 KiB
Python
784 lines
31 KiB
Python
"""
|
|
측량/GIS/드론 관련 자료 PDF 변환 및 정리 시스템
|
|
- 모든 파일 형식을 PDF로 변환
|
|
- DWG 파일: DWG TrueView를 사용한 자동 PDF 변환
|
|
- 동영상 파일: Whisper를 사용한 음성→텍스트 변환 후 PDF 생성
|
|
- 원본 경로와 변환 파일 경로를 엑셀로 관리
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import openpyxl
|
|
from openpyxl.styles import Font, PatternFill, Alignment
|
|
import win32com.client
|
|
import pythoncom
|
|
from PIL import Image
|
|
import subprocess
|
|
import json
|
|
|
|
class SurveyingFileConverter:
|
|
def _dbg(self, msg):
|
|
if getattr(self, "debug", False):
|
|
print(msg)
|
|
|
|
def _ensure_ffmpeg_on_path(self):
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
found = shutil.which("ffmpeg")
|
|
self._dbg(f"DEBUG ffmpeg which before: {found}")
|
|
if found:
|
|
self.ffmpeg_exe = found
|
|
return True
|
|
|
|
try:
|
|
import imageio_ffmpeg
|
|
|
|
src = Path(imageio_ffmpeg.get_ffmpeg_exe())
|
|
self._dbg(f"DEBUG imageio ffmpeg exe: {src}")
|
|
self._dbg(f"DEBUG imageio ffmpeg exists: {src.exists()}")
|
|
|
|
if not src.exists():
|
|
return False
|
|
|
|
tools_dir = Path(self.output_dir) / "tools_ffmpeg"
|
|
tools_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
dst = tools_dir / "ffmpeg.exe"
|
|
|
|
if not dst.exists():
|
|
shutil.copyfile(str(src), str(dst))
|
|
|
|
os.environ["PATH"] = str(tools_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
|
|
found2 = shutil.which("ffmpeg")
|
|
self._dbg(f"DEBUG ffmpeg which after: {found2}")
|
|
|
|
if found2:
|
|
self.ffmpeg_exe = found2
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
self._dbg(f"DEBUG ensure ffmpeg error: {e}")
|
|
return False
|
|
|
|
|
|
def __init__(self, source_dir, output_dir):
|
|
self.source_dir = Path(source_dir)
|
|
self.output_dir = Path(output_dir)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.debug = True
|
|
self.ffmpeg_exe = None
|
|
ok = self._ensure_ffmpeg_on_path()
|
|
self._dbg(f"DEBUG ensure_ffmpeg_on_path result: {ok}")
|
|
|
|
# 변환 로그를 저장할 리스트
|
|
self.conversion_log = []
|
|
|
|
# ★ 추가: 도메인 용어 사전
|
|
self.domain_terms = ""
|
|
|
|
# HWP 보안 모듈 후보 목록 추가
|
|
self.hwp_security_modules = [
|
|
"FilePathCheckerModuleExample",
|
|
"SecurityModule",
|
|
""
|
|
]
|
|
|
|
# 지원 파일 확장자 정의
|
|
self.image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.tif', '.webp'}
|
|
self.office_extensions = {'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.hwpx'}
|
|
self.video_extensions = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv', '.m4v'}
|
|
self.text_extensions = {'.txt', '.csv', '.log', '.md'}
|
|
self.pdf_extension = {'.pdf'}
|
|
self.dwg_extensions = {'.dwg', '.dxf'}
|
|
|
|
# DWG TrueView 경로 설정 (설치 버전에 맞게 조정)
|
|
self.trueview_path = self._find_trueview()
|
|
|
|
def _find_trueview(self):
|
|
"""DWG TrueView 설치 경로 자동 탐색"""
|
|
possible_paths = [
|
|
r"C:\Program Files\Autodesk\DWG TrueView 2025\dwgviewr.exe",
|
|
r"C:\Program Files\Autodesk\DWG TrueView 2024\dwgviewr.exe",
|
|
r"C:\Program Files\Autodesk\DWG TrueView 2023\dwgviewr.exe",
|
|
r"C:\Program Files (x86)\Autodesk\DWG TrueView 2025\dwgviewr.exe",
|
|
r"C:\Program Files (x86)\Autodesk\DWG TrueView 2024\dwgviewr.exe",
|
|
]
|
|
|
|
for path in possible_paths:
|
|
if Path(path).exists():
|
|
return path
|
|
|
|
return None
|
|
|
|
def get_all_files(self):
|
|
"""하위 모든 폴더의 파일 목록 가져오기"""
|
|
all_files = []
|
|
for file_path in self.source_dir.rglob('*'):
|
|
if file_path.is_file():
|
|
all_files.append(file_path)
|
|
return all_files
|
|
|
|
def extract_audio_from_video(self, video_path, audio_output_path):
|
|
try:
|
|
import imageio_ffmpeg
|
|
from pathlib import Path
|
|
|
|
ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
|
|
self._dbg(f"DEBUG extract ffmpeg_exe: {ffmpeg_exe}")
|
|
self._dbg(f"DEBUG extract ffmpeg_exe exists: {Path(ffmpeg_exe).exists()}")
|
|
self._dbg(f"DEBUG extract input exists: {Path(video_path).exists()}")
|
|
self._dbg(f"DEBUG extract out path: {audio_output_path}")
|
|
|
|
cmd = [
|
|
ffmpeg_exe,
|
|
"-i", str(video_path),
|
|
"-vn",
|
|
"-acodec", "pcm_s16le",
|
|
"-ar", "16000",
|
|
"-ac", "1",
|
|
"-y",
|
|
str(audio_output_path),
|
|
]
|
|
self._dbg("DEBUG extract cmd: " + " ".join(cmd))
|
|
|
|
result = subprocess.run(cmd, capture_output=True, timeout=300, check=True, text=True)
|
|
self._dbg(f"DEBUG extract returncode: {result.returncode}")
|
|
self._dbg(f"DEBUG extract stderr tail: {(result.stderr or '')[-300:]}")
|
|
return True
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
self._dbg(f"DEBUG extract CalledProcessError returncode: {e.returncode}")
|
|
self._dbg(f"DEBUG extract stderr tail: {(e.stderr or '')[-300:]}")
|
|
return False
|
|
except Exception as e:
|
|
self._dbg(f"DEBUG extract exception: {e}")
|
|
return False
|
|
|
|
def transcribe_audio_with_whisper(self, audio_path):
|
|
try:
|
|
self._ensure_ffmpeg_on_path()
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
ffmpeg_path = shutil.which("ffmpeg")
|
|
self._dbg(f"DEBUG whisper ffmpeg which: {ffmpeg_path}")
|
|
|
|
if not ffmpeg_path:
|
|
if self.ffmpeg_exe:
|
|
import os
|
|
os.environ["PATH"] = str(Path(self.ffmpeg_exe).parent) + os.pathsep + os.environ.get("PATH", "")
|
|
|
|
audio_file = Path(audio_path)
|
|
self._dbg(f"DEBUG whisper audio exists: {audio_file.exists()}")
|
|
self._dbg(f"DEBUG whisper audio size: {audio_file.stat().st_size if audio_file.exists() else 'NA'}")
|
|
|
|
if not audio_file.exists() or audio_file.stat().st_size == 0:
|
|
return "[오디오 파일이 비어있거나 존재하지 않음]"
|
|
|
|
import whisper
|
|
model = whisper.load_model("medium") # ★ base → medium 변경
|
|
|
|
# ★ domain_terms를 initial_prompt로 사용
|
|
result = model.transcribe(
|
|
str(audio_path),
|
|
language="ko",
|
|
task="transcribe",
|
|
initial_prompt=self.domain_terms if self.domain_terms else None,
|
|
condition_on_previous_text=True, # ★ 다시 True로
|
|
)
|
|
|
|
# ★ 후처리: 반복 및 이상한 텍스트 제거
|
|
text = result["text"]
|
|
text = self.clean_transcript(text)
|
|
return text
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
self._dbg(f"DEBUG whisper traceback: {traceback.format_exc()}")
|
|
return f"[음성 인식 실패: {str(e)}]"
|
|
|
|
def clean_transcript(self, text):
|
|
"""Whisper 결과 후처리 - 반복/환각 제거"""
|
|
import re
|
|
|
|
# 1. 영어/일본어/중국어 환각 제거
|
|
text = re.sub(r'[A-Za-z]{3,}', '', text) # 3글자 이상 영어 제거
|
|
text = re.sub(r'[\u3040-\u309F\u30A0-\u30FF]+', '', text) # 일본어 제거
|
|
text = re.sub(r'[\u4E00-\u9FFF]+', '', text) # 한자 제거 (필요시)
|
|
|
|
# 2. 반복 문장 제거
|
|
sentences = text.split('.')
|
|
seen = set()
|
|
unique_sentences = []
|
|
for s in sentences:
|
|
s_clean = s.strip()
|
|
if s_clean and s_clean not in seen:
|
|
seen.add(s_clean)
|
|
unique_sentences.append(s_clean)
|
|
|
|
text = '. '.join(unique_sentences)
|
|
|
|
# 3. 이상한 문자 정리
|
|
text = re.sub(r'\s+', ' ', text) # 다중 공백 제거
|
|
text = text.strip()
|
|
|
|
return text
|
|
|
|
def get_video_transcript(self, video_path):
|
|
"""동영상 파일의 음성을 텍스트로 변환"""
|
|
try:
|
|
# 임시 오디오 파일 경로
|
|
temp_audio = video_path.parent / f"{video_path.stem}_temp_audio.wav"
|
|
|
|
# 1. 동영상에서 오디오 추출
|
|
if not self.extract_audio_from_video(video_path, temp_audio):
|
|
return self.get_basic_file_info(video_path) + "\n\n[오디오 추출 실패]"
|
|
if (not temp_audio.exists()) or temp_audio.stat().st_size == 0:
|
|
return self.get_basic_file_info(video_path) + "\n\n[오디오 파일 생성 실패]"
|
|
|
|
# 2. Whisper로 음성 인식
|
|
transcript = self.transcribe_audio_with_whisper(temp_audio)
|
|
|
|
# 3. 임시 오디오 파일 삭제
|
|
if temp_audio.exists():
|
|
temp_audio.unlink()
|
|
|
|
# 4. 결과 포맷팅
|
|
stat = video_path.stat()
|
|
lines = []
|
|
lines.append(f"동영상 파일 음성 전사 (Speech-to-Text)")
|
|
lines.append(f"=" * 60)
|
|
lines.append(f"파일명: {video_path.name}")
|
|
lines.append(f"경로: {video_path}")
|
|
lines.append(f"파일 크기: {self.format_file_size(stat.st_size)}")
|
|
lines.append(f"생성일: {datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')}")
|
|
lines.append("")
|
|
lines.append("=" * 60)
|
|
lines.append("음성 내용:")
|
|
lines.append("=" * 60)
|
|
lines.append("")
|
|
lines.append(transcript)
|
|
|
|
return "\n".join(lines)
|
|
|
|
except Exception as e:
|
|
return self.get_basic_file_info(video_path) + f"\n\n[음성 인식 오류: {str(e)}]"
|
|
|
|
def convert_dwg_to_pdf_trueview(self, dwg_path, pdf_path):
|
|
"""DWG TrueView를 사용한 DWG → PDF 변환"""
|
|
if not self.trueview_path:
|
|
return False, "DWG TrueView가 설치되지 않음"
|
|
|
|
try:
|
|
# AutoCAD 스크립트 생성
|
|
script_content = f"""_-EXPORT_PDF{pdf_path}_Y"""
|
|
script_path = dwg_path.parent / f"{dwg_path.stem}_plot.scr"
|
|
with open(script_path, 'w') as f:
|
|
f.write(script_content)
|
|
|
|
# TrueView 실행
|
|
cmd = [
|
|
self.trueview_path,
|
|
str(dwg_path.absolute()),
|
|
"/b", str(script_path.absolute()),
|
|
"/nologo"
|
|
]
|
|
|
|
result = subprocess.run(cmd, timeout=120, capture_output=True)
|
|
|
|
# 스크립트 파일 삭제
|
|
if script_path.exists():
|
|
try:
|
|
script_path.unlink()
|
|
except:
|
|
pass
|
|
|
|
# PDF 생성 확인
|
|
if pdf_path.exists():
|
|
return True, "성공"
|
|
else:
|
|
return False, "PDF 생성 실패"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "변환 시간 초과"
|
|
except Exception as e:
|
|
return False, f"DWG 변환 실패: {str(e)}"
|
|
|
|
def get_basic_file_info(self, file_path):
|
|
"""기본 파일 정보 반환"""
|
|
stat = file_path.stat()
|
|
lines = []
|
|
lines.append(f"파일 정보")
|
|
lines.append(f"=" * 60)
|
|
lines.append(f"파일명: {file_path.name}")
|
|
lines.append(f"경로: {file_path}")
|
|
lines.append(f"파일 크기: {self.format_file_size(stat.st_size)}")
|
|
lines.append(f"생성일: {datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')}")
|
|
lines.append(f"수정일: {datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')}")
|
|
return "\n".join(lines)
|
|
|
|
def format_file_size(self, size_bytes):
|
|
"""파일 크기를 읽기 쉬운 형식으로 변환"""
|
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
if size_bytes < 1024.0:
|
|
return f"{size_bytes:.2f} {unit}"
|
|
size_bytes /= 1024.0
|
|
return f"{size_bytes:.2f} TB"
|
|
|
|
def convert_image_to_pdf(self, image_path, output_path):
|
|
"""이미지 파일을 PDF로 변환"""
|
|
try:
|
|
img = Image.open(image_path)
|
|
# RGB 모드로 변환 (RGBA나 다른 모드 처리)
|
|
if img.mode in ('RGBA', 'LA', 'P'):
|
|
# 흰색 배경 생성
|
|
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
if img.mode == 'P':
|
|
img = img.convert('RGBA')
|
|
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
|
img = background
|
|
elif img.mode != 'RGB':
|
|
img = img.convert('RGB')
|
|
|
|
img.save(output_path, 'PDF', resolution=100.0)
|
|
return True, "성공"
|
|
except Exception as e:
|
|
return False, f"이미지 변환 실패: {str(e)}"
|
|
|
|
def convert_office_to_pdf(self, file_path, output_path):
|
|
"""Office 문서를 PDF로 변환"""
|
|
pythoncom.CoInitialize()
|
|
try:
|
|
ext = file_path.suffix.lower()
|
|
|
|
if ext in {'.hwp', '.hwpx'}:
|
|
return self.convert_hwp_to_pdf(file_path, output_path)
|
|
elif ext in {'.doc', '.docx'}:
|
|
return self.convert_word_to_pdf(file_path, output_path)
|
|
elif ext in {'.xls', '.xlsx'}:
|
|
return self.convert_excel_to_pdf(file_path, output_path)
|
|
elif ext in {'.ppt', '.pptx'}:
|
|
return self.convert_ppt_to_pdf(file_path, output_path)
|
|
else:
|
|
return False, "지원하지 않는 Office 형식"
|
|
|
|
except Exception as e:
|
|
return False, f"Office 변환 실패: {str(e)}"
|
|
finally:
|
|
pythoncom.CoUninitialize()
|
|
|
|
def convert_word_to_pdf(self, file_path, output_path):
|
|
"""Word 문서를 PDF로 변환"""
|
|
try:
|
|
word = win32com.client.Dispatch("Word.Application")
|
|
word.Visible = False
|
|
doc = word.Documents.Open(str(file_path.absolute()))
|
|
doc.SaveAs(str(output_path.absolute()), FileFormat=17) # 17 = PDF
|
|
doc.Close()
|
|
word.Quit()
|
|
return True, "성공"
|
|
except Exception as e:
|
|
return False, f"Word 변환 실패: {str(e)}"
|
|
|
|
def convert_excel_to_pdf(self, file_path, output_path):
|
|
"""Excel 파일을 PDF로 변환 - 열 너비에 맞춰 출력"""
|
|
try:
|
|
excel = win32com.client.Dispatch("Excel.Application")
|
|
excel.Visible = False
|
|
wb = excel.Workbooks.Open(str(file_path.absolute()))
|
|
|
|
# 모든 시트에 대해 페이지 설정
|
|
for ws in wb.Worksheets:
|
|
# 페이지 설정
|
|
ws.PageSetup.Zoom = False # 자동 크기 조정 비활성화
|
|
ws.PageSetup.FitToPagesWide = 1 # 너비를 1페이지에 맞춤
|
|
ws.PageSetup.FitToPagesTall = False # 높이는 자동 (내용에 따라)
|
|
|
|
# 여백 최소화 (단위: 포인트, 1cm ≈ 28.35 포인트)
|
|
ws.PageSetup.LeftMargin = excel.CentimetersToPoints(1)
|
|
ws.PageSetup.RightMargin = excel.CentimetersToPoints(1)
|
|
ws.PageSetup.TopMargin = excel.CentimetersToPoints(1)
|
|
ws.PageSetup.BottomMargin = excel.CentimetersToPoints(1)
|
|
|
|
# 용지 방향 자동 결정 (가로가 긴 경우 가로 방향)
|
|
used_range = ws.UsedRange
|
|
if used_range.Columns.Count > used_range.Rows.Count:
|
|
ws.PageSetup.Orientation = 2 # xlLandscape (가로)
|
|
else:
|
|
ws.PageSetup.Orientation = 1 # xlPortrait (세로)
|
|
|
|
# PDF로 저장
|
|
wb.ExportAsFixedFormat(0, str(output_path.absolute())) # 0 = PDF
|
|
wb.Close()
|
|
excel.Quit()
|
|
return True, "성공"
|
|
except Exception as e:
|
|
return False, f"Excel 변환 실패: {str(e)}"
|
|
|
|
|
|
def convert_ppt_to_pdf(self, file_path, output_path):
|
|
"""PowerPoint 파일을 PDF로 변환"""
|
|
try:
|
|
ppt = win32com.client.Dispatch("PowerPoint.Application")
|
|
ppt.Visible = True
|
|
presentation = ppt.Presentations.Open(str(file_path.absolute()))
|
|
presentation.SaveAs(str(output_path.absolute()), 32) # 32 = PDF
|
|
presentation.Close()
|
|
ppt.Quit()
|
|
return True, "성공"
|
|
except Exception as e:
|
|
return False, f"PowerPoint 변환 실패: {str(e)}"
|
|
|
|
def convert_hwp_to_pdf(self, file_path, output_path):
|
|
hwp = None
|
|
try:
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
hwp = win32com.client.gencache.EnsureDispatch("HWPFrame.HwpObject")
|
|
except Exception:
|
|
hwp = win32com.client.Dispatch("HWPFrame.HwpObject")
|
|
|
|
registered = False
|
|
last_reg_error = None
|
|
|
|
for module_name in getattr(self, "hwp_security_modules", [""]):
|
|
try:
|
|
hwp.RegisterModule("FilePathCheckDLL", module_name)
|
|
registered = True
|
|
break
|
|
except Exception as e:
|
|
last_reg_error = e
|
|
|
|
if not registered:
|
|
return False, f"HWP 보안 모듈 등록 실패: {last_reg_error}"
|
|
|
|
hwp.Open(str(file_path.absolute()), "", "")
|
|
|
|
hwp.HAction.GetDefault("FileSaveAsPdf", hwp.HParameterSet.HFileOpenSave.HSet)
|
|
hwp.HParameterSet.HFileOpenSave.filename = str(output_path.absolute())
|
|
hwp.HParameterSet.HFileOpenSave.Format = "PDF"
|
|
hwp.HAction.Execute("FileSaveAsPdf", hwp.HParameterSet.HFileOpenSave.HSet)
|
|
|
|
if output_path.exists() and output_path.stat().st_size > 0:
|
|
return True, "성공"
|
|
return False, "PDF 생성 확인 실패"
|
|
|
|
except Exception as e:
|
|
return False, f"HWP 변환 실패: {str(e)}"
|
|
finally:
|
|
try:
|
|
if hwp:
|
|
try:
|
|
hwp.Clear(1)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
hwp.Quit()
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
|
|
def convert_text_to_pdf(self, text_path, output_path):
|
|
"""텍스트 파일을 PDF로 변환 (reportlab 사용)"""
|
|
try:
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
|
|
# 한글 폰트 등록 (시스템에 설치된 폰트 사용)
|
|
try:
|
|
pdfmetrics.registerFont(TTFont('Malgun', 'malgun.ttf'))
|
|
font_name = 'Malgun'
|
|
except:
|
|
font_name = 'Helvetica'
|
|
|
|
# 텍스트 읽기
|
|
with open(text_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
content = f.read()
|
|
|
|
# PDF 생성
|
|
c = canvas.Canvas(str(output_path), pagesize=A4)
|
|
width, height = A4
|
|
|
|
c.setFont(font_name, 10)
|
|
|
|
# 여백 설정
|
|
margin = 50
|
|
y = height - margin
|
|
line_height = 14
|
|
|
|
# 줄 단위로 처리
|
|
for line in content.split('\n'):
|
|
if y < margin: # 페이지 넘김
|
|
c.showPage()
|
|
c.setFont(font_name, 10)
|
|
y = height - margin
|
|
|
|
# 긴 줄은 자동으로 줄바꿈
|
|
if len(line) > 100:
|
|
chunks = [line[i:i+100] for i in range(0, len(line), 100)]
|
|
for chunk in chunks:
|
|
c.drawString(margin, y, chunk)
|
|
y -= line_height
|
|
else:
|
|
c.drawString(margin, y, line)
|
|
y -= line_height
|
|
|
|
c.save()
|
|
return True, "성공"
|
|
except Exception as e:
|
|
return False, f"텍스트 변환 실패: {str(e)}"
|
|
|
|
def process_file(self, file_path):
|
|
"""개별 파일 처리"""
|
|
ext = file_path.suffix.lower()
|
|
|
|
# 출력 파일명 생성 (원본 경로 구조 유지)
|
|
relative_path = file_path.relative_to(self.source_dir)
|
|
output_subdir = self.output_dir / relative_path.parent
|
|
output_subdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# PDF 파일명
|
|
output_pdf = output_subdir / f"{file_path.stem}.pdf"
|
|
|
|
success = False
|
|
message = ""
|
|
|
|
try:
|
|
# 이미 PDF인 경우
|
|
if ext in self.pdf_extension:
|
|
shutil.copy2(file_path, output_pdf)
|
|
success = True
|
|
message = "PDF 복사 완료"
|
|
|
|
# DWG/DXF 파일
|
|
elif ext in self.dwg_extensions:
|
|
success, message = self.convert_dwg_to_pdf_trueview(file_path, output_pdf)
|
|
|
|
# 이미지 파일
|
|
elif ext in self.image_extensions:
|
|
success, message = self.convert_image_to_pdf(file_path, output_pdf)
|
|
|
|
# Office 문서
|
|
elif ext in self.office_extensions:
|
|
success, message = self.convert_office_to_pdf(file_path, output_pdf)
|
|
|
|
# 동영상 파일 - 음성을 텍스트로 변환 후 PDF 생성
|
|
elif ext in self.video_extensions:
|
|
# 음성→텍스트 변환
|
|
transcript_text = self.get_video_transcript(file_path)
|
|
|
|
# 임시 txt 파일 생성
|
|
temp_txt = output_subdir / f"{file_path.stem}_transcript.txt"
|
|
with open(temp_txt, 'w', encoding='utf-8') as f:
|
|
f.write(transcript_text)
|
|
|
|
# txt를 PDF로 변환
|
|
success, message = self.convert_text_to_pdf(temp_txt, output_pdf)
|
|
|
|
if success:
|
|
message = "성공 (음성 인식 완료)"
|
|
|
|
# 임시 txt 파일은 남겨둠 (참고용)
|
|
|
|
# 텍스트 파일
|
|
elif ext in self.text_extensions:
|
|
success, message = self.convert_text_to_pdf(file_path, output_pdf)
|
|
|
|
else:
|
|
message = f"지원하지 않는 파일 형식: {ext}"
|
|
|
|
except Exception as e:
|
|
message = f"처리 중 오류: {str(e)}"
|
|
|
|
# 로그 기록
|
|
self.conversion_log.append({
|
|
'원본 경로': str(file_path),
|
|
'파일명': file_path.name,
|
|
'파일 형식': ext,
|
|
'변환 PDF 경로': str(output_pdf) if success else "",
|
|
'상태': "성공" if success else "실패",
|
|
'메시지': message,
|
|
'처리 시간': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
})
|
|
|
|
return success, message
|
|
|
|
def create_excel_report(self, excel_path):
|
|
"""변환 결과를 엑셀로 저장"""
|
|
wb = openpyxl.Workbook()
|
|
ws = wb.active
|
|
ws.title = "변환 결과"
|
|
|
|
# 헤더 스타일
|
|
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
|
|
header_font = Font(bold=True, color="FFFFFF")
|
|
|
|
# 헤더 작성
|
|
headers = ['번호', '원본 경로', '파일명', '파일 형식', '변환 PDF 경로', '상태', '메시지', '처리 시간']
|
|
for col, header in enumerate(headers, 1):
|
|
cell = ws.cell(row=1, column=col, value=header)
|
|
cell.fill = header_fill
|
|
cell.font = header_font
|
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
|
|
|
# 데이터 작성
|
|
for idx, log in enumerate(self.conversion_log, 2):
|
|
ws.cell(row=idx, column=1, value=idx-1)
|
|
ws.cell(row=idx, column=2, value=log['원본 경로'])
|
|
ws.cell(row=idx, column=3, value=log['파일명'])
|
|
ws.cell(row=idx, column=4, value=log['파일 형식'])
|
|
ws.cell(row=idx, column=5, value=log['변환 PDF 경로'])
|
|
|
|
# 상태에 따라 색상 표시
|
|
status_cell = ws.cell(row=idx, column=6, value=log['상태'])
|
|
if log['상태'] == "성공":
|
|
status_cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
|
status_cell.font = Font(color="006100")
|
|
else:
|
|
status_cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
|
status_cell.font = Font(color="9C0006")
|
|
|
|
ws.cell(row=idx, column=7, value=log['메시지'])
|
|
ws.cell(row=idx, column=8, value=log['처리 시간'])
|
|
|
|
# 열 너비 자동 조정
|
|
for column in ws.columns:
|
|
max_length = 0
|
|
column_letter = column[0].column_letter
|
|
for cell in column:
|
|
try:
|
|
if len(str(cell.value)) > max_length:
|
|
max_length = len(str(cell.value))
|
|
except:
|
|
pass
|
|
adjusted_width = min(max_length + 2, 50)
|
|
ws.column_dimensions[column_letter].width = adjusted_width
|
|
|
|
# 요약 시트 추가
|
|
summary_ws = wb.create_sheet(title="요약")
|
|
|
|
total_files = len(self.conversion_log)
|
|
success_count = sum(1 for log in self.conversion_log if log['상태'] == "성공")
|
|
fail_count = total_files - success_count
|
|
|
|
summary_data = [
|
|
['항목', '값'],
|
|
['총 파일 수', total_files],
|
|
['변환 성공', success_count],
|
|
['변환 실패', fail_count],
|
|
['성공률', f"{(success_count/total_files*100):.1f}%" if total_files > 0 else "0%"],
|
|
['', ''],
|
|
['원본 폴더', str(self.source_dir)],
|
|
['출력 폴더', str(self.output_dir)],
|
|
['작업 완료 시간', datetime.now().strftime('%Y-%m-%d %H:%M:%S')]
|
|
]
|
|
|
|
for row_idx, row_data in enumerate(summary_data, 1):
|
|
for col_idx, value in enumerate(row_data, 1):
|
|
cell = summary_ws.cell(row=row_idx, column=col_idx, value=value)
|
|
if row_idx == 1:
|
|
cell.fill = header_fill
|
|
cell.font = header_font
|
|
cell.alignment = Alignment(horizontal='center' if col_idx == 1 else 'left')
|
|
|
|
summary_ws.column_dimensions['A'].width = 20
|
|
summary_ws.column_dimensions['B'].width = 60
|
|
|
|
# 저장
|
|
wb.save(excel_path)
|
|
print(f"\n엑셀 보고서 생성 완료: {excel_path}")
|
|
|
|
def run(self):
|
|
"""전체 변환 작업 실행"""
|
|
print(f"작업 시작: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print(f"원본 폴더: {self.source_dir}")
|
|
print(f"출력 폴더: {self.output_dir}")
|
|
|
|
# DWG TrueView 확인
|
|
if self.trueview_path:
|
|
print(f"DWG TrueView 발견: {self.trueview_path}")
|
|
else:
|
|
print("경고: DWG TrueView를 찾을 수 없습니다. DWG 파일 변환이 불가능합니다.")
|
|
|
|
print("-" * 80)
|
|
|
|
# 모든 파일 가져오기
|
|
all_files = self.get_all_files()
|
|
total_files = len(all_files)
|
|
|
|
# ★ 파일 분류: 동영상 vs 나머지
|
|
video_files = []
|
|
other_files = []
|
|
|
|
for file_path in all_files:
|
|
if file_path.suffix.lower() in self.video_extensions:
|
|
video_files.append(file_path)
|
|
else:
|
|
other_files.append(file_path)
|
|
|
|
print(f"\n총 {total_files}개 파일 발견")
|
|
print(f" - 문서/이미지 등: {len(other_files)}개")
|
|
print(f" - 동영상: {len(video_files)}개")
|
|
print("\n[1단계] 문서 파일 변환 시작...\n")
|
|
|
|
# ★ 1단계: 문서 파일 먼저 처리
|
|
for idx, file_path in enumerate(other_files, 1):
|
|
print(f"[{idx}/{len(other_files)}] {file_path.name} 처리 중...", end=' ')
|
|
success, message = self.process_file(file_path)
|
|
print(f"{'✓' if success else '✗'} {message}")
|
|
|
|
# ★ 2단계: domain.txt 로드
|
|
domain_path = self.source_dir.parent / "domain.txt" # D:\for python\테스트 중(측량)\domain.txt
|
|
if domain_path.exists():
|
|
self.domain_terms = domain_path.read_text(encoding='utf-8')
|
|
print(f"\n[2단계] 도메인 용어 사전 로드 완료: {domain_path}")
|
|
print(f" - 용어 수: 약 {len(self.domain_terms.split())}개 단어")
|
|
else:
|
|
print(f"\n[2단계] 도메인 용어 사전 없음: {domain_path}")
|
|
print(" - 기본 음성 인식으로 진행합니다.")
|
|
|
|
# ★ 3단계: 동영상 파일 처리
|
|
if video_files:
|
|
print(f"\n[3단계] 동영상 음성 인식 시작...\n")
|
|
for idx, file_path in enumerate(video_files, 1):
|
|
print(f"[{idx}/{len(video_files)}] {file_path.name} 처리 중...", end=' ')
|
|
success, message = self.process_file(file_path)
|
|
print(f"{'✓' if success else '✗'} {message}")
|
|
|
|
# 엑셀 보고서 생성
|
|
excel_path = self.output_dir / f"변환_결과_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
|
self.create_excel_report(excel_path)
|
|
|
|
# 최종 요약
|
|
success_count = sum(1 for log in self.conversion_log if log['상태'] == "성공")
|
|
print("\n" + "=" * 80)
|
|
print(f"작업 완료!")
|
|
print(f"총 파일: {total_files}개")
|
|
print(f"성공: {success_count}개")
|
|
print(f"실패: {total_files - success_count}개")
|
|
print(f"성공률: {(success_count/total_files*100):.1f}%" if total_files > 0 else "0%")
|
|
print("=" * 80)
|
|
|
|
if __name__ == "__main__":
|
|
# 경로 설정
|
|
SOURCE_DIR = r"D:\for python\테스트 중(측량)\측량_GIS_드론 관련 자료들"
|
|
OUTPUT_DIR = r"D:\for python\테스트 중(측량)\추출"
|
|
|
|
# 변환기 실행
|
|
converter = SurveyingFileConverter(SOURCE_DIR, OUTPUT_DIR)
|
|
converter.run() |