HyunsangJoo commited on
Commit
209d412
·
1 Parent(s): 8a27d1f

옵션 추가, 로컬 ui 테스트, readme 업데이트

Browse files
Files changed (3) hide show
  1. README.md +52 -0
  2. app.py +33 -5
  3. dots_ocr/utils/pptx_generator.py +53 -22
README.md CHANGED
@@ -12,3 +12,55 @@ short_description: Convert pdf/image to pptx with text ready to edit.
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
+
16
+
17
+
18
+ ## 🏗 전체 과정 요약 (4단계)
19
+
20
+ 마치 **[사진 촬영] → [탐정 수사] → [설계도 작성] → [건물 조립]** 과정과 같습니다.
21
+
22
+ ### 1단계: 사진 찍기 (이미지 준비) 📸
23
+ * **담당:** `dots_ocr/utils/doc_utils.py`
24
+ * **내용:** PDF 파일은 AI가 바로 보기 어렵습니다. 그래서 책을 스캔하듯이 모든 페이지를 **고화질 이미지(사진)**로 변환합니다.
25
+ * **핵심 기술:** 작은 글씨도 잘 보이게 **2배 확대(Zoom-in)**해서 찍습니다.
26
+
27
+ ### 2단계: 탐정 로봇의 분석 (좌표 & 내용 추출) 🕵️
28
+ * **담당:** `dots_ocr/parser.py`, `dots_ocr/model/inference.py`
29
+ * **내용:** 똑똑한 AI(탐정)가 사진을 보고 두 가지를 찾아냅니다.
30
+ 1. **글자 읽기:** "여기에 '안녕하세요'라고 써있네."
31
+ 2. **위치 찾기(좌표):** "이 글자는 종이 왼쪽에서 10칸, 위에서 20칸 떨어진 곳에 있어."
32
+ * **핵심 기술:** AI에게 이미지를 보낼 때 `<|img|>` 같은 특수 암호를 써서 전송합니다.
33
+
34
+ ### 3단계: 설계도 그리기 (데이터 정리) 📝
35
+ * **담당:** `dots_ocr/utils/output_cleaner.py`
36
+ * **내용:** AI가 찾아낸 뒤죽박죽인 정보들을 깔끔한 **설계도(JSON)**로 정리합니다.
37
+ * "1번 상자: [10, 20] 위치, 내용 '제목'"
38
+ * "2번 상자: [50, 80] 위치, 내용 '본문'"
39
+
40
+ ### 4단계: 건축가의 조립 (PPT 만들기) 🔨
41
+ * **담당:** `dots_ocr/utils/pptx_generator.py`
42
+ * **내용:** 빈 PPT 슬라이드를 꺼내고, 설계도를 보며 글상자와 표를 배치합니다.
43
+ * **핵심 기술:**
44
+ * **비율 계산:** 사진 크기와 PPT 크기가 달라도 비율(%)을 계산해서 정확한 위치에 넣습니다.
45
+ * **폰트 계산:** 글자가 상자 밖으로 튀어나가지 않게 적절한 폰트 크기를 자동으로 계산합니다.
46
+
47
+ ---
48
+
49
+ ## 🔍 심화 탐구: 좌표와 크기의 비밀
50
+
51
+ ### Q1. 좌표는 어떻게 측정하나요?
52
+ * **기준점:** 종이의 **왼쪽 맨 위 모서리**가 `(0, 0)`입니다.
53
+ * **방향:** 오른쪽으로 갈수록 x값이 커지고, 아래로 갈수록 y값이 커집니다.
54
+ * **단위:** **픽셀(Pixel)**이라는 점의 개수를 셉니다.
55
+
56
+ ### Q2. 이미지를 확대하면 좌표가 망가지지 않나요?
57
+ 망가지지 않습니다! **비율(Scale)**을 사용하기 때문입니다.
58
+ * **작은 사진:** 가로 100 중에 10 위치 (10%)
59
+ * **큰 사진:** 가로 200 중에 20 위치 (10%)
60
+ * **결론:** 크기는 달라져도 "전체에서 10% 지점"이라는 사실은 변하지 않으므로, PPT에서도 제자리에 들어갑니다.
61
+
62
+ ### Q3. PPT 페이지 크기는 어떻게 정하나요?
63
+ 프로그램이 눈치껏 상황에 맞춰 결정합니다.
64
+ 1. **배경 이미지가 있을 때:** 원본 이미지 크기를 그대로 따라갑니다. (가장 정확!)
65
+ 2. **이미지가 없을 때:** 가장 멀리 있는 좌표(오른쪽 끝, 아래쪽 끝)를 찾아서 크기를 짐작합니다.
66
+ 3. **최종 설정:** PPT 프로그램에 맞게 **가로를 10인치(약 25.4cm)**로 고정하고, 세로 길이는 비율에 맞춰 자동으로 늘리거나 줄입니다.
app.py CHANGED
@@ -781,7 +781,9 @@ def save_results(
781
  all_results: List[Dict],
782
  file_stem: str,
783
  include_background: bool,
784
- images: List[Image.Image]
 
 
785
  ) -> Tuple[Optional[str], Optional[str], List[str], List[List[Dict]]]:
