seawolf2357 commited on
Commit
ca0c7c6
ยท
1 Parent(s): e593180

Create app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +1945 -0
app-backup.py ADDED
@@ -0,0 +1,1945 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, BackgroundTasks, Request
2
+ from fastapi.responses import HTMLResponse, JSONResponse, Response
3
+ from fastapi.staticfiles import StaticFiles
4
+ import pathlib, os, uvicorn, base64, json, uuid, time
5
+ from typing import Dict, List, Any, Optional
6
+ import asyncio
7
+ import logging
8
+ import threading
9
+ import concurrent.futures
10
+ import requests
11
+ import fitz
12
+
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+ BASE = pathlib.Path(__file__).parent
17
+ app = FastAPI()
18
+ app.mount("/static", StaticFiles(directory=BASE), name="static")
19
+
20
+ CACHE_DIR = BASE / "cache"
21
+ if not CACHE_DIR.exists():
22
+ CACHE_DIR.mkdir(parents=True)
23
+
24
+ EMBEDDING_DIR = BASE / "embeddings"
25
+ if not EMBEDDING_DIR.exists():
26
+ EMBEDDING_DIR.mkdir(parents=True)
27
+
28
+ # Fireworks AI API ์„ค์ • (VLM ๋ชจ๋ธ)
29
+ FIREWORKS_API_KEY = os.getenv("FIREWORKS_API", "").strip() # ์ค„๋ฐ”๊ฟˆ/๊ณต๋ฐฑ ์ œ๊ฑฐ
30
+ FIREWORKS_API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
31
+ FIREWORKS_VLM_MODEL = "accounts/fireworks/models/qwen3-vl-235b-a22b-instruct"
32
+ HAS_VALID_API_KEY = bool(FIREWORKS_API_KEY)
33
+
34
+ if HAS_VALID_API_KEY:
35
+ logger.info("Fireworks AI VLM API ํ‚ค ์„ค์ • ์™„๋ฃŒ")
36
+ else:
37
+ logger.warning("์œ ํšจํ•œ Fireworks AI API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. AI ๊ธฐ๋Šฅ์ด ์ œํ•œ๋ฉ๋‹ˆ๋‹ค.")
38
+
39
+ # ๊ณ ์ • PDF ํŒŒ์ผ ๊ฒฝ๋กœ
40
+ PROMPT_PDF_PATH = BASE / "prompt.pdf"
41
+ PROMPT_PDF_ID = "prompt_pdf_main"
42
+
43
+ pdf_cache: Dict[str, Dict[str, Any]] = {}
44
+ cache_locks = {}
45
+ pdf_embeddings: Dict[str, Dict[str, Any]] = {}
46
+
47
+ # VLM ๋ถ„์„ ์ƒํƒœ ์ถ”์  (๋ฉ”๋ชจ๋ฆฌ)
48
+ analysis_status: Dict[str, Dict[str, Any]] = {}
49
+
50
+
51
+ def get_cache_path(pdf_name: str):
52
+ return CACHE_DIR / f"{pdf_name}_cache.json"
53
+
54
+
55
+ def get_embedding_path(pdf_id: str):
56
+ return EMBEDDING_DIR / f"{pdf_id}_embedding.json"
57
+
58
+
59
+ def get_analysis_cache_path(pdf_id: str):
60
+ """VLM ๋ถ„์„ ๊ฒฐ๊ณผ ์บ์‹œ ๊ฒฝ๋กœ"""
61
+ return EMBEDDING_DIR / f"{pdf_id}_vlm_analysis.json"
62
+
63
+
64
+ def load_analysis_cache(pdf_id: str) -> Optional[Dict[str, Any]]:
65
+ """VLM ๋ถ„์„ ์บ์‹œ ๋กœ๋“œ"""
66
+ cache_path = get_analysis_cache_path(pdf_id)
67
+ if cache_path.exists():
68
+ try:
69
+ with open(cache_path, "r", encoding="utf-8") as f:
70
+ data = json.load(f)
71
+ logger.info(f"VLM ๋ถ„์„ ์บ์‹œ ๋กœ๋“œ ์™„๋ฃŒ: {pdf_id}")
72
+ return data
73
+ except Exception as e:
74
+ logger.error(f"๋ถ„์„ ์บ์‹œ ๋กœ๋“œ ์˜ค๋ฅ˜: {e}")
75
+ return None
76
+
77
+
78
+ def save_analysis_cache(pdf_id: str, analysis_data: Dict[str, Any]):
79
+ """VLM ๋ถ„์„ ๊ฒฐ๊ณผ ์บ์‹œ ์ €์žฅ"""
80
+ cache_path = get_analysis_cache_path(pdf_id)
81
+ try:
82
+ with open(cache_path, "w", encoding="utf-8") as f:
83
+ json.dump(analysis_data, f, ensure_ascii=False, indent=2)
84
+ logger.info(f"VLM ๋ถ„์„ ์บ์‹œ ์ €์žฅ ์™„๋ฃŒ: {pdf_id}")
85
+ except Exception as e:
86
+ logger.error(f"๋ถ„์„ ์บ์‹œ ์ €์žฅ ์˜ค๋ฅ˜: {e}")
87
+
88
+
89
+ def get_pdf_page_as_base64(pdf_path: str, page_num: int, scale: float = 1.0) -> str:
90
+ """PDF ํŽ˜์ด์ง€๋ฅผ base64 ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜"""
91
+ try:
92
+ doc = fitz.open(pdf_path)
93
+ if page_num >= doc.page_count:
94
+ doc.close()
95
+ return None
96
+
97
+ page = doc[page_num]
98
+ pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale))
99
+ img_data = pix.tobytes("jpeg", 85)
100
+ b64_img = base64.b64encode(img_data).decode('utf-8')
101
+ doc.close()
102
+ return b64_img
103
+ except Exception as e:
104
+ logger.error(f"PDF ํŽ˜์ด์ง€ ์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {e}")
105
+ return None
106
+
107
+
108
+ 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]]:
109
+ """PDF ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ base64 ์ด๋ฏธ์ง€ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ (๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์šฉ)"""
110
+ try:
111
+ doc = fitz.open(pdf_path)
112
+ total_pages = doc.page_count
113
+ end_page = min(start_page + max_pages, total_pages)
114
+
115
+ images = []
116
+ for page_num in range(start_page, end_page):
117
+ page = doc[page_num]
118
+ pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale))
119
+ img_data = pix.tobytes("jpeg", 75)
120
+ b64_img = base64.b64encode(img_data).decode('utf-8')
121
+ images.append({
122
+ "page": page_num + 1,
123
+ "image_base64": b64_img
124
+ })
125
+
126
+ doc.close()
127
+ logger.info(f"PDF {start_page+1}~{end_page}/{total_pages}ํŽ˜์ด์ง€ ์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ์™„๋ฃŒ")
128
+ return images, total_pages
129
+ except Exception as e:
130
+ logger.error(f"PDF ํŽ˜์ด์ง€๋“ค ์ด๋ฏธ์ง€ ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {e}")
131
+ return [], 0
132
+
133
+
134
+ def call_fireworks_vlm_api(messages: List[Dict], max_tokens: int = 4096, temperature: float = 0.6) -> str:
135
+ """Fireworks AI VLM API ํ˜ธ์ถœ (์ด๋ฏธ์ง€ ๋ถ„์„ ์ง€์›)"""
136
+ if not HAS_VALID_API_KEY:
137
+ raise Exception("API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
138
+
139
+ payload = {
140
+ "model": FIREWORKS_VLM_MODEL,
141
+ "max_tokens": max_tokens,
142
+ "top_p": 1,
143
+ "top_k": 40,
144
+ "presence_penalty": 0,
145
+ "frequency_penalty": 0,
146
+ "temperature": temperature,
147
+ "messages": messages
148
+ }
149
+
150
+ headers = {
151
+ "Accept": "application/json",
152
+ "Content-Type": "application/json",
153
+ "Authorization": f"Bearer {FIREWORKS_API_KEY}"
154
+ }
155
+
156
+ response = requests.post(FIREWORKS_API_URL, headers=headers, data=json.dumps(payload), timeout=180)
157
+
158
+ if response.status_code != 200:
159
+ raise Exception(f"API ์˜ค๋ฅ˜: {response.status_code} - {response.text}")
160
+
161
+ result = response.json()
162
+ return result["choices"][0]["message"]["content"]
163
+
164
+
165
+ def analyze_batch_pages_sync(pdf_path: str, start_page: int, batch_size: int = 5) -> str:
166
+ """๋ฐฐ์น˜ ํŽ˜์ด์ง€ ๋ถ„์„ (๋™๊ธฐ)"""
167
+ page_images, total_pages = get_pdf_pages_as_base64(pdf_path, start_page, batch_size, scale=0.6)
168
+
169
+ if not page_images:
170
+ return ""
171
+
172
+ content_parts = []
173
+ for img_data in page_images:
174
+ content_parts.append({
175
+ "type": "image_url",
176
+ "image_url": {
177
+ "url": f"data:image/jpeg;base64,{img_data['image_base64']}"
178
+ }
179
+ })
180
+
181
+ page_range = f"{start_page + 1}~{start_page + len(page_images)}"
182
+ content_parts.append({
183
+ "type": "text",
184
+ "text": f"""์œ„ ์ด๋ฏธ์ง€๋“ค์€ PDF ๋ฌธ์„œ์˜ {page_range}ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค.
185
+
186
+ ๊ฐ ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ์„ ์ƒ์„ธํ•˜๊ฒŒ ๋ถ„์„ํ•˜์—ฌ ํ…์ŠคํŠธ๋กœ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”.
187
+ - ๋ชจ๋“  ํ…์ŠคํŠธ ๋‚ด์šฉ์„ ๋น ์ง์—†์ด ์ถ”์ถœ
188
+ - ํ‘œ, ์ฐจํŠธ, ๊ทธ๋ž˜ํ”„๊ฐ€ ์žˆ์œผ๋ฉด ๋‚ด์šฉ ์„ค๋ช…
189
+ - ์ด๋ฏธ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ์„ค๋ช…
190
+ - ํŽ˜์ด์ง€๋ณ„๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ž‘์„ฑ
191
+
192
+ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”."""
193
+ })
194
+
195
+ messages = [{"role": "user", "content": content_parts}]
196
+
197
+ return call_fireworks_vlm_api(messages, max_tokens=4096, temperature=0.3)
198
+
199
+
200
+ async def analyze_pdf_with_vlm_batched(pdf_id: str, force_refresh: bool = False) -> Dict[str, Any]:
201
+ """VLM์œผ๋กœ PDF ๋ฐฐ์น˜ ๋ถ„์„ ํ›„ ์บ์‹œ์— ์ €์žฅ"""
202
+ global analysis_status
203
+
204
+ # ์ด๋ฏธ ๋ถ„์„ ์ค‘์ธ์ง€ ํ™•์ธ
205
+ if pdf_id in analysis_status and analysis_status[pdf_id].get("status") == "analyzing":
206
+ logger.info(f"PDF {pdf_id} ์ด๋ฏธ ๋ถ„์„ ์ค‘...")
207
+ return {"status": "analyzing", "progress": analysis_status[pdf_id].get("progress", 0)}
208
+
209
+ # ์บ์‹œ ํ™•์ธ
210
+ if not force_refresh:
211
+ cached = load_analysis_cache(pdf_id)
212
+ if cached:
213
+ analysis_status[pdf_id] = {"status": "completed", "progress": 100}
214
+ return cached
215
+
216
+ pdf_path = str(PROMPT_PDF_PATH)
217
+ if not PROMPT_PDF_PATH.exists():
218
+ analysis_status[pdf_id] = {"status": "error", "error": "PDF ํŒŒ์ผ ์—†์Œ"}
219
+ return {"error": "PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}
220
+
221
+ if not HAS_VALID_API_KEY:
222
+ analysis_status[pdf_id] = {"status": "error", "error": "API ํ‚ค ์—†์Œ"}
223
+ return {"error": "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}
224
+
225
+ # ๋ถ„์„ ์‹œ์ž‘
226
+ analysis_status[pdf_id] = {"status": "analyzing", "progress": 0, "started_at": time.time()}
227
+
228
+ try:
229
+ # PDF ์ด ํŽ˜์ด์ง€ ์ˆ˜ ํ™•์ธ
230
+ doc = fitz.open(pdf_path)
231
+ total_pages = doc.page_count
232
+ doc.close()
233
+
234
+ logger.info(f"PDF ๋ถ„์„ ์‹œ์ž‘: ์ด {total_pages}ํŽ˜์ด์ง€")
235
+
236
+ # ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ ์„œ ๋ถ„์„ (5ํŽ˜์ด์ง€์”ฉ)
237
+ batch_size = 5
238
+ all_analyses = []
239
+
240
+ for start_page in range(0, min(total_pages, 25), batch_size): # ์ตœ๋Œ€ 25ํŽ˜์ด์ง€
241
+ try:
242
+ progress = int((start_page / min(total_pages, 25)) * 100)
243
+ analysis_status[pdf_id]["progress"] = progress
244
+ logger.info(f"๋ฐฐ์น˜ ๋ถ„์„ ์ค‘: {start_page + 1}ํŽ˜์ด์ง€๋ถ€ํ„ฐ (์ง„ํ–‰๋ฅ : {progress}%)")
245
+
246
+ # ๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰
247
+ loop = asyncio.get_event_loop()
248
+ batch_result = await loop.run_in_executor(
249
+ None,
250
+ analyze_batch_pages_sync,
251
+ pdf_path,
252
+ start_page,
253
+ batch_size
254
+ )
255
+
256
+ if batch_result:
257
+ all_analyses.append(f"### ํŽ˜์ด์ง€ {start_page + 1}~{min(start_page + batch_size, total_pages)}\n{batch_result}")
258
+
259
+ # API ๋ ˆ์ดํŠธ ๋ฆฌ๋ฐ‹ ๋ฐฉ์ง€
260
+ await asyncio.sleep(2)
261
+
262
+ except Exception as batch_error:
263
+ logger.error(f"๋ฐฐ์น˜ {start_page} ๋ถ„์„ ์˜ค๋ฅ˜: {batch_error}")
264
+ all_analyses.append(f"### ํŽ˜์ด์ง€ {start_page + 1}~{min(start_page + batch_size, total_pages)}\n[๋ถ„์„ ์‹คํŒจ: {str(batch_error)}]")
265
+
266
+ # ์ „์ฒด ๋ถ„์„ ๊ฒฐ๊ณผ ํ•ฉ์น˜๊ธฐ
267
+ combined_analysis = "\n\n".join(all_analyses)
268
+
269
+ # ์„ฑ๊ณตํ•œ ๋ถ„์„์ด ์žˆ๋Š”์ง€ ํ™•์ธ
270
+ successful_analyses = [a for a in all_analyses if "[๋ถ„์„ ์‹คํŒจ:" not in a]
271
+ if not successful_analyses:
272
+ logger.error("๋ชจ๋“  ๋ฐฐ์น˜ ๋ถ„์„ ์‹คํŒจ")
273
+ analysis_status[pdf_id] = {"status": "error", "error": "๋ชจ๋“  ํŽ˜์ด์ง€ ๋ถ„์„ ์‹คํŒจ"}
274
+ return {"error": "PDF ๋ถ„์„์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. API ํ‚ค๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”."}
275
+
276
+ # ์š”์•ฝ ์ƒ์„ฑ
277
+ summary = ""
278
+ if combined_analysis:
279
+ try:
280
+ summary_messages = [
281
+ {
282
+ "role": "system",
283
+ "content": "๋‹ค์Œ PDF ๋ถ„์„ ๋‚ด์šฉ์„ 500์ž ์ด๋‚ด๋กœ ์š”์•ฝํ•ด์ฃผ์„ธ์š”. ํ•ต์‹ฌ ๋‚ด์šฉ๊ณผ ์ฃผ์š” ํ‚ค์›Œ๋“œ๋ฅผ ํฌํ•จํ•ด์ฃผ์„ธ์š”."
284
+ },
285
+ {
286
+ "role": "user",
287
+ "content": combined_analysis[:8000] # ํ† ํฐ ์ œํ•œ
288
+ }
289
+ ]
290
+ summary = call_fireworks_vlm_api(summary_messages, max_tokens=1024, temperature=0.5)
291
+ except Exception as sum_err:
292
+ logger.error(f"์š”์•ฝ ์ƒ์„ฑ ์˜ค๋ฅ˜: {sum_err}")
293
+ summary = combined_analysis[:500] + "..."
294
+
295
+ analysis_data = {
296
+ "pdf_id": pdf_id,
297
+ "total_pages": total_pages,
298
+ "analyzed_pages": min(total_pages, 25),
299
+ "analysis": combined_analysis,
300
+ "summary": summary,
301
+ "created_at": time.time()
302
+ }
303
+
304
+ # ์บ์‹œ์— ์ €์žฅ
305
+ save_analysis_cache(pdf_id, analysis_data)
306
+
307
+ analysis_status[pdf_id] = {"status": "completed", "progress": 100}
308
+ logger.info(f"PDF ๋ถ„์„ ์™„๋ฃŒ: {pdf_id}")
309
+
310
+ return analysis_data
311
+
312
+ except Exception as e:
313
+ logger.error(f"VLM PDF ๋ถ„์„ ์˜ค๋ฅ˜: {e}")
314
+ analysis_status[pdf_id] = {"status": "error", "error": str(e)}
315
+ return {"error": str(e)}
316
+
317
+
318
+ async def run_initial_analysis():
319
+ """์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐ ๋ถ„์„ ์‹คํ–‰"""
320
+ logger.info("์ดˆ๊ธฐ PDF ๋ถ„์„ ์‹œ์ž‘...")
321
+ try:
322
+ result = await analyze_pdf_with_vlm_batched(PROMPT_PDF_ID)
323
+ if "error" in result:
324
+ logger.error(f"์ดˆ๊ธฐ ๋ถ„์„ ์‹คํŒจ: {result['error']}")
325
+ else:
326
+ logger.info("์ดˆ๊ธฐ PDF ๋ถ„์„ ์™„๋ฃŒ!")
327
+ except Exception as e:
328
+ logger.error(f"์ดˆ๊ธฐ ๋ถ„์„ ์˜ˆ์™ธ: {e}")
329
+
330
+
331
+ def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
332
+ try:
333
+ doc = fitz.open(pdf_path)
334
+ chunks = []
335
+ for page_num in range(len(doc)):
336
+ page = doc[page_num]
337
+ text = page.get_text("text")
338
+
339
+ if not text.strip():
340
+ text = page.get_text("blocks")
341
+ if text:
342
+ text = "\n".join([block[4] for block in text if len(block) > 4 and isinstance(block[4], str)])
343
+
344
+ if not text.strip():
345
+ text = f"[ํŽ˜์ด์ง€ {page_num + 1} - ์ด๋ฏธ์ง€ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€]"
346
+
347
+ chunks.append({
348
+ "page": page_num + 1,
349
+ "text": text.strip() if text.strip() else f"[ํŽ˜์ด์ง€ {page_num + 1}]",
350
+ "chunk_id": f"page_{page_num + 1}"
351
+ })
352
+
353
+ doc.close()
354
+ return chunks
355
+ except Exception as e:
356
+ logger.error(f"PDF ํ…์ŠคํŠธ ์ถ”์ถœ ์˜ค๋ฅ˜: {e}")
357
+ return []
358
+
359
+
360
+ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
361
+ """์บ์‹œ๋œ VLM ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์งˆ์˜์‘๋‹ต"""
362
+ try:
363
+ if not HAS_VALID_API_KEY:
364
+ return {
365
+ "error": "Fireworks AI API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.",
366
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ AI ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
367
+ }
368
+
369
+ # ์บ์‹œ๋œ ๋ถ„์„ ๊ฒฐ๊ณผ ํ™•์ธ
370
+ analysis_data = load_analysis_cache(pdf_id)
371
+
372
+ if not analysis_data:
373
+ # ๋ถ„์„ ์ƒํƒœ ํ™•์ธ
374
+ if pdf_id in analysis_status:
375
+ status = analysis_status[pdf_id].get("status")
376
+ if status == "analyzing":
377
+ progress = analysis_status[pdf_id].get("progress", 0)
378
+ return {"error": f"๋ถ„์„ ์ง„ํ–‰ ์ค‘ ({progress}%)", "answer": f"PDF ๋ถ„์„์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค ({progress}%). ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”."}
379
+ elif status == "error":
380
+ return {"error": "๋ถ„์„ ์‹คํŒจ", "answer": f"PDF ๋ถ„์„์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค: {analysis_status[pdf_id].get('error', '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜')}"}
381
+ return {"error": "๋ถ„์„ ๋ฐ์ดํ„ฐ ์—†์Œ", "answer": "PDF๊ฐ€ ์•„์ง ๋ถ„์„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}
382
+
383
+ analysis_text = analysis_data.get("analysis", "")
384
+ total_pages = analysis_data.get("total_pages", 0)
385
+
386
+ if not analysis_text:
387
+ return {"error": "๋ถ„์„ ๋ฐ์ดํ„ฐ ์—†์Œ", "answer": "PDF ๋ถ„์„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}
388
+
389
+ # ์บ์‹œ๋œ ๋ถ„์„ ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์งˆ๋ฌธ์— ๋‹ต๋ณ€
390
+ messages = [
391
+ {
392
+ "role": "system",
393
+ "content": f"""๋‹น์‹ ์€ PDF ๋ฌธ์„œ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
394
+ ์•„๋ž˜๋Š” {total_pages}ํŽ˜์ด์ง€ PDF ๋ฌธ์„œ๋ฅผ VLM์œผ๋กœ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
395
+ ์ด ๋ถ„์„ ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ์ •ํ™•ํ•˜๊ณ  ์นœ์ ˆํ•˜๊ฒŒ ํ•œ๊ตญ์–ด๋กœ ๋‹ต๋ณ€ํ•ด์ฃผ์„ธ์š”.
396
+ ๋‹ต๋ณ€ํ•  ๋•Œ ๊ด€๋ จ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ์žˆ์œผ๋ฉด ์–ธ๊ธ‰ํ•ด์ฃผ์„ธ์š”.
397
+ ๋ถ„์„ ๋‚ด์šฉ์— ์—†๋Š” ์ •๋ณด๋Š” "ํ•ด๋‹น ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ์†”์งํžˆ ๋‹ตํ•ด์ฃผ์„ธ์š”.
398
+
399
+ === PDF ๋ถ„์„ ๊ฒฐ๊ณผ ===
400
+ {analysis_text[:12000]}
401
+ =================="""
402
+ },
403
+ {
404
+ "role": "user",
405
+ "content": query
406
+ }
407
+ ]
408
+
409
+ try:
410
+ answer = call_fireworks_vlm_api(messages, max_tokens=4096, temperature=0.6)
411
+ return {
412
+ "answer": answer,
413
+ "pdf_id": pdf_id,
414
+ "query": query
415
+ }
416
+ except Exception as api_error:
417
+ logger.error(f"Fireworks API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {api_error}")
418
+ error_message = str(api_error)
419
+ return {"error": f"AI ์˜ค๋ฅ˜: {error_message}", "answer": "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}
420
+
421
+ except Exception as e:
422
+ logger.error(f"์งˆ์˜์‘๋‹ต ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
423
+ return {"error": str(e), "answer": "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}
424
+
425
+
426
+ async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
427
+ """์บ์‹œ๋œ VLM ๋ถ„์„ ๊ฒฐ๊ณผ์—์„œ ์š”์•ฝ ์ถ”์ถœ"""
428
+ try:
429
+ if not HAS_VALID_API_KEY:
430
+ return {
431
+ "error": "Fireworks AI API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.",
432
+ "summary": "API ํ‚ค๊ฐ€ ์—†์–ด ์š”์•ฝ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
433
+ }
434
+
435
+ # ์บ์‹œ๋œ ๋ถ„์„ ๊ฒฐ๊ณผ ํ™•์ธ
436
+ analysis_data = load_analysis_cache(pdf_id)
437
+
438
+ if not analysis_data:
439
+ # ๋ถ„์„ ์ƒํƒœ ํ™•์ธ
440
+ if pdf_id in analysis_status:
441
+ status = analysis_status[pdf_id].get("status")
442
+ if status == "analyzing":
443
+ progress = analysis_status[pdf_id].get("progress", 0)
444
+ return {"error": f"๋ถ„์„ ์ง„ํ–‰ ์ค‘", "summary": f"PDF ๋ถ„์„์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค ({progress}%). ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”."}
445
+ elif status == "error":
446
+ return {"error": "๋ถ„์„ ์‹คํŒจ", "summary": f"PDF ๋ถ„์„์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."}
447
+ return {"error": "๋ถ„์„ ๋ฐ์ดํ„ฐ ์—†์Œ", "summary": "PDF๊ฐ€ ์•„์ง ๋ถ„์„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}
448
+
449
+ summary = analysis_data.get("summary", "")
450
+ total_pages = analysis_data.get("total_pages", 0)
451
+ analyzed_pages = analysis_data.get("analyzed_pages", total_pages)
452
+
453
+ if summary:
454
+ return {
455
+ "summary": summary,
456
+ "pdf_id": pdf_id,
457
+ "total_pages": total_pages,
458
+ "analyzed_pages": analyzed_pages
459
+ }
460
+
461
+ # ์š”์•ฝ์ด ์—†์œผ๋ฉด ๋ถ„์„ ๋‚ด์šฉ์—์„œ ์ถ”์ถœ
462
+ analysis_text = analysis_data.get("analysis", "")
463
+ if analysis_text:
464
+ return {
465
+ "summary": analysis_text[:500] + "...",
466
+ "pdf_id": pdf_id,
467
+ "total_pages": total_pages
468
+ }
469
+
470
+ return {"error": "์š”์•ฝ ์—†์Œ", "summary": "์š”์•ฝ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}
471
+
472
+ except Exception as e:
473
+ logger.error(f"PDF ์š”์•ฝ ์ƒ์„ฑ ์˜ค๋ฅ˜: {e}")
474
+ return {
475
+ "error": str(e),
476
+ "summary": "PDF ์š”์•ฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
477
+ }
478
+
479
+
480
+ async def cache_pdf(pdf_path: str):
481
+ try:
482
+ pdf_file = pathlib.Path(pdf_path)
483
+ pdf_name = pdf_file.stem
484
+
485
+ if pdf_name not in cache_locks:
486
+ cache_locks[pdf_name] = threading.Lock()
487
+
488
+ if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
489
+ logger.info(f"PDF {pdf_name} ์ด๋ฏธ ์บ์‹ฑ ์™„๋ฃŒ ๋˜๋Š” ์ง„ํ–‰ ์ค‘")
490
+ return
491
+
492
+ with cache_locks[pdf_name]:
493
+ if pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") in ["processing", "completed"]:
494
+ return
495
+
496
+ pdf_cache[pdf_name] = {"status": "processing", "progress": 0, "pages": []}
497
+
498
+ cache_path = get_cache_path(pdf_name)
499
+ if cache_path.exists():
500
+ try:
501
+ with open(cache_path, "r") as cache_file:
502
+ cached_data = json.load(cache_file)
503
+ if cached_data.get("status") == "completed" and cached_data.get("pages"):
504
+ pdf_cache[pdf_name] = cached_data
505
+ pdf_cache[pdf_name]["status"] = "completed"
506
+ logger.info(f"์บ์‹œ ํŒŒ์ผ์—์„œ {pdf_name} ๋กœ๋“œ ์™„๋ฃŒ")
507
+ return
508
+ except Exception as e:
509
+ logger.error(f"์บ์‹œ ํŒŒ์ผ ๋กœ๋“œ ์‹คํŒจ: {e}")
510
+
511
+ doc = fitz.open(pdf_path)
512
+ total_pages = doc.page_count
513
+
514
+ if total_pages > 0:
515
+ page = doc[0]
516
+ pix_thumb = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2))
517
+ thumb_data = pix_thumb.tobytes("png")
518
+ b64_thumb = base64.b64encode(thumb_data).decode('utf-8')
519
+ thumb_src = f"data:image/png;base64,{b64_thumb}"
520
+ pdf_cache[pdf_name]["pages"] = [{"thumb": thumb_src, "src": ""}]
521
+ pdf_cache[pdf_name]["progress"] = 1
522
+ pdf_cache[pdf_name]["total_pages"] = total_pages
523
+
524
+ scale_factor = 1.0
525
+ jpeg_quality = 80
526
+
527
+ def process_page(page_num):
528
+ try:
529
+ page = doc[page_num]
530
+ pix = page.get_pixmap(matrix=fitz.Matrix(scale_factor, scale_factor))
531
+ img_data = pix.tobytes("jpeg", jpeg_quality)
532
+ b64_img = base64.b64encode(img_data).decode('utf-8')
533
+ img_src = f"data:image/jpeg;base64,{b64_img}"
534
+ thumb_src = "" if page_num > 0 else pdf_cache[pdf_name]["pages"][0]["thumb"]
535
+ return {
536
+ "page_num": page_num,
537
+ "src": img_src,
538
+ "thumb": thumb_src
539
+ }
540
+ except Exception as e:
541
+ logger.error(f"ํŽ˜์ด์ง€ {page_num} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {e}")
542
+ return {
543
+ "page_num": page_num,
544
+ "src": "",
545
+ "thumb": "",
546
+ "error": str(e)
547
+ }
548
+
549
+ pages = [None] * total_pages
550
+ processed_count = 0
551
+ batch_size = 5
552
+
553
+ for batch_start in range(0, total_pages, batch_size):
554
+ batch_end = min(batch_start + batch_size, total_pages)
555
+ current_batch = list(range(batch_start, batch_end))
556
+
557
+ with concurrent.futures.ThreadPoolExecutor(max_workers=min(5, batch_size)) as executor:
558
+ batch_results = list(executor.map(process_page, current_batch))
559
+
560
+ for result in batch_results:
561
+ page_num = result["page_num"]
562
+ pages[page_num] = {
563
+ "src": result["src"],
564
+ "thumb": result["thumb"]
565
+ }
566
+ processed_count += 1
567
+ progress = round(processed_count / total_pages * 100)
568
+ pdf_cache[pdf_name]["progress"] = progress
569
+
570
+ pdf_cache[pdf_name]["pages"] = pages
571
+
572
+ pdf_cache[pdf_name] = {
573
+ "status": "completed",
574
+ "progress": 100,
575
+ "pages": pages,
576
+ "total_pages": total_pages
577
+ }
578
+
579
+ try:
580
+ with open(cache_path, "w") as cache_file:
581
+ json.dump(pdf_cache[pdf_name], cache_file)
582
+ logger.info(f"PDF {pdf_name} ์บ์‹ฑ ์™„๋ฃŒ, {total_pages}ํŽ˜์ด์ง€")
583
+ except Exception as e:
584
+ logger.error(f"์ตœ์ข… ์บ์‹œ ์ €์žฅ ์‹คํŒจ: {e}")
585
+ except Exception as e:
586
+ import traceback
587
+ logger.error(f"PDF ์บ์‹ฑ ์˜ค๋ฅ˜: {str(e)}\n{traceback.format_exc()}")
588
+ if 'pdf_name' in locals() and pdf_name in pdf_cache:
589
+ pdf_cache[pdf_name]["status"] = "error"
590
+ pdf_cache[pdf_name]["error"] = str(e)
591
+
592
+
593
+ @app.on_event("startup")
594
+ async def startup_event():
595
+ if PROMPT_PDF_PATH.exists():
596
+ logger.info(f"prompt.pdf ํŒŒ์ผ ๋ฐœ๊ฒฌ: {PROMPT_PDF_PATH}")
597
+ # ํ”Œ๋ฆฝ๋ถ ์บ์‹ฑ
598
+ asyncio.create_task(cache_pdf(str(PROMPT_PDF_PATH)))
599
+ # VLM ๋ถ„์„ - ์—๋Ÿฌ ํ•ธ๋“ค๋ง ํฌํ•จ
600
+ asyncio.create_task(run_initial_analysis())
601
+ else:
602
+ logger.warning(f"prompt.pdf ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {PROMPT_PDF_PATH}")
603
+
604
+
605
+ @app.get("/api/pdf-info")
606
+ async def get_pdf_info():
607
+ if not PROMPT_PDF_PATH.exists():
608
+ return {"exists": False, "error": "PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
609
+
610
+ pdf_name = PROMPT_PDF_PATH.stem
611
+ is_cached = pdf_name in pdf_cache and pdf_cache[pdf_name].get("status") == "completed"
612
+
613
+ # VLM ๋ถ„์„ ์บ์‹œ ํ™•์ธ
614
+ analysis_cached = load_analysis_cache(PROMPT_PDF_ID) is not None
615
+
616
+ return {
617
+ "path": str(PROMPT_PDF_PATH),
618
+ "name": pdf_name,
619
+ "id": PROMPT_PDF_ID,
620
+ "exists": True,
621
+ "cached": is_cached,
622
+ "analysis_cached": analysis_cached
623
+ }
624
+
625
+
626
+ @app.get("/api/analysis-status")
627
+ async def get_analysis_status():
628
+ """VLM ๋ถ„์„ ์ƒํƒœ ํ™•์ธ"""
629
+ # ๋จผ์ € ์บ์‹œ ํŒŒ์ผ ํ™•์ธ
630
+ cached = load_analysis_cache(PROMPT_PDF_ID)
631
+ if cached:
632
+ return {
633
+ "status": "completed",
634
+ "total_pages": cached.get("total_pages", 0),
635
+ "analyzed_pages": cached.get("analyzed_pages", 0),
636
+ "created_at": cached.get("created_at", 0)
637
+ }
638
+
639
+ # ๋ฉ”๏ฟฝ๏ฟฝ๏ฟฝ๋ฆฌ ์ƒํƒœ ํ™•์ธ
640
+ if PROMPT_PDF_ID in analysis_status:
641
+ status_info = analysis_status[PROMPT_PDF_ID]
642
+ return {
643
+ "status": status_info.get("status", "unknown"),
644
+ "progress": status_info.get("progress", 0),
645
+ "error": status_info.get("error")
646
+ }
647
+
648
+ return {"status": "not_started"}
649
+
650
+
651
+ @app.post("/api/reanalyze-pdf")
652
+ async def reanalyze_pdf():
653
+ """PDF ์žฌ๋ถ„์„ (์บ์‹œ ๋ฌด์‹œ)"""
654
+ try:
655
+ if not HAS_VALID_API_KEY:
656
+ return JSONResponse(content={"error": "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}, status_code=400)
657
+
658
+ # ๊ธฐ์กด ์บ์‹œ ์‚ญ์ œ
659
+ cache_path = get_analysis_cache_path(PROMPT_PDF_ID)
660
+ if cache_path.exists():
661
+ cache_path.unlink()
662
+ logger.info("๊ธฐ์กด VLM ๋ถ„์„ ์บ์‹œ ์‚ญ์ œ")
663
+
664
+ # ์ƒํƒœ ์ดˆ๊ธฐํ™”
665
+ if PROMPT_PDF_ID in analysis_status:
666
+ del analysis_status[PROMPT_PDF_ID]
667
+
668
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์žฌ๋ถ„์„ ์‹œ์ž‘
669
+ asyncio.create_task(run_initial_analysis())
670
+
671
+ return {"status": "started", "message": "PDF ์žฌ๋ถ„์„์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค."}
672
+ except Exception as e:
673
+ logger.error(f"์žฌ๋ถ„์„ ์‹œ์ž‘ ์˜ค๋ฅ˜: {e}")
674
+ return JSONResponse(content={"error": str(e)}, status_code=500)
675
+
676
+
677
+ @app.get("/api/pdf-thumbnail")
678
+ async def get_pdf_thumbnail():
679
+ try:
680
+ if not PROMPT_PDF_PATH.exists():
681
+ return {"thumbnail": None, "error": "PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}
682
+
683
+ pdf_name = PROMPT_PDF_PATH.stem
684
+ if pdf_name in pdf_cache and pdf_cache[pdf_name].get("pages"):
685
+ if pdf_cache[pdf_name]["pages"][0].get("thumb"):
686
+ return {"thumbnail": pdf_cache[pdf_name]["pages"][0]["thumb"]}
687
+
688
+ doc = fitz.open(str(PROMPT_PDF_PATH))
689
+ if doc.page_count > 0:
690
+ page = doc[0]
691
+ pix = page.get_pixmap(matrix=fitz.Matrix(0.2, 0.2))
692
+ img_data = pix.tobytes("jpeg", 70)
693
+ b64_img = base64.b64encode(img_data).decode('utf-8')
694
+ asyncio.create_task(cache_pdf(str(PROMPT_PDF_PATH)))
695
+ return {"thumbnail": f"data:image/jpeg;base64,{b64_img}"}
696
+ return {"thumbnail": None}
697
+ except Exception as e:
698
+ logger.error(f"์ธ๋„ค์ผ ์ƒ์„ฑ ์˜ค๋ฅ˜: {str(e)}")
699
+ return {"error": str(e), "thumbnail": None}
700
+
701
+
702
+ @app.get("/api/cache-status")
703
+ async def get_cache_status():
704
+ pdf_name = PROMPT_PDF_PATH.stem
705
+ if pdf_name in pdf_cache:
706
+ return pdf_cache[pdf_name]
707
+ return {"status": "not_cached"}
708
+
709
+
710
+ @app.post("/api/ai/query-pdf")
711
+ async def api_query_pdf(query: Dict[str, str]):
712
+ try:
713
+ user_query = query.get("query", "")
714
+ if not user_query:
715
+ return JSONResponse(content={"error": "์งˆ๋ฌธ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"}, status_code=400)
716
+
717
+ if not PROMPT_PDF_PATH.exists():
718
+ return JSONResponse(content={"error": "PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}, status_code=404)
719
+
720
+ result = await query_pdf(PROMPT_PDF_ID, user_query)
721
+ if "answer" in result:
722
+ return result
723
+ if "error" in result:
724
+ return JSONResponse(content=result, status_code=200)
725
+ return result
726
+ except Exception as e:
727
+ logger.error(f"์งˆ์˜์‘๋‹ต API ์˜ค๋ฅ˜: {e}")
728
+ return JSONResponse(content={"error": str(e), "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}, status_code=200)
729
+
730
+
731
+ @app.get("/api/ai/summarize-pdf")
732
+ async def api_summarize_pdf():
733
+ try:
734
+ if not PROMPT_PDF_PATH.exists():
735
+ return JSONResponse(content={"error": "PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"}, status_code=404)
736
+
737
+ result = await summarize_pdf(PROMPT_PDF_ID)
738
+ if "summary" in result:
739
+ return result
740
+ if "error" in result:
741
+ return JSONResponse(content=result, status_code=200)
742
+ return result
743
+ except Exception as e:
744
+ logger.error(f"PDF ์š”์•ฝ API ์˜ค๋ฅ˜: {e}")
745
+ return JSONResponse(content={"error": str(e), "summary": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์š”์•ฝ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}, status_code=200)
746
+
747
+
748
+ @app.get("/api/cached-pdf")
749
+ async def get_cached_pdf(background_tasks: BackgroundTasks):
750
+ try:
751
+ pdf_name = PROMPT_PDF_PATH.stem
752
+ if pdf_name in pdf_cache:
753
+ status = pdf_cache[pdf_name].get("status", "")
754
+ if status == "completed":
755
+ return pdf_cache[pdf_name]
756
+ elif status == "processing":
757
+ progress = pdf_cache[pdf_name].get("progress", 0)
758
+ pages = pdf_cache[pdf_name].get("pages", [])
759
+ total_pages = pdf_cache[pdf_name].get("total_pages", 0)
760
+ return {
761
+ "status": "processing",
762
+ "progress": progress,
763
+ "pages": pages,
764
+ "total_pages": total_pages,
765
+ "available_pages": len([p for p in pages if p and p.get("src")])
766
+ }
767
+
768
+ background_tasks.add_task(cache_pdf, str(PROMPT_PDF_PATH))
769
+ return {"status": "started", "progress": 0}
770
+ except Exception as e:
771
+ logger.error(f"์บ์‹œ๋œ PDF ์ œ๊ณต ์˜ค๋ฅ˜: {str(e)}")
772
+ return {"error": str(e), "status": "error"}
773
+
774
+
775
+ @app.get("/", response_class=HTMLResponse)
776
+ async def root():
777
+ return get_html_content()
778
+
779
+
780
+ def get_html_content():
781
+ return HTMLResponse(content=HTML)
782
+
783
+
784
+ HTML = """
785
+ <!doctype html>
786
+ <html lang="ko">
787
+ <head>
788
+ <meta charset="utf-8">
789
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
790
+ <title>๐ŸŽจ AI ํ”Œ๋ฆฝ๋ถ - Comic Style</title>
791
+ <link rel="stylesheet" href="/static/flipbook.css">
792
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
793
+ <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap" rel="stylesheet">
794
+ <script src="/static/three.js"></script>
795
+ <script src="/static/iscroll.js"></script>
796
+ <script src="/static/mark.js"></script>
797
+ <script src="/static/mod3d.js"></script>
798
+ <script src="/static/pdf.js"></script>
799
+ <script src="/static/flipbook.js"></script>
800
+ <script src="/static/flipbook.book3.js"></script>
801
+ <script src="/static/flipbook.scroll.js"></script>
802
+ <script src="/static/flipbook.swipe.js"></script>
803
+ <script src="/static/flipbook.webgl.js"></script>
804
+ <style>
805
+ /* ============================================
806
+ Comic Style CSS - Z-Image Style Applied
807
+ ============================================ */
808
+ :root {
809
+ --primary-color: #3B82F6;
810
+ --secondary-color: #8B5CF6;
811
+ --accent-color: #FACC15;
812
+ --ai-color: #10B981;
813
+ --ai-hover: #059669;
814
+ --bg-yellow: #FEF9C3;
815
+ --text-dark: #1F2937;
816
+ --card-bg: #ffffff;
817
+ --shadow-comic: 5px 5px 0 #1F2937;
818
+ --shadow-lg: 8px 8px 0 #1F2937;
819
+ --border-comic: 3px solid #1F2937;
820
+ --radius-sm: 8px;
821
+ --radius-md: 12px;
822
+ --transition: all 0.2s ease;
823
+ }
824
+
825
+ * {
826
+ box-sizing: border-box;
827
+ }
828
+
829
+ body {
830
+ margin: 0;
831
+ font-family: 'Comic Neue', cursive, sans-serif;
832
+ color: var(--text-dark);
833
+ background-color: var(--bg-yellow);
834
+ background-image: radial-gradient(#1F2937 1px, transparent 1px);
835
+ background-size: 20px 20px;
836
+ background-attachment: fixed;
837
+ min-height: 100vh;
838
+ }
839
+
840
+ /* Header Info - Compact Comic Style */
841
+ .header-info {
842
+ position: fixed;
843
+ top: 10px;
844
+ left: 50%;
845
+ transform: translateX(-50%);
846
+ text-align: center;
847
+ z-index: 100;
848
+ background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
849
+ border: 3px solid #1F2937;
850
+ padding: 8px 25px;
851
+ border-radius: 10px;
852
+ box-shadow: 4px 4px 0 #1F2937;
853
+ }
854
+
855
+ .header-info .title {
856
+ font-family: 'Bangers', cursive;
857
+ font-size: 1.3rem;
858
+ color: #FFF;
859
+ text-shadow: 2px 2px 0 #1F2937;
860
+ letter-spacing: 2px;
861
+ margin: 0;
862
+ }
863
+
864
+ /* Floating AI Button - Comic Style */
865
+ .floating-ai {
866
+ position: fixed;
867
+ top: 10px;
868
+ right: 20px;
869
+ width: 50px;
870
+ height: 50px;
871
+ border-radius: 50%;
872
+ background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
873
+ border: 3px solid #1F2937;
874
+ box-shadow: 4px 4px 0 #1F2937;
875
+ z-index: 9999;
876
+ display: flex;
877
+ justify-content: center;
878
+ align-items: center;
879
+ cursor: pointer;
880
+ transition: var(--transition);
881
+ overflow: hidden;
882
+ }
883
+
884
+ .floating-ai:hover {
885
+ transform: translate(-2px, -2px);
886
+ box-shadow: 6px 6px 0 #1F2937;
887
+ }
888
+
889
+ .floating-ai:active {
890
+ transform: translate(2px, 2px);
891
+ box-shadow: 2px 2px 0 #1F2937;
892
+ }
893
+
894
+ .floating-ai .icon {
895
+ display: flex;
896
+ justify-content: center;
897
+ align-items: center;
898
+ width: 100%;
899
+ height: 100%;
900
+ font-size: 20px;
901
+ color: white;
902
+ text-shadow: 1px 1px 0 #1F2937;
903
+ transition: var(--transition);
904
+ }
905
+
906
+ .floating-ai .ai-title {
907
+ position: absolute;
908
+ right: 58px;
909
+ background: #FFF;
910
+ padding: 8px 14px;
911
+ border-radius: 8px;
912
+ border: 2px solid #1F2937;
913
+ box-shadow: 3px 3px 0 #1F2937;
914
+ font-family: 'Bangers', cursive;
915
+ font-size: 0.95rem;
916
+ letter-spacing: 1px;
917
+ white-space: nowrap;
918
+ pointer-events: none;
919
+ opacity: 0;
920
+ transform: translateX(10px);
921
+ transition: all 0.3s ease;
922
+ color: var(--text-dark);
923
+ }
924
+
925
+ .floating-ai:hover .ai-title {
926
+ opacity: 1;
927
+ transform: translateX(0);
928
+ }
929
+
930
+ /* Viewer Container - Comic Style */
931
+ #viewer {
932
+ width: 94%;
933
+ height: 90vh;
934
+ max-width: 94%;
935
+ margin: 0;
936
+ background: var(--card-bg);
937
+ border: 4px solid #1F2937;
938
+ border-radius: var(--radius-md);
939
+ position: fixed;
940
+ top: 50%;
941
+ left: 50%;
942
+ transform: translate(-50%, -50%);
943
+ z-index: 1000;
944
+ box-shadow: var(--shadow-lg);
945
+ overflow: hidden;
946
+ }
947
+
948
+ .flipbook-container .fb3d-menu-bar {
949
+ z-index: 2000 !important;
950
+ opacity: 1 !important;
951
+ bottom: 0 !important;
952
+ background: linear-gradient(135deg, #FACC15 0%, #F59E0B 100%) !important;
953
+ border-top: 3px solid #1F2937 !important;
954
+ border-radius: 0 0 var(--radius-md) var(--radius-md) !important;
955
+ padding: 12px 0 !important;
956
+ }
957
+
958
+ .flipbook-container .fb3d-menu-bar > ul > li > img,
959
+ .flipbook-container .fb3d-menu-bar > ul > li > div {
960
+ opacity: 1 !important;
961
+ transform: scale(1.2) !important;
962
+ filter: drop-shadow(2px 2px 0 #1F2937) !important;
963
+ }
964
+
965
+ .flipbook-container .fb3d-menu-bar > ul > li {
966
+ margin: 0 12px !important;
967
+ }
968
+
969
+ .flipbook-container .fb3d-menu-bar > ul > li > span {
970
+ background: #FFF !important;
971
+ color: #1F2937 !important;
972
+ border: 2px solid #1F2937 !important;
973
+ border-radius: var(--radius-sm) !important;
974
+ padding: 8px 12px !important;
975
+ font-size: 13px !important;
976
+ bottom: 55px !important;
977
+ font-family: 'Comic Neue', cursive !important;
978
+ font-weight: 700 !important;
979
+ box-shadow: 3px 3px 0 #1F2937 !important;
980
+ }
981
+
982
+ @keyframes spin {
983
+ 0% { transform: rotate(0deg); }
984
+ 100% { transform: rotate(360deg); }
985
+ }
986
+
987
+ /* Loading Container - Comic Style */
988
+ .loading-container {
989
+ position: fixed;
990
+ top: 50%;
991
+ left: 50%;
992
+ transform: translate(-50%, -50%);
993
+ text-align: center;
994
+ background: #FFF;
995
+ border: 4px solid #1F2937;
996
+ padding: 40px;
997
+ border-radius: var(--radius-md);
998
+ box-shadow: var(--shadow-lg);
999
+ z-index: 9999;
1000
+ }
1001
+
1002
+ .loading-spinner {
1003
+ border: 5px solid #FEF9C3;
1004
+ border-top: 5px solid #3B82F6;
1005
+ border-radius: 50%;
1006
+ width: 55px;
1007
+ height: 55px;
1008
+ margin: 0 auto;
1009
+ animation: spin 1s linear infinite;
1010
+ }
1011
+
1012
+ .loading-text {
1013
+ margin-top: 20px;
1014
+ font-family: 'Bangers', cursive;
1015
+ font-size: 1.3rem;
1016
+ color: var(--text-dark);
1017
+ letter-spacing: 1px;
1018
+ }
1019
+
1020
+ .progress-bar-container {
1021
+ width: 220px;
1022
+ height: 20px;
1023
+ background: #FEF9C3;
1024
+ border: 3px solid #1F2937;
1025
+ border-radius: 10px;
1026
+ margin-top: 15px;
1027
+ overflow: hidden;
1028
+ }
1029
+
1030
+ .progress-bar {
1031
+ height: 100%;
1032
+ background: linear-gradient(to right, #3B82F6, #8B5CF6);
1033
+ border-radius: 7px;
1034
+ transition: width 0.3s ease;
1035
+ }
1036
+
1037
+ .loading-pages {
1038
+ position: fixed;
1039
+ bottom: 20px;
1040
+ left: 50%;
1041
+ transform: translateX(-50%);
1042
+ background: #FFF;
1043
+ border: 3px solid #1F2937;
1044
+ padding: 12px 25px;
1045
+ border-radius: 25px;
1046
+ box-shadow: 4px 4px 0 #1F2937;
1047
+ font-family: 'Comic Neue', cursive;
1048
+ font-size: 14px;
1049
+ font-weight: 700;
1050
+ color: var(--text-dark);
1051
+ z-index: 9998;
1052
+ }
1053
+
1054
+ /* AI Chat Container - Comic Style */
1055
+ #aiChatContainer {
1056
+ position: fixed;
1057
+ top: 0;
1058
+ right: 0;
1059
+ width: 420px;
1060
+ height: 100%;
1061
+ background: #FFF;
1062
+ border-left: 4px solid #1F2937;
1063
+ box-shadow: -8px 0 0 #1F2937;
1064
+ z-index: 10000;
1065
+ transform: translateX(100%);
1066
+ transition: transform 0.3s ease;
1067
+ display: flex;
1068
+ flex-direction: column;
1069
+ }
1070
+
1071
+ #aiChatContainer.active {
1072
+ transform: translateX(0);
1073
+ }
1074
+
1075
+ #aiChatHeader {
1076
+ display: flex;
1077
+ justify-content: space-between;
1078
+ align-items: center;
1079
+ padding: 20px;
1080
+ border-bottom: 4px solid #1F2937;
1081
+ background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
1082
+ }
1083
+
1084
+ #aiChatHeader h3 {
1085
+ margin: 0;
1086
+ color: white;
1087
+ font-family: 'Bangers', cursive;
1088
+ font-size: 1.5rem;
1089
+ letter-spacing: 2px;
1090
+ text-shadow: 2px 2px 0 #1F2937;
1091
+ display: flex;
1092
+ align-items: center;
1093
+ }
1094
+
1095
+ #aiChatHeader h3 i {
1096
+ margin-right: 10px;
1097
+ }
1098
+
1099
+ #aiChatClose {
1100
+ background: #FFF;
1101
+ border: 3px solid #1F2937;
1102
+ cursor: pointer;
1103
+ font-size: 18px;
1104
+ color: #1F2937;
1105
+ width: 40px;
1106
+ height: 40px;
1107
+ border-radius: 8px;
1108
+ display: flex;
1109
+ align-items: center;
1110
+ justify-content: center;
1111
+ box-shadow: 3px 3px 0 #1F2937;
1112
+ transition: var(--transition);
1113
+ }
1114
+
1115
+ #aiChatClose:hover {
1116
+ background: #FACC15;
1117
+ transform: translate(-2px, -2px);
1118
+ box-shadow: 5px 5px 0 #1F2937;
1119
+ }
1120
+
1121
+ #aiChatMessages {
1122
+ flex: 1;
1123
+ overflow-y: auto;
1124
+ padding: 20px;
1125
+ background: #FEF9C3;
1126
+ }
1127
+
1128
+ .chat-message {
1129
+ margin-bottom: 15px;
1130
+ display: flex;
1131
+ align-items: flex-start;
1132
+ }
1133
+
1134
+ .chat-message.user {
1135
+ flex-direction: row-reverse;
1136
+ }
1137
+
1138
+ .chat-avatar {
1139
+ width: 40px;
1140
+ height: 40px;
1141
+ border-radius: 8px;
1142
+ border: 3px solid #1F2937;
1143
+ display: flex;
1144
+ justify-content: center;
1145
+ align-items: center;
1146
+ flex-shrink: 0;
1147
+ box-shadow: 2px 2px 0 #1F2937;
1148
+ }
1149
+
1150
+ .chat-message.user .chat-avatar {
1151
+ margin-left: 10px;
1152
+ background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 100%);
1153
+ color: white;
1154
+ }
1155
+
1156
+ .chat-message.ai .chat-avatar {
1157
+ margin-right: 10px;
1158
+ background: linear-gradient(135deg, #10B981 0%, #059669 100%);
1159
+ color: white;
1160
+ }
1161
+
1162
+ .chat-bubble {
1163
+ max-width: 75%;
1164
+ }
1165
+
1166
+ .chat-content {
1167
+ padding: 14px 18px;
1168
+ border-radius: 12px;
1169
+ border: 3px solid #1F2937;
1170
+ word-break: break-word;
1171
+ font-family: 'Comic Neue', cursive;
1172
+ font-size: 14px;
1173
+ font-weight: 700;
1174
+ line-height: 1.6;
1175
+ box-shadow: 3px 3px 0 #1F2937;
1176
+ }
1177
+
1178
+ .chat-message.user .chat-content {
1179
+ background: linear-gradient(135deg, #8B5CF6 0%, #A855F7 100%);
1180
+ color: white;
1181
+ border-bottom-right-radius: 4px;
1182
+ }
1183
+
1184
+ .chat-message.ai .chat-content {
1185
+ background: #FFF;
1186
+ color: #1F2937;
1187
+ border-bottom-left-radius: 4px;
1188
+ }
1189
+
1190
+ .chat-time {
1191
+ font-size: 11px;
1192
+ color: #6B7280;
1193
+ margin-top: 6px;
1194
+ text-align: right;
1195
+ font-weight: 700;
1196
+ }
1197
+
1198
+ .chat-message.ai .chat-time {
1199
+ text-align: left;
1200
+ }
1201
+
1202
+ #aiChatForm {
1203
+ display: flex;
1204
+ padding: 15px 20px;
1205
+ border-top: 4px solid #1F2937;
1206
+ background: #FFF;
1207
+ gap: 10px;
1208
+ }
1209
+
1210
+ #aiChatInput {
1211
+ flex: 1;
1212
+ padding: 14px 20px;
1213
+ border: 3px solid #1F2937;
1214
+ border-radius: 25px;
1215
+ font-family: 'Comic Neue', cursive;
1216
+ font-size: 14px;
1217
+ font-weight: 700;
1218
+ outline: none;
1219
+ transition: var(--transition);
1220
+ background: #FEF9C3;
1221
+ }
1222
+
1223
+ #aiChatInput:focus {
1224
+ border-color: #3B82F6;
1225
+ box-shadow: 3px 3px 0 #3B82F6;
1226
+ }
1227
+
1228
+ #aiChatSubmit {
1229
+ background: linear-gradient(135deg, #EF4444 0%, #F97316 100%);
1230
+ border: 3px solid #1F2937;
1231
+ color: white;
1232
+ width: 50px;
1233
+ height: 50px;
1234
+ border-radius: 50%;
1235
+ display: flex;
1236
+ justify-content: center;
1237
+ align-items: center;
1238
+ cursor: pointer;
1239
+ box-shadow: 3px 3px 0 #1F2937;
1240
+ transition: var(--transition);
1241
+ }
1242
+
1243
+ #aiChatSubmit:hover {
1244
+ transform: translate(-2px, -2px);
1245
+ box-shadow: 5px 5px 0 #1F2937;
1246
+ }
1247
+
1248
+ #aiChatSubmit:active {
1249
+ transform: translate(2px, 2px);
1250
+ box-shadow: 1px 1px 0 #1F2937;
1251
+ }
1252
+
1253
+ #aiChatSubmit:disabled {
1254
+ background: #9CA3AF;
1255
+ cursor: not-allowed;
1256
+ transform: none;
1257
+ box-shadow: 3px 3px 0 #1F2937;
1258
+ }
1259
+
1260
+ .typing-indicator {
1261
+ display: flex;
1262
+ align-items: center;
1263
+ padding: 10px;
1264
+ }
1265
+
1266
+ .typing-indicator .chat-avatar {
1267
+ margin-right: 10px;
1268
+ background: linear-gradient(135deg, #10B981 0%, #059669 100%);
1269
+ color: white;
1270
+ }
1271
+
1272
+ .typing-indicator span {
1273
+ height: 10px;
1274
+ width: 10px;
1275
+ background: #10B981;
1276
+ border-radius: 50%;
1277
+ display: inline-block;
1278
+ margin-right: 5px;
1279
+ border: 2px solid #1F2937;
1280
+ animation: typing 1s infinite;
1281
+ }
1282
+
1283
+ .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
1284
+ .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
1285
+
1286
+ @keyframes typing {
1287
+ 0%, 100% { transform: translateY(0); }
1288
+ 50% { transform: translateY(-8px); }
1289
+ }
1290
+
1291
+ .error-container {
1292
+ position: fixed;
1293
+ top: 50%;
1294
+ left: 50%;
1295
+ transform: translate(-50%, -50%);
1296
+ text-align: center;
1297
+ background: #FFF;
1298
+ border: 4px solid #1F2937;
1299
+ padding: 40px;
1300
+ border-radius: var(--radius-md);
1301
+ box-shadow: var(--shadow-lg);
1302
+ z-index: 9999;
1303
+ }
1304
+
1305
+ .error-container i {
1306
+ font-size: 50px;
1307
+ color: #EF4444;
1308
+ margin-bottom: 20px;
1309
+ text-shadow: 3px 3px 0 #1F2937;
1310
+ }
1311
+
1312
+ .error-container p {
1313
+ font-family: 'Comic Neue', cursive;
1314
+ font-size: 16px;
1315
+ font-weight: 700;
1316
+ color: var(--text-dark);
1317
+ margin-bottom: 20px;
1318
+ }
1319
+
1320
+ .error-container button {
1321
+ padding: 12px 30px;
1322
+ background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
1323
+ color: white;
1324
+ border: 3px solid #1F2937;
1325
+ border-radius: 8px;
1326
+ cursor: pointer;
1327
+ font-family: 'Bangers', cursive;
1328
+ font-size: 1.2rem;
1329
+ letter-spacing: 1px;
1330
+ box-shadow: 4px 4px 0 #1F2937;
1331
+ transition: var(--transition);
1332
+ }
1333
+
1334
+ .error-container button:hover {
1335
+ transform: translate(-2px, -2px);
1336
+ box-shadow: 6px 6px 0 #1F2937;
1337
+ }
1338
+
1339
+ /* Footer - Comic Style */
1340
+ .footer-comic {
1341
+ position: fixed;
1342
+ bottom: 10px;
1343
+ right: 10px;
1344
+ text-align: center;
1345
+ padding: 10px 20px;
1346
+ background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
1347
+ border: 3px solid #1F2937;
1348
+ border-radius: 10px;
1349
+ box-shadow: 4px 4px 0 #1F2937;
1350
+ z-index: 100;
1351
+ }
1352
+
1353
+ .footer-comic p {
1354
+ font-family: 'Comic Neue', cursive;
1355
+ color: #FFF;
1356
+ margin: 3px 0;
1357
+ font-weight: 700;
1358
+ font-size: 0.85rem;
1359
+ }
1360
+
1361
+ .footer-comic a {
1362
+ color: #FACC15;
1363
+ text-decoration: none;
1364
+ font-weight: 700;
1365
+ }
1366
+
1367
+ .footer-comic a:hover {
1368
+ text-decoration: underline;
1369
+ }
1370
+
1371
+ /* Scrollbar - Comic Style */
1372
+ ::-webkit-scrollbar {
1373
+ width: 14px;
1374
+ height: 14px;
1375
+ }
1376
+
1377
+ ::-webkit-scrollbar-track {
1378
+ background: #FEF9C3;
1379
+ border: 2px solid #1F2937;
1380
+ }
1381
+
1382
+ ::-webkit-scrollbar-thumb {
1383
+ background: linear-gradient(135deg, #3B82F6, #8B5CF6);
1384
+ border: 2px solid #1F2937;
1385
+ border-radius: 7px;
1386
+ }
1387
+
1388
+ ::-webkit-scrollbar-thumb:hover {
1389
+ background: linear-gradient(135deg, #EF4444, #F97316);
1390
+ }
1391
+
1392
+ ::selection {
1393
+ background: #FACC15;
1394
+ color: #1F2937;
1395
+ }
1396
+
1397
+ @media (max-width: 768px) {
1398
+ .header-info {
1399
+ top: 8px;
1400
+ padding: 6px 15px;
1401
+ max-width: 70%;
1402
+ }
1403
+
1404
+ .header-info .title {
1405
+ font-size: 1.1rem;
1406
+ }
1407
+
1408
+ .floating-ai {
1409
+ width: 45px;
1410
+ height: 45px;
1411
+ top: 8px;
1412
+ right: 10px;
1413
+ }
1414
+
1415
+ .floating-ai .icon {
1416
+ font-size: 18px;
1417
+ }
1418
+
1419
+ #aiChatContainer {
1420
+ width: 100%;
1421
+ }
1422
+
1423
+ #viewer {
1424
+ width: 98%;
1425
+ height: 92vh;
1426
+ top: 50%;
1427
+ }
1428
+
1429
+ .footer-comic {
1430
+ display: none;
1431
+ }
1432
+ }
1433
+ </style>
1434
+ </head>
1435
+ <body>
1436
+ <!-- Header Info - Compact Comic Style -->
1437
+ <div class="header-info">
1438
+ <div class="title">๐Ÿ“š AI ํ”Œ๋ฆฝ๋ถ</div>
1439
+ </div>
1440
+
1441
+ <!-- Floating AI Button - Comic Style -->
1442
+ <div id="aiButton" class="floating-ai">
1443
+ <div class="icon"><i class="fas fa-robot"></i></div>
1444
+ <div class="ai-title">๐Ÿค– AI ์–ด์‹œ์Šคํ„ดํŠธ</div>
1445
+ </div>
1446
+
1447
+ <!-- AI Chat Container -->
1448
+ <div id="aiChatContainer">
1449
+ <div id="aiChatHeader">
1450
+ <h3><i class="fas fa-robot"></i> AI ์–ด์‹œ์Šคํ„ดํŠธ</h3>
1451
+ <button id="aiChatClose"><i class="fas fa-times"></i></button>
1452
+ </div>
1453
+ <div id="aiChatMessages"></div>
1454
+ <form id="aiChatForm">
1455
+ <input type="text" id="aiChatInput" placeholder="PDF์— ๋Œ€ํ•ด ์งˆ๋ฌธํ•˜์„ธ์š”..." autocomplete="off">
1456
+ <button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button>
1457
+ </form>
1458
+ </div>
1459
+
1460
+ <!-- PDF Viewer -->
1461
+ <div id="viewer"></div>
1462
+ <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0%</span></div>
1463
+
1464
+ <!-- Footer - Comic Style -->
1465
+ <div class="footer-comic">
1466
+ <p style="font-family:'Bangers',cursive;font-size:1.1rem;letter-spacing:1px">๐Ÿ“š AI FLIPBOOK ๐Ÿ“š</p>
1467
+ <p>Powered by VLM + 3D FlipBook</p>
1468
+ <p><a href="https://ginigen.ai" target="_blank">๐Ÿ  ginigen.ai</a></p>
1469
+ </div>
1470
+
1471
+ <script>
1472
+ let fb = null;
1473
+ const viewer = document.getElementById('viewer');
1474
+ pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/pdf.worker.js';
1475
+
1476
+ let pageLoadingInterval = null;
1477
+ let audioInitialized = false;
1478
+ let isAiChatActive = false;
1479
+ let isAiProcessing = false;
1480
+ let hasLoadedSummary = false;
1481
+ let analysisCheckInterval = null;
1482
+
1483
+ function $id(id) { return document.getElementById(id); }
1484
+
1485
+ function formatTime() {
1486
+ const now = new Date();
1487
+ return now.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
1488
+ }
1489
+
1490
+ function initializeAudio() {
1491
+ if (audioInitialized) return Promise.resolve();
1492
+ return new Promise((resolve) => {
1493
+ const audio = new Audio('/static/turnPage2.mp3');
1494
+ audio.volume = 0.01;
1495
+ audio.play().then(() => {
1496
+ audio.pause();
1497
+ audioInitialized = true;
1498
+ resolve();
1499
+ }).catch(() => {
1500
+ const initOnClick = () => {
1501
+ const tempAudio = new Audio('/static/turnPage2.mp3');
1502
+ tempAudio.volume = 0.01;
1503
+ tempAudio.play().then(() => {
1504
+ tempAudio.pause();
1505
+ audioInitialized = true;
1506
+ document.removeEventListener('click', initOnClick);
1507
+ resolve();
1508
+ }).catch(() => {});
1509
+ };
1510
+ document.addEventListener('click', initOnClick, { once: true });
1511
+ resolve();
1512
+ });
1513
+ });
1514
+ }
1515
+
1516
+ function addChatMessage(content, isUser = false) {
1517
+ const messagesContainer = $id('aiChatMessages');
1518
+ const messageElement = document.createElement('div');
1519
+ messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`;
1520
+ messageElement.innerHTML = `
1521
+ <div class="chat-avatar">
1522
+ <i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i>
1523
+ </div>
1524
+ <div class="chat-bubble">
1525
+ <div class="chat-content">${content}</div>
1526
+ <div class="chat-time">${formatTime()}</div>
1527
+ </div>
1528
+ `;
1529
+ messagesContainer.appendChild(messageElement);
1530
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
1531
+ return messageElement;
1532
+ }
1533
+
1534
+ function addTypingIndicator() {
1535
+ const messagesContainer = $id('aiChatMessages');
1536
+ const indicator = document.createElement('div');
1537
+ indicator.className = 'typing-indicator';
1538
+ indicator.id = 'typingIndicator';
1539
+ indicator.innerHTML = `
1540
+ <div class="chat-avatar"><i class="fas fa-robot"></i></div>
1541
+ <div><span></span><span></span><span></span></div>
1542
+ `;
1543
+ messagesContainer.appendChild(indicator);
1544
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
1545
+ return indicator;
1546
+ }
1547
+
1548
+ function removeTypingIndicator() {
1549
+ const indicator = $id('typingIndicator');
1550
+ if (indicator) indicator.remove();
1551
+ }
1552
+
1553
+ function toggleAiChat(show = true) {
1554
+ const container = $id('aiChatContainer');
1555
+ if (show) {
1556
+ container.classList.add('active');
1557
+ isAiChatActive = true;
1558
+ if (!hasLoadedSummary) {
1559
+ loadPdfSummary();
1560
+ }
1561
+ $id('aiChatInput').focus();
1562
+ } else {
1563
+ container.classList.remove('active');
1564
+ isAiChatActive = false;
1565
+ }
1566
+ }
1567
+
1568
+ async function checkAnalysisStatus() {
1569
+ try {
1570
+ const response = await fetch('/api/analysis-status');
1571
+ const data = await response.json();
1572
+ return data;
1573
+ } catch (e) {
1574
+ console.error("๋ถ„์„ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
1575
+ return { status: "error" };
1576
+ }
1577
+ }
1578
+
1579
+ async function loadPdfSummary() {
1580
+ if (isAiProcessing || hasLoadedSummary) return;
1581
+
1582
+ try {
1583
+ isAiProcessing = true;
1584
+ addTypingIndicator();
1585
+
1586
+ // ๋ถ„์„ ์ƒํƒœ ํ™•์ธ
1587
+ const statusData = await checkAnalysisStatus();
1588
+
1589
+ if (statusData.status === 'analyzing') {
1590
+ removeTypingIndicator();
1591
+ const progress = statusData.progress || 0;
1592
+ addChatMessage(`์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ’ฅ ํ˜„์žฌ PDF๋ฅผ AI๊ฐ€ ๋ถ„์„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๐Ÿ“Š<br><br>์ง„ํ–‰๋ฅ : <strong>${progress}%</strong><br><small style="color:#6B7280;">๋ถ„์„์ด ์™„๋ฃŒ๋˜๋ฉด ์ž๋™์œผ๋กœ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.</small>`);
1593
+ hasLoadedSummary = true;
1594
+ isAiProcessing = false;
1595
+
1596
+ // ๋ถ„์„ ์™„๋ฃŒ ํด๋ง
1597
+ startAnalysisPolling();
1598
+ return;
1599
+ }
1600
+
1601
+ if (statusData.status === 'error') {
1602
+ removeTypingIndicator();
1603
+ addChatMessage(`์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ’ฅ PDF ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. โš ๏ธ<br><br><small style="color:#EF4444;">${statusData.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}</small><br><br>ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.`);
1604
+ hasLoadedSummary = true;
1605
+ isAiProcessing = false;
1606
+ return;
1607
+ }
1608
+
1609
+ if (statusData.status === 'not_started') {
1610
+ removeTypingIndicator();
1611
+ addChatMessage(`์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ’ฅ PDF ๋ถ„์„์ด ์•„์ง ์‹œ์ž‘๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๐Ÿ”„<br><br><small style="color:#6B7280;">์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”...</small>`);
1612
+ hasLoadedSummary = true;
1613
+ isAiProcessing = false;
1614
+ startAnalysisPolling();
1615
+ return;
1616
+ }
1617
+
1618
+ // ๋ถ„์„ ์™„๋ฃŒ๋จ - ์š”์•ฝ ๊ฐ€์ ธ์˜ค๊ธฐ
1619
+ const response = await fetch('/api/ai/summarize-pdf');
1620
+ const data = await response.json();
1621
+
1622
+ removeTypingIndicator();
1623
+
1624
+ if (data.summary) {
1625
+ const pageInfo = data.analyzed_pages ? ` (${data.analyzed_pages}/${data.total_pages}ํŽ˜์ด์ง€ ๋ถ„์„์™„๋ฃŒ)` : '';
1626
+ addChatMessage(`์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ’ฅ ์ด PDF์— ๋Œ€ํ•ด ๋ฌด์—‡์ด๋“  ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”.${pageInfo}<br><br><strong>๐Ÿ“„ PDF ์š”์•ฝ:</strong><br>${data.summary}`);
1627
+ } else {
1628
+ addChatMessage("์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ’ฅ PDF์— ๋Œ€ํ•ด ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”. ์ตœ์„ ์„ ๋‹คํ•ด ๋‹ต๋ณ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.");
1629
+ }
1630
+ hasLoadedSummary = true;
1631
+
1632
+ } catch (error) {
1633
+ console.error("PDF ์š”์•ฝ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
1634
+ removeTypingIndicator();
1635
+ addChatMessage("์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ’ฅ PDF์— ๋Œ€ํ•ด ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”.");
1636
+ hasLoadedSummary = true;
1637
+ } finally {
1638
+ isAiProcessing = false;
1639
+ }
1640
+ }
1641
+
1642
+ function startAnalysisPolling() {
1643
+ if (analysisCheckInterval) return;
1644
+
1645
+ analysisCheckInterval = setInterval(async () => {
1646
+ try {
1647
+ const data = await checkAnalysisStatus();
1648
+
1649
+ if (data.status === 'completed') {
1650
+ clearInterval(analysisCheckInterval);
1651
+ analysisCheckInterval = null;
1652
+ addChatMessage(`โœ… PDF ๋ถ„์„์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! (${data.analyzed_pages || data.total_pages}ํŽ˜์ด์ง€)<br>์ด์ œ ์ž์œ ๋กญ๊ฒŒ ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”.`);
1653
+ } else if (data.status === 'analyzing') {
1654
+ // ์ง„ํ–‰๋ฅ  ์—…๋ฐ์ดํŠธ (์„ ํƒ์ )
1655
+ console.log(`๋ถ„์„ ์ง„ํ–‰ ์ค‘: ${data.progress}%`);
1656
+ } else if (data.status === 'error') {
1657
+ clearInterval(analysisCheckInterval);
1658
+ analysisCheckInterval = null;
1659
+ addChatMessage(`โš ๏ธ PDF ๋ถ„์„ ์‹คํŒจ: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}`);
1660
+ }
1661
+ } catch (e) {
1662
+ console.error("ํด๋ง ์˜ค๋ฅ˜:", e);
1663
+ }
1664
+ }, 5000); // 5์ดˆ๋งˆ๋‹ค ํ™•์ธ
1665
+
1666
+ // 5๋ถ„ ํ›„ ์ž๋™ ์ค‘์ง€
1667
+ setTimeout(() => {
1668
+ if (analysisCheckInterval) {
1669
+ clearInterval(analysisCheckInterval);
1670
+ analysisCheckInterval = null;
1671
+ }
1672
+ }, 300000);
1673
+ }
1674
+
1675
+ async function submitQuestion(question) {
1676
+ if (isAiProcessing || !question.trim()) return;
1677
+
1678
+ try {
1679
+ isAiProcessing = true;
1680
+ $id('aiChatSubmit').disabled = true;
1681
+
1682
+ addChatMessage(question, true);
1683
+
1684
+ // ๋ถ„์„ ์ƒํƒœ ํ™•์ธ
1685
+ const statusData = await checkAnalysisStatus();
1686
+
1687
+ if (statusData.status !== 'completed') {
1688
+ if (statusData.status === 'analyzing') {
1689
+ addChatMessage(`PDF ๋ถ„์„์ด ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค (${statusData.progress || 0}%). ์™„๋ฃŒ ํ›„ ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”. โณ`);
1690
+ } else {
1691
+ addChatMessage("PDF ๋ถ„์„์ด ์•„์ง ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”.");
1692
+ }
1693
+ isAiProcessing = false;
1694
+ $id('aiChatSubmit').disabled = false;
1695
+ $id('aiChatInput').value = question;
1696
+ return;
1697
+ }
1698
+
1699
+ addTypingIndicator();
1700
+
1701
+ const response = await fetch('/api/ai/query-pdf', {
1702
+ method: 'POST',
1703
+ headers: { 'Content-Type': 'application/json' },
1704
+ body: JSON.stringify({ query: question }),
1705
+ signal: AbortSignal.timeout(120000)
1706
+ });
1707
+
1708
+ const data = await response.json();
1709
+ removeTypingIndicator();
1710
+
1711
+ if (data.answer) {
1712
+ addChatMessage(data.answer);
1713
+ } else if (data.error) {
1714
+ addChatMessage(`์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ${data.error}`);
1715
+ } else {
1716
+ addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.");
1717
+ }
1718
+ } catch (error) {
1719
+ console.error("์งˆ๋ฌธ ์ œ์ถœ ์˜ค๋ฅ˜:", error);
1720
+ removeTypingIndicator();
1721
+ if (error.name === 'AbortError') {
1722
+ addChatMessage("์‘๋‹ต ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
1723
+ } else {
1724
+ addChatMessage("์„œ๋ฒ„์™€ ํ†ต์‹  ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
1725
+ }
1726
+ } finally {
1727
+ isAiProcessing = false;
1728
+ $id('aiChatSubmit').disabled = false;
1729
+ $id('aiChatInput').value = '';
1730
+ $id('aiChatInput').focus();
1731
+ }
1732
+ }
1733
+
1734
+ function showLoading(message) {
1735
+ hideLoading();
1736
+ const container = document.createElement('div');
1737
+ container.className = 'loading-container';
1738
+ container.id = 'loadingContainer';
1739
+ container.innerHTML = `
1740
+ <div class="loading-spinner"></div>
1741
+ <p class="loading-text">${message || '๋กœ๋”ฉ ์ค‘...'}</p>
1742
+ <div class="progress-bar-container">
1743
+ <div id="progressBar" class="progress-bar" style="width: 0%;"></div>
1744
+ </div>
1745
+ `;
1746
+ document.body.appendChild(container);
1747
+ }
1748
+
1749
+ function updateLoading(message, progress) {
1750
+ const text = document.querySelector('.loading-text');
1751
+ if (text) text.textContent = message;
1752
+ const bar = $id('progressBar');
1753
+ if (bar && progress !== undefined) bar.style.width = `${progress}%`;
1754
+ }
1755
+
1756
+ function hideLoading() {
1757
+ const container = $id('loadingContainer');
1758
+ if (container) container.remove();
1759
+ }
1760
+
1761
+ function showError(message) {
1762
+ hideLoading();
1763
+ const container = document.createElement('div');
1764
+ container.className = 'error-container';
1765
+ container.id = 'errorContainer';
1766
+ container.innerHTML = `
1767
+ <i class="fas fa-exclamation-circle"></i>
1768
+ <p>${message}</p>
1769
+ <button onclick="location.reload()">๋‹ค์‹œ ์‹œ๋„</button>
1770
+ `;
1771
+ document.body.appendChild(container);
1772
+ }
1773
+
1774
+ function createFlipBook(pages) {
1775
+ try {
1776
+ const windowWidth = window.innerWidth;
1777
+ const windowHeight = window.innerHeight;
1778
+ const aspectRatio = windowWidth / windowHeight;
1779
+
1780
+ let width, height;
1781
+ if (aspectRatio > 1) {
1782
+ // ๊ฐ€๋กœ ๋ชจ๋“œ: ๋†’์ด ๊ธฐ์ค€์œผ๋กœ 90% ์‚ฌ์šฉ
1783
+ height = Math.min(windowHeight * 0.88, windowHeight - 60);
1784
+ width = height * aspectRatio * 0.75;
1785
+ if (width > windowWidth * 0.94) {
1786
+ width = windowWidth * 0.94;
1787
+ height = width / (aspectRatio * 0.75);
1788
+ }
1789
+ } else {
1790
+ // ์„ธ๋กœ ๋ชจ๋“œ: ๋„ˆ๋น„ ๊ธฐ์ค€์œผ๋กœ 98% ์‚ฌ์šฉ
1791
+ width = Math.min(windowWidth * 0.98, windowWidth - 10);
1792
+ height = width / aspectRatio * 0.9;
1793
+ if (height > windowHeight * 0.9) {
1794
+ height = windowHeight * 0.9;
1795
+ width = height * aspectRatio * 0.9;
1796
+ }
1797
+ }
1798
+
1799
+ viewer.style.width = Math.round(width) + 'px';
1800
+ viewer.style.height = Math.round(height) + 'px';
1801
+
1802
+ const validPages = pages.map(page => {
1803
+ if (!page || !page.src) {
1804
+ return {
1805
+ src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PC9zdmc+',
1806
+ thumb: ''
1807
+ };
1808
+ }
1809
+ return page;
1810
+ });
1811
+
1812
+ fb = new FlipBook(viewer, {
1813
+ pages: validPages,
1814
+ viewMode: 'webgl',
1815
+ autoSize: true,
1816
+ flipDuration: 800,
1817
+ backgroundColor: '#fff',
1818
+ sound: true,
1819
+ assets: { flipMp3: '/static/turnPage2.mp3', hardFlipMp3: '/static/turnPage2.mp3' },
1820
+ controlsProps: {
1821
+ enableFullscreen: true,
1822
+ enableToc: true,
1823
+ enableDownload: false,
1824
+ enablePrint: false,
1825
+ enableZoom: true,
1826
+ enableShare: false,
1827
+ enableSearch: true,
1828
+ enableAutoPlay: true,
1829
+ enableSound: true,
1830
+ layout: 10,
1831
+ skin: 'light',
1832
+ autoNavigationTime: 3600,
1833
+ hideControls: false,
1834
+ paddingTop: 10,
1835
+ paddingLeft: 10,
1836
+ paddingRight: 10,
1837
+ paddingBottom: 10,
1838
+ pageTextureSize: 1024,
1839
+ thumbnails: true,
1840
+ autoHideControls: false,
1841
+ controlsTimeout: 8000
1842
+ }
1843
+ });
1844
+
1845
+ window.addEventListener('resize', () => {
1846
+ if (fb) fb.resize();
1847
+ });
1848
+
1849
+ console.log('FlipBook ์ƒ์„ฑ ์™„๋ฃŒ');
1850
+ } catch (error) {
1851
+ console.error('FlipBook ์ƒ์„ฑ ์˜ค๋ฅ˜:', error);
1852
+ showError("ํ”Œ๋ฆฝ๋ถ์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1853
+ }
1854
+ }
1855
+
1856
+ async function loadPDF() {
1857
+ try {
1858
+ showLoading("PDF ์ •๋ณด ํ™•์ธ ์ค‘...");
1859
+
1860
+ const infoResponse = await fetch('/api/pdf-info');
1861
+ const pdfInfo = await infoResponse.json();
1862
+
1863
+ if (!pdfInfo.exists) {
1864
+ hideLoading();
1865
+ showError("PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. prompt.pdf ํŒŒ์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.");
1866
+ return;
1867
+ }
1868
+
1869
+ updateLoading("PDF ๋กœ๋”ฉ ์ค‘...", 10);
1870
+
1871
+ if (pdfInfo.cached) {
1872
+ const cacheResponse = await fetch('/api/cached-pdf');
1873
+ const cachedData = await cacheResponse.json();
1874
+
1875
+ if (cachedData.status === "completed" && cachedData.pages) {
1876
+ hideLoading();
1877
+ createFlipBook(cachedData.pages);
1878
+ return;
1879
+ }
1880
+ }
1881
+
1882
+ const cacheResponse = await fetch('/api/cached-pdf');
1883
+ let cachedData = await cacheResponse.json();
1884
+
1885
+ if (cachedData.status === "completed" && cachedData.pages) {
1886
+ hideLoading();
1887
+ createFlipBook(cachedData.pages);
1888
+ return;
1889
+ }
1890
+
1891
+ while (cachedData.status === "processing" || cachedData.status === "started") {
1892
+ await new Promise(resolve => setTimeout(resolve, 1000));
1893
+
1894
+ const statusResponse = await fetch('/api/cache-status');
1895
+ cachedData = await statusResponse.json();
1896
+
1897
+ if (cachedData.progress) {
1898
+ updateLoading(`PDF ์ฒ˜๋ฆฌ ์ค‘... ${cachedData.progress}%`, cachedData.progress);
1899
+ }
1900
+
1901
+ if (cachedData.status === "completed") {
1902
+ const finalResponse = await fetch('/api/cached-pdf');
1903
+ cachedData = await finalResponse.json();
1904
+ break;
1905
+ }
1906
+ }
1907
+
1908
+ hideLoading();
1909
+
1910
+ if (cachedData.pages && cachedData.pages.length > 0) {
1911
+ createFlipBook(cachedData.pages);
1912
+ } else {
1913
+ showError("PDF๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
1914
+ }
1915
+
1916
+ } catch (error) {
1917
+ console.error("PDF ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
1918
+ hideLoading();
1919
+ showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
1920
+ }
1921
+ }
1922
+
1923
+ document.addEventListener('DOMContentLoaded', function() {
1924
+ initializeAudio();
1925
+
1926
+ $id('aiButton').addEventListener('click', () => toggleAiChat(!isAiChatActive));
1927
+ $id('aiChatClose').addEventListener('click', () => toggleAiChat(false));
1928
+
1929
+ $id('aiChatForm').addEventListener('submit', function(e) {
1930
+ e.preventDefault();
1931
+ const question = $id('aiChatInput').value.trim();
1932
+ if (question && !isAiProcessing) {
1933
+ submitQuestion(question);
1934
+ }
1935
+ });
1936
+
1937
+ loadPDF();
1938
+ });
1939
+ </script>
1940
+ </body>
1941
+ </html>
1942
+ """
1943
+
1944
+ if __name__ == "__main__":
1945
+ uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))