seawolf2357 commited on
Commit
2d6ba5f
Β·
verified Β·
1 Parent(s): 18163a5

Update app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +428 -28
app-backup.py CHANGED
@@ -40,6 +40,23 @@ else:
40
  PROMPT_PDF_PATH = BASE / "prompt.pdf"
41
  PROMPT_PDF_ID = "prompt_pdf_main"
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  pdf_cache: Dict[str, Dict[str, Any]] = {}
44
  cache_locks = {}
45
  pdf_embeddings: Dict[str, Dict[str, Any]] = {}
@@ -602,21 +619,186 @@ async def startup_event():
602
  logger.warning(f"prompt.pdf νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {PROMPT_PDF_PATH}")
603
 
604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  @app.get("/api/pdf-info")
606
  async def get_pdf_info():
607
- if not PROMPT_PDF_PATH.exists():
 
 
 
 
 
608
  return {"exists": False, "error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}
609
 
610
- pdf_name = PROMPT_PDF_PATH.stem
611
  is_cached = pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed"
612
 
613
  # VLM 뢄석 μΊμ‹œ 확인
614
- analysis_cached = load_analysis_cache(PROMPT_PDF_ID) is not None
615
 
616
  return {
617
- "path": str(PROMPT_PDF_PATH),
618
  "name": pdf_name,
619
- "id": PROMPT_PDF_ID,
 
 
620
  "exists": True,
621
  "cached": is_cached,
622
  "analysis_cached": analysis_cached
@@ -626,8 +808,12 @@ async def get_pdf_info():
626
  @app.get("/api/analysis-status")
627
  async def get_analysis_status():
628
  """VLM 뢄석 μƒνƒœ 확인"""
 
 
 
 
629
  # λ¨Όμ € μΊμ‹œ 파일 확인
630
- cached = load_analysis_cache(PROMPT_PDF_ID)
631
  if cached:
632
  return {
633
  "status": "completed",
@@ -637,8 +823,8 @@ async def get_analysis_status():
637
  }
638
 
639
  # λ©”λͺ¨λ¦¬ μƒνƒœ 확인
640
- if PROMPT_PDF_ID in analysis_status:
641
- status_info = analysis_status[PROMPT_PDF_ID]
642
  return {
643
  "status": status_info.get("status", "unknown"),
644
  "progress": status_info.get("progress", 0),
@@ -651,22 +837,26 @@ async def get_analysis_status():
651
  @app.post("/api/reanalyze-pdf")
652
  async def reanalyze_pdf():
653
  """PDF μž¬λΆ„μ„ (μΊμ‹œ λ¬΄μ‹œ)"""
 
654
  try:
655
  if not HAS_VALID_API_KEY:
656
  return JSONResponse(content={"error": "API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}, status_code=400)
657
 
 
 
 
658
  # κΈ°μ‘΄ μΊμ‹œ μ‚­μ œ
659
- cache_path = get_analysis_cache_path(PROMPT_PDF_ID)
660
  if cache_path.exists():
661
  cache_path.unlink()
662
  logger.info("κΈ°μ‘΄ VLM 뢄석 μΊμ‹œ μ‚­μ œ")
663
 
664
  # μƒνƒœ μ΄ˆκΈ°ν™”
665
- if PROMPT_PDF_ID in analysis_status:
666
- del analysis_status[PROMPT_PDF_ID]
667
 
668
  # λ°±κ·ΈλΌμš΄λ“œμ—μ„œ μž¬λΆ„μ„ μ‹œμž‘
669
- asyncio.create_task(run_initial_analysis())
670
 
671
  return {"status": "started", "message": "PDF μž¬λΆ„μ„μ„ μ‹œμž‘ν•©λ‹ˆλ‹€."}
672
  except Exception as e:
@@ -676,22 +866,26 @@ async def reanalyze_pdf():
676
 
677
  @app.get("/api/pdf-thumbnail")
678
  async def get_pdf_thumbnail():
 
679
  try:
680
- if not PROMPT_PDF_PATH.exists():
 
 
 
681
  return {"thumbnail": None, "error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}
682
 
683
- pdf_name = PROMPT_PDF_PATH.stem
684
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("pages"):
685
  if pdf_cache[pdf_name]["pages"][0].get("thumb"):
686
  return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
687
 
688
- doc = fitz.open(str(PROMPT_PDF_PATH))
689
  if doc.page_count > 0:
690
  page = doc[0]
691
  pix = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2))
692
  img_data = pix.tobytes("jpeg", 70)
693
  b64_img = base64.b64encode(img_data).decode('utf-8')
694
- asyncio.create_task(cache_pdf(str(PROMPT_PDF_PATH)))
695
  return {"thumbnail": f"data:image/jpeg;base64,{b64_img}"}
696
  return {"thumbnail": None}
697
  except Exception as e:
@@ -701,7 +895,9 @@ async def get_pdf_thumbnail():
701
 
702
  @app.get("/api/cache-status")
703
  async def get_cache_status():
704
- pdf_name = PROMPT_PDF_PATH.stem
 
 
705
  if pdf_name in pdf_cache:
706
  return pdf_cache[pdf_name]
707
  return {"status": "not_cached"}
@@ -709,15 +905,17 @@ async def get_cache_status():
709
 
710
  @app.post("/api/ai/query-pdf")
711
  async def api_query_pdf(query: Dict[str, str]):
 
712
  try:
713
  user_query = query.get("query", "")
714
  if not user_query:
715
  return JSONResponse(content={"error": "질문이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€"}, status_code=400)
716
 
717
- if not PROMPT_PDF_PATH.exists():
 
718
  return JSONResponse(content={"error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}, status_code=404)
719
 
720
- result = await query_pdf(PROMPT_PDF_ID, user_query)
721
  if "answer" in result:
722
  return result
723
  if "error" in result:
@@ -730,11 +928,13 @@ async def api_query_pdf(query: Dict[str, str]):
730
 
731
  @app.get("/api/ai/summarize-pdf")
732
  async def api_summarize_pdf():
 
733
  try:
734
- if not PROMPT_PDF_PATH.exists():
 
735
  return JSONResponse(content={"error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}, status_code=404)
736
 
737
- result = await summarize_pdf(PROMPT_PDF_ID)
738
  if "summary" in result:
739
  return result
740
  if "error" in result:
@@ -747,8 +947,12 @@ async def api_summarize_pdf():
747
 
748
  @app.get("/api/cached-pdf")
749
  async def get_cached_pdf(background_tasks: BackgroundTasks):
 
750
  try:
751
- pdf_name = PROMPT_PDF_PATH.stem
 
 
 
752
  if pdf_name in pdf_cache:
753
  status = pdf_cache[pdf_name].get("status", "")
754
  if status == "completed":
@@ -765,7 +969,7 @@ async def get_cached_pdf(background_tasks: BackgroundTasks):
765
  "available_pages": len([p for p in pages if p and p.get("src")])
766
  }
767
 
768
- background_tasks.add_task(cache_pdf, str(PROMPT_PDF_PATH))
769
  return {"status": "started", "progress": 0}
770
  except Exception as e:
771
  logger.error(f"μΊμ‹œλœ PDF 제곡 였λ₯˜: {str(e)}")
@@ -927,6 +1131,94 @@ HTML = """
927
  transform: translateX(0);
928
  }
929
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  /* Viewer Container - Comic Style */
931
  #viewer {
932
  width: 94%;
@@ -1398,22 +1690,43 @@ HTML = """
1398
  .header-info {
1399
  top: 8px;
1400
  padding: 6px 15px;
1401
- max-width: 70%;
1402
  }
1403
 
1404
  .header-info .title {
1405
- font-size: 1.1rem;
1406
  }
1407
 
1408
  .floating-ai {
1409
- width: 45px;
1410
- height: 45px;
1411
  top: 8px;
1412
  right: 10px;
1413
  }
1414
 
1415
  .floating-ai .icon {
1416
- font-size: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
  }
1418
 
1419
  #aiChatContainer {
@@ -1444,6 +1757,16 @@ HTML = """
1444
  <div class="ai-title">πŸ€– AI μ–΄μ‹œμŠ€ν„΄νŠΈ</div>
1445
  </div>
1446
 
 
 
 
 
 
 
 
 
 
 
1447
  <!-- AI Chat Container -->
1448
  <div id="aiChatContainer">
1449
  <div id="aiChatHeader">
@@ -1479,6 +1802,8 @@ HTML = """
1479
  let isAiProcessing = false;
1480
  let hasLoadedSummary = false;
1481
  let analysisCheckInterval = null;
 
 
1482
 
1483
  function $id(id) { return document.getElementById(id); }
1484
 
@@ -1853,6 +2178,77 @@ HTML = """
1853
  }
1854
  }
1855
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1856
  async function loadPDF() {
1857
  try {
1858
  showLoading("PDF 정보 확인 쀑...");
@@ -1934,6 +2330,10 @@ HTML = """
1934
  }
1935
  });
1936
 
 
 
 
 
1937
  loadPDF();
1938
  });
1939
  </script>
 
40
  PROMPT_PDF_PATH = BASE / "prompt.pdf"
41
  PROMPT_PDF_ID = "prompt_pdf_main"
42
 
43
+ # 닀쀑 PDF 지원
44
+ PDF_FILES = {
45
+ "prompt": {
46
+ "path": BASE / "prompt.pdf",
47
+ "id": "prompt_pdf_main",
48
+ "name": "μƒ˜ν”Œ-ν”„λ‘¬ν”„νŠΈλΆ"
49
+ },
50
+ "ktx": {
51
+ "path": BASE / "ktx2512.pdf",
52
+ "id": "ktx_pdf_main",
53
+ "name": "μƒ˜ν”Œ-μ½”λ ˆμΌμž‘μ§€"
54
+ }
55
+ }
56
+
57
+ # ν˜„μž¬ μ„ νƒλœ PDF (κΈ°λ³Έκ°’: prompt)
58
+ current_pdf_key = "prompt"
59
+
60
  pdf_cache: Dict[str, Dict[str, Any]] = {}
61
  cache_locks = {}
62
  pdf_embeddings: Dict[str, Dict[str, Any]] = {}
 
619
  logger.warning(f"prompt.pdf νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {PROMPT_PDF_PATH}")
620
 
621
 
622
+ @app.get("/api/pdf-list")
623
+ async def get_pdf_list():
624
+ """μ‚¬μš© κ°€λŠ₯ν•œ PDF λͺ©λ‘ λ°˜ν™˜"""
625
+ global current_pdf_key
626
+ pdf_list = []
627
+ for key, info in PDF_FILES.items():
628
+ pdf_list.append({
629
+ "key": key,
630
+ "name": info["name"],
631
+ "exists": info["path"].exists(),
632
+ "is_current": key == current_pdf_key
633
+ })
634
+ return {"pdfs": pdf_list, "current": current_pdf_key}
635
+
636
+
637
+ @app.post("/api/switch-pdf/{pdf_key}")
638
+ async def switch_pdf(pdf_key: str, background_tasks: BackgroundTasks):
639
+ """PDF μ „ν™˜"""
640
+ global current_pdf_key
641
+
642
+ if pdf_key not in PDF_FILES:
643
+ return JSONResponse(content={"error": "μœ νš¨ν•˜μ§€ μ•Šμ€ PDF ν‚€μž…λ‹ˆλ‹€."}, status_code=400)
644
+
645
+ pdf_info = PDF_FILES[pdf_key]
646
+ if not pdf_info["path"].exists():
647
+ return JSONResponse(content={"error": f"PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {pdf_info['name']}"}, status_code=404)
648
+
649
+ current_pdf_key = pdf_key
650
+ logger.info(f"PDF μ „ν™˜: {pdf_key} - {pdf_info['name']}")
651
+
652
+ # 캐싱 μ‹œμž‘
653
+ pdf_name = pdf_info["path"].stem
654
+ if pdf_name not in pdf_cache or pdf_cache[pdf_name].get("status") != "completed":
655
+ background_tasks.add_task(cache_pdf, str(pdf_info["path"]))
656
+
657
+ # VLM 뢄석 μ‹œμž‘ (μΊμ‹œκ°€ μ—†λŠ” 경우)
658
+ if not load_analysis_cache(pdf_info["id"]):
659
+ asyncio.create_task(analyze_pdf_with_vlm_batched_for_key(pdf_key))
660
+
661
+ return {
662
+ "success": True,
663
+ "current": pdf_key,
664
+ "name": pdf_info["name"],
665
+ "id": pdf_info["id"]
666
+ }
667
+
668
+
669
+ async def analyze_pdf_with_vlm_batched_for_key(pdf_key: str, force_refresh: bool = False) -> Dict[str, Any]:
670
+ """νŠΉμ • PDF에 λŒ€ν•œ VLM 뢄석"""
671
+ global analysis_status
672
+
673
+ if pdf_key not in PDF_FILES:
674
+ return {"error": "μœ νš¨ν•˜μ§€ μ•Šμ€ PDF ν‚€"}
675
+
676
+ pdf_info = PDF_FILES[pdf_key]
677
+ pdf_id = pdf_info["id"]
678
+ pdf_path = str(pdf_info["path"])
679
+
680
+ # 이미 뢄석 쀑인지 확인
681
+ if pdf_id in analysis_status and analysis_status[pdf_id].get("status") == "analyzing":
682
+ logger.info(f"PDF {pdf_id} 이미 뢄석 쀑...")
683
+ return {"status": "analyzing", "progress": analysis_status[pdf_id].get("progress", 0)}
684
+
685
+ # μΊμ‹œ 확인
686
+ if not force_refresh:
687
+ cached = load_analysis_cache(pdf_id)
688
+ if cached:
689
+ analysis_status[pdf_id] = {"status": "completed", "progress": 100}
690
+ return cached
691
+
692
+ if not pdf_info["path"].exists():
693
+ analysis_status[pdf_id] = {"status": "error", "error": "PDF 파일 μ—†μŒ"}
694
+ return {"error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}
695
+
696
+ if not HAS_VALID_API_KEY:
697
+ analysis_status[pdf_id] = {"status": "error", "error": "API ν‚€ μ—†μŒ"}
698
+ return {"error": "API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}
699
+
700
+ # 뢄석 μ‹œμž‘
701
+ analysis_status[pdf_id] = {"status": "analyzing", "progress": 0, "started_at": time.time()}
702
+
703
+ try:
704
+ doc = fitz.open(pdf_path)
705
+ total_pages = doc.page_count
706
+ doc.close()
707
+
708
+ logger.info(f"PDF 뢄석 μ‹œμž‘ ({pdf_key}): 총 {total_pages}νŽ˜μ΄μ§€")
709
+
710
+ batch_size = 5
711
+ all_analyses = []
712
+
713
+ for start_page in range(0, min(total_pages, 25), batch_size):
714
+ try:
715
+ progress = int((start_page / min(total_pages, 25)) * 100)
716
+ analysis_status[pdf_id]["progress"] = progress
717
+ logger.info(f"배치 뢄석 쀑 ({pdf_key}): {start_page + 1}νŽ˜μ΄μ§€λΆ€ν„° (μ§„ν–‰λ₯ : {progress}%)")
718
+
719
+ loop = asyncio.get_event_loop()
720
+ batch_result = await loop.run_in_executor(
721
+ None,
722
+ analyze_batch_pages_sync,
723
+ pdf_path,
724
+ start_page,
725
+ batch_size
726
+ )
727
+
728
+ if batch_result:
729
+ all_analyses.append(f"### νŽ˜μ΄μ§€ {start_page + 1}~{min(start_page + batch_size, total_pages)}\n{batch_result}")
730
+
731
+ await asyncio.sleep(2)
732
+
733
+ except Exception as batch_error:
734
+ logger.error(f"배치 {start_page} 뢄석 였λ₯˜: {batch_error}")
735
+ all_analyses.append(f"### νŽ˜μ΄μ§€ {start_page + 1}~{min(start_page + batch_size, total_pages)}\n[뢄석 μ‹€νŒ¨: {str(batch_error)}]")
736
+
737
+ combined_analysis = "\n\n".join(all_analyses)
738
+
739
+ successful_analyses = [a for a in all_analyses if "[뢄석 μ‹€νŒ¨:" not in a]
740
+ if not successful_analyses:
741
+ logger.error("λͺ¨λ“  배치 뢄석 μ‹€νŒ¨")
742
+ analysis_status[pdf_id] = {"status": "error", "error": "λͺ¨λ“  νŽ˜μ΄μ§€ 뢄석 μ‹€νŒ¨"}
743
+ return {"error": "PDF 뢄석에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."}
744
+
745
+ summary = ""
746
+ if combined_analysis:
747
+ try:
748
+ summary_messages = [
749
+ {"role": "system", "content": "λ‹€μŒ PDF 뢄석 λ‚΄μš©μ„ 500자 μ΄λ‚΄λ‘œ μš”μ•½ν•΄μ£Όμ„Έμš”. 핡심 λ‚΄μš©κ³Ό μ£Όμš” ν‚€μ›Œλ“œλ₯Ό ν¬ν•¨ν•΄μ£Όμ„Έμš”."},
750
+ {"role": "user", "content": combined_analysis[:8000]}
751
+ ]
752
+ summary = call_fireworks_vlm_api(summary_messages, max_tokens=1024, temperature=0.5)
753
+ except Exception as sum_err:
754
+ logger.error(f"μš”μ•½ 생성 였λ₯˜: {sum_err}")
755
+ summary = combined_analysis[:500] + "..."
756
+
757
+ analysis_data = {
758
+ "pdf_id": pdf_id,
759
+ "pdf_key": pdf_key,
760
+ "total_pages": total_pages,
761
+ "analyzed_pages": min(total_pages, 25),
762
+ "analysis": combined_analysis,
763
+ "summary": summary,
764
+ "created_at": time.time()
765
+ }
766
+
767
+ save_analysis_cache(pdf_id, analysis_data)
768
+
769
+ analysis_status[pdf_id] = {"status": "completed", "progress": 100}
770
+ logger.info(f"PDF 뢄석 μ™„λ£Œ: {pdf_id}")
771
+
772
+ return analysis_data
773
+
774
+ except Exception as e:
775
+ logger.error(f"VLM PDF 뢄석 였λ₯˜: {e}")
776
+ analysis_status[pdf_id] = {"status": "error", "error": str(e)}
777
+ return {"error": str(e)}
778
+
779
+
780
  @app.get("/api/pdf-info")
781
  async def get_pdf_info():
782
+ global current_pdf_key
783
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
784
+ pdf_path = pdf_info["path"]
785
+ pdf_id = pdf_info["id"]
786
+
787
+ if not pdf_path.exists():
788
  return {"exists": False, "error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}
789
 
790
+ pdf_name = pdf_path.stem
791
  is_cached = pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed"
792
 
793
  # VLM 뢄석 μΊμ‹œ 확인
794
+ analysis_cached = load_analysis_cache(pdf_id) is not None
795
 
796
  return {
797
+ "path": str(pdf_path),
798
  "name": pdf_name,
799
+ "display_name": pdf_info["name"],
800
+ "id": pdf_id,
801
+ "key": current_pdf_key,
802
  "exists": True,
803
  "cached": is_cached,
804
  "analysis_cached": analysis_cached
 
808
  @app.get("/api/analysis-status")
809
  async def get_analysis_status():
810
  """VLM 뢄석 μƒνƒœ 확인"""
811
+ global current_pdf_key
812
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
813
+ pdf_id = pdf_info["id"]
814
+
815
  # λ¨Όμ € μΊμ‹œ 파일 확인
816
+ cached = load_analysis_cache(pdf_id)
817
  if cached:
818
  return {
819
  "status": "completed",
 
823
  }
824
 
825
  # λ©”λͺ¨λ¦¬ μƒνƒœ 확인
826
+ if pdf_id in analysis_status:
827
+ status_info = analysis_status[pdf_id]
828
  return {
829
  "status": status_info.get("status", "unknown"),
830
  "progress": status_info.get("progress", 0),
 
837
  @app.post("/api/reanalyze-pdf")
838
  async def reanalyze_pdf():
839
  """PDF μž¬λΆ„μ„ (μΊμ‹œ λ¬΄μ‹œ)"""
840
+ global current_pdf_key
841
  try:
842
  if not HAS_VALID_API_KEY:
843
  return JSONResponse(content={"error": "API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}, status_code=400)
844
 
845
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
846
+ pdf_id = pdf_info["id"]
847
+
848
  # κΈ°μ‘΄ μΊμ‹œ μ‚­μ œ
849
+ cache_path = get_analysis_cache_path(pdf_id)
850
  if cache_path.exists():
851
  cache_path.unlink()
852
  logger.info("κΈ°μ‘΄ VLM 뢄석 μΊμ‹œ μ‚­μ œ")
853
 
854
  # μƒνƒœ μ΄ˆκΈ°ν™”
855
+ if pdf_id in analysis_status:
856
+ del analysis_status[pdf_id]
857
 
858
  # λ°±κ·ΈλΌμš΄λ“œμ—μ„œ μž¬λΆ„μ„ μ‹œμž‘
859
+ asyncio.create_task(analyze_pdf_with_vlm_batched_for_key(current_pdf_key))
860
 
861
  return {"status": "started", "message": "PDF μž¬λΆ„μ„μ„ μ‹œμž‘ν•©λ‹ˆλ‹€."}
862
  except Exception as e:
 
866
 
867
  @app.get("/api/pdf-thumbnail")
868
  async def get_pdf_thumbnail():
869
+ global current_pdf_key
870
  try:
871
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
872
+ pdf_path = pdf_info["path"]
873
+
874
+ if not pdf_path.exists():
875
  return {"thumbnail": None, "error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}
876
 
877
+ pdf_name = pdf_path.stem
878
  if pdf_name in pdf_cache and pdf_cache[pdf_name].get("pages"):
879
  if pdf_cache[pdf_name]["pages"][0].get("thumb"):
880
  return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
881
 
882
+ doc = fitz.open(str(pdf_path))
883
  if doc.page_count > 0:
884
  page = doc[0]
885
  pix = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2))
886
  img_data = pix.tobytes("jpeg", 70)
887
  b64_img = base64.b64encode(img_data).decode('utf-8')
888
+ asyncio.create_task(cache_pdf(str(pdf_path)))
889
  return {"thumbnail": f"data:image/jpeg;base64,{b64_img}"}
890
  return {"thumbnail": None}
891
  except Exception as e:
 
895
 
896
  @app.get("/api/cache-status")
897
  async def get_cache_status():
898
+ global current_pdf_key
899
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
900
+ pdf_name = pdf_info["path"].stem
901
  if pdf_name in pdf_cache:
902
  return pdf_cache[pdf_name]
903
  return {"status": "not_cached"}
 
905
 
906
  @app.post("/api/ai/query-pdf")
907
  async def api_query_pdf(query: Dict[str, str]):
908
+ global current_pdf_key
909
  try:
910
  user_query = query.get("query", "")
911
  if not user_query:
912
  return JSONResponse(content={"error": "질문이 μ œκ³΅λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€"}, status_code=400)
913
 
914
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
915
+ if not pdf_info["path"].exists():
916
  return JSONResponse(content={"error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}, status_code=404)
917
 
918
+ result = await query_pdf(pdf_info["id"], user_query)
919
  if "answer" in result:
920
  return result
921
  if "error" in result:
 
928
 
929
  @app.get("/api/ai/summarize-pdf")
930
  async def api_summarize_pdf():
931
+ global current_pdf_key
932
  try:
933
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
934
+ if not pdf_info["path"].exists():
935
  return JSONResponse(content={"error": "PDF νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"}, status_code=404)
936
 
937
+ result = await summarize_pdf(pdf_info["id"])
938
  if "summary" in result:
939
  return result
940
  if "error" in result:
 
947
 
948
  @app.get("/api/cached-pdf")
949
  async def get_cached_pdf(background_tasks: BackgroundTasks):
950
+ global current_pdf_key
951
  try:
952
+ pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"])
953
+ pdf_path = pdf_info["path"]
954
+ pdf_name = pdf_path.stem
955
+
956
  if pdf_name in pdf_cache:
957
  status = pdf_cache[pdf_name].get("status", "")
958
  if status == "completed":
 
969
  "available_pages": len([p for p in pages if p and p.get("src")])
970
  }
971
 
972
+ background_tasks.add_task(cache_pdf, str(pdf_path))
973
  return {"status": "started", "progress": 0}
974
  except Exception as e:
975
  logger.error(f"μΊμ‹œλœ PDF 제곡 였λ₯˜: {str(e)}")
 
1131
  transform: translateX(0);
1132
  }
1133
 
1134
+ /* PDF Selection Buttons */
1135
+ .floating-pdf-btn {
1136
+ position: fixed;
1137
+ right: 20px;
1138
+ width: 50px;
1139
+ height: 50px;
1140
+ border-radius: 50%;
1141
+ background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
1142
+ border: 3px solid #1F2937;
1143
+ box-shadow: 4px 4px 0 #1F2937;
1144
+ z-index: 9998;
1145
+ display: flex;
1146
+ justify-content: center;
1147
+ align-items: center;
1148
+ cursor: pointer;
1149
+ transition: var(--transition);
1150
+ overflow: hidden;
1151
+ }
1152
+
1153
+ #pdfPromptBtn {
1154
+ top: 70px;
1155
+ }
1156
+
1157
+ #pdfKtxBtn {
1158
+ top: 130px;
1159
+ }
1160
+
1161
+ .floating-pdf-btn:hover {
1162
+ transform: translate(-2px, -2px);
1163
+ box-shadow: 6px 6px 0 #1F2937;
1164
+ }
1165
+
1166
+ .floating-pdf-btn:active {
1167
+ transform: translate(2px, 2px);
1168
+ box-shadow: 2px 2px 0 #1F2937;
1169
+ }
1170
+
1171
+ .floating-pdf-btn.active {
1172
+ background: linear-gradient(135deg, #10B981 0%, #059669 100%);
1173
+ box-shadow: 4px 4px 0 #065F46;
1174
+ }
1175
+
1176
+ .floating-pdf-btn .icon {
1177
+ display: flex;
1178
+ justify-content: center;
1179
+ align-items: center;
1180
+ width: 100%;
1181
+ height: 100%;
1182
+ font-size: 18px;
1183
+ color: white;
1184
+ text-shadow: 1px 1px 0 #1F2937;
1185
+ transition: var(--transition);
1186
+ }
1187
+
1188
+ .floating-pdf-btn .pdf-title {
1189
+ position: absolute;
1190
+ right: 58px;
1191
+ background: #FFF;
1192
+ padding: 8px 14px;
1193
+ border-radius: 8px;
1194
+ border: 2px solid #1F2937;
1195
+ box-shadow: 3px 3px 0 #1F2937;
1196
+ font-family: 'Comic Neue', cursive;
1197
+ font-size: 0.85rem;
1198
+ font-weight: 700;
1199
+ letter-spacing: 0.5px;
1200
+ white-space: nowrap;
1201
+ pointer-events: none;
1202
+ opacity: 0;
1203
+ transform: translateX(10px);
1204
+ transition: all 0.3s ease;
1205
+ color: var(--text-dark);
1206
+ }
1207
+
1208
+ .floating-pdf-btn:hover .pdf-title {
1209
+ opacity: 1;
1210
+ transform: translateX(0);
1211
+ }
1212
+
1213
+ .floating-pdf-btn.loading {
1214
+ pointer-events: none;
1215
+ opacity: 0.7;
1216
+ }
1217
+
1218
+ .floating-pdf-btn.loading .icon {
1219
+ animation: spin 1s linear infinite;
1220
+ }
1221
+
1222
  /* Viewer Container - Comic Style */
1223
  #viewer {
1224
  width: 94%;
 
1690
  .header-info {
1691
  top: 8px;
1692
  padding: 6px 15px;
1693
+ max-width: 60%;
1694
  }
1695
 
1696
  .header-info .title {
1697
+ font-size: 1rem;
1698
  }
1699
 
1700
  .floating-ai {
1701
+ width: 40px;
1702
+ height: 40px;
1703
  top: 8px;
1704
  right: 10px;
1705
  }
1706
 
1707
  .floating-ai .icon {
1708
+ font-size: 16px;
1709
+ }
1710
+
1711
+ .floating-pdf-btn {
1712
+ width: 40px;
1713
+ height: 40px;
1714
+ }
1715
+
1716
+ #pdfPromptBtn {
1717
+ top: 55px;
1718
+ }
1719
+
1720
+ #pdfKtxBtn {
1721
+ top: 102px;
1722
+ }
1723
+
1724
+ .floating-pdf-btn .icon {
1725
+ font-size: 14px;
1726
+ }
1727
+
1728
+ .floating-pdf-btn .pdf-title {
1729
+ display: none;
1730
  }
1731
 
1732
  #aiChatContainer {
 
1757
  <div class="ai-title">πŸ€– AI μ–΄μ‹œμŠ€ν„΄νŠΈ</div>
1758
  </div>
1759
 
1760
+ <!-- PDF Selection Buttons -->
1761
+ <div id="pdfPromptBtn" class="floating-pdf-btn active" data-pdf="prompt">
1762
+ <div class="icon"><i class="fas fa-book"></i></div>
1763
+ <div class="pdf-title">μƒ˜ν”Œ-ν”„λ‘¬ν”„νŠΈλΆ</div>
1764
+ </div>
1765
+ <div id="pdfKtxBtn" class="floating-pdf-btn" data-pdf="ktx">
1766
+ <div class="icon"><i class="fas fa-train"></i></div>
1767
+ <div class="pdf-title">μƒ˜ν”Œ-μ½”λ ˆμΌμž‘μ§€</div>
1768
+ </div>
1769
+
1770
  <!-- AI Chat Container -->
1771
  <div id="aiChatContainer">
1772
  <div id="aiChatHeader">
 
1802
  let isAiProcessing = false;
1803
  let hasLoadedSummary = false;
1804
  let analysisCheckInterval = null;
1805
+ let currentPdfKey = 'prompt';
1806
+ let isPdfSwitching = false;
1807
 
1808
  function $id(id) { return document.getElementById(id); }
1809
 
 
2178
  }
2179
  }
2180
 
2181
+ async function switchPDF(pdfKey) {
2182
+ if (isPdfSwitching || pdfKey === currentPdfKey) return;
2183
+
2184
+ try {
2185
+ isPdfSwitching = true;
2186
+
2187
+ // λ²„νŠΌ μƒνƒœ μ—…λ°μ΄νŠΈ
2188
+ document.querySelectorAll('.floating-pdf-btn').forEach(btn => {
2189
+ btn.classList.remove('active');
2190
+ if (btn.dataset.pdf === pdfKey) {
2191
+ btn.classList.add('loading');
2192
+ }
2193
+ });
2194
+
2195
+ showLoading("PDF μ „ν™˜ 쀑...");
2196
+
2197
+ // μ„œλ²„μ— PDF μ „ν™˜ μš”μ²­
2198
+ const switchResponse = await fetch(`/api/switch-pdf/${pdfKey}`, { method: 'POST' });
2199
+ const switchResult = await switchResponse.json();
2200
+
2201
+ if (!switchResult.success) {
2202
+ hideLoading();
2203
+ showError(switchResult.error || "PDF μ „ν™˜μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");
2204
+ isPdfSwitching = false;
2205
+ document.querySelectorAll('.floating-pdf-btn').forEach(btn => {
2206
+ btn.classList.remove('loading');
2207
+ if (btn.dataset.pdf === currentPdfKey) btn.classList.add('active');
2208
+ });
2209
+ return;
2210
+ }
2211
+
2212
+ currentPdfKey = pdfKey;
2213
+
2214
+ // 기쑴 FlipBook 제거
2215
+ if (fb) {
2216
+ viewer.innerHTML = '';
2217
+ fb = null;
2218
+ }
2219
+
2220
+ // AI 챗봇 μ΄ˆκΈ°ν™”
2221
+ hasLoadedSummary = false;
2222
+ $id('aiChatMessages').innerHTML = '';
2223
+ if (analysisCheckInterval) {
2224
+ clearInterval(analysisCheckInterval);
2225
+ analysisCheckInterval = null;
2226
+ }
2227
+
2228
+ // μƒˆ PDF λ‘œλ“œ
2229
+ await loadPDF();
2230
+
2231
+ // λ²„νŠΌ μƒνƒœ μ΅œμ’… μ—…λ°μ΄νŠΈ
2232
+ document.querySelectorAll('.floating-pdf-btn').forEach(btn => {
2233
+ btn.classList.remove('loading');
2234
+ btn.classList.remove('active');
2235
+ if (btn.dataset.pdf === pdfKey) btn.classList.add('active');
2236
+ });
2237
+
2238
+ isPdfSwitching = false;
2239
+
2240
+ } catch (error) {
2241
+ console.error("PDF μ „ν™˜ 였λ₯˜:", error);
2242
+ hideLoading();
2243
+ showError("PDF μ „ν™˜ 쀑 였λ₯˜οΏ½οΏ½ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.");
2244
+ isPdfSwitching = false;
2245
+ document.querySelectorAll('.floating-pdf-btn').forEach(btn => {
2246
+ btn.classList.remove('loading');
2247
+ if (btn.dataset.pdf === currentPdfKey) btn.classList.add('active');
2248
+ });
2249
+ }
2250
+ }
2251
+
2252
  async function loadPDF() {
2253
  try {
2254
  showLoading("PDF 정보 확인 쀑...");
 
2330
  }
2331
  });
2332
 
2333
+ // PDF 선택 λ²„νŠΌ 이벀트
2334
+ $id('pdfPromptBtn').addEventListener('click', () => switchPDF('prompt'));
2335
+ $id('pdfKtxBtn').addEventListener('click', () => switchPDF('ktx'));
2336
+
2337
  loadPDF();
2338
  });
2339
  </script>