786
  """결과 저장 로직 분리 (부분 저장 지원용)"""
787
  try:
@@ -799,7 +801,11 @@ def save_results(
799
  background_images = images[:processed_count] if include_background else []
800
 
801
  page_count, box_count = build_pptx_from_results(
802
- all_results, background_images, Path(pptx_path)
 
 
 
 
803
  )
804
  print(f"📊 PPTX 저장: {page_count}페이지, {box_count}개 텍스트박스")
805
 
@@ -838,6 +844,8 @@ def process_document(
838
  prompt_mode: str,
839
  quality_mode: str,
840
  include_background: bool,
 
 
841
  ) -> Tuple[Optional[str], Optional[str], Optional[str], str, Any, str]:
842
  """문서 처리 메인 함수"""
843
 
@@ -863,6 +871,7 @@ def process_document(
863
  print(f" 프롬프트 모드: {prompt_mode}")
864
  print(f" 품질 모드: {quality_mode} ({target_max_pixels} pixels)")
865
  print(f" 배경 포함: {include_background}")
 
866
  print("=" * 60)
867
 
868
  # 프롬프트 모드 변환
@@ -927,7 +936,12 @@ def process_document(
927
 
928
  # 결과 저장 (정상 완료 시)
929
  pptx_path, json_path, layout_img_paths, json_data = save_results(
930
- all_results, file_stem, include_background, images
 
 
 
 
 
931
  )
932
 
933
  # Markdown 결과
@@ -961,7 +975,12 @@ def process_document(
961
  if all_results:
962
  print(f"⚠️ 에러 발생! 현재까지 처리된 {len(all_results)}페이지 결과를 저장합니다...")
963
  pptx_path, json_path, layout_img_paths, json_data = save_results(
964
- all_results, file_stem, include_background, images
 
 
 
 
 
965
  )
966
 
967
  combined_markdown = "\n\n---\n\n".join(all_markdown) if all_markdown else f"*처리 도중 오류 발생: {str(e)}*"
@@ -1119,6 +1138,15 @@ with gr.Blocks(title="PDF/Image to PPTX Converter") as demo:
1119
  label="PPTX에 배경 이미지 포함",
1120
  value=True
1121
  )
 
 
 
 
 
 
 
 
 
1122
 
1123
  run_btn = gr.Button("🚀 변환 실행", variant="primary", size="lg")
1124
 
@@ -1177,7 +1205,7 @@ with gr.Blocks(title="PDF/Image to PPTX Converter") as demo:
1177
 
1178
  run_btn.click(
1179
  fn=process_document,
1180
- inputs=[file_input, prompt_mode, quality_mode, include_background],
1181
  outputs=[pptx_output, json_output, processed_image, extracted_content, layout_json, log_output]
1182
  )
1183
 
 
781
  all_results: List[Dict],
782
  file_stem: str,
783
  include_background: bool,
784
+ images: List[Image.Image],
785
+ use_dark_mode: bool = False,
786
+ show_border: bool = False
787
  ) -> Tuple[Optional[str], Optional[str], List[str], List[List[Dict]]]:
788
  """결과 저장 로직 분리 (부분 저장 지원용)"""
789
  try:
 
801
  background_images = images[:processed_count] if include_background else []
802
 
803
  page_count, box_count = build_pptx_from_results(
804
+ all_results,
805
+ background_images,
806
+ Path(pptx_path),
807
+ use_dark_mode=use_dark_mode,
808
+ show_border=show_border
809
  )
810
  print(f"📊 PPTX 저장: {page_count}페이지, {box_count}개 텍스트박스")
811
 
 
844
  prompt_mode: str,
845
  quality_mode: str,
846
  include_background: bool,
847
+ use_dark_mode: bool,
848
+ show_border: bool,
849
  ) -> Tuple[Optional[str], Optional[str], Optional[str], str, Any, str]:
850
  """문서 처리 메인 함수"""
851
 
 
871
  print(f" 프롬프트 모드: {prompt_mode}")
872
  print(f" 품질 모드: {quality_mode} ({target_max_pixels} pixels)")
873
  print(f" 배경 포함: {include_background}")
874
+ print(f" 스타일 옵션: 다크모드={use_dark_mode}, 테두리={show_border}")
875
  print("=" * 60)
876
 
877
  # 프롬프트 모드 변환
 
936
 
937
  # 결과 저장 (정상 완료 시)
938
  pptx_path, json_path, layout_img_paths, json_data = save_results(
939
+ all_results,
940
+ file_stem,
941
+ include_background,
942
+ images,
943
+ use_dark_mode=use_dark_mode,
944
+ show_border=show_border
945
  )
946
 
947
  # Markdown 결과
 
975
  if all_results:
976
  print(f"⚠️ 에러 발생! 현재까지 처리된 {len(all_results)}페이지 결과를 저장합니다...")
977
  pptx_path, json_path, layout_img_paths, json_data = save_results(
978
+ all_results,
979
+ file_stem,
980
+ include_background,
981
+ images,
982
+ use_dark_mode=use_dark_mode,
983
+ show_border=show_border
984
  )
985
 
986
  combined_markdown = "\n\n---\n\n".join(all_markdown) if all_markdown else f"*처리 도중 오류 발생: {str(e)}*"
 
1138
  label="PPTX에 배경 이미지 포함",
1139
  value=True
1140
  )
1141
+ with gr.Row():
1142
+ use_dark_mode = gr.Checkbox(
1143
+ label="다크 모드 (검정 배경/흰 글씨)",
1144
+ value=False
1145
+ )
1146
+ show_border = gr.Checkbox(
1147
+ label="텍스트 박스 테두리 표시",
1148
+ value=False
1149
+ )
1150
 
1151
  run_btn = gr.Button("🚀 변환 실행", variant="primary", size="lg")
1152
 
 
1205
 
1206
  run_btn.click(
1207
  fn=process_document,
1208
+ inputs=[file_input, prompt_mode, quality_mode, include_background, use_dark_mode, show_border],
1209
  outputs=[pptx_output, json_output, processed_image, extracted_content, layout_json, log_output]
1210
  )
1211
 
dots_ocr/utils/pptx_generator.py CHANGED
@@ -254,22 +254,30 @@ def _add_textbox(
254
  scale_y,
255
  category: str = "",
256
  page_height: int = 0,
257
- use_dark_bg: bool = False
 
258
  ) -> None:
259
  """
260
  텍스트 박스 추가 (AutoSize 강제 적용 - 순서 수정 최종 버전)
261
  """
262
  try:
263
- left = int(bbox[0] * scale_x)
264
- top = int(bbox[1] * scale_y)
265
- width = int((bbox[2] - bbox[0]) * scale_x)
266
- height = int((bbox[3] - bbox[1]) * scale_y)
 
267
 
268
  if width <= 0 or height <= 0:
269
  return
270
 
271
  textbox = slide.shapes.add_textbox(left, top, width, height)
272
 
 
 
 
 
 
 
273
  # 1. 텍스트 프레임 설정
274
  text_frame = textbox.text_frame
275
  text_frame.clear()
@@ -301,7 +309,7 @@ def _add_textbox(
301
  run.font.size = _calculate_font_size(width, height, cleaned_text, category, is_bold=is_bold)
302
 
303
  # 4. 색상 및 배경
304
- if use_dark_bg:
305
  run.font.color.rgb = RGBColor(255, 255, 255)
306
  textbox.fill.solid()
307
  textbox.fill.fore_color.rgb = RGBColor(0, 0, 0)
@@ -345,7 +353,9 @@ def _add_table(
345
  bbox,
346
  html_text,
347
  scale_x,
348
- scale_y
 
 
349
  ) -> None:
350
  """PPTX 슬라이드에 표 추가"""
351
  try:
@@ -360,11 +370,11 @@ def _add_table(
360
  if rows == 0 or cols == 0:
361
  return
362
 
363
- # 2. 위치 및 크기 계산
364
- left = int(bbox[0] * scale_x)
365
- top = int(bbox[1] * scale_y)
366
- width = int((bbox[2] - bbox[0]) * scale_x)
367
- height = int((bbox[3] - bbox[1]) * scale_y)
368
 
369
  # 3. 표 생성
370
  graphic_frame = slide.shapes.add_table(rows, cols, left, top, width, height)
@@ -387,17 +397,34 @@ def _add_table(
387
  for paragraph in cell.text_frame.paragraphs:
388
  paragraph.font.size = Pt(9)
389
 
390
- # 헤더 행(첫 행) 스타일링: 검정 배경 + 흰색 글씨 + 볼드
391
  if r_idx == 0:
392
  paragraph.font.bold = True
393
- paragraph.font.color.rgb = RGBColor(255, 255, 255) # 흰색 글씨
394
- cell.fill.solid()
395
- cell.fill.fore_color.rgb = RGBColor(0, 0, 0) # 검정 배경
 
 
 
 
 
 
 
396
  else:
397
- # 나머지 행: 흰색 배경(기본값) + 검정 글씨
398
- paragraph.font.color.rgb = RGBColor(0, 0, 0)
399
- cell.fill.solid()
400
- cell.fill.fore_color.rgb = RGBColor(255, 255, 255)
 
 
 
 
 
 
 
 
 
 
401
 
402
  except Exception as e:
403
  print(f"Table add failed: {e}")
@@ -407,6 +434,8 @@ def build_pptx_from_results(
407
  parse_results: List[Dict],
408
  background_images: List[Image.Image],
409
  output_path: Path,
 
 
410
  ) -> Tuple[int, int]:
411
  """파싱 결과로부터 PPTX 생성"""
412
  prs = Presentation()
@@ -467,7 +496,7 @@ def build_pptx_from_results(
467
 
468
  if category == "Table":
469
  if not text.strip(): continue
470
- _add_table(slide, bbox, text, scale_x, scale_y)
471
  total_boxes += 1
472
  continue
473
 
@@ -477,7 +506,9 @@ def build_pptx_from_results(
477
  _add_textbox(
478
  slide, bbox, text, scale_x, scale_y,
479
  category=category,
480
- page_height=page_height
 
 
481
  )
482
  total_boxes += 1
483
 
 
254
  scale_y,
255
  category: str = "",
256
  page_height: int = 0,
257
+ use_dark_mode: bool = False,
258
+ show_border: bool = False
259
  ) -> None:
260
  """
261
  텍스트 박스 추가 (AutoSize 강제 적용 - 순서 수정 최종 버전)
262
  """
263
  try:
264
+ # [수정] int(버림) 대신 round(반올림)를 사용하여 좌표 정밀도 향상
265
+ left = int(round(bbox[0] * scale_x))
266
+ top = int(round(bbox[1] * scale_y))
267
+ width = int(round((bbox[2] - bbox[0]) * scale_x))
268
+ height = int(round((bbox[3] - bbox[1]) * scale_y))
269
 
270
  if width <= 0 or height <= 0:
271
  return
272
 
273
  textbox = slide.shapes.add_textbox(left, top, width, height)
274
 
275
+ # [추가] 테두리 옵션
276
+ if show_border:
277
+ line = textbox.line
278
+ line.color.rgb = RGBColor(200, 200, 200) # 연한 회색 테두리
279
+ line.width = Pt(1)
280
+
281
  # 1. 텍스트 프레임 설정
282
  text_frame = textbox.text_frame
283
  text_frame.clear()
 
309
  run.font.size = _calculate_font_size(width, height, cleaned_text, category, is_bold=is_bold)
310
 
311
  # 4. 색상 및 배경
312
+ if use_dark_mode:
313
  run.font.color.rgb = RGBColor(255, 255, 255)
314
  textbox.fill.solid()
315
  textbox.fill.fore_color.rgb = RGBColor(0, 0, 0)
 
353
  bbox,
354
  html_text,
355
  scale_x,
356
+ scale_y,
357
+ use_dark_mode: bool = False,
358
+ show_border: bool = False
359
  ) -> None:
360
  """PPTX 슬라이드에 표 추가"""
361
  try:
 
370
  if rows == 0 or cols == 0:
371
  return
372
 
373
+ # 2. 위치 및 크기 계산 (반올림 적용)
374
+ left = int(round(bbox[0] * scale_x))
375
+ top = int(round(bbox[1] * scale_y))
376
+ width = int(round((bbox[2] - bbox[0]) * scale_x))
377
+ height = int(round((bbox[3] - bbox[1]) * scale_y))
378
 
379
  # 3. 표 생성
380
  graphic_frame = slide.shapes.add_table(rows, cols, left, top, width, height)
 
397
  for paragraph in cell.text_frame.paragraphs:
398
  paragraph.font.size = Pt(9)
399
 
400
+ # 헤더 행(첫 행) 스타일링
401
  if r_idx == 0:
402
  paragraph.font.bold = True
403
+ if use_dark_mode:
404
+ # 다크모드 헤더: 흰 글씨 / 짙은 회색 배경
405
+ paragraph.font.color.rgb = RGBColor(255, 255, 255)
406
+ cell.fill.solid()
407
+ cell.fill.fore_color.rgb = RGBColor(50, 50, 50)
408
+ else:
409
+ # 기본 헤더: 흰 글씨 / 검정 배경
410
+ paragraph.font.color.rgb = RGBColor(255, 255, 255)
411
+ cell.fill.solid()
412
+ cell.fill.fore_color.rgb = RGBColor(0, 0, 0)
413
  else:
414
+ # 나머지
415
+ if use_dark_mode:
416
+ # 다크모드 내용: 흰 글씨 / 검정 배경
417
+ paragraph.font.color.rgb = RGBColor(255, 255, 255)
418
+ cell.fill.solid()
419
+ cell.fill.fore_color.rgb = RGBColor(0, 0, 0)
420
+ else:
421
+ # 기본 내용: 검정 글씨 / 흰 배경
422
+ paragraph.font.color.rgb = RGBColor(0, 0, 0)
423
+ cell.fill.solid()
424
+ cell.fill.fore_color.rgb = RGBColor(255, 255, 255)
425
+
426
+ # 표 테두리는 기본적으로 존재하므로 show_border 옵션은 표에서는 생략하거나
427
+ # 필요하다면 별도 스타일 적용 가능 (여기서는 텍스트박스와 일관성을 위해 매개변수만 ��아둠)
428
 
429
  except Exception as e:
430
  print(f"Table add failed: {e}")
 
434
  parse_results: List[Dict],
435
  background_images: List[Image.Image],
436
  output_path: Path,
437
+ use_dark_mode: bool = False,
438
+ show_border: bool = False
439
  ) -> Tuple[int, int]:
440
  """파싱 결과로부터 PPTX 생성"""
441
  prs = Presentation()
 
496
 
497
  if category == "Table":
498
  if not text.strip(): continue
499
+ _add_table(slide, bbox, text, scale_x, scale_y, use_dark_mode=use_dark_mode, show_border=show_border)
500
  total_boxes += 1
501
  continue
502
 
 
506
  _add_textbox(
507
  slide, bbox, text, scale_x, scale_y,
508
  category=category,
509
+ page_height=page_height,
510
+ use_dark_mode=use_dark_mode,
511
+ show_border=show_border
512
  )
513
  total_boxes += 1
514