Update app.py
Browse files
app.py
CHANGED
|
@@ -44,6 +44,9 @@ pdf_cache: Dict[str, Dict[str, Any]] = {}
|
|
| 44 |
cache_locks = {}
|
| 45 |
pdf_embeddings: Dict[str, Dict[str, Any]] = {}
|
| 46 |
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def get_cache_path(pdf_name: str):
|
| 49 |
return CACHE_DIR / f"{pdf_name}_cache.json"
|
|
@@ -102,18 +105,18 @@ def get_pdf_page_as_base64(pdf_path: str, page_num: int, scale: float = 1.0) ->
|
|
| 102 |
return None
|
| 103 |
|
| 104 |
|
| 105 |
-
def get_pdf_pages_as_base64(pdf_path: str, max_pages: int =
|
| 106 |
-
"""PDF μ¬λ¬ νμ΄μ§λ₯Ό base64 μ΄λ―Έμ§ 리μ€νΈλ‘ λ³ν (
|
| 107 |
try:
|
| 108 |
doc = fitz.open(pdf_path)
|
| 109 |
total_pages = doc.page_count
|
| 110 |
-
|
| 111 |
|
| 112 |
images = []
|
| 113 |
-
for page_num in range(
|
| 114 |
page = doc[page_num]
|
| 115 |
pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale))
|
| 116 |
-
img_data = pix.tobytes("jpeg",
|
| 117 |
b64_img = base64.b64encode(img_data).decode('utf-8')
|
| 118 |
images.append({
|
| 119 |
"page": page_num + 1,
|
|
@@ -121,35 +124,52 @@ def get_pdf_pages_as_base64(pdf_path: str, max_pages: int = 25, scale: float = 0
|
|
| 121 |
})
|
| 122 |
|
| 123 |
doc.close()
|
| 124 |
-
logger.info(f"PDF {
|
| 125 |
-
return images
|
| 126 |
except Exception as e:
|
| 127 |
logger.error(f"PDF νμ΄μ§λ€ μ΄λ―Έμ§ λ³ν μ€λ₯: {e}")
|
| 128 |
-
return []
|
| 129 |
|
| 130 |
|
| 131 |
-
|
| 132 |
-
"""
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
if not page_images:
|
| 148 |
-
return
|
| 149 |
|
| 150 |
-
# VLMμΌλ‘ μ 체 λ΄μ© λΆμ
|
| 151 |
content_parts = []
|
| 152 |
-
|
| 153 |
for img_data in page_images:
|
| 154 |
content_parts.append({
|
| 155 |
"type": "image_url",
|
|
@@ -158,85 +178,147 @@ async def analyze_pdf_with_vlm(pdf_id: str, force_refresh: bool = False) -> Dict
|
|
| 158 |
}
|
| 159 |
})
|
| 160 |
|
|
|
|
| 161 |
content_parts.append({
|
| 162 |
"type": "text",
|
| 163 |
-
"text": f"""μ μ΄λ―Έμ§λ€μ PDF λ¬Έμμ νμ΄μ§
|
| 164 |
-
|
| 165 |
-
μ΄ PDF λ¬Έμμ μ 체 λ΄μ©μ μμΈνκ² λΆμν΄μ£ΌμΈμ.
|
| 166 |
-
|
| 167 |
-
λ€μ νμμΌλ‘ μμ±ν΄μ£ΌμΈμ:
|
| 168 |
-
|
| 169 |
-
## λ¬Έμ κ°μ
|
| 170 |
-
- λ¬Έμ μ λͺ©/μ£Όμ :
|
| 171 |
-
- λ¬Έμ μ ν:
|
| 172 |
-
- μμ± λͺ©μ :
|
| 173 |
-
|
| 174 |
-
## νμ΄μ§λ³ ν΅μ¬ λ΄μ©
|
| 175 |
-
κ° νμ΄μ§μ μ£Όμ λ΄μ©μ μμ½ν΄μ£ΌμΈμ.
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
## μμΈ λ΄μ©
|
| 184 |
-
λ¬Έμμ ν¬ν¨λ λͺ¨λ μ€μν μ 보, λ°μ΄ν°, ν, κ·Έλν λ΄μ© λ±μ μμΈν κΈ°μ ν΄μ£ΌμΈμ.
|
| 185 |
|
| 186 |
νκ΅μ΄λ‘ μμ±ν΄μ£ΌμΈμ."""
|
| 187 |
})
|
| 188 |
|
| 189 |
messages = [{"role": "user", "content": content_parts}]
|
|
|
|
|
|
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
try:
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
analysis_data = {
|
| 196 |
"pdf_id": pdf_id,
|
| 197 |
-
"total_pages":
|
| 198 |
-
"
|
|
|
|
|
|
|
| 199 |
"created_at": time.time()
|
| 200 |
}
|
| 201 |
|
| 202 |
# μΊμμ μ μ₯
|
| 203 |
save_analysis_cache(pdf_id, analysis_data)
|
| 204 |
|
|
|
|
|
|
|
|
|
|
| 205 |
return analysis_data
|
|
|
|
| 206 |
except Exception as e:
|
| 207 |
logger.error(f"VLM PDF λΆμ μ€λ₯: {e}")
|
|
|
|
| 208 |
return {"error": str(e)}
|
| 209 |
|
| 210 |
|
| 211 |
-
def
|
| 212 |
-
"""
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
"
|
| 222 |
-
"frequency_penalty": 0,
|
| 223 |
-
"temperature": temperature,
|
| 224 |
-
"messages": messages
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
headers = {
|
| 228 |
-
"Accept": "application/json",
|
| 229 |
-
"Content-Type": "application/json",
|
| 230 |
-
"Authorization": f"Bearer {FIREWORKS_API_KEY}"
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
response = requests.post(FIREWORKS_API_URL, headers=headers, data=json.dumps(payload), timeout=120)
|
| 234 |
-
|
| 235 |
-
if response.status_code != 200:
|
| 236 |
-
raise Exception(f"API μ€λ₯: {response.status_code} - {response.text}")
|
| 237 |
-
|
| 238 |
-
result = response.json()
|
| 239 |
-
return result["choices"][0]["message"]["content"]
|
| 240 |
|
| 241 |
|
| 242 |
def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
|
|
@@ -245,18 +327,15 @@ def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
|
|
| 245 |
chunks = []
|
| 246 |
for page_num in range(len(doc)):
|
| 247 |
page = doc[page_num]
|
| 248 |
-
# μ¬λ¬ λ°©λ²μΌλ‘ ν
μ€νΈ μΆμΆ μλ
|
| 249 |
text = page.get_text("text")
|
| 250 |
|
| 251 |
-
# ν
μ€νΈκ° μμΌλ©΄ λ€λ₯Έ λ°©λ² μλ
|
| 252 |
if not text.strip():
|
| 253 |
text = page.get_text("blocks")
|
| 254 |
if text:
|
| 255 |
text = "\n".join([block[4] for block in text if len(block) > 4 and isinstance(block[4], str)])
|
| 256 |
|
| 257 |
-
# μ¬μ ν ν
μ€νΈκ° μμΌλ©΄ νμ΄μ§ μ 보λ§μ΄λΌλ μΆκ°
|
| 258 |
if not text.strip():
|
| 259 |
-
text = f"[νμ΄μ§ {page_num + 1} - μ΄λ―Έμ§
|
| 260 |
|
| 261 |
chunks.append({
|
| 262 |
"page": page_num + 1,
|
|
@@ -271,54 +350,6 @@ def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
|
|
| 271 |
return []
|
| 272 |
|
| 273 |
|
| 274 |
-
async def get_pdf_embedding(pdf_id: str) -> Dict[str, Any]:
|
| 275 |
-
try:
|
| 276 |
-
embedding_path = get_embedding_path(pdf_id)
|
| 277 |
-
if embedding_path.exists():
|
| 278 |
-
try:
|
| 279 |
-
with open(embedding_path, "r", encoding="utf-8") as f:
|
| 280 |
-
return json.load(f)
|
| 281 |
-
except Exception as e:
|
| 282 |
-
logger.error(f"μλ² λ© μΊμ λ‘λ μ€λ₯: {e}")
|
| 283 |
-
|
| 284 |
-
pdf_path = str(PROMPT_PDF_PATH)
|
| 285 |
-
if not PROMPT_PDF_PATH.exists():
|
| 286 |
-
raise ValueError(f"PDF νμΌμ μ°Ύμ μ μμ΅λλ€: {pdf_path}")
|
| 287 |
-
|
| 288 |
-
chunks = extract_pdf_text(pdf_path)
|
| 289 |
-
|
| 290 |
-
# ν
μ€νΈ μΆμΆ μ€ν¨ν΄λ κΈ°λ³Έ μ 보 μ 곡
|
| 291 |
-
if not chunks:
|
| 292 |
-
logger.warning(f"PDFμμ ν
μ€νΈλ₯Ό μΆμΆν μ μμ΅λλ€. κΈ°λ³Έ μ λ³΄λ‘ λ체ν©λλ€.")
|
| 293 |
-
try:
|
| 294 |
-
doc = fitz.open(pdf_path)
|
| 295 |
-
total_pages = doc.page_count
|
| 296 |
-
doc.close()
|
| 297 |
-
chunks = [{"page": i+1, "text": f"[νμ΄μ§ {i+1}]", "chunk_id": f"page_{i+1}"} for i in range(total_pages)]
|
| 298 |
-
except:
|
| 299 |
-
chunks = [{"page": 1, "text": "[PDF λ΄μ©μ μ½μ μ μμ΅λλ€]", "chunk_id": "page_1"}]
|
| 300 |
-
|
| 301 |
-
embedding_data = {
|
| 302 |
-
"pdf_id": pdf_id,
|
| 303 |
-
"pdf_path": pdf_path,
|
| 304 |
-
"chunks": chunks,
|
| 305 |
-
"created_at": time.time()
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
with open(embedding_path, "w", encoding="utf-8") as f:
|
| 309 |
-
json.dump(embedding_data, f, ensure_ascii=False)
|
| 310 |
-
|
| 311 |
-
return embedding_data
|
| 312 |
-
except Exception as e:
|
| 313 |
-
logger.error(f"PDF μλ² λ© μμ± μ€λ₯: {e}")
|
| 314 |
-
return {"error": str(e), "pdf_id": pdf_id, "chunks": []}
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
def call_fireworks_api(messages: List[Dict], max_tokens: int = 4096, temperature: float = 0.6) -> str:
|
| 318 |
-
"""Fireworks AI VLM API νΈμΆ"""
|
| 319 |
-
return call_fireworks_vlm_api(messages, max_tokens, temperature)
|
| 320 |
-
|
| 321 |
-
|
| 322 |
async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
| 323 |
"""μΊμλ VLM λΆμ κ²°κ³ΌοΏ½οΏ½ κΈ°λ°μΌλ‘ μ§μμλ΅"""
|
| 324 |
try:
|
|
@@ -329,10 +360,18 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
|
| 329 |
}
|
| 330 |
|
| 331 |
# μΊμλ λΆμ κ²°κ³Ό νμΈ
|
| 332 |
-
analysis_data =
|
| 333 |
|
| 334 |
-
if
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
analysis_text = analysis_data.get("analysis", "")
|
| 338 |
total_pages = analysis_data.get("total_pages", 0)
|
|
@@ -351,7 +390,7 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
|
| 351 |
λΆμ λ΄μ©μ μλ μ 보λ "ν΄λΉ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€"λΌκ³ μμ§ν λ΅ν΄μ£ΌμΈμ.
|
| 352 |
|
| 353 |
=== PDF λΆμ κ²°κ³Ό ===
|
| 354 |
-
{analysis_text}
|
| 355 |
=================="""
|
| 356 |
},
|
| 357 |
{
|
|
@@ -361,32 +400,17 @@ async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
|
| 361 |
]
|
| 362 |
|
| 363 |
try:
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
"query": query
|
| 371 |
-
}
|
| 372 |
-
except Exception as api_error:
|
| 373 |
-
logger.error(f"Fireworks API νΈμΆ μ€λ₯ (μλ {attempt+1}/3): {api_error}")
|
| 374 |
-
if attempt == 2:
|
| 375 |
-
raise api_error
|
| 376 |
-
await asyncio.sleep(2 * (attempt + 1))
|
| 377 |
-
|
| 378 |
-
raise Exception("API νΈμΆ μ¬μλ λͺ¨λ μ€ν¨")
|
| 379 |
except Exception as api_error:
|
| 380 |
-
logger.error(f"Fireworks API νΈμΆ
|
| 381 |
error_message = str(api_error)
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
elif "401" in error_message or "Unauthorized" in error_message:
|
| 385 |
-
return {"error": "API ν€κ° μ ν¨νμ§ μμ΅λλ€.", "answer": "API μΈμ¦μ μ€ν¨νμ΅λλ€."}
|
| 386 |
-
elif "429" in error_message or "Rate limit" in error_message:
|
| 387 |
-
return {"error": "API νΈμΆ νλ μ΄κ³Ό", "answer": "μ μ ν λ€μ μλν΄μ£ΌμΈμ."}
|
| 388 |
-
else:
|
| 389 |
-
return {"error": f"AI μ€λ₯: {error_message}", "answer": "μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€."}
|
| 390 |
except Exception as e:
|
| 391 |
logger.error(f"μ§μμλ΅ μ²λ¦¬ μ€λ₯: {e}")
|
| 392 |
return {"error": str(e), "answer": "μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€."}
|
|
@@ -401,51 +425,42 @@ async def summarize_pdf(pdf_id: str) -> Dict[str, Any]:
|
|
| 401 |
"summary": "API ν€κ° μμ΄ μμ½μ μμ±ν μ μμ΅λλ€."
|
| 402 |
}
|
| 403 |
|
| 404 |
-
# μΊμλ λΆμ κ²°κ³Ό νμΈ
|
| 405 |
-
analysis_data =
|
| 406 |
|
| 407 |
-
if
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
|
| 411 |
total_pages = analysis_data.get("total_pages", 0)
|
|
|
|
| 412 |
|
| 413 |
-
if
|
| 414 |
-
return {"error": "λΆμ λ°μ΄ν° μμ", "summary": "PDF λΆμ λ°μ΄ν°λ₯Ό μ°Ύμ μ μμ΅λλ€."}
|
| 415 |
-
|
| 416 |
-
# λΆμ κ²°κ³Όμμ μμ½ λΆλΆ μΆμΆ λλ μλ‘ μμ½ μμ±
|
| 417 |
-
messages = [
|
| 418 |
-
{
|
| 419 |
-
"role": "system",
|
| 420 |
-
"content": """μλλ PDF λ¬Έμλ₯Ό VLMμΌλ‘ λΆμν κ²°κ³Όμ
λλ€.
|
| 421 |
-
μ΄ λΆμ λ΄μ©μ λ°νμΌλ‘ μ¬μ©μμκ² λ¬Έμλ₯Ό μκ°νλ μΉμ ν μμ½μ μμ±ν΄μ£ΌμΈμ.
|
| 422 |
-
500μ μ΄λ΄λ‘ ν΅μ¬ λ΄μ©μ κ°κ²°νκ² νκ΅μ΄λ‘ μμ½ν΄μ£ΌμΈμ."""
|
| 423 |
-
},
|
| 424 |
-
{
|
| 425 |
-
"role": "user",
|
| 426 |
-
"content": f"λ€μ PDF λΆμ κ²°κ³Όλ₯Ό μμ½ν΄μ£ΌμΈμ:\n\n{analysis_text}"
|
| 427 |
-
}
|
| 428 |
-
]
|
| 429 |
-
|
| 430 |
-
try:
|
| 431 |
-
summary = call_fireworks_vlm_api(messages, max_tokens=1024, temperature=0.5)
|
| 432 |
return {
|
| 433 |
"summary": summary,
|
| 434 |
"pdf_id": pdf_id,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
"total_pages": total_pages
|
| 436 |
}
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
# μΊμλ λΆμ κ²°κ³Όμμ μ§μ μμ½ λΆλΆ μΆμΆ μλ
|
| 440 |
-
if "## μ 체 μμ½" in analysis_text:
|
| 441 |
-
try:
|
| 442 |
-
start = analysis_text.index("## μ 체 μμ½")
|
| 443 |
-
end = analysis_text.index("##", start + 10) if "##" in analysis_text[start+10:] else len(analysis_text)
|
| 444 |
-
summary_part = analysis_text[start:end].replace("## μ 체 μμ½", "").strip()
|
| 445 |
-
return {"summary": summary_part, "pdf_id": pdf_id, "total_pages": total_pages}
|
| 446 |
-
except:
|
| 447 |
-
pass
|
| 448 |
-
return {"error": str(api_error), "summary": "μμ½ μμ± μ€ μ€λ₯κ° λ°μνμ΅λλ€."}
|
| 449 |
|
| 450 |
except Exception as e:
|
| 451 |
logger.error(f"PDF μμ½ μμ± μ€λ₯: {e}")
|
|
@@ -572,9 +587,10 @@ async def cache_pdf(pdf_path: str):
|
|
| 572 |
async def startup_event():
|
| 573 |
if PROMPT_PDF_PATH.exists():
|
| 574 |
logger.info(f"prompt.pdf νμΌ λ°κ²¬: {PROMPT_PDF_PATH}")
|
|
|
|
| 575 |
asyncio.create_task(cache_pdf(str(PROMPT_PDF_PATH)))
|
| 576 |
-
# VLM λΆμ
|
| 577 |
-
asyncio.create_task(
|
| 578 |
else:
|
| 579 |
logger.warning(f"prompt.pdf νμΌμ μ°Ύμ μ μμ΅λλ€: {PROMPT_PDF_PATH}")
|
| 580 |
|
|
@@ -602,15 +618,27 @@ async def get_pdf_info():
|
|
| 602 |
|
| 603 |
@app.get("/api/analysis-status")
|
| 604 |
async def get_analysis_status():
|
| 605 |
-
"""VLM λΆμ
|
|
|
|
| 606 |
cached = load_analysis_cache(PROMPT_PDF_ID)
|
| 607 |
if cached:
|
| 608 |
return {
|
| 609 |
"status": "completed",
|
| 610 |
"total_pages": cached.get("total_pages", 0),
|
|
|
|
| 611 |
"created_at": cached.get("created_at", 0)
|
| 612 |
}
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
|
| 615 |
|
| 616 |
@app.post("/api/reanalyze-pdf")
|
|
@@ -626,8 +654,12 @@ async def reanalyze_pdf():
|
|
| 626 |
cache_path.unlink()
|
| 627 |
logger.info("κΈ°μ‘΄ VLM λΆμ μΊμ μμ ")
|
| 628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
# λ°±κ·ΈλΌμ΄λμμ μ¬λΆμ μμ
|
| 630 |
-
asyncio.create_task(
|
| 631 |
|
| 632 |
return {"status": "started", "message": "PDF μ¬λΆμμ μμν©λλ€."}
|
| 633 |
except Exception as e:
|
|
@@ -679,11 +711,10 @@ async def api_query_pdf(query: Dict[str, str]):
|
|
| 679 |
return JSONResponse(content={"error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
| 680 |
|
| 681 |
result = await query_pdf(PROMPT_PDF_ID, user_query)
|
| 682 |
-
# μλ¬κ° μμ΄λ answerκ° μμΌλ©΄ μ μ μλ΅μΌλ‘ μ²λ¦¬
|
| 683 |
if "answer" in result:
|
| 684 |
return result
|
| 685 |
if "error" in result:
|
| 686 |
-
return JSONResponse(content=result, status_code=200)
|
| 687 |
return result
|
| 688 |
except Exception as e:
|
| 689 |
logger.error(f"μ§μμλ΅ API μ€λ₯: {e}")
|
|
@@ -697,11 +728,10 @@ async def api_summarize_pdf():
|
|
| 697 |
return JSONResponse(content={"error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
| 698 |
|
| 699 |
result = await summarize_pdf(PROMPT_PDF_ID)
|
| 700 |
-
# μλ¬κ° μμ΄λ summaryκ° μμΌλ©΄ μ μ μλ΅μΌλ‘ μ²λ¦¬
|
| 701 |
if "summary" in result:
|
| 702 |
return result
|
| 703 |
if "error" in result:
|
| 704 |
-
return JSONResponse(content=result, status_code=200)
|
| 705 |
return result
|
| 706 |
except Exception as e:
|
| 707 |
logger.error(f"PDF μμ½ API μ€λ₯: {e}")
|
|
@@ -1325,6 +1355,7 @@ HTML = """
|
|
| 1325 |
let isAiChatActive = false;
|
| 1326 |
let isAiProcessing = false;
|
| 1327 |
let hasLoadedSummary = false;
|
|
|
|
| 1328 |
|
| 1329 |
function $id(id) { return document.getElementById(id); }
|
| 1330 |
|
|
@@ -1411,6 +1442,17 @@ HTML = """
|
|
| 1411 |
}
|
| 1412 |
}
|
| 1413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1414 |
async function loadPdfSummary() {
|
| 1415 |
if (isAiProcessing || hasLoadedSummary) return;
|
| 1416 |
|
|
@@ -1418,65 +1460,93 @@ HTML = """
|
|
| 1418 |
isAiProcessing = true;
|
| 1419 |
addTypingIndicator();
|
| 1420 |
|
| 1421 |
-
//
|
| 1422 |
-
const
|
| 1423 |
-
const statusData = await statusResponse.json();
|
| 1424 |
|
| 1425 |
-
if (statusData.status
|
| 1426 |
removeTypingIndicator();
|
| 1427 |
-
|
|
|
|
| 1428 |
hasLoadedSummary = true;
|
| 1429 |
isAiProcessing = false;
|
| 1430 |
|
| 1431 |
-
// λΆμ μλ£
|
| 1432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1433 |
return;
|
| 1434 |
}
|
| 1435 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1436 |
const response = await fetch('/api/ai/summarize-pdf');
|
| 1437 |
const data = await response.json();
|
| 1438 |
|
| 1439 |
removeTypingIndicator();
|
| 1440 |
|
| 1441 |
if (data.summary) {
|
| 1442 |
-
const pageInfo = data.
|
| 1443 |
addChatMessage(`μλ
νμΈμ! μ΄ PDFμ λν΄ λ¬΄μμ΄λ μ§λ¬Έν΄μ£ΌμΈμ.${pageInfo}<br><br><strong>π PDF μμ½:</strong><br>${data.summary}`);
|
| 1444 |
-
hasLoadedSummary = true;
|
| 1445 |
-
} else if (data.error) {
|
| 1446 |
-
addChatMessage(`μλ
νμΈμ! PDFμ λν΄ κΆκΈν κ²μ μ§λ¬Έν΄μ£ΌμΈμ.<br><br><small style="color:#999;">β οΈ ${data.error}</small>`);
|
| 1447 |
-
hasLoadedSummary = true;
|
| 1448 |
} else {
|
| 1449 |
addChatMessage("μλ
νμΈμ! PDFμ λν΄ μ§λ¬Έν΄μ£ΌμΈμ. μ΅μ μ λ€ν΄ λ΅λ³νκ² μ΅λλ€.");
|
| 1450 |
-
hasLoadedSummary = true;
|
| 1451 |
}
|
|
|
|
|
|
|
| 1452 |
} catch (error) {
|
| 1453 |
console.error("PDF μμ½ λ‘λ μ€λ₯:", error);
|
| 1454 |
removeTypingIndicator();
|
| 1455 |
-
addChatMessage("μλ
νμΈμ! PDFμ λν΄ μ§λ¬Έν΄μ£ΌμΈμ.
|
| 1456 |
hasLoadedSummary = true;
|
| 1457 |
} finally {
|
| 1458 |
isAiProcessing = false;
|
| 1459 |
}
|
| 1460 |
}
|
| 1461 |
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
|
|
|
| 1465 |
try {
|
| 1466 |
-
const
|
| 1467 |
-
const data = await response.json();
|
| 1468 |
|
| 1469 |
if (data.status === 'completed') {
|
| 1470 |
-
clearInterval(
|
| 1471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1472 |
}
|
| 1473 |
} catch (e) {
|
| 1474 |
-
console.error("
|
| 1475 |
}
|
| 1476 |
-
},
|
| 1477 |
|
| 1478 |
// 5λΆ ν μλ μ€μ§
|
| 1479 |
-
setTimeout(() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1480 |
}
|
| 1481 |
|
| 1482 |
async function submitQuestion(question) {
|
|
@@ -1489,14 +1559,17 @@ HTML = """
|
|
| 1489 |
addChatMessage(question, true);
|
| 1490 |
|
| 1491 |
// λΆμ μν νμΈ
|
| 1492 |
-
const
|
| 1493 |
-
const statusData = await statusResponse.json();
|
| 1494 |
|
| 1495 |
if (statusData.status !== 'completed') {
|
| 1496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1497 |
isAiProcessing = false;
|
| 1498 |
$id('aiChatSubmit').disabled = false;
|
| 1499 |
-
$id('aiChatInput').value = question;
|
| 1500 |
return;
|
| 1501 |
}
|
| 1502 |
|
|
@@ -1506,7 +1579,7 @@ HTML = """
|
|
| 1506 |
method: 'POST',
|
| 1507 |
headers: { 'Content-Type': 'application/json' },
|
| 1508 |
body: JSON.stringify({ query: question }),
|
| 1509 |
-
signal: AbortSignal.timeout(120000)
|
| 1510 |
});
|
| 1511 |
|
| 1512 |
const data = await response.json();
|
|
@@ -1551,7 +1624,7 @@ HTML = """
|
|
| 1551 |
}
|
| 1552 |
|
| 1553 |
function updateLoading(message, progress) {
|
| 1554 |
-
const text =
|
| 1555 |
if (text) text.textContent = message;
|
| 1556 |
const bar = $id('progressBar');
|
| 1557 |
if (bar && progress !== undefined) bar.style.width = `${progress}%`;
|
|
@@ -1681,7 +1754,6 @@ HTML = """
|
|
| 1681 |
}
|
| 1682 |
}
|
| 1683 |
|
| 1684 |
-
// μΊμκ° μμΌλ©΄ μΊμ± μμνκ³ μ§ν μν© λͺ¨λν°λ§
|
| 1685 |
const cacheResponse = await fetch('/api/cached-pdf');
|
| 1686 |
let cachedData = await cacheResponse.json();
|
| 1687 |
|
|
@@ -1691,7 +1763,6 @@ HTML = """
|
|
| 1691 |
return;
|
| 1692 |
}
|
| 1693 |
|
| 1694 |
-
// μΊμ± μ§ν μ€μ΄λ©΄ μλ£λ λκΉμ§ λκΈ°
|
| 1695 |
while (cachedData.status === "processing" || cachedData.status === "started") {
|
| 1696 |
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 1697 |
|
|
@@ -1727,11 +1798,9 @@ HTML = """
|
|
| 1727 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1728 |
initializeAudio();
|
| 1729 |
|
| 1730 |
-
// AI λ²νΌ μ΄λ²€νΈ
|
| 1731 |
$id('aiButton').addEventListener('click', () => toggleAiChat(!isAiChatActive));
|
| 1732 |
$id('aiChatClose').addEventListener('click', () => toggleAiChat(false));
|
| 1733 |
|
| 1734 |
-
// μ±ν
νΌ μ΄λ²€νΈ
|
| 1735 |
$id('aiChatForm').addEventListener('submit', function(e) {
|
| 1736 |
e.preventDefault();
|
| 1737 |
const question = $id('aiChatInput').value.trim();
|
|
@@ -1740,7 +1809,6 @@ HTML = """
|
|
| 1740 |
}
|
| 1741 |
});
|
| 1742 |
|
| 1743 |
-
// PDF μλ λ‘λ
|
| 1744 |
loadPDF();
|
| 1745 |
});
|
| 1746 |
</script>
|
|
|
|
| 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"
|
|
|
|
| 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,
|
|
|
|
| 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",
|
|
|
|
| 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 |
+
summary = ""
|
| 271 |
+
if combined_analysis:
|
| 272 |
+
try:
|
| 273 |
+
summary_messages = [
|
| 274 |
+
{
|
| 275 |
+
"role": "system",
|
| 276 |
+
"content": "λ€μ PDF λΆμ λ΄μ©μ 500μ μ΄λ΄λ‘ μμ½ν΄μ£ΌμΈμ. ν΅μ¬ λ΄μ©κ³Ό μ£Όμ ν€μλλ₯Ό ν¬ν¨ν΄μ£ΌμΈμ."
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"role": "user",
|
| 280 |
+
"content": combined_analysis[:8000] # ν ν° μ ν
|
| 281 |
+
}
|
| 282 |
+
]
|
| 283 |
+
summary = call_fireworks_vlm_api(summary_messages, max_tokens=1024, temperature=0.5)
|
| 284 |
+
except Exception as sum_err:
|
| 285 |
+
logger.error(f"μμ½ μμ± μ€λ₯: {sum_err}")
|
| 286 |
+
summary = combined_analysis[:500] + "..."
|
| 287 |
|
| 288 |
analysis_data = {
|
| 289 |
"pdf_id": pdf_id,
|
| 290 |
+
"total_pages": total_pages,
|
| 291 |
+
"analyzed_pages": min(total_pages, 25),
|
| 292 |
+
"analysis": combined_analysis,
|
| 293 |
+
"summary": summary,
|
| 294 |
"created_at": time.time()
|
| 295 |
}
|
| 296 |
|
| 297 |
# μΊμμ μ μ₯
|
| 298 |
save_analysis_cache(pdf_id, analysis_data)
|
| 299 |
|
| 300 |
+
analysis_status[pdf_id] = {"status": "completed", "progress": 100}
|
| 301 |
+
logger.info(f"PDF λΆμ μλ£: {pdf_id}")
|
| 302 |
+
|
| 303 |
return analysis_data
|
| 304 |
+
|
| 305 |
except Exception as e:
|
| 306 |
logger.error(f"VLM PDF λΆμ μ€λ₯: {e}")
|
| 307 |
+
analysis_status[pdf_id] = {"status": "error", "error": str(e)}
|
| 308 |
return {"error": str(e)}
|
| 309 |
|
| 310 |
|
| 311 |
+
async def run_initial_analysis():
|
| 312 |
+
"""μλ² μμ μ μ΄κΈ° λΆμ μ€ν"""
|
| 313 |
+
logger.info("μ΄κΈ° PDF λΆμ μμ...")
|
| 314 |
+
try:
|
| 315 |
+
result = await analyze_pdf_with_vlm_batched(PROMPT_PDF_ID)
|
| 316 |
+
if "error" in result:
|
| 317 |
+
logger.error(f"μ΄κΈ° λΆμ μ€ν¨: {result['error']}")
|
| 318 |
+
else:
|
| 319 |
+
logger.info("μ΄κΈ° PDF λΆμ μλ£!")
|
| 320 |
+
except Exception as e:
|
| 321 |
+
logger.error(f"μ΄κΈ° λΆμ μμΈ: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
|
| 324 |
def extract_pdf_text(pdf_path: str) -> List[Dict[str, Any]]:
|
|
|
|
| 327 |
chunks = []
|
| 328 |
for page_num in range(len(doc)):
|
| 329 |
page = doc[page_num]
|
|
|
|
| 330 |
text = page.get_text("text")
|
| 331 |
|
|
|
|
| 332 |
if not text.strip():
|
| 333 |
text = page.get_text("blocks")
|
| 334 |
if text:
|
| 335 |
text = "\n".join([block[4] for block in text if len(block) > 4 and isinstance(block[4], str)])
|
| 336 |
|
|
|
|
| 337 |
if not text.strip():
|
| 338 |
+
text = f"[νμ΄μ§ {page_num + 1} - μ΄λ―Έμ§ κΈ°λ° νμ΄μ§]"
|
| 339 |
|
| 340 |
chunks.append({
|
| 341 |
"page": page_num + 1,
|
|
|
|
| 350 |
return []
|
| 351 |
|
| 352 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
async def query_pdf(pdf_id: str, query: str) -> Dict[str, Any]:
|
| 354 |
"""μΊμλ VLM λΆμ κ²°κ³ΌοΏ½οΏ½ κΈ°λ°μΌλ‘ μ§μμλ΅"""
|
| 355 |
try:
|
|
|
|
| 360 |
}
|
| 361 |
|
| 362 |
# μΊμλ λΆμ κ²°κ³Ό νμΈ
|
| 363 |
+
analysis_data = load_analysis_cache(pdf_id)
|
| 364 |
|
| 365 |
+
if not analysis_data:
|
| 366 |
+
# λΆμ μν νμΈ
|
| 367 |
+
if pdf_id in analysis_status:
|
| 368 |
+
status = analysis_status[pdf_id].get("status")
|
| 369 |
+
if status == "analyzing":
|
| 370 |
+
progress = analysis_status[pdf_id].get("progress", 0)
|
| 371 |
+
return {"error": f"λΆμ μ§ν μ€ ({progress}%)", "answer": f"PDF λΆμμ΄ μ§ν μ€μ
λλ€ ({progress}%). μ μλ§ κΈ°λ€λ €μ£ΌμΈμ."}
|
| 372 |
+
elif status == "error":
|
| 373 |
+
return {"error": "λΆμ μ€ν¨", "answer": f"PDF λΆμμ μ€ν¨νμ΅λλ€: {analysis_status[pdf_id].get('error', 'μ μ μλ μ€λ₯')}"}
|
| 374 |
+
return {"error": "λΆμ λ°μ΄ν° μμ", "answer": "PDFκ° μμ§ λΆμλμ§ μμμ΅λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ."}
|
| 375 |
|
| 376 |
analysis_text = analysis_data.get("analysis", "")
|
| 377 |
total_pages = analysis_data.get("total_pages", 0)
|
|
|
|
| 390 |
λΆμ λ΄μ©μ μλ μ 보λ "ν΄λΉ μ 보λ₯Ό μ°Ύμ μ μμ΅λλ€"λΌκ³ μμ§ν λ΅ν΄μ£ΌμΈμ.
|
| 391 |
|
| 392 |
=== PDF λΆμ κ²°κ³Ό ===
|
| 393 |
+
{analysis_text[:12000]}
|
| 394 |
=================="""
|
| 395 |
},
|
| 396 |
{
|
|
|
|
| 400 |
]
|
| 401 |
|
| 402 |
try:
|
| 403 |
+
answer = call_fireworks_vlm_api(messages, max_tokens=4096, temperature=0.6)
|
| 404 |
+
return {
|
| 405 |
+
"answer": answer,
|
| 406 |
+
"pdf_id": pdf_id,
|
| 407 |
+
"query": query
|
| 408 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
except Exception as api_error:
|
| 410 |
+
logger.error(f"Fireworks API νΈμΆ μ€λ₯: {api_error}")
|
| 411 |
error_message = str(api_error)
|
| 412 |
+
return {"error": f"AI μ€λ₯: {error_message}", "answer": "μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ."}
|
| 413 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
except Exception as e:
|
| 415 |
logger.error(f"μ§μμλ΅ μ²λ¦¬ μ€λ₯: {e}")
|
| 416 |
return {"error": str(e), "answer": "μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€."}
|
|
|
|
| 425 |
"summary": "API ν€κ° μμ΄ μμ½μ μμ±ν μ μμ΅λλ€."
|
| 426 |
}
|
| 427 |
|
| 428 |
+
# μΊμλ λΆμ κ²°κ³Ό νμΈ
|
| 429 |
+
analysis_data = load_analysis_cache(pdf_id)
|
| 430 |
|
| 431 |
+
if not analysis_data:
|
| 432 |
+
# λΆμ μν νμΈ
|
| 433 |
+
if pdf_id in analysis_status:
|
| 434 |
+
status = analysis_status[pdf_id].get("status")
|
| 435 |
+
if status == "analyzing":
|
| 436 |
+
progress = analysis_status[pdf_id].get("progress", 0)
|
| 437 |
+
return {"error": f"λΆμ μ§ν μ€", "summary": f"PDF λΆμμ΄ μ§ν μ€μ
λλ€ ({progress}%). μ μλ§ κΈ°λ€λ €μ£ΌμΈμ."}
|
| 438 |
+
elif status == "error":
|
| 439 |
+
return {"error": "λΆμ μ€ν¨", "summary": f"PDF λΆμμ μ€ν¨νμ΅λλ€."}
|
| 440 |
+
return {"error": "λΆμ λ°μ΄ν° μμ", "summary": "PDFκ° μμ§ λΆμλμ§ μμμ΅λλ€."}
|
| 441 |
|
| 442 |
+
summary = analysis_data.get("summary", "")
|
| 443 |
total_pages = analysis_data.get("total_pages", 0)
|
| 444 |
+
analyzed_pages = analysis_data.get("analyzed_pages", total_pages)
|
| 445 |
|
| 446 |
+
if summary:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
return {
|
| 448 |
"summary": summary,
|
| 449 |
"pdf_id": pdf_id,
|
| 450 |
+
"total_pages": total_pages,
|
| 451 |
+
"analyzed_pages": analyzed_pages
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
# μμ½μ΄ μμΌλ©΄ λΆμ λ΄μ©μμ μΆμΆ
|
| 455 |
+
analysis_text = analysis_data.get("analysis", "")
|
| 456 |
+
if analysis_text:
|
| 457 |
+
return {
|
| 458 |
+
"summary": analysis_text[:500] + "...",
|
| 459 |
+
"pdf_id": pdf_id,
|
| 460 |
"total_pages": total_pages
|
| 461 |
}
|
| 462 |
+
|
| 463 |
+
return {"error": "μμ½ μμ", "summary": "μμ½μ μμ±ν μ μμ΅λλ€."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
|
| 465 |
except Exception as e:
|
| 466 |
logger.error(f"PDF μμ½ μμ± μ€λ₯: {e}")
|
|
|
|
| 587 |
async def startup_event():
|
| 588 |
if PROMPT_PDF_PATH.exists():
|
| 589 |
logger.info(f"prompt.pdf νμΌ λ°κ²¬: {PROMPT_PDF_PATH}")
|
| 590 |
+
# νλ¦½λΆ μΊμ±
|
| 591 |
asyncio.create_task(cache_pdf(str(PROMPT_PDF_PATH)))
|
| 592 |
+
# VLM λΆμ - μλ¬ νΈλ€λ§ ν¬ν¨
|
| 593 |
+
asyncio.create_task(run_initial_analysis())
|
| 594 |
else:
|
| 595 |
logger.warning(f"prompt.pdf νμΌμ μ°Ύμ μ μμ΅λλ€: {PROMPT_PDF_PATH}")
|
| 596 |
|
|
|
|
| 618 |
|
| 619 |
@app.get("/api/analysis-status")
|
| 620 |
async def get_analysis_status():
|
| 621 |
+
"""VLM λΆμ μν νμΈ"""
|
| 622 |
+
# λ¨Όμ μΊμ νμΌ νμΈ
|
| 623 |
cached = load_analysis_cache(PROMPT_PDF_ID)
|
| 624 |
if cached:
|
| 625 |
return {
|
| 626 |
"status": "completed",
|
| 627 |
"total_pages": cached.get("total_pages", 0),
|
| 628 |
+
"analyzed_pages": cached.get("analyzed_pages", 0),
|
| 629 |
"created_at": cached.get("created_at", 0)
|
| 630 |
}
|
| 631 |
+
|
| 632 |
+
# λ©λͺ¨λ¦¬ μν νμΈ
|
| 633 |
+
if PROMPT_PDF_ID in analysis_status:
|
| 634 |
+
status_info = analysis_status[PROMPT_PDF_ID]
|
| 635 |
+
return {
|
| 636 |
+
"status": status_info.get("status", "unknown"),
|
| 637 |
+
"progress": status_info.get("progress", 0),
|
| 638 |
+
"error": status_info.get("error")
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
return {"status": "not_started"}
|
| 642 |
|
| 643 |
|
| 644 |
@app.post("/api/reanalyze-pdf")
|
|
|
|
| 654 |
cache_path.unlink()
|
| 655 |
logger.info("κΈ°μ‘΄ VLM λΆμ μΊμ μμ ")
|
| 656 |
|
| 657 |
+
# μν μ΄κΈ°ν
|
| 658 |
+
if PROMPT_PDF_ID in analysis_status:
|
| 659 |
+
del analysis_status[PROMPT_PDF_ID]
|
| 660 |
+
|
| 661 |
# λ°±κ·ΈλΌμ΄λμμ μ¬λΆμ μμ
|
| 662 |
+
asyncio.create_task(run_initial_analysis())
|
| 663 |
|
| 664 |
return {"status": "started", "message": "PDF μ¬λΆμμ μμν©λλ€."}
|
| 665 |
except Exception as e:
|
|
|
|
| 711 |
return JSONResponse(content={"error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
| 712 |
|
| 713 |
result = await query_pdf(PROMPT_PDF_ID, user_query)
|
|
|
|
| 714 |
if "answer" in result:
|
| 715 |
return result
|
| 716 |
if "error" in result:
|
| 717 |
+
return JSONResponse(content=result, status_code=200)
|
| 718 |
return result
|
| 719 |
except Exception as e:
|
| 720 |
logger.error(f"μ§μμλ΅ API μ€λ₯: {e}")
|
|
|
|
| 728 |
return JSONResponse(content={"error": "PDF νμΌμ μ°Ύμ μ μμ΅λλ€"}, status_code=404)
|
| 729 |
|
| 730 |
result = await summarize_pdf(PROMPT_PDF_ID)
|
|
|
|
| 731 |
if "summary" in result:
|
| 732 |
return result
|
| 733 |
if "error" in result:
|
| 734 |
+
return JSONResponse(content=result, status_code=200)
|
| 735 |
return result
|
| 736 |
except Exception as e:
|
| 737 |
logger.error(f"PDF μμ½ API μ€λ₯: {e}")
|
|
|
|
| 1355 |
let isAiChatActive = false;
|
| 1356 |
let isAiProcessing = false;
|
| 1357 |
let hasLoadedSummary = false;
|
| 1358 |
+
let analysisCheckInterval = null;
|
| 1359 |
|
| 1360 |
function $id(id) { return document.getElementById(id); }
|
| 1361 |
|
|
|
|
| 1442 |
}
|
| 1443 |
}
|
| 1444 |
|
| 1445 |
+
async function checkAnalysisStatus() {
|
| 1446 |
+
try {
|
| 1447 |
+
const response = await fetch('/api/analysis-status');
|
| 1448 |
+
const data = await response.json();
|
| 1449 |
+
return data;
|
| 1450 |
+
} catch (e) {
|
| 1451 |
+
console.error("λΆμ μν νμΈ μ€λ₯:", e);
|
| 1452 |
+
return { status: "error" };
|
| 1453 |
+
}
|
| 1454 |
+
}
|
| 1455 |
+
|
| 1456 |
async function loadPdfSummary() {
|
| 1457 |
if (isAiProcessing || hasLoadedSummary) return;
|
| 1458 |
|
|
|
|
| 1460 |
isAiProcessing = true;
|
| 1461 |
addTypingIndicator();
|
| 1462 |
|
| 1463 |
+
// λΆμ μν νμΈ
|
| 1464 |
+
const statusData = await checkAnalysisStatus();
|
|
|
|
| 1465 |
|
| 1466 |
+
if (statusData.status === 'analyzing') {
|
| 1467 |
removeTypingIndicator();
|
| 1468 |
+
const progress = statusData.progress || 0;
|
| 1469 |
+
addChatMessage(`μλ
νμΈμ! νμ¬ PDFλ₯Ό AIκ° λΆμνκ³ μμ΅λλ€. π<br><br>μ§νλ₯ : <strong>${progress}%</strong><br><small style="color:#999;">λΆμμ΄ μλ£λλ©΄ μλμΌλ‘ μλ €λλ¦¬κ² μ΅λλ€.</small>`);
|
| 1470 |
hasLoadedSummary = true;
|
| 1471 |
isAiProcessing = false;
|
| 1472 |
|
| 1473 |
+
// λΆμ μλ£ ν΄λ§
|
| 1474 |
+
startAnalysisPolling();
|
| 1475 |
+
return;
|
| 1476 |
+
}
|
| 1477 |
+
|
| 1478 |
+
if (statusData.status === 'error') {
|
| 1479 |
+
removeTypingIndicator();
|
| 1480 |
+
addChatMessage(`μλ
νμΈμ! PDF λΆμ μ€ μ€λ₯κ° λ°μνμ΅λλ€. β οΈ<br><br><small style="color:#e74c3c;">${statusData.error || 'μ μ μλ μ€λ₯'}</small><br><br>νμ΄μ§λ₯Ό μλ‘κ³ μΉ¨νκ±°λ μ μ ν λ€μ μλν΄μ£ΌμΈμ.`);
|
| 1481 |
+
hasLoadedSummary = true;
|
| 1482 |
+
isAiProcessing = false;
|
| 1483 |
return;
|
| 1484 |
}
|
| 1485 |
|
| 1486 |
+
if (statusData.status === 'not_started') {
|
| 1487 |
+
removeTypingIndicator();
|
| 1488 |
+
addChatMessage(`μλ
νμΈμ! PDF λΆμμ΄ μμ§ μμλμ§ μμμ΅λλ€. π<br><br><small style="color:#999;">μ μλ§ κΈ°λ€λ €μ£ΌμΈμ...</small>`);
|
| 1489 |
+
hasLoadedSummary = true;
|
| 1490 |
+
isAiProcessing = false;
|
| 1491 |
+
startAnalysisPolling();
|
| 1492 |
+
return;
|
| 1493 |
+
}
|
| 1494 |
+
|
| 1495 |
+
// λΆμ μλ£λ¨ - μμ½ κ°μ Έμ€κΈ°
|
| 1496 |
const response = await fetch('/api/ai/summarize-pdf');
|
| 1497 |
const data = await response.json();
|
| 1498 |
|
| 1499 |
removeTypingIndicator();
|
| 1500 |
|
| 1501 |
if (data.summary) {
|
| 1502 |
+
const pageInfo = data.analyzed_pages ? ` (${data.analyzed_pages}/${data.total_pages}νμ΄μ§ λΆμμλ£)` : '';
|
| 1503 |
addChatMessage(`μλ
νμΈμ! μ΄ PDFμ λν΄ λ¬΄μμ΄λ μ§λ¬Έν΄μ£ΌμΈμ.${pageInfo}<br><br><strong>π PDF μμ½:</strong><br>${data.summary}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1504 |
} else {
|
| 1505 |
addChatMessage("μλ
νμΈμ! PDFμ λν΄ μ§λ¬Έν΄μ£ΌμΈμ. μ΅μ μ λ€ν΄ λ΅λ³νκ² μ΅λλ€.");
|
|
|
|
| 1506 |
}
|
| 1507 |
+
hasLoadedSummary = true;
|
| 1508 |
+
|
| 1509 |
} catch (error) {
|
| 1510 |
console.error("PDF μμ½ λ‘λ μ€λ₯:", error);
|
| 1511 |
removeTypingIndicator();
|
| 1512 |
+
addChatMessage("μλ
νμΈμ! PDFμ λν΄ μ§λ¬Έν΄μ£ΌμΈμ.");
|
| 1513 |
hasLoadedSummary = true;
|
| 1514 |
} finally {
|
| 1515 |
isAiProcessing = false;
|
| 1516 |
}
|
| 1517 |
}
|
| 1518 |
|
| 1519 |
+
function startAnalysisPolling() {
|
| 1520 |
+
if (analysisCheckInterval) return;
|
| 1521 |
+
|
| 1522 |
+
analysisCheckInterval = setInterval(async () => {
|
| 1523 |
try {
|
| 1524 |
+
const data = await checkAnalysisStatus();
|
|
|
|
| 1525 |
|
| 1526 |
if (data.status === 'completed') {
|
| 1527 |
+
clearInterval(analysisCheckInterval);
|
| 1528 |
+
analysisCheckInterval = null;
|
| 1529 |
+
addChatMessage(`β
PDF λΆμμ΄ μλ£λμμ΅λλ€! (${data.analyzed_pages || data.total_pages}νμ΄μ§)<br>μ΄μ μμ λ‘κ² μ§λ¬Έν΄μ£ΌμΈμ.`);
|
| 1530 |
+
} else if (data.status === 'analyzing') {
|
| 1531 |
+
// μ§νλ₯ μ
λ°μ΄νΈ (μ νμ )
|
| 1532 |
+
console.log(`λΆμ μ§ν μ€: ${data.progress}%`);
|
| 1533 |
+
} else if (data.status === 'error') {
|
| 1534 |
+
clearInterval(analysisCheckInterval);
|
| 1535 |
+
analysisCheckInterval = null;
|
| 1536 |
+
addChatMessage(`β οΈ PDF λΆμ μ€ν¨: ${data.error || 'μ μ μλ μ€λ₯'}`);
|
| 1537 |
}
|
| 1538 |
} catch (e) {
|
| 1539 |
+
console.error("ν΄λ§ μ€λ₯:", e);
|
| 1540 |
}
|
| 1541 |
+
}, 5000); // 5μ΄λ§λ€ νμΈ
|
| 1542 |
|
| 1543 |
// 5λΆ ν μλ μ€μ§
|
| 1544 |
+
setTimeout(() => {
|
| 1545 |
+
if (analysisCheckInterval) {
|
| 1546 |
+
clearInterval(analysisCheckInterval);
|
| 1547 |
+
analysisCheckInterval = null;
|
| 1548 |
+
}
|
| 1549 |
+
}, 300000);
|
| 1550 |
}
|
| 1551 |
|
| 1552 |
async function submitQuestion(question) {
|
|
|
|
| 1559 |
addChatMessage(question, true);
|
| 1560 |
|
| 1561 |
// λΆμ μν νμΈ
|
| 1562 |
+
const statusData = await checkAnalysisStatus();
|
|
|
|
| 1563 |
|
| 1564 |
if (statusData.status !== 'completed') {
|
| 1565 |
+
if (statusData.status === 'analyzing') {
|
| 1566 |
+
addChatMessage(`PDF λΆμμ΄ μ§ν μ€μ
λλ€ (${statusData.progress || 0}%). μλ£ ν μ§λ¬Έν΄μ£ΌμΈμ. β³`);
|
| 1567 |
+
} else {
|
| 1568 |
+
addChatMessage("PDF λΆμμ΄ μμ§ μλ£λμ§ μμμ΅λλ€. μ μλ§ κΈ°λ€λ €μ£ΌμΈμ.");
|
| 1569 |
+
}
|
| 1570 |
isAiProcessing = false;
|
| 1571 |
$id('aiChatSubmit').disabled = false;
|
| 1572 |
+
$id('aiChatInput').value = question;
|
| 1573 |
return;
|
| 1574 |
}
|
| 1575 |
|
|
|
|
| 1579 |
method: 'POST',
|
| 1580 |
headers: { 'Content-Type': 'application/json' },
|
| 1581 |
body: JSON.stringify({ query: question }),
|
| 1582 |
+
signal: AbortSignal.timeout(120000)
|
| 1583 |
});
|
| 1584 |
|
| 1585 |
const data = await response.json();
|
|
|
|
| 1624 |
}
|
| 1625 |
|
| 1626 |
function updateLoading(message, progress) {
|
| 1627 |
+
const text = document.querySelector('.loading-text');
|
| 1628 |
if (text) text.textContent = message;
|
| 1629 |
const bar = $id('progressBar');
|
| 1630 |
if (bar && progress !== undefined) bar.style.width = `${progress}%`;
|
|
|
|
| 1754 |
}
|
| 1755 |
}
|
| 1756 |
|
|
|
|
| 1757 |
const cacheResponse = await fetch('/api/cached-pdf');
|
| 1758 |
let cachedData = await cacheResponse.json();
|
| 1759 |
|
|
|
|
| 1763 |
return;
|
| 1764 |
}
|
| 1765 |
|
|
|
|
| 1766 |
while (cachedData.status === "processing" || cachedData.status === "started") {
|
| 1767 |
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 1768 |
|
|
|
|
| 1798 |
document.addEventListener('DOMContentLoaded', function() {
|
| 1799 |
initializeAudio();
|
| 1800 |
|
|
|
|
| 1801 |
$id('aiButton').addEventListener('click', () => toggleAiChat(!isAiChatActive));
|
| 1802 |
$id('aiChatClose').addEventListener('click', () => toggleAiChat(false));
|
| 1803 |
|
|
|
|
| 1804 |
$id('aiChatForm').addEventListener('submit', function(e) {
|
| 1805 |
e.preventDefault();
|
| 1806 |
const question = $id('aiChatInput').value.trim();
|
|
|
|
| 1809 |
}
|
| 1810 |
});
|
| 1811 |
|
|
|
|
| 1812 |
loadPDF();
|
| 1813 |
});
|
| 1814 |
</script>
|