Update app-backup.py
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
return {"exists": False, "error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}
|
| 609 |
|
| 610 |
-
pdf_name =
|
| 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(
|
| 615 |
|
| 616 |
return {
|
| 617 |
-
"path": str(
|
| 618 |
"name": pdf_name,
|
| 619 |
-
"
|
|
|
|
|
|
|
| 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(
|
| 631 |
if cached:
|
| 632 |
return {
|
| 633 |
"status": "completed",
|
|
@@ -637,8 +823,8 @@ async def get_analysis_status():
|
|
| 637 |
}
|
| 638 |
|
| 639 |
# λ©λͺ¨λ¦¬ μν νμΈ
|
| 640 |
-
if
|
| 641 |
-
status_info = analysis_status[
|
| 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(
|
| 660 |
if cache_path.exists():
|
| 661 |
cache_path.unlink()
|
| 662 |
logger.info("κΈ°μ‘΄ VLM λΆμ μΊμ μμ ")
|
| 663 |
|
| 664 |
# μν μ΄κΈ°ν
|
| 665 |
-
if
|
| 666 |
-
del analysis_status[
|
| 667 |
|
| 668 |
# λ°±κ·ΈλΌμ΄λμμ μ¬λΆμ μμ
|
| 669 |
-
asyncio.create_task(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 681 |
return {"thumbnail": None, "error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}
|
| 682 |
|
| 683 |
-
pdf_name =
|
| 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(
|
| 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(
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 718 |
return JSONResponse(content={"error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
| 719 |
|
| 720 |
-
result = await query_pdf(
|
| 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 |
-
|
|
|
|
| 735 |
return JSONResponse(content={"error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
| 736 |
|
| 737 |
-
result = await summarize_pdf(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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:
|
| 1402 |
}
|
| 1403 |
|
| 1404 |
.header-info .title {
|
| 1405 |
-
font-size:
|
| 1406 |
}
|
| 1407 |
|
| 1408 |
.floating-ai {
|
| 1409 |
-
width:
|
| 1410 |
-
height:
|
| 1411 |
top: 8px;
|
| 1412 |
right: 10px;
|
| 1413 |
}
|
| 1414 |
|
| 1415 |
.floating-ai .icon {
|
| 1416 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|