| | 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))) |