| from fastapi import FastAPI, BackgroundTasks, Request |
| from fastapi.responses import HTMLResponse, JSONResponse, Response |
| from fastapi.staticfiles import StaticFiles |
| import pathlib, os, uvicorn, base64, json, uuid, time |
| from typing import Dict, List, Any, Optional |
| import asyncio |
| import logging |
| import threading |
| import concurrent.futures |
| import requests |
| import fitz |
|
|
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| BASE = pathlib.Path(__file__).parent |
| app = FastAPI() |
| app.mount("/static", StaticFiles(directory=BASE), name="static") |
|
|
| CACHE_DIR = BASE / "cache" |
| if not CACHE_DIR.exists(): |
| CACHE_DIR.mkdir(parents=True) |
|
|
| EMBEDDING_DIR = BASE / "embeddings" |
| if not EMBEDDING_DIR.exists(): |
| EMBEDDING_DIR.mkdir(parents=True) |
|
|
| |
| FIREWORKS_API_KEY = os.getenv("FIREWORKS_API", "").strip() |
| FIREWORKS_API_URL = "https://api.fireworks.ai/inference/v1/chat/completions" |
| FIREWORKS_VLM_MODEL = "accounts/fireworks/models/qwen3-vl-235b-a22b-instruct" |
| HAS_VALID_API_KEY = bool(FIREWORKS_API_KEY) |
|
|
| if HAS_VALID_API_KEY: |
| logger.info("Fireworks AI VLM API ํค ์ค์ ์๋ฃ") |
| else: |
| logger.warning("์ ํจํ Fireworks AI API ํค๊ฐ ์์ต๋๋ค. AI ๊ธฐ๋ฅ์ด ์ ํ๋ฉ๋๋ค.") |
|
|
| |
| PROMPT_PDF_PATH = BASE / "prompt.pdf" |
| PROMPT_PDF_ID = "prompt_pdf_main" |
|
|
| |
| PDF_FILES = { |
| "prompt": { |
| "path": BASE / "prompt.pdf", |
| "id": "prompt_pdf_main", |
| "name": "์ํ-ํ๋กฌํํธ๋ถ" |
| } |
| } |
|
|
| |
| current_pdf_key = "prompt" |
|
|
| pdf_cache: Dict[str, Dict[str, Any]] = {} |
| cache_locks = {} |
| pdf_embeddings: Dict[str, Dict[str, Any]] = {} |
|
|
| |
| analysis_status: Dict[str, Dict[str, Any]] = {} |
|
|
|
|
| def get_cache_path(pdf_name: str): |
| return CACHE_DIR / f"{pdf_name}_cache.json" |
|
|
|
|
| def get_embedding_path(pdf_id: str): |
| return EMBEDDING_DIR / f"{pdf_id}_embedding.json" |
|
|
|
|
| def get_analysis_cache_path(pdf_id: str): |
| """VLM ๋ถ์ ๊ฒฐ๊ณผ ์บ์ ๊ฒฝ๋ก""" |
| return EMBEDDING_DIR / f"{pdf_id}_vlm_analysis.json" |
|
|
|
|
| def load_analysis_cache(pdf_id: str) -> Optional[Dict[str, Any]]: |
| """VLM ๋ถ์ ์บ์ ๋ก๋""" |
| cache_path = get_analysis_cache_path(pdf_id) |
| if cache_path.exists(): |
| try: |
| with open(cache_path, "r", encoding="utf-8") as f: |
| data = json.load(f) |
| logger.info(f"VLM ๋ถ์ ์บ์ ๋ก๋ ์๋ฃ: {pdf_id}") |
| return data |
| except Exception as e: |
| logger.error(f"๋ถ์ ์บ์ ๋ก๋ ์ค๋ฅ: {e}") |
| return None |
|
|
|
|
| def save_analysis_cache(pdf_id: str, analysis_data: Dict[str, Any]): |
| """VLM ๋ถ์ ๊ฒฐ๊ณผ ์บ์ ์ ์ฅ""" |
| cache_path = get_analysis_cache_path(pdf_id) |
| try: |
| with open(cache_path, "w", encoding="utf-8") as f: |
| json.dump(analysis_data, f, ensure_ascii=False, indent=2) |
| logger.info(f"VLM ๋ถ์ ์บ์ ์ ์ฅ ์๋ฃ: {pdf_id}") |
| except Exception as e: |
| logger.error(f"๋ถ์ ์บ์ ์ ์ฅ ์ค๋ฅ: {e}") |
|
|
|
|
| def get_pdf_page_as_base64(pdf_path: str, page_num: int, scale: float = 1.0) -> str: |
| """PDF ํ์ด์ง๋ฅผ base64 ์ด๋ฏธ์ง๋ก ๋ณํ""" |
| try: |
| doc = fitz.open(pdf_path) |
| if page_num >= doc.page_count: |
| doc.close() |
| return None |
| |
| page = doc[page_num] |
| pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale)) |
| img_data = pix.tobytes("jpeg", 85) |
| b64_img = base64.b64encode(img_data).decode('utf-8') |
| doc.close() |
| return b64_img |
| except Exception as e: |
| logger.error(f"PDF ํ์ด์ง ์ด๋ฏธ์ง ๋ณํ ์ค๋ฅ: {e}") |
| return None |
|
|
|
|
| def get_pdf_pages_as_base64(pdf_path: str, start_page: int = 0, max_pages: int = 10, scale: float = 0.7) -> List[Dict[str, Any]]: |
| """PDF ์ฌ๋ฌ ํ์ด์ง๋ฅผ base64 ์ด๋ฏธ์ง ๋ฆฌ์คํธ๋ก ๋ณํ (๋ฐฐ์น ์ฒ๋ฆฌ์ฉ)""" |
| try: |
| doc = fitz.open(pdf_path) |
| total_pages = doc.page_count |
| end_page = min(start_page + max_pages, total_pages) |
| |
| images = [] |
| for page_num in range(start_page, end_page): |
| page = doc[page_num] |
| pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale)) |
| img_data = pix.tobytes("jpeg", 75) |
| b64_img = base64.b64encode(img_data).decode('utf-8') |
| images.append({ |
| "page": page_num + 1, |
| "image_base64": b64_img |
| }) |
| |
| doc.close() |
| logger.info(f"PDF {start_page+1}~{end_page}/{total_pages}ํ์ด์ง ์ด๋ฏธ์ง ๋ณํ ์๋ฃ") |
| return images, total_pages |
| except Exception as e: |
| logger.error(f"PDF ํ์ด์ง๋ค ์ด๋ฏธ์ง ๋ณํ ์ค๋ฅ: {e}") |
| return [], 0 |
|
|
|
|
| def call_fireworks_vlm_api(messages: List[Dict], max_tokens: int = 4096, temperature: float = 0.6) -> str: |
| """Fireworks AI VLM API ํธ์ถ (์ด๋ฏธ์ง ๋ถ์ ์ง์)""" |
| if not HAS_VALID_API_KEY: |
| raise Exception("API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.") |
| |
| payload = { |
| "model": FIREWORKS_VLM_MODEL, |
| "max_tokens": max_tokens, |
| "top_p": 1, |
| "top_k": 40, |
| "presence_penalty": 0, |
| "frequency_penalty": 0, |
| "temperature": temperature, |
| "messages": messages |
| } |
| |
| headers = { |
| "Accept": "application/json", |
| "Content-Type": "application/json", |
| "Authorization": f"Bearer {FIREWORKS_API_KEY}" |
| } |
| |
| response = requests.post(FIREWORKS_API_URL, headers=headers, data=json.dumps(payload), timeout=180) |
| |
| if response.status_code != 200: |
| raise Exception(f"API ์ค๋ฅ: {response.status_code} - {response.text}") |
| |
| result = response.json() |
| return result["choices"][0]["message"]["content"] |
|
|
|
|
| def analyze_batch_pages_sync(pdf_path: str, start_page: int, batch_size: int = 5) -> str: |
| """๋ฐฐ์น ํ์ด์ง ๋ถ์ (๋๊ธฐ)""" |
| page_images, total_pages = get_pdf_pages_as_base64(pdf_path, start_page, batch_size, scale=0.6) |
| |
| if not page_images: |
| return "" |
| |
| content_parts = [] |
| for img_data in page_images: |
| content_parts.append({ |
| "type": "image_url", |
| "image_url": { |
| "url": f"data:image/jpeg;base64,{img_data['image_base64']}" |
| } |
| }) |
| |
| page_range = f"{start_page + 1}~{start_page + len(page_images)}" |
| content_parts.append({ |
| "type": "text", |
| "text": f"""์ ์ด๋ฏธ์ง๋ค์ PDF ๋ฌธ์์ {page_range}ํ์ด์ง์
๋๋ค. |
| |
| ๊ฐ ํ์ด์ง์ ๋ด์ฉ์ ์์ธํ๊ฒ ๋ถ์ํ์ฌ ํ
์คํธ๋ก ์ถ์ถํด์ฃผ์ธ์. |
| - ๋ชจ๋ ํ
์คํธ ๋ด์ฉ์ ๋น ์ง์์ด ์ถ์ถ |
| - ํ, ์ฐจํธ, ๊ทธ๋ํ๊ฐ ์์ผ๋ฉด ๋ด์ฉ ์ค๋ช
|
| - ์ด๋ฏธ์ง๊ฐ ์์ผ๋ฉด ์ค๋ช
|
| - ํ์ด์ง๋ณ๋ก ๊ตฌ๋ถํ์ฌ ์์ฑ |
| |
| ํ๊ตญ์ด๋ก ์์ฑํด์ฃผ์ธ์.""" |
| }) |
|
|
| messages = [{"role": "user", "content": content_parts}] |
| |
| return call_fireworks_vlm_api(messages, max_tokens=4096, temperature=0.3) |
|
|
|
|
| async def analyze_pdf_with_vlm_batched(pdf_id: str, force_refresh: bool = False) -> Dict[str, Any]: |
| """VLM์ผ๋ก PDF ๋ฐฐ์น ๋ถ์ ํ ์บ์์ ์ ์ฅ""" |
| global analysis_status |
| |
| |
| if pdf_id in analysis_status and analysis_status[pdf_id].get("status") == "analyzing": |
| logger.info(f"PDF {pdf_id} ์ด๋ฏธ ๋ถ์ ์ค...") |
| return {"status": "analyzing", "progress": analysis_status[pdf_id].get("progress", 0)} |
| |
| |
| if not force_refresh: |
| cached = load_analysis_cache(pdf_id) |
| if cached: |
| analysis_status[pdf_id] = {"status": "completed", "progress": 100} |
| return cached |
| |
| pdf_path = str(PROMPT_PDF_PATH) |
| if not PROMPT_PDF_PATH.exists(): |
| analysis_status[pdf_id] = {"status": "error", "error": "PDF ํ์ผ ์์"} |
| return {"error": "PDF ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค."} |
| |
| if not HAS_VALID_API_KEY: |
| analysis_status[pdf_id] = {"status": "error", "error": "API ํค ์์"} |
| return {"error": "API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค."} |
| |
| |
| analysis_status[pdf_id] = {"status": "analyzing", "progress": 0, "started_at": time.time()} |
| |
| try: |
| |
| doc = fitz.open(pdf_path) |
| total_pages = doc.page_count |
| doc.close() |
| |
| logger.info(f"PDF ๋ถ์ ์์: ์ด {total_pages}ํ์ด์ง") |
| |
| |
| batch_size = 5 |
| all_analyses = [] |
| |
| for start_page in range(0, min(total_pages, 25), batch_size): |
| try: |
| progress = int((start_page / min(total_pages, 25)) * 100) |
| analysis_status[pdf_id]["progress"] = progress |
| logger.info(f"๋ฐฐ์น ๋ถ์ ์ค: {start_page + 1}ํ์ด์ง๋ถํฐ (์งํ๋ฅ : {progress}%)") |
| |
| |
| loop = asyncio.get_event_loop() |
| batch_result = await loop.run_in_executor( |
| None, |
| analyze_batch_pages_sync, |
| pdf_path, |
| start_page, |
| batch_size |
| ) |
| |
| if batch_result: |
| all_analyses.append(f"### ํ์ด์ง {start_page + 1}~{min(start_page + batch_size, total_pages)}\n{batch_result}") |
| |
| |
| await asyncio.sleep(2) |
| |
| except Exception as batch_error: |
| logger.error(f"๋ฐฐ์น {start_page} ๋ถ์ ์ค๋ฅ: {batch_error}") |
| all_analyses.append(f"### ํ์ด์ง {start_page + 1}~{min(start_page + batch_size, total_pages)}\n[๋ถ์ ์คํจ: {str(batch_error)}]") |
| |
| |
| combined_analysis = "\n\n".join(all_analyses) |
| |
| |
| successful_analyses = [a for a in all_analyses if "[๋ถ์ ์คํจ:" not in a] |
| if not successful_analyses: |
| logger.error("๋ชจ๋ ๋ฐฐ์น ๋ถ์ ์คํจ") |
| analysis_status[pdf_id] = {"status": "error", "error": "๋ชจ๋ ํ์ด์ง ๋ถ์ ์คํจ"} |
| return {"error": "PDF ๋ถ์์ ์คํจํ์ต๋๋ค. API ํค๋ฅผ ํ์ธํด์ฃผ์ธ์."} |
| |
| |
| summary = "" |
| if combined_analysis: |
| try: |
| summary_messages = [ |
| { |
| "role": "system", |
| "content": "๋ค์ PDF ๋ถ์ ๋ด์ฉ์ 500์ ์ด๋ด๋ก ์์ฝํด์ฃผ์ธ์. ํต์ฌ ๋ด์ฉ๊ณผ ์ฃผ์ ํค์๋๋ฅผ ํฌํจํด์ฃผ์ธ์." |
| }, |
| { |
| "role": "user", |
| "content": combined_analysis[:8000] |
| } |
| ] |
| summary = call_fireworks_vlm_api(summary_messages, max_tokens=1024, temperature=0.5) |
| except Exception as sum_err: |
| logger.error(f"์์ฝ ์์ฑ ์ค๋ฅ: {sum_err}") |
| summary = combined_analysis[:500] + "..." |
| |
| analysis_data = { |
| "pdf_id": pdf_id, |
| "total_pages": total_pages, |
| "analyzed_pages": min(total_pages, 25), |
| "analysis": combined_analysis, |
| "summary": summary, |
| "created_at": time.time() |
| } |
| |
| |
| save_analysis_cache(pdf_id, analysis_data) |
| |
| analysis_status[pdf_id] = {"status": "completed", "progress": 100} |
| logger.info(f"PDF ๋ถ์ ์๋ฃ: {pdf_id}") |
| |
| return analysis_data |
| |
| except Exception as e: |
| logger.error(f"VLM PDF ๋ถ์ ์ค๋ฅ: {e}") |
| analysis_status[pdf_id] = {"status": "error", "error": str(e)} |
| return {"error": str(e)} |
|
|
|
|
| async def run_initial_analysis(): |
| """์๋ฒ ์์ ์ ์ด๊ธฐ ๋ถ์ ์คํ""" |
| logger.info("์ด๊ธฐ PDF ๋ถ์ ์์...") |
| try: |
| result = await analyze_pdf_with_vlm_batched(PROMPT_PDF_ID) |
| if "error" in result: |
| logger.error(f"์ด๊ธฐ ๋ถ์ ์คํจ: {result['error']}") |
| else: |
| logger.info("์ด๊ธฐ PDF ๋ถ์ ์๋ฃ!") |
| except Exception as e: |
| logger.error(f"์ด๊ธฐ ๋ถ์ ์์ธ: {e}") |
|
|
|
|
| def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]: |
| try: |
| doc = fitz.open(pdf_path) |
| chunks = [] |
| for page_num in range(len(doc)): |
| page = doc[page_num] |
| text = page.get_text("text") |
| |
| if not text.strip(): |
| text = page.get_text("blocks") |
| if text: |
| text = "\n".join([block[4] for block in text if len(block) > 4 and isinstance(block[4], str)]) |
| |
| if not text.strip(): |
| text = f"[ํ์ด์ง {page_num + 1} - ์ด๋ฏธ์ง ๊ธฐ๋ฐ ํ์ด์ง]" |
| |
| chunks.append({ |
| "page": page_num + 1, |
| "text": text.strip() if text.strip() else f"[ํ์ด์ง {page_num + 1}]", |
| "chunk_id": f"page_{page_num + 1}" |
| }) |
| |
| doc.close() |
| return chunks |
| except Exception as e: |
| logger.error(f"PDF ํ
์คํธ ์ถ์ถ ์ค๋ฅ: {e}") |
| return [] |
|
|
|
|
| async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]: |
| """์บ์๋ VLM ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ง์์๋ต""" |
| try: |
| if not HAS_VALID_API_KEY: |
| return { |
| "error": "Fireworks AI API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.", |
| "answer": "์ฃ์กํฉ๋๋ค. ํ์ฌ AI ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋์ด ์์ด ์ง๋ฌธ์ ๋ต๋ณํ ์ ์์ต๋๋ค." |
| } |
|
|
| |
| analysis_data = load_analysis_cache(pdf_id) |
| |
| if not analysis_data: |
| |
| if pdf_id in analysis_status: |
| status = analysis_status[pdf_id].get("status") |
| if status == "analyzing": |
| progress = analysis_status[pdf_id].get("progress", 0) |
| return {"error": f"๋ถ์ ์งํ ์ค ({progress}%)", "answer": f"PDF ๋ถ์์ด ์งํ ์ค์
๋๋ค ({progress}%). ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์."} |
| elif status == "error": |
| return {"error": "๋ถ์ ์คํจ", "answer": f"PDF ๋ถ์์ ์คํจํ์ต๋๋ค: {analysis_status[pdf_id].get('error', '์ ์ ์๋ ์ค๋ฅ')}"} |
| return {"error": "๋ถ์ ๋ฐ์ดํฐ ์์", "answer": "PDF๊ฐ ์์ง ๋ถ์๋์ง ์์์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์."} |
| |
| analysis_text = analysis_data.get("analysis", "") |
| total_pages = analysis_data.get("total_pages", 0) |
| |
| if not analysis_text: |
| return {"error": "๋ถ์ ๋ฐ์ดํฐ ์์", "answer": "PDF ๋ถ์ ๋ฐ์ดํฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."} |
|
|
| |
| messages = [ |
| { |
| "role": "system", |
| "content": f"""๋น์ ์ PDF ๋ฌธ์ ๋ถ์ ์ ๋ฌธ๊ฐ์
๋๋ค. |
| ์๋๋ {total_pages}ํ์ด์ง PDF ๋ฌธ์๋ฅผ VLM์ผ๋ก ๋ถ์ํ ๊ฒฐ๊ณผ์
๋๋ค. |
| ์ด ๋ถ์ ๋ด์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์์ ์ง๋ฌธ์ ์ ํํ๊ณ ์น์ ํ๊ฒ ํ๊ตญ์ด๋ก ๋ต๋ณํด์ฃผ์ธ์. |
| ๋ต๋ณํ ๋ ๊ด๋ จ ํ์ด์ง ๋ฒํธ๊ฐ ์์ผ๋ฉด ์ธ๊ธํด์ฃผ์ธ์. |
| ๋ถ์ ๋ด์ฉ์ ์๋ ์ ๋ณด๋ "ํด๋น ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค"๋ผ๊ณ ์์งํ ๋ตํด์ฃผ์ธ์. |
| |
| === PDF ๋ถ์ ๊ฒฐ๊ณผ === |
| {analysis_text[:12000]} |
| ==================""" |
| }, |
| { |
| "role": "user", |
| "content": query |
| } |
| ] |
|
|
| try: |
| answer = call_fireworks_vlm_api(messages, max_tokens=4096, temperature=0.6) |
| return { |
| "answer": answer, |
| "pdf_id": pdf_id, |
| "query": query |
| } |
| except Exception as api_error: |
| logger.error(f"Fireworks API ํธ์ถ ์ค๋ฅ: {api_error}") |
| error_message = str(api_error) |
| return {"error": f"AI ์ค๋ฅ: {error_message}", "answer": "์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์."} |
| |
| except Exception as e: |
| logger.error(f"์ง์์๋ต ์ฒ๋ฆฌ ์ค๋ฅ: {e}") |
| return {"error": str(e), "answer": "์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."} |
|
|
|
|
| async def summarize_pdf(pdf_id: str) -> Dict[str, Any]: |
| """์บ์๋ VLM ๋ถ์ ๊ฒฐ๊ณผ์์ ์์ฝ ์ถ์ถ""" |
| try: |
| if not HAS_VALID_API_KEY: |
| return { |
| "error": "Fireworks AI API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค.", |
| "summary": "API ํค๊ฐ ์์ด ์์ฝ์ ์์ฑํ ์ ์์ต๋๋ค." |
| } |
|
|
| |
| analysis_data = load_analysis_cache(pdf_id) |
| |
| if not analysis_data: |
| |
| if pdf_id in analysis_status: |
| status = analysis_status[pdf_id].get("status") |
| if status == "analyzing": |
| progress = analysis_status[pdf_id].get("progress", 0) |
| return {"error": f"๋ถ์ ์งํ ์ค", "summary": f"PDF ๋ถ์์ด ์งํ ์ค์
๋๋ค ({progress}%). ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์."} |
| elif status == "error": |
| return {"error": "๋ถ์ ์คํจ", "summary": f"PDF ๋ถ์์ ์คํจํ์ต๋๋ค."} |
| return {"error": "๋ถ์ ๋ฐ์ดํฐ ์์", "summary": "PDF๊ฐ ์์ง ๋ถ์๋์ง ์์์ต๋๋ค."} |
| |
| summary = analysis_data.get("summary", "") |
| total_pages = analysis_data.get("total_pages", 0) |
| analyzed_pages = analysis_data.get("analyzed_pages", total_pages) |
| |
| if summary: |
| return { |
| "summary": summary, |
| "pdf_id": pdf_id, |
| "total_pages": total_pages, |
| "analyzed_pages": analyzed_pages |
| } |
| |
| |
| analysis_text = analysis_data.get("analysis", "") |
| if analysis_text: |
| return { |
| "summary": analysis_text[:500] + "...", |
| "pdf_id": pdf_id, |
| "total_pages": total_pages |
| } |
| |
| return {"error": "์์ฝ ์์", "summary": "์์ฝ์ ์์ฑํ ์ ์์ต๋๋ค."} |
| |
| except Exception as e: |
| logger.error(f"PDF ์์ฝ ์์ฑ ์ค๋ฅ: {e}") |
| return { |
| "error": str(e), |
| "summary": "PDF ์์ฝ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค." |
| } |
|
|
|
|
| async def cache_pdf(pdf_path: str): |
| try: |
| pdf_file = pathlib.Path(pdf_path) |
| pdf_name = pdf_file.stem |
|
|
| if pdf_name not in cache_locks: |
| cache_locks[pdf_name] = threading.Lock() |
|
|
| if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]: |
| logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์ฑ ์๋ฃ ๋๋ ์งํ ์ค") |
| return |
|
|
| with cache_locks[pdf_name]: |
| if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]: |
| return |
|
|
| pdf_cache[pdf_name] = {"status": "processing", "progress": 0, "pages": []} |
|
|
| cache_path = get_cache_path(pdf_name) |
| if cache_path.exists(): |
| try: |
| with open(cache_path, "r") as cache_file: |
| cached_data = json.load(cache_file) |
| if cached_data.get("status") == "completed" and cached_data.get("pages"): |
| pdf_cache[pdf_name] = cached_data |
| pdf_cache[pdf_name]["status"] = "completed" |
| logger.info(f"์บ์ ํ์ผ์์ {pdf_name} ๋ก๋ ์๋ฃ") |
| return |
| except Exception as e: |
| logger.error(f"์บ์ ํ์ผ ๋ก๋ ์คํจ: {e}") |
|
|
| doc = fitz.open(pdf_path) |
| total_pages = doc.page_count |
|
|
| if total_pages > 0: |
| page = doc[0] |
| pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2)) |
| thumb_data = pix_thumb.tobytes("png") |
| b64_thumb = base64.b64encode(thumb_data).decode('utf-8') |
| thumb_src = f"data:image/png;base64,{b64_thumb}" |
| pdf_cache[pdf_name]["pages"] = [{"thumb": thumb_src, "src": ""}] |
| pdf_cache[pdf_name]["progress"] = 1 |
| pdf_cache[pdf_name]["total_pages"] = total_pages |
|
|
| scale_factor = 1.0 |
| jpeg_quality = 80 |
|
|
| def process_page(page_num): |
| try: |
| page = doc[page_num] |
| pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor)) |
| img_data = pix.tobytes("jpeg", jpeg_quality) |
| b64_img = base64.b64encode(img_data).decode('utf-8') |
| img_src = f"data:image/jpeg;base64,{b64_img}" |
| thumb_src = "" if page_num > 0 else pdf_cache[pdf_name]["pages"][0]["thumb"] |
| return { |
| "page_num": page_num, |
| "src": img_src, |
| "thumb": thumb_src |
| } |
| except Exception as e: |
| logger.error(f"ํ์ด์ง {page_num} ์ฒ๋ฆฌ ์ค๋ฅ: {e}") |
| return { |
| "page_num": page_num, |
| "src": "", |
| "thumb": "", |
| "error": str(e) |
| } |
|
|
| pages = [None] * total_pages |
| processed_count = 0 |
| batch_size = 5 |
|
|
| for batch_start in range(0, total_pages, batch_size): |
| batch_end = min(batch_start + batch_size, total_pages) |
| current_batch = list(range(batch_start, batch_end)) |
|
|
| with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, batch_size)) as executor: |
| batch_results = list(executor.map(process_page, current_batch)) |
|
|
| for result in batch_results: |
| page_num = result["page_num"] |
| pages[page_num] = { |
| "src": result["src"], |
| "thumb": result["thumb"] |
| } |
| processed_count += 1 |
| progress = round(processed_count / total_pages * 100) |
| pdf_cache[pdf_name]["progress"] = progress |
|
|
| pdf_cache[pdf_name]["pages"] = pages |
|
|
| pdf_cache[pdf_name] = { |
| "status": "completed", |
| "progress": 100, |
| "pages": pages, |
| "total_pages": total_pages |
| } |
|
|
| try: |
| with open(cache_path, "w") as cache_file: |
| json.dump(pdf_cache[pdf_name], cache_file) |
| logger.info(f"PDF {pdf_name} ์บ์ฑ ์๋ฃ, {total_pages}ํ์ด์ง") |
| except Exception as e: |
| logger.error(f"์ต์ข
์บ์ ์ ์ฅ ์คํจ: {e}") |
| except Exception as e: |
| import traceback |
| logger.error(f"PDF ์บ์ฑ ์ค๋ฅ: {str(e)}\n{traceback.format_exc()}") |
| if 'pdf_name' in locals() and pdf_name in pdf_cache: |
| pdf_cache[pdf_name]["status"] = "error" |
| pdf_cache[pdf_name]["error"] = str(e) |
|
|
|
|
| @app.on_event("startup") |
| async def startup_event(): |
| if PROMPT_PDF_PATH.exists(): |
| logger.info(f"prompt.pdf ํ์ผ ๋ฐ๊ฒฌ: {PROMPT_PDF_PATH}") |
| |
| asyncio.create_task(cache_pdf(str(PROMPT_PDF_PATH))) |
| |
| asyncio.create_task(run_initial_analysis()) |
| else: |
| logger.warning(f"prompt.pdf ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {PROMPT_PDF_PATH}") |
|
|
|
|
| @app.get("/api/pdf-list") |
| async def get_pdf_list(): |
| """์ฌ์ฉ ๊ฐ๋ฅํ PDF ๋ชฉ๋ก ๋ฐํ""" |
| global current_pdf_key |
| pdf_list = [] |
| for key, info in PDF_FILES.items(): |
| pdf_list.append({ |
| "key": key, |
| "name": info["name"], |
| "exists": info["path"].exists(), |
| "is_current": key == current_pdf_key |
| }) |
| return {"pdfs": pdf_list, "current": current_pdf_key} |
|
|
|
|
| @app.get("/api/pdf-info") |
| async def get_pdf_info(): |
| global current_pdf_key |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| pdf_path = pdf_info["path"] |
| pdf_id = pdf_info["id"] |
| |
| if not pdf_path.exists(): |
| return {"exists": False, "error": "PDF ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค"} |
| |
| pdf_name = pdf_path.stem |
| is_cached = pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed" |
| |
| |
| analysis_cached = load_analysis_cache(pdf_id) is not None |
| |
| return { |
| "path": str(pdf_path), |
| "name": pdf_name, |
| "display_name": pdf_info["name"], |
| "id": pdf_id, |
| "key": current_pdf_key, |
| "exists": True, |
| "cached": is_cached, |
| "analysis_cached": analysis_cached |
| } |
|
|
|
|
| @app.get("/api/analysis-status") |
| async def get_analysis_status(): |
| """VLM ๋ถ์ ์ํ ํ์ธ""" |
| global current_pdf_key |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| pdf_id = pdf_info["id"] |
| |
| |
| cached = load_analysis_cache(pdf_id) |
| if cached: |
| return { |
| "status": "completed", |
| "total_pages": cached.get("total_pages", 0), |
| "analyzed_pages": cached.get("analyzed_pages", 0), |
| "created_at": cached.get("created_at", 0) |
| } |
| |
| |
| if pdf_id in analysis_status: |
| status_info = analysis_status[pdf_id] |
| return { |
| "status": status_info.get("status", "unknown"), |
| "progress": status_info.get("progress", 0), |
| "error": status_info.get("error") |
| } |
| |
| return {"status": "not_started"} |
|
|
|
|
| @app.post("/api/reanalyze-pdf") |
| async def reanalyze_pdf(): |
| """PDF ์ฌ๋ถ์ (์บ์ ๋ฌด์)""" |
| global current_pdf_key |
| try: |
| if not HAS_VALID_API_KEY: |
| return JSONResponse(content={"error": "API ํค๊ฐ ์ค์ ๋์ง ์์์ต๋๋ค."}, status_code=400) |
| |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| pdf_id = pdf_info["id"] |
| |
| |
| cache_path = get_analysis_cache_path(pdf_id) |
| if cache_path.exists(): |
| cache_path.unlink() |
| logger.info("๊ธฐ์กด VLM ๋ถ์ ์บ์ ์ญ์ ") |
| |
| |
| if pdf_id in analysis_status: |
| del analysis_status[pdf_id] |
| |
| |
| asyncio.create_task(analyze_pdf_with_vlm_batched(pdf_id)) |
| |
| return {"status": "started", "message": "PDF ์ฌ๋ถ์์ ์์ํฉ๋๋ค."} |
| except Exception as e: |
| logger.error(f"์ฌ๋ถ์ ์์ ์ค๋ฅ: {e}") |
| return JSONResponse(content={"error": str(e)}, status_code=500) |
|
|
|
|
| @app.get("/api/pdf-thumbnail") |
| async def get_pdf_thumbnail(): |
| global current_pdf_key |
| try: |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| pdf_path = pdf_info["path"] |
| |
| if not pdf_path.exists(): |
| return {"thumbnail": None, "error": "PDF ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค"} |
| |
| pdf_name = pdf_path.stem |
| if pdf_name in pdf_cache and pdf_cache[pdf_name].get("pages"): |
| if pdf_cache[pdf_name]["pages"][0].get("thumb"): |
| return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]} |
|
|
| doc = fitz.open(str(pdf_path)) |
| if doc.page_count > 0: |
| page = doc[0] |
| pix = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2)) |
| img_data = pix.tobytes("jpeg", 70) |
| b64_img = base64.b64encode(img_data).decode('utf-8') |
| asyncio.create_task(cache_pdf(str(pdf_path))) |
| return {"thumbnail": f"data:image/jpeg;base64,{b64_img}"} |
| return {"thumbnail": None} |
| except Exception as e: |
| logger.error(f"์ธ๋ค์ผ ์์ฑ ์ค๋ฅ: {str(e)}") |
| return {"error": str(e), "thumbnail": None} |
|
|
|
|
| @app.get("/api/cache-status") |
| async def get_cache_status(): |
| global current_pdf_key |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| pdf_name = pdf_info["path"].stem |
| if pdf_name in pdf_cache: |
| return pdf_cache[pdf_name] |
| return {"status": "not_cached"} |
|
|
|
|
| @app.post("/api/ai/query-pdf") |
| async def api_query_pdf(query: Dict[str, str]): |
| global current_pdf_key |
| try: |
| user_query = query.get("query", "") |
| if not user_query: |
| return JSONResponse(content={"error": "์ง๋ฌธ์ด ์ ๊ณต๋์ง ์์์ต๋๋ค"}, status_code=400) |
|
|
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| if not pdf_info["path"].exists(): |
| return JSONResponse(content={"error": "PDF ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค"}, status_code=404) |
|
|
| result = await query_pdf(pdf_info["id"], user_query) |
| if "answer" in result: |
| return result |
| if "error" in result: |
| return JSONResponse(content=result, status_code=200) |
| return result |
| except Exception as e: |
| logger.error(f"์ง์์๋ต API ์ค๋ฅ: {e}") |
| return JSONResponse(content={"error": str(e), "answer": "์ฃ์กํฉ๋๋ค. ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."}, status_code=200) |
|
|
|
|
| @app.get("/api/ai/summarize-pdf") |
| async def api_summarize_pdf(): |
| global current_pdf_key |
| try: |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| if not pdf_info["path"].exists(): |
| return JSONResponse(content={"error": "PDF ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค"}, status_code=404) |
|
|
| result = await summarize_pdf(pdf_info["id"]) |
| if "summary" in result: |
| return result |
| if "error" in result: |
| return JSONResponse(content=result, status_code=200) |
| return result |
| except Exception as e: |
| logger.error(f"PDF ์์ฝ API ์ค๋ฅ: {e}") |
| return JSONResponse(content={"error": str(e), "summary": "์ฃ์กํฉ๋๋ค. ์์ฝ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."}, status_code=200) |
|
|
|
|
| @app.get("/api/cached-pdf") |
| async def get_cached_pdf(background_tasks: BackgroundTasks): |
| global current_pdf_key |
| try: |
| pdf_info = PDF_FILES.get(current_pdf_key, PDF_FILES["prompt"]) |
| pdf_path = pdf_info["path"] |
| pdf_name = pdf_path.stem |
| |
| if pdf_name in pdf_cache: |
| status = pdf_cache[pdf_name].get("status", "") |
| if status == "completed": |
| return pdf_cache[pdf_name] |
| elif status == "processing": |
| progress = pdf_cache[pdf_name].get("progress", 0) |
| pages = pdf_cache[pdf_name].get("pages", []) |
| total_pages = pdf_cache[pdf_name].get("total_pages", 0) |
| return { |
| "status": "processing", |
| "progress": progress, |
| "pages": pages, |
| "total_pages": total_pages, |
| "available_pages": len([p for p in pages if p and p.get("src")]) |
| } |
|
|
| background_tasks.add_task(cache_pdf, str(pdf_path)) |
| return {"status": "started", "progress": 0} |
| except Exception as e: |
| logger.error(f"์บ์๋ PDF ์ ๊ณต ์ค๋ฅ: {str(e)}") |
| return {"error": str(e), "status": "error"} |
|
|
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def root(): |
| return get_html_content() |
|
|
|
|
| def get_html_content(): |
| return HTMLResponse(content=HTML) |
|
|
|
|
| HTML = """ |
| <!doctype html> |
| <html lang="ko"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>๐จ AI ํ๋ฆฝ๋ถ - Comic Style</title> |
| <link rel="stylesheet" href="/static/flipbook.css"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap" rel="stylesheet"> |
| <script src="/static/three.js"></script> |
| <script src="/static/iscroll.js"></script> |
| <script src="/static/mark.js"></script> |
| <script src="/static/mod3d.js"></script> |
| <script src="/static/pdf.js"></script> |
| <script src="/static/flipbook.js"></script> |
| <script src="/static/flipbook.book3.js"></script> |
| <script src="/static/flipbook.scroll.js"></script> |
| <script src="/static/flipbook.swipe.js"></script> |
| <script src="/static/flipbook.webgl.js"></script> |
| <style> |
| /* ============================================ |
| Comic Style CSS - Z-Image Style Applied |
| ============================================ */ |
| :root { |
| --primary-color: #3B82F6; |
| --secondary-color: #8B5CF6; |
| --accent-color: #FACC15; |
| --ai-color: #10B981; |
| --ai-hover: #059669; |
| --bg-yellow: #FEF9C3; |
| --text-dark: #1F2937; |
| --card-bg: #ffffff; |
| --shadow-comic: 5px 5px 0 #1F2937; |
| --shadow-lg: 8px 8px 0 #1F2937; |
| --border-comic: 3px solid #1F2937; |
| --radius-sm: 8px; |
| --radius-md: 12px; |
| --transition: all 0.2s ease; |
| } |
| |
| * { |
| box-sizing: border-box; |
| } |
| |
| body { |
| margin: 0; |
| font-family: 'Comic Neue', cursive, sans-serif; |
| color: var(--text-dark); |
| background-color: var(--bg-yellow); |
| background-image: radial-gradient(#1F2937 1px, transparent 1px); |
| background-size: 20px 20px; |
| background-attachment: fixed; |
| min-height: 100vh; |
| } |
| |
| /* Header Info - Compact Comic Style */ |
| .header-info { |
| position: fixed; |
| top: 10px; |
| left: 50%; |
| transform: translateX(-50%); |
| text-align: center; |
| z-index: 100; |
| background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); |
| border: 3px solid #1F2937; |
| padding: 8px 25px; |
| border-radius: 10px; |
| box-shadow: 4px 4px 0 #1F2937; |
| } |
| |
| .header-info .title { |
| font-family: 'Bangers', cursive; |
| font-size: 1.3rem; |
| color: #FFF; |
| text-shadow: 2px 2px 0 #1F2937; |
| letter-spacing: 2px; |
| margin: 0; |
| } |
| |
| /* Floating AI Button - Comic Style */ |
| .floating-ai { |
| position: fixed; |
| top: 10px; |
| right: 20px; |
| width: 50px; |
| height: 50px; |
| border-radius: 50%; |
| background: linear-gradient(135deg, #EF4444 0%, #F97316 100%); |
| border: 3px solid #1F2937; |
| box-shadow: 4px 4px 0 #1F2937; |
| z-index: 9999; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| cursor: pointer; |
| transition: var(--transition); |
| overflow: hidden; |
| } |
| |
| .floating-ai:hover { |
| transform: translate(-2px, -2px); |
| box-shadow: 6px 6px 0 #1F2937; |
| } |
| |
| .floating-ai:active { |
| transform: translate(2px, 2px); |
| box-shadow: 2px 2px 0 #1F2937; |
| } |
| |
| .floating-ai .icon { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| width: 100%; |
| height: 100%; |
| font-size: 20px; |
| color: white; |
| text-shadow: 1px 1px 0 #1F2937; |
| transition: var(--transition); |
| } |
| |
| .floating-ai .ai-title { |
| position: absolute; |
| right: 58px; |
| background: #FFF; |
| padding: 8px 14px; |
| border-radius: 8px; |
| border: 2px solid #1F2937; |
| box-shadow: 3px 3px 0 #1F2937; |
| font-family: 'Bangers', cursive; |
| font-size: 0.95rem; |
| letter-spacing: 1px; |
| white-space: nowrap; |
| pointer-events: none; |
| opacity: 0; |
| transform: translateX(10px); |
| transition: all 0.3s ease; |
| color: var(--text-dark); |
| } |
| |
| .floating-ai:hover .ai-title { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| |
| /* Viewer Container - Comic Style */ |
| #viewer { |
| width: 94%; |
| height: 90vh; |
| max-width: 94%; |
| margin: 0; |
| background: var(--card-bg); |
| border: 4px solid #1F2937; |
| border-radius: var(--radius-md); |
| position: fixed; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| z-index: 1000; |
| box-shadow: var(--shadow-lg); |
| overflow: hidden; |
| } |
| |
| .flipbook-container .fb3d-menu-bar { |
| z-index: 2000 !important; |
| opacity: 1 !important; |
| bottom: 0 !important; |
| background: linear-gradient(135deg, #FACC15 0%, #F59E0B 100%) !important; |
| border-top: 3px solid #1F2937 !important; |
| border-radius: 0 0 var(--radius-md) var(--radius-md) !important; |
| padding: 12px 0 !important; |
| } |
| |
| .flipbook-container .fb3d-menu-bar > ul > li > img, |
| .flipbook-container .fb3d-menu-bar > ul > li > div { |
| opacity: 1 !important; |
| transform: scale(1.2) !important; |
| filter: drop-shadow(2px 2px 0 #1F2937) !important; |
| } |
| |
| .flipbook-container .fb3d-menu-bar > ul > li { |
| margin: 0 12px !important; |
| } |
| |
| .flipbook-container .fb3d-menu-bar > ul > li > span { |
| background: #FFF !important; |
| color: #1F2937 !important; |
| border: 2px solid #1F2937 !important; |
| border-radius: var(--radius-sm) !important; |
| padding: 8px 12px !important; |
| font-size: 13px !important; |
| bottom: 55px !important; |
| font-family: 'Comic Neue', cursive !important; |
| font-weight: 700 !important; |
| box-shadow: 3px 3px 0 #1F2937 !important; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| /* Loading Container - Comic Style */ |
| .loading-container { |
| position: fixed; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| text-align: center; |
| background: #FFF; |
| border: 4px solid #1F2937; |
| padding: 40px; |
| border-radius: var(--radius-md); |
| box-shadow: var(--shadow-lg); |
| z-index: 9999; |
| } |
| |
| .loading-spinner { |
| border: 5px solid #FEF9C3; |
| border-top: 5px solid #3B82F6; |
| border-radius: 50%; |
| width: 55px; |
| height: 55px; |
| margin: 0 auto; |
| animation: spin 1s linear infinite; |
| } |
| |
| .loading-text { |
| margin-top: 20px; |
| font-family: 'Bangers', cursive; |
| font-size: 1.3rem; |
| color: var(--text-dark); |
| letter-spacing: 1px; |
| } |
| |
| .progress-bar-container { |
| width: 220px; |
| height: 20px; |
| background: #FEF9C3; |
| border: 3px solid #1F2937; |
| border-radius: 10px; |
| margin-top: 15px; |
| overflow: hidden; |
| } |
| |
| .progress-bar { |
| height: 100%; |
| background: linear-gradient(to right, #3B82F6, #8B5CF6); |
| border-radius: 7px; |
| transition: width 0.3s ease; |
| } |
| |
| .loading-pages { |
| position: fixed; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: #FFF; |
| border: 3px solid #1F2937; |
| padding: 12px 25px; |
| border-radius: 25px; |
| box-shadow: 4px 4px 0 #1F2937; |
| font-family: 'Comic Neue', cursive; |
| font-size: 14px; |
| font-weight: 700; |
| color: var(--text-dark); |
| z-index: 9998; |
| } |
| |
| /* AI Chat Container - Comic Style */ |
| #aiChatContainer { |
| position: fixed; |
| top: 0; |
| right: 0; |
| width: 420px; |
| height: 100%; |
| background: #FFF; |
| border-left: 4px solid #1F2937; |
| box-shadow: -8px 0 0 #1F2937; |
| z-index: 10000; |
| transform: translateX(100%); |
| transition: transform 0.3s ease; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| #aiChatContainer.active { |
| transform: translateX(0); |
| } |
| |
| #aiChatHeader { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 20px; |
| border-bottom: 4px solid #1F2937; |
| background: linear-gradient(135deg, #EF4444 0%, #F97316 100%); |
| } |
| |
| #aiChatHeader h3 { |
| margin: 0; |
| color: white; |
| font-family: 'Bangers', cursive; |
| font-size: 1.5rem; |
| letter-spacing: 2px; |
| text-shadow: 2px 2px 0 #1F2937; |
| display: flex; |
| align-items: center; |
| } |
| |
| #aiChatHeader h3 i { |
| margin-right: 10px; |
| } |
| |
| #aiChatClose { |
| background: #FFF; |
| border: 3px solid #1F2937; |
| cursor: pointer; |
| font-size: 18px; |
| color: #1F2937; |
| width: 40px; |
| height: 40px; |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: 3px 3px 0 #1F2937; |
| transition: var(--transition); |
| } |
| |
| #aiChatClose:hover { |
| background: #FACC15; |
| transform: translate(-2px, -2px); |
| box-shadow: 5px 5px 0 #1F2937; |
| } |
| |
| #aiChatMessages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 20px; |
| background: #FEF9C3; |
| } |
| |
| .chat-message { |
| margin-bottom: 15px; |
| display: flex; |
| align-items: flex-start; |
| } |
| |
| .chat-message.user { |
| flex-direction: row-reverse; |
| } |
| |
| .chat-avatar { |
| width: 40px; |
| height: 40px; |
| border-radius: 8px; |
| border: 3px solid #1F2937; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| flex-shrink: 0; |
| box-shadow: 2px 2px 0 #1F2937; |
| } |
| |
| .chat-message.user .chat-avatar { |
| margin-left: 10px; |
| background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 100%); |
| color: white; |
| } |
| |
| .chat-message.ai .chat-avatar { |
| margin-right: 10px; |
| background: linear-gradient(135deg, #10B981 0%, #059669 100%); |
| color: white; |
| } |
| |
| .chat-bubble { |
| max-width: 75%; |
| } |
| |
| .chat-content { |
| padding: 14px 18px; |
| border-radius: 12px; |
| border: 3px solid #1F2937; |
| word-break: break-word; |
| font-family: 'Comic Neue', cursive; |
| font-size: 14px; |
| font-weight: 700; |
| line-height: 1.6; |
| box-shadow: 3px 3px 0 #1F2937; |
| } |
| |
| .chat-message.user .chat-content { |
| background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 100%); |
| color: white; |
| border-bottom-right-radius: 4px; |
| } |
| |
| .chat-message.ai .chat-content { |
| background: #FFF; |
| color: #1F2937; |
| border-bottom-left-radius: 4px; |
| } |
| |
| /* ๋งํฌ๋ค์ด ๋ ๋๋ง ์คํ์ผ */ |
| .chat-content.markdown-body { |
| font-size: 14px; |
| line-height: 1.6; |
| } |
| |
| .chat-content.markdown-body h1, |
| .chat-content.markdown-body h2, |
| .chat-content.markdown-body h3, |
| .chat-content.markdown-body h4 { |
| margin: 12px 0 8px 0; |
| font-family: 'Bangers', cursive; |
| color: #3B82F6; |
| border-bottom: 2px solid #E5E7EB; |
| padding-bottom: 4px; |
| } |
| |
| .chat-content.markdown-body h1 { font-size: 1.4em; } |
| .chat-content.markdown-body h2 { font-size: 1.25em; } |
| .chat-content.markdown-body h3 { font-size: 1.1em; } |
| .chat-content.markdown-body h4 { font-size: 1em; } |
| |
| .chat-content.markdown-body p { |
| margin: 8px 0; |
| } |
| |
| .chat-content.markdown-body strong { |
| color: #8B5CF6; |
| font-weight: 700; |
| } |
| |
| .chat-content.markdown-body em { |
| color: #059669; |
| font-style: italic; |
| } |
| |
| .chat-content.markdown-body ul, |
| .chat-content.markdown-body ol { |
| margin: 8px 0; |
| padding-left: 20px; |
| } |
| |
| .chat-content.markdown-body li { |
| margin: 4px 0; |
| position: relative; |
| } |
| |
| .chat-content.markdown-body ul li::marker { |
| color: #EF4444; |
| } |
| |
| .chat-content.markdown-body ol li::marker { |
| color: #3B82F6; |
| font-weight: 700; |
| } |
| |
| .chat-content.markdown-body blockquote { |
| margin: 10px 0; |
| padding: 8px 12px; |
| border-left: 4px solid #FACC15; |
| background: #FEF9C3; |
| border-radius: 0 8px 8px 0; |
| font-style: italic; |
| } |
| |
| .chat-content.markdown-body code { |
| background: #F3F4F6; |
| padding: 2px 6px; |
| border-radius: 4px; |
| font-family: 'Consolas', monospace; |
| font-size: 0.9em; |
| color: #EF4444; |
| } |
| |
| .chat-content.markdown-body pre { |
| background: #1F2937; |
| color: #F9FAFB; |
| padding: 12px; |
| border-radius: 8px; |
| overflow-x: auto; |
| margin: 10px 0; |
| } |
| |
| .chat-content.markdown-body pre code { |
| background: transparent; |
| color: inherit; |
| padding: 0; |
| } |
| |
| .chat-content.markdown-body hr { |
| border: none; |
| border-top: 2px dashed #D1D5DB; |
| margin: 12px 0; |
| } |
| |
| .chat-content.markdown-body a { |
| color: #3B82F6; |
| text-decoration: underline; |
| } |
| |
| .chat-content.markdown-body table { |
| width: 100%; |
| border-collapse: collapse; |
| margin: 10px 0; |
| } |
| |
| .chat-content.markdown-body th, |
| .chat-content.markdown-body td { |
| border: 2px solid #E5E7EB; |
| padding: 8px; |
| text-align: left; |
| } |
| |
| .chat-content.markdown-body th { |
| background: #F3F4F6; |
| font-weight: 700; |
| } |
| |
| .chat-time { |
| font-size: 11px; |
| color: #6B7280; |
| margin-top: 6px; |
| text-align: right; |
| font-weight: 700; |
| } |
| |
| .chat-message.ai .chat-time { |
| text-align: left; |
| } |
| |
| #aiChatForm { |
| display: flex; |
| padding: 15px 20px; |
| border-top: 4px solid #1F2937; |
| background: #FFF; |
| gap: 10px; |
| } |
| |
| #aiChatInput { |
| flex: 1; |
| padding: 14px 20px; |
| border: 3px solid #1F2937; |
| border-radius: 25px; |
| font-family: 'Comic Neue', cursive; |
| font-size: 14px; |
| font-weight: 700; |
| outline: none; |
| transition: var(--transition); |
| background: #FEF9C3; |
| } |
| |
| #aiChatInput:focus { |
| border-color: #3B82F6; |
| box-shadow: 3px 3px 0 #3B82F6; |
| } |
| |
| #aiChatSubmit { |
| background: linear-gradient(135deg, #EF4444 0%, #F97316 100%); |
| border: 3px solid #1F2937; |
| color: white; |
| width: 50px; |
| height: 50px; |
| border-radius: 50%; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| cursor: pointer; |
| box-shadow: 3px 3px 0 #1F2937; |
| transition: var(--transition); |
| } |
| |
| #aiChatSubmit:hover { |
| transform: translate(-2px, -2px); |
| box-shadow: 5px 5px 0 #1F2937; |
| } |
| |
| #aiChatSubmit:active { |
| transform: translate(2px, 2px); |
| box-shadow: 1px 1px 0 #1F2937; |
| } |
| |
| #aiChatSubmit:disabled { |
| background: #9CA3AF; |
| cursor: not-allowed; |
| transform: none; |
| box-shadow: 3px 3px 0 #1F2937; |
| } |
| |
| .typing-indicator { |
| display: flex; |
| align-items: center; |
| padding: 10px; |
| } |
| |
| .typing-indicator .chat-avatar { |
| margin-right: 10px; |
| background: linear-gradient(135deg, #10B981 0%, #059669 100%); |
| color: white; |
| } |
| |
| .typing-indicator span { |
| height: 10px; |
| width: 10px; |
| background: #10B981; |
| border-radius: 50%; |
| display: inline-block; |
| margin-right: 5px; |
| border: 2px solid #1F2937; |
| animation: typing 1s infinite; |
| } |
| |
| .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } |
| .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } |
| |
| @keyframes typing { |
| 0%, 100% { transform: translateY(0); } |
| 50% { transform: translateY(-8px); } |
| } |
| |
| .error-container { |
| position: fixed; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| text-align: center; |
| background: #FFF; |
| border: 4px solid #1F2937; |
| padding: 40px; |
| border-radius: var(--radius-md); |
| box-shadow: var(--shadow-lg); |
| z-index: 9999; |
| } |
| |
| .error-container i { |
| font-size: 50px; |
| color: #EF4444; |
| margin-bottom: 20px; |
| text-shadow: 3px 3px 0 #1F2937; |
| } |
| |
| .error-container p { |
| font-family: 'Comic Neue', cursive; |
| font-size: 16px; |
| font-weight: 700; |
| color: var(--text-dark); |
| margin-bottom: 20px; |
| } |
| |
| .error-container button { |
| padding: 12px 30px; |
| background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); |
| color: white; |
| border: 3px solid #1F2937; |
| border-radius: 8px; |
| cursor: pointer; |
| font-family: 'Bangers', cursive; |
| font-size: 1.2rem; |
| letter-spacing: 1px; |
| box-shadow: 4px 4px 0 #1F2937; |
| transition: var(--transition); |
| } |
| |
| .error-container button:hover { |
| transform: translate(-2px, -2px); |
| box-shadow: 6px 6px 0 #1F2937; |
| } |
| |
| /* Footer - Comic Style */ |
| .footer-comic { |
| position: fixed; |
| bottom: 10px; |
| right: 10px; |
| text-align: center; |
| padding: 10px 20px; |
| background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); |
| border: 3px solid #1F2937; |
| border-radius: 10px; |
| box-shadow: 4px 4px 0 #1F2937; |
| z-index: 100; |
| } |
| |
| .footer-comic p { |
| font-family: 'Comic Neue', cursive; |
| color: #FFF; |
| margin: 3px 0; |
| font-weight: 700; |
| font-size: 0.85rem; |
| } |
| |
| .footer-comic a { |
| color: #FACC15; |
| text-decoration: none; |
| font-weight: 700; |
| } |
| |
| .footer-comic a:hover { |
| text-decoration: underline; |
| } |
| |
| /* Scrollbar - Comic Style */ |
| ::-webkit-scrollbar { |
| width: 14px; |
| height: 14px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: #FEF9C3; |
| border: 2px solid #1F2937; |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: linear-gradient(135deg, #3B82F6, #8B5CF6); |
| border: 2px solid #1F2937; |
| border-radius: 7px; |
| } |
| |
| ::-webkit-scrollbar-thumb:hover { |
| background: linear-gradient(135deg, #EF4444, #F97316); |
| } |
| |
| ::selection { |
| background: #FACC15; |
| color: #1F2937; |
| } |
| |
| @media (max-width: 768px) { |
| .header-info { |
| top: 8px; |
| padding: 6px 15px; |
| max-width: 60%; |
| } |
| |
| .header-info .title { |
| font-size: 1rem; |
| } |
| |
| .floating-ai { |
| width: 40px; |
| height: 40px; |
| top: 8px; |
| right: 10px; |
| } |
| |
| .floating-ai .icon { |
| font-size: 16px; |
| } |
| |
| #aiChatContainer { |
| width: 100%; |
| } |
| |
| #viewer { |
| width: 98%; |
| height: 92vh; |
| top: 50%; |
| } |
| |
| .footer-comic { |
| display: none; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <!-- Header Info - Compact Comic Style --> |
| <div class="header-info"> |
| <div class="title">๐ AI ํ๋ฆฝ๋ถ</div> |
| </div> |
| |
| <!-- Floating AI Button - Comic Style --> |
| <div id="aiButton" class="floating-ai"> |
| <div class="icon"><i class="fas fa-robot"></i></div> |
| <div class="ai-title">๐ค AI ์ด์์คํดํธ</div> |
| </div> |
| |
| <!-- AI Chat Container --> |
| <div id="aiChatContainer"> |
| <div id="aiChatHeader"> |
| <h3><i class="fas fa-robot"></i> AI ์ด์์คํดํธ</h3> |
| <button id="aiChatClose"><i class="fas fa-times"></i></button> |
| </div> |
| <div id="aiChatMessages"></div> |
| <form id="aiChatForm"> |
| <input type="text" id="aiChatInput" placeholder="PDF์ ๋ํด ์ง๋ฌธํ์ธ์..." autocomplete="off"> |
| <button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button> |
| </form> |
| </div> |
| |
| <!-- PDF Viewer --> |
| <div id="viewer"></div> |
| <div id="loadingPages" class="loading-pages" style="display:none;">ํ์ด์ง ๋ก๋ฉ ์ค... <span id="loadingPagesCount">0%</span></div> |
| |
| <!-- Footer - Comic Style --> |
| <div class="footer-comic"> |
| <p style="font-family:'Bangers',cursive;font-size:1.1rem;letter-spacing:1px">๐ AI FLIPBOOK ๐</p> |
| <p>Powered by VLM + 3D FlipBook</p> |
| <p><a href="https://ginigen.ai" target="_blank">๐ ginigen.ai</a></p> |
| </div> |
| |
| <script> |
| let fb = null; |
| const viewer = document.getElementById('viewer'); |
| pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/pdf.worker.js'; |
| |
| let pageLoadingInterval = null; |
| let audioInitialized = false; |
| let isAiChatActive = false; |
| let isAiProcessing = false; |
| let hasLoadedSummary = false; |
| let analysisCheckInterval = null; |
| |
| function $id(id) { return document.getElementById(id); } |
| |
| function formatTime() { |
| const now = new Date(); |
| return now.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }); |
| } |
| |
| function initializeAudio() { |
| if (audioInitialized) return Promise.resolve(); |
| return new Promise((resolve) => { |
| const audio = new Audio('/static/turnPage2.mp3'); |
| audio.volume = 0.01; |
| audio.play().then(() => { |
| audio.pause(); |
| audioInitialized = true; |
| resolve(); |
| }).catch(() => { |
| const initOnClick = () => { |
| const tempAudio = new Audio('/static/turnPage2.mp3'); |
| tempAudio.volume = 0.01; |
| tempAudio.play().then(() => { |
| tempAudio.pause(); |
| audioInitialized = true; |
| document.removeEventListener('click', initOnClick); |
| resolve(); |
| }).catch(() => {}); |
| }; |
| document.addEventListener('click', initOnClick, { once: true }); |
| resolve(); |
| }); |
| }); |
| } |
| |
| function addChatMessage(content, isUser = false) { |
| const messagesContainer = $id('aiChatMessages'); |
| const messageElement = document.createElement('div'); |
| messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`; |
| |
| // AI ๋ฉ์์ง๋ ๋งํฌ๋ค์ด์ HTML๋ก ๋ณํ |
| let displayContent = content; |
| if (!isUser && typeof marked !== 'undefined') { |
| try { |
| marked.setOptions({ |
| breaks: true, // ์ค๋ฐ๊ฟ์ <br>๋ก ๋ณํ |
| gfm: true // GitHub Flavored Markdown |
| }); |
| displayContent = marked.parse(content); |
| } catch (e) { |
| console.error('๋งํฌ๋ค์ด ํ์ฑ ์ค๋ฅ:', e); |
| displayContent = content; |
| } |
| } |
| |
| messageElement.innerHTML = ` |
| <div class="chat-avatar"> |
| <i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i> |
| </div> |
| <div class="chat-bubble"> |
| <div class="chat-content markdown-body">${displayContent}</div> |
| <div class="chat-time">${formatTime()}</div> |
| </div> |
| `; |
| messagesContainer.appendChild(messageElement); |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; |
| return messageElement; |
| } |
| |
| function addTypingIndicator() { |
| const messagesContainer = $id('aiChatMessages'); |
| const indicator = document.createElement('div'); |
| indicator.className = 'typing-indicator'; |
| indicator.id = 'typingIndicator'; |
| indicator.innerHTML = ` |
| <div class="chat-avatar"><i class="fas fa-robot"></i></div> |
| <div><span></span><span></span><span></span></div> |
| `; |
| messagesContainer.appendChild(indicator); |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; |
| return indicator; |
| } |
| |
| function removeTypingIndicator() { |
| const indicator = $id('typingIndicator'); |
| if (indicator) indicator.remove(); |
| } |
| |
| function toggleAiChat(show = true) { |
| const container = $id('aiChatContainer'); |
| if (show) { |
| container.classList.add('active'); |
| isAiChatActive = true; |
| if (!hasLoadedSummary) { |
| loadPdfSummary(); |
| } |
| $id('aiChatInput').focus(); |
| } else { |
| container.classList.remove('active'); |
| isAiChatActive = false; |
| } |
| } |
| |
| async function checkAnalysisStatus() { |
| try { |
| const response = await fetch('/api/analysis-status'); |
| const data = await response.json(); |
| return data; |
| } catch (e) { |
| console.error("๋ถ์ ์ํ ํ์ธ ์ค๋ฅ:", e); |
| return { status: "error" }; |
| } |
| } |
| |
| async function loadPdfSummary() { |
| if (isAiProcessing || hasLoadedSummary) return; |
| |
| try { |
| isAiProcessing = true; |
| addTypingIndicator(); |
| |
| // ๋ถ์ ์ํ ํ์ธ |
| const statusData = await checkAnalysisStatus(); |
| |
| if (statusData.status === 'analyzing') { |
| removeTypingIndicator(); |
| const progress = statusData.progress || 0; |
| addChatMessage(`์๋
ํ์ธ์! ๐ฅ ํ์ฌ PDF๋ฅผ AI๊ฐ ๋ถ์ํ๊ณ ์์ต๋๋ค. ๐<br><br>์งํ๋ฅ : <strong>${progress}%</strong><br><small style="color:#6B7280;">๋ถ์์ด ์๋ฃ๋๋ฉด ์๋์ผ๋ก ์๋ ค๋๋ฆฌ๊ฒ ์ต๋๋ค.</small>`); |
| hasLoadedSummary = true; |
| isAiProcessing = false; |
| |
| // ๋ถ์ ์๋ฃ ํด๋ง |
| startAnalysisPolling(); |
| return; |
| } |
| |
| if (statusData.status === 'error') { |
| removeTypingIndicator(); |
| addChatMessage(`์๋
ํ์ธ์! ๐ฅ PDF ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. โ ๏ธ<br><br><small style="color:#EF4444;">${statusData.error || '์ ์ ์๋ ์ค๋ฅ'}</small><br><br>ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.`); |
| hasLoadedSummary = true; |
| isAiProcessing = false; |
| return; |
| } |
| |
| if (statusData.status === 'not_started') { |
| removeTypingIndicator(); |
| addChatMessage(`์๋
ํ์ธ์! ๐ฅ PDF ๋ถ์์ด ์์ง ์์๋์ง ์์์ต๋๋ค. ๐<br><br><small style="color:#6B7280;">์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์...</small>`); |
| hasLoadedSummary = true; |
| isAiProcessing = false; |
| startAnalysisPolling(); |
| return; |
| } |
| |
| // ๋ถ์ ์๋ฃ๋จ - ์์ฝ ๊ฐ์ ธ์ค๊ธฐ |
| const response = await fetch('/api/ai/summarize-pdf'); |
| const data = await response.json(); |
| |
| removeTypingIndicator(); |
| |
| if (data.summary) { |
| const pageInfo = data.analyzed_pages ? ` (${data.analyzed_pages}/${data.total_pages}ํ์ด์ง ๋ถ์์๋ฃ)` : ''; |
| addChatMessage(`์๋
ํ์ธ์! ๐ฅ ์ด PDF์ ๋ํด ๋ฌด์์ด๋ ์ง๋ฌธํด์ฃผ์ธ์.${pageInfo}<br><br><strong>๐ PDF ์์ฝ:</strong><br>${data.summary}`); |
| } else { |
| addChatMessage("์๋
ํ์ธ์! ๐ฅ PDF์ ๋ํด ์ง๋ฌธํด์ฃผ์ธ์. ์ต์ ์ ๋คํด ๋ต๋ณํ๊ฒ ์ต๋๋ค."); |
| } |
| hasLoadedSummary = true; |
| |
| } catch (error) { |
| console.error("PDF ์์ฝ ๋ก๋ ์ค๋ฅ:", error); |
| removeTypingIndicator(); |
| addChatMessage("์๋
ํ์ธ์! ๐ฅ PDF์ ๋ํด ์ง๋ฌธํด์ฃผ์ธ์."); |
| hasLoadedSummary = true; |
| } finally { |
| isAiProcessing = false; |
| } |
| } |
| |
| function startAnalysisPolling() { |
| if (analysisCheckInterval) return; |
| |
| analysisCheckInterval = setInterval(async () => { |
| try { |
| const data = await checkAnalysisStatus(); |
| |
| if (data.status === 'completed') { |
| clearInterval(analysisCheckInterval); |
| analysisCheckInterval = null; |
| addChatMessage(`โ
PDF ๋ถ์์ด ์๋ฃ๋์์ต๋๋ค! (${data.analyzed_pages || data.total_pages}ํ์ด์ง)<br>์ด์ ์์ ๋กญ๊ฒ ์ง๋ฌธํด์ฃผ์ธ์.`); |
| } else if (data.status === 'analyzing') { |
| // ์งํ๋ฅ ์
๋ฐ์ดํธ (์ ํ์ ) |
| console.log(`๋ถ์ ์งํ ์ค: ${data.progress}%`); |
| } else if (data.status === 'error') { |
| clearInterval(analysisCheckInterval); |
| analysisCheckInterval = null; |
| addChatMessage(`โ ๏ธ PDF ๋ถ์ ์คํจ: ${data.error || '์ ์ ์๋ ์ค๋ฅ'}`); |
| } |
| } catch (e) { |
| console.error("ํด๋ง ์ค๋ฅ:", e); |
| } |
| }, 5000); // 5์ด๋ง๋ค ํ์ธ |
| |
| // 5๋ถ ํ ์๋ ์ค์ง |
| setTimeout(() => { |
| if (analysisCheckInterval) { |
| clearInterval(analysisCheckInterval); |
| analysisCheckInterval = null; |
| } |
| }, 300000); |
| } |
| |
| async function submitQuestion(question) { |
| if (isAiProcessing || !question.trim()) return; |
| |
| try { |
| isAiProcessing = true; |
| $id('aiChatSubmit').disabled = true; |
| |
| addChatMessage(question, true); |
| |
| // ๋ถ์ ์ํ ํ์ธ |
| const statusData = await checkAnalysisStatus(); |
| |
| if (statusData.status !== 'completed') { |
| if (statusData.status === 'analyzing') { |
| addChatMessage(`PDF ๋ถ์์ด ์งํ ์ค์
๋๋ค (${statusData.progress || 0}%). ์๋ฃ ํ ์ง๋ฌธํด์ฃผ์ธ์. โณ`); |
| } else { |
| addChatMessage("PDF ๋ถ์์ด ์์ง ์๋ฃ๋์ง ์์์ต๋๋ค. ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์."); |
| } |
| isAiProcessing = false; |
| $id('aiChatSubmit').disabled = false; |
| $id('aiChatInput').value = question; |
| return; |
| } |
| |
| addTypingIndicator(); |
| |
| const response = await fetch('/api/ai/query-pdf', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ query: question }), |
| signal: AbortSignal.timeout(120000) |
| }); |
| |
| const data = await response.json(); |
| removeTypingIndicator(); |
| |
| if (data.answer) { |
| addChatMessage(data.answer); |
| } else if (data.error) { |
| addChatMessage(`์ฃ์กํฉ๋๋ค. ${data.error}`); |
| } else { |
| addChatMessage("์ฃ์กํฉ๋๋ค. ๋ต๋ณ์ ์์ฑํ์ง ๋ชปํ์ต๋๋ค."); |
| } |
| } catch (error) { |
| console.error("์ง๋ฌธ ์ ์ถ ์ค๋ฅ:", error); |
| removeTypingIndicator(); |
| if (error.name === 'AbortError') { |
| addChatMessage("์๋ต ์๊ฐ์ด ์ด๊ณผ๋์์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์."); |
| } else { |
| addChatMessage("์๋ฒ์ ํต์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์."); |
| } |
| } finally { |
| isAiProcessing = false; |
| $id('aiChatSubmit').disabled = false; |
| $id('aiChatInput').value = ''; |
| $id('aiChatInput').focus(); |
| } |
| } |
| |
| function showLoading(message) { |
| hideLoading(); |
| const container = document.createElement('div'); |
| container.className = 'loading-container'; |
| container.id = 'loadingContainer'; |
| container.innerHTML = ` |
| <div class="loading-spinner"></div> |
| <p class="loading-text">${message || '๋ก๋ฉ ์ค...'}</p> |
| <div class="progress-bar-container"> |
| <div id="progressBar" class="progress-bar" style="width: 0%;"></div> |
| </div> |
| `; |
| document.body.appendChild(container); |
| } |
| |
| function updateLoading(message, progress) { |
| const text = document.querySelector('.loading-text'); |
| if (text) text.textContent = message; |
| const bar = $id('progressBar'); |
| if (bar && progress !== undefined) bar.style.width = `${progress}%`; |
| } |
| |
| function hideLoading() { |
| const container = $id('loadingContainer'); |
| if (container) container.remove(); |
| } |
| |
| function showError(message) { |
| hideLoading(); |
| const container = document.createElement('div'); |
| container.className = 'error-container'; |
| container.id = 'errorContainer'; |
| container.innerHTML = ` |
| <i class="fas fa-exclamation-circle"></i> |
| <p>${message}</p> |
| <button onclick="location.reload()">๋ค์ ์๋</button> |
| `; |
| document.body.appendChild(container); |
| } |
| |
| function createFlipBook(pages) { |
| try { |
| const windowWidth = window.innerWidth; |
| const windowHeight = window.innerHeight; |
| const aspectRatio = windowWidth / windowHeight; |
| |
| let width, height; |
| if (aspectRatio > 1) { |
| // ๊ฐ๋ก ๋ชจ๋: ๋์ด ๊ธฐ์ค์ผ๋ก 90% ์ฌ์ฉ |
| height = Math.min(windowHeight * 0.88, windowHeight - 60); |
| width = height * aspectRatio * 0.75; |
| if (width > windowWidth * 0.94) { |
| width = windowWidth * 0.94; |
| height = width / (aspectRatio * 0.75); |
| } |
| } else { |
| // ์ธ๋ก ๋ชจ๋: ๋๋น ๊ธฐ์ค์ผ๋ก 98% ์ฌ์ฉ |
| width = Math.min(windowWidth * 0.98, windowWidth - 10); |
| height = width / aspectRatio * 0.9; |
| if (height > windowHeight * 0.9) { |
| height = windowHeight * 0.9; |
| width = height * aspectRatio * 0.9; |
| } |
| } |
| |
| viewer.style.width = Math.round(width) + 'px'; |
| viewer.style.height = Math.round(height) + 'px'; |
| |
| const validPages = pages.map(page => { |
| if (!page || !page.src) { |
| return { |
| src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PC9zdmc+', |
| thumb: '' |
| }; |
| } |
| return page; |
| }); |
| |
| fb = new FlipBook(viewer, { |
| pages: validPages, |
| viewMode: 'webgl', |
| autoSize: true, |
| flipDuration: 800, |
| backgroundColor: '#fff', |
| sound: true, |
| assets: { flipMp3: '/static/turnPage2.mp3', hardFlipMp3: '/static/turnPage2.mp3' }, |
| controlsProps: { |
| enableFullscreen: true, |
| enableToc: true, |
| enableDownload: false, |
| enablePrint: false, |
| enableZoom: true, |
| enableShare: false, |
| enableSearch: true, |
| enableAutoPlay: true, |
| enableSound: true, |
| layout: 10, |
| skin: 'light', |
| autoNavigationTime: 3600, |
| hideControls: false, |
| paddingTop: 10, |
| paddingLeft: 10, |
| paddingRight: 10, |
| paddingBottom: 10, |
| pageTextureSize: 1024, |
| thumbnails: true, |
| autoHideControls: false, |
| controlsTimeout: 8000 |
| } |
| }); |
| |
| window.addEventListener('resize', () => { |
| if (fb) fb.resize(); |
| }); |
| |
| console.log('FlipBook ์์ฑ ์๋ฃ'); |
| } catch (error) { |
| console.error('FlipBook ์์ฑ ์ค๋ฅ:', error); |
| showError("ํ๋ฆฝ๋ถ์ ์์ฑํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."); |
| } |
| } |
| |
| async function loadPDF() { |
| try { |
| showLoading("PDF ์ ๋ณด ํ์ธ ์ค..."); |
| |
| const infoResponse = await fetch('/api/pdf-info'); |
| const pdfInfo = await infoResponse.json(); |
| |
| if (!pdfInfo.exists) { |
| hideLoading(); |
| showError("PDF ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค. prompt.pdf ํ์ผ์ ํ์ธํด์ฃผ์ธ์."); |
| return; |
| } |
| |
| updateLoading("PDF ๋ก๋ฉ ์ค...", 10); |
| |
| if (pdfInfo.cached) { |
| const cacheResponse = await fetch('/api/cached-pdf'); |
| const cachedData = await cacheResponse.json(); |
| |
| if (cachedData.status === "completed" && cachedData.pages) { |
| hideLoading(); |
| createFlipBook(cachedData.pages); |
| return; |
| } |
| } |
| |
| const cacheResponse = await fetch('/api/cached-pdf'); |
| let cachedData = await cacheResponse.json(); |
| |
| if (cachedData.status === "completed" && cachedData.pages) { |
| hideLoading(); |
| createFlipBook(cachedData.pages); |
| return; |
| } |
| |
| while (cachedData.status === "processing" || cachedData.status === "started") { |
| await new Promise(resolve => setTimeout(resolve, 1000)); |
| |
| const statusResponse = await fetch('/api/cache-status'); |
| cachedData = await statusResponse.json(); |
| |
| if (cachedData.progress) { |
| updateLoading(`PDF ์ฒ๋ฆฌ ์ค... ${cachedData.progress}%`, cachedData.progress); |
| } |
| |
| if (cachedData.status === "completed") { |
| const finalResponse = await fetch('/api/cached-pdf'); |
| cachedData = await finalResponse.json(); |
| break; |
| } |
| } |
| |
| hideLoading(); |
| |
| if (cachedData.pages && cachedData.pages.length > 0) { |
| createFlipBook(cachedData.pages); |
| } else { |
| showError("PDF๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค."); |
| } |
| |
| } catch (error) { |
| console.error("PDF ๋ก๋ ์ค๋ฅ:", error); |
| hideLoading(); |
| showError("PDF๋ฅผ ๋ก๋ํ๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."); |
| } |
| } |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| initializeAudio(); |
| |
| $id('aiButton').addEventListener('click', () => toggleAiChat(!isAiChatActive)); |
| $id('aiChatClose').addEventListener('click', () => toggleAiChat(false)); |
| |
| $id('aiChatForm').addEventListener('submit', function(e) { |
| e.preventDefault(); |
| const question = $id('aiChatInput').value.trim(); |
| if (question && !isAiProcessing) { |
| submitQuestion(question); |
| } |
| }); |
| |
| loadPDF(); |
| }); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| if __name__ == "__main__": |
| uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860))) |