Files
game-fe-agent/skills/plan-analyzer/scripts/extract_pptx.py

316 lines
12 KiB
Python

"""
PPT 기획서 추출 스크립트 (extract_pptx.py)
========================================
PPTX 파일을 파싱하여 Claude AI가 분석할 수 있는 JSON 구조로 변환합니다.
의미 분석(화면 매핑, API 추출, 플로우 추론)은 Claude AI가 담당합니다.
사용법:
python extract_pptx.py <pptx경로> [옵션]
옵션:
--extract-images 슬라이드 이미지를 PNG로 추출 (--output-dir 로 저장 경로 지정)
--output-dir <dir> 이미지 추출 디렉토리 (기본: /tmp/pptx_<파일명>/)
--slides <범위> 처리할 슬라이드 범위 (예: 1-10, 5, 3-7,10-12)
--auto-install python-pptx 자동 설치 후 실행
--pretty JSON 출력 시 들여쓰기 적용
출력:
stdout 에 JSON 데이터 출력
"""
import sys
import os
import json
import argparse
import tempfile
import re
# ─────────────────────────────────────────────
# 의존성 확인 및 자동 설치
# ─────────────────────────────────────────────
def _ensure_pptx(auto_install: bool) -> None:
"""python-pptx 설치 여부 확인. 미설치 시 안내 또는 자동 설치."""
try:
import pptx # noqa: F401
except ImportError:
if auto_install:
import subprocess
print("📦 python-pptx 설치 중...", file=sys.stderr)
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'python-pptx'])
print("✅ python-pptx 설치 완료\n", file=sys.stderr)
else:
print(
"❌ python-pptx 패키지가 설치되어 있지 않습니다.\n"
"\n"
"설치 명령어:\n"
" pip3 install python-pptx\n"
"\n"
"또는 자동 설치 옵션을 사용하세요:\n"
f" python3 {sys.argv[0]} --auto-install <파일경로>",
file=sys.stderr,
)
sys.exit(1)
# ─────────────────────────────────────────────
# 슬라이드 범위 파싱
# ─────────────────────────────────────────────
def parse_slide_range(spec: str, total: int) -> set[int]:
"""
"1-5,8,10-12" 형태의 슬라이드 범위 문자열을 슬라이드 번호 set 으로 변환.
번호는 1-based.
"""
result: set[int] = set()
for part in spec.split(','):
part = part.strip()
if '-' in part:
start, end = part.split('-', 1)
result.update(range(int(start), int(end) + 1))
else:
result.add(int(part))
return {n for n in result if 1 <= n <= total}
# ─────────────────────────────────────────────
# 도형(Shape) 분류
# ─────────────────────────────────────────────
def _shape_type_name(shape) -> str:
"""python-pptx MSO_SHAPE_TYPE 값을 사람이 읽기 쉬운 문자열로 변환."""
from pptx.util import Emu # noqa: F401 — 모듈 로드 확인용
try:
return shape.shape_type.name.lower() # e.g. 'auto_shape', 'picture', 'line'
except Exception:
return 'unknown'
def _is_connector(shape) -> bool:
"""화살표/커넥터 도형 여부 확인."""
try:
# MSO_SHAPE_TYPE.LINE = 9, FREEFORM = 5
return shape.shape_type in (9,)
except Exception:
return False
# ─────────────────────────────────────────────
# 슬라이드 데이터 추출
# ─────────────────────────────────────────────
def extract_slide(slide, slide_number: int, extract_images: bool, output_dir: str) -> dict:
"""단일 슬라이드에서 모든 관련 데이터를 추출하여 dict 반환."""
from pptx.enum.shapes import PP_PLACEHOLDER # noqa: F401
result: dict = {
'number': slide_number,
'title': '',
'texts': [],
'notes': '',
'images': [],
'shapes': [],
'tables': [],
}
# ── 제목 추출 ──────────────────────────────
for shape in slide.shapes:
try:
if shape.is_placeholder:
ph_type = shape.placeholder_format.type
# PP_PLACEHOLDER.TITLE = 1, CENTER_TITLE = 3
if ph_type in (1, 3):
result['title'] = shape.text.strip()
break
except Exception:
pass
# ── 모든 도형 순회 ──────────────────────────
for shape in slide.shapes:
shape_info: dict = {
'type': _shape_type_name(shape),
'name': getattr(shape, 'name', ''),
'left': int(shape.left or 0),
'top': int(shape.top or 0),
'width': int(shape.width or 0),
'height': int(shape.height or 0),
'text': '',
}
# 텍스트 프레임
if shape.has_text_frame:
text = shape.text_frame.text.strip()
shape_info['text'] = text
if text:
result['texts'].append({
'text': text,
'left': shape_info['left'],
'top': shape_info['top'],
'width': shape_info['width'],
'height': shape_info['height'],
'shape_name': shape_info['name'],
})
# 테이블
if shape.has_table:
table = shape.table
all_rows = list(table.rows)
headers = [cell.text.strip() for cell in all_rows[0].cells]
rows = [
[cell.text.strip() for cell in row.cells]
for row in all_rows[1:]
]
result['tables'].append({'headers': headers, 'rows': rows})
shape_info['type'] = 'table'
# 이미지
if shape.shape_type == 13: # MSO_SHAPE_TYPE.PICTURE = 13
img_info: dict = {
'name': shape.name,
'left': shape_info['left'],
'top': shape_info['top'],
'width': shape_info['width'],
'height': shape_info['height'],
'path': '',
}
if extract_images:
try:
img_bytes = shape.image.blob
ext = shape.image.ext # e.g. 'png', 'jpeg'
safe_name = re.sub(r'[^\w\-.]', '_', shape.name)
img_filename = f"slide{slide_number:03d}_{safe_name}.{ext}"
img_path = os.path.join(output_dir, img_filename)
os.makedirs(output_dir, exist_ok=True)
with open(img_path, 'wb') as f:
f.write(img_bytes)
img_info['path'] = img_path
except Exception as e:
img_info['error'] = str(e)
result['images'].append(img_info)
shape_info['type'] = 'picture'
# 커넥터/화살표 처리
if _is_connector(shape):
shape_info['type'] = 'connector'
result['shapes'].append(shape_info)
# ── 발표자 노트 ────────────────────────────
try:
if slide.has_notes_slide:
notes_text = slide.notes_slide.notes_text_frame.text.strip()
result['notes'] = notes_text
except Exception:
pass
return result
# ─────────────────────────────────────────────
# 메인 추출 함수
# ─────────────────────────────────────────────
def extract_pptx(
filepath: str,
extract_images: bool = False,
output_dir: str = '',
slide_range: str = '',
pretty: bool = False,
) -> None:
"""
PPTX 파일 전체를 파싱하여 JSON 구조를 stdout 에 출력합니다.
Args:
filepath: PPTX 파일 절대/상대 경로
extract_images: True 이면 슬라이드 이미지를 output_dir 에 PNG로 추출
output_dir: 이미지 추출 디렉토리 (기본: /tmp/pptx_<파일명>/)
slide_range: 처리할 슬라이드 범위 문자열 (예: "1-10", "" = 전체)
pretty: True 이면 JSON 들여쓰기 출력
"""
from pptx import Presentation
if not os.path.exists(filepath):
print(f"❌ 파일을 찾을 수 없습니다: {filepath}", file=sys.stderr)
sys.exit(1)
prs = Presentation(filepath)
total_slides = len(prs.slides)
filename = os.path.basename(filepath)
# 출력 디렉토리 결정
if not output_dir:
stem = re.sub(r'[^\w\-]', '_', os.path.splitext(filename)[0])
output_dir = os.path.join(tempfile.gettempdir(), f'pptx_{stem}')
# 처리 대상 슬라이드 번호 결정
if slide_range:
target_slides = parse_slide_range(slide_range, total_slides)
else:
target_slides = set(range(1, total_slides + 1))
print(
f"🔍 파싱 중: {filename} ({total_slides}장 중 {len(target_slides)}장 처리)",
file=sys.stderr,
)
if extract_images:
print(f"🖼️ 이미지 추출 디렉토리: {output_dir}", file=sys.stderr)
# 슬라이드 추출
slides_data = []
for idx, slide in enumerate(prs.slides, start=1):
if idx not in target_slides:
continue
slide_data = extract_slide(slide, idx, extract_images, output_dir)
slides_data.append(slide_data)
print(f" 슬라이드 {idx}/{total_slides}: {slide_data['title'] or '(제목 없음)'}", file=sys.stderr)
output = {
'filename': filename,
'filepath': os.path.abspath(filepath),
'total_slides': total_slides,
'processed_slides': len(slides_data),
'image_output_dir': output_dir if extract_images else '',
'slides': slides_data,
}
indent = 2 if pretty else None
print(json.dumps(output, ensure_ascii=False, indent=indent))
print(f"\n✅ 추출 완료: {len(slides_data)}개 슬라이드", file=sys.stderr)
# ─────────────────────────────────────────────
# CLI 진입점
# ─────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description='PPTX 기획서를 JSON 구조로 추출합니다.',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('filepath', nargs='?', help='PPTX 파일 경로')
parser.add_argument('--extract-images', action='store_true', help='슬라이드 이미지를 PNG로 추출')
parser.add_argument('--output-dir', default='', help='이미지 추출 디렉토리')
parser.add_argument('--slides', default='', help='처리할 슬라이드 범위 (예: 1-10, 5, 3-7,10)')
parser.add_argument('--auto-install', action='store_true', help='python-pptx 자동 설치')
parser.add_argument('--pretty', action='store_true', help='JSON 들여쓰기 출력')
args = parser.parse_args()
_ensure_pptx(args.auto_install)
if not args.filepath:
parser.print_help()
print("\n❌ PPTX 파일 경로를 입력해 주세요.", file=sys.stderr)
sys.exit(1)
extract_pptx(
filepath=args.filepath,
extract_images=args.extract_images,
output_dir=args.output_dir,
slide_range=args.slides,
pretty=args.pretty,
)
if __name__ == '__main__':
main()