|
|
| """
|
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| โ ๐ Book Analysis Pipeline โ Phoenix AI โ
|
| โ ุชุญููู ุงููุชุจ ูุชุญููููุง ุฅูู ู
ูุฎุตุงุช ุฐููุฉ ุจุงุณุชุฎุฏุงู
GLM-4.5 โ
|
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
| ุงูุฎุทูุงุช:
|
| 1. ุชูุณูู
ุงููุชุงุจ (PDF Chunking)
|
| 2. ุงุณุชุฎุฑุงุฌ ุงููุต ุฃู ุชุญููู ุงูุตูุญุงุช ุฅูู ุตูุฑ
|
| 3. ุฅุฑุณุงู ูู ู
ูุทุน ุฅูู GLM-4.5 ุนุจุฑ OpenRouter API
|
| 4. ุชุฌู
ูุน ุงููุชุงุฆุฌ ูุญูุธูุง ุจุตูุบุฉ JSON
|
|
|
| ุงูุงุณุชุฎุฏุงู
:
|
| python book_pipeline.py "path/to/book.pdf"
|
| python book_pipeline.py "path/to/book.pdf" --output results.json
|
| python book_pipeline.py "path/to/book.pdf" --pages-per-chunk 5 --mode vision
|
| """
|
|
|
| import os
|
| import sys
|
| import json
|
| import time
|
| import base64
|
| import logging
|
| import argparse
|
| import re
|
| from pathlib import Path
|
| from typing import Optional
|
| from dataclasses import dataclass, field, asdict
|
| from io import BytesIO
|
|
|
|
|
| if sys.platform == "win32":
|
| import io
|
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
|
|
|
|
| from dotenv import load_dotenv
|
| load_dotenv(Path(__file__).parent / ".env")
|
|
|
|
|
| logging.basicConfig(
|
| level=logging.INFO,
|
| format="%(asctime)s โ %(levelname)-7s โ %(message)s",
|
| datefmt="%H:%M:%S",
|
| )
|
| logger = logging.getLogger("book_pipeline")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @dataclass
|
| class ChunkResult:
|
| """ูุชูุฌุฉ ุชุญููู ู
ูุทุน ูุงุญุฏ ู
ู ุงููุชุงุจ"""
|
| chunk_index: int
|
| pages: str
|
| cleaned_text: str = ""
|
| summary: str = ""
|
| key_points: list = field(default_factory=list)
|
| rating: dict = field(default_factory=lambda: {"score": "0/10", "reason": ""})
|
| error: Optional[str] = None
|
|
|
|
|
| @dataclass
|
| class BookAnalysis:
|
| """ุงููุชูุฌุฉ ุงูููุงุฆูุฉ ูุชุญููู ุงููุชุงุจ ุจุงููุงู
ู"""
|
| book_path: str
|
| total_pages: int
|
| total_chunks: int
|
| processing_time_seconds: float
|
| chunks: list = field(default_factory=list)
|
| final_summary: str = ""
|
| overall_rating: dict = field(default_factory=lambda: {"score": "0/10", "reason": ""})
|
|
|
|
|
|
|
|
|
|
|
|
|
| def extract_text_from_pdf(pdf_path: str) -> list[str]:
|
| """
|
| ุงุณุชุฎุฑุงุฌ ุงููุต ู
ู ู
ูู PDF โ ุตูุญุฉ ุจุตูุญุฉ.
|
| ููุฑุฌุน ูุงุฆู
ุฉ ู
ู ุงููุตูุต (ูู ุนูุตุฑ = ุตูุญุฉ ูุงุญุฏุฉ).
|
|
|
| ูุณุชุฎุฏู
PyMuPDF (fitz) ูุฃูู ุณุฑูุน ูุฏููู.
|
| """
|
| try:
|
| import fitz
|
| except ImportError as e:
|
| logger.error("โ ู
ูุชุจุฉ PyMuPDF ุบูุฑ ู
ุซุจุชุฉ. ูู
ุจุชุซุจูุชูุง: pip install PyMuPDF")
|
| raise ImportError("โ ู
ูุชุจุฉ PyMuPDF ุบูุฑ ู
ุซุจุชุฉ. ูู
ุจุชุซุจูุชูุง: pip install PyMuPDF") from e
|
|
|
| doc = fitz.open(pdf_path)
|
| pages_text = []
|
|
|
| for page_num in range(len(doc)):
|
| page = doc[page_num]
|
| text = page.get_text("text").strip()
|
| pages_text.append(text)
|
| logger.debug(f" ุตูุญุฉ {page_num + 1}: {len(text)} ุญุฑู")
|
|
|
| doc.close()
|
| logger.info(f"๐ ุชู
ุงุณุชุฎุฑุงุฌ ุงููุต ู
ู {len(pages_text)} ุตูุญุฉ")
|
| return pages_text
|
|
|
|
|
| def convert_pdf_pages_to_images(pdf_path: str, dpi: int = 200) -> list[bytes]:
|
| """
|
| ุชุญููู ุตูุญุงุช PDF ุฅูู ุตูุฑ PNG (ูููุถุน Vision).
|
| ููุฑุฌุน ูุงุฆู
ุฉ ู
ู ุงูุจูุงูุงุช ุงูุซูุงุฆูุฉ ููู ุตูุฑุฉ.
|
| """
|
| try:
|
| import fitz
|
| except ImportError as e:
|
| logger.error("โ ู
ูุชุจุฉ PyMuPDF ุบูุฑ ู
ุซุจุชุฉ. ูู
ุจุชุซุจูุชูุง: pip install PyMuPDF")
|
| raise ImportError("โ ู
ูุชุจุฉ PyMuPDF ุบูุฑ ู
ุซุจุชุฉ. ูู
ุจุชุซุจูุชูุง: pip install PyMuPDF") from e
|
|
|
| doc = fitz.open(pdf_path)
|
| images = []
|
| zoom = dpi / 72
|
|
|
| for page_num in range(len(doc)):
|
| page = doc[page_num]
|
| mat = fitz.Matrix(zoom, zoom)
|
| pix = page.get_pixmap(matrix=mat)
|
| img_bytes = pix.tobytes("png")
|
| images.append(img_bytes)
|
| logger.debug(f" ุตูุฑุฉ ุตูุญุฉ {page_num + 1}: {len(img_bytes) // 1024} KB")
|
|
|
| doc.close()
|
| logger.info(f"๐ผ๏ธ ุชู
ุชุญููู {len(images)} ุตูุญุฉ ุฅูู ุตูุฑ")
|
| return images
|
|
|
|
|
| def chunk_pages(items: list, pages_per_chunk: int) -> list[list]:
|
| """
|
| ุชูุณูู
ุงูุตูุญุงุช ุฅูู ู
ุฌู
ูุนุงุช (Chunks) ูุชุฌูุจ ุชุฌุงูุฒ ุญุฏ ุงูู Context Window.
|
|
|
| Args:
|
| items: ูุงุฆู
ุฉ ุงููุตูุต ุฃู ุงูุตูุฑ
|
| pages_per_chunk: ุนุฏุฏ ุงูุตูุญุงุช ูู ูู ู
ุฌู
ูุนุฉ
|
|
|
| Returns:
|
| ูุงุฆู
ุฉ ู
ู ุงูู
ุฌู
ูุนุงุช
|
| """
|
| chunks = []
|
| for i in range(0, len(items), pages_per_chunk):
|
| chunks.append(items[i:i + pages_per_chunk])
|
|
|
| logger.info(f"โ๏ธ ุชู
ุชูุณูู
{len(items)} ุตูุญุฉ ุฅูู {len(chunks)} ู
ุฌู
ูุนุฉ ({pages_per_chunk} ุตูุญุงุช/ู
ุฌู
ูุนุฉ)")
|
| return chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ANALYSIS_SYSTEM_PROMPT = """ุฃูุช ู
ุญูู ูุชุจ ุฎุจูุฑ ูู
ุชุฎุตุต ูู ุชุญููู ุงููุตูุต ุงูุนุฑุจูุฉ ูุงูุฅูุฌููุฒูุฉ. ู
ูู
ุชู ูู ุชุญููู ุงููุต ุงูู
ูุฑุณู ุฅููู ูุงูููุงู
ุจุงูู
ูุงู
ุงูุชุงููุฉ:
|
|
|
| 1. **ุชูุธูู ุงููุต**: ุตุญูุญ ุฃู ุฃุฎุทุงุก ุฅู
ูุงุฆูุฉ ุฃู ูุบููุฉ ุฃู ุฃุฎุทุงุก ูุงุชุฌุฉ ุนู OCR. ุฃุนุฏ ุตูุงุบุฉ ุงูุฌู
ู ุงูู
ูุณูุฑุฉ ูุชุตุจุญ ู
ูููู
ุฉ.
|
| 2. **ูุชุงุจุฉ ู
ูุฎุต ู
ูุซู**: ุงูุชุจ ู
ูุฎุตุงู ุดุงู
ูุงู ูู
ูุซูุงู ูููุต ูุบุทู ุงูุฃููุงุฑ ุงูุฑุฆูุณูุฉ.
|
| 3. **ุงุณุชุฎุฑุงุฌ ุฃูู
ุงูููุงุท**: ุงุณุชุฎุฑุฌ ุฃุจุฑุฒ ุงูููุงุท ูุงูุฃููุงุฑ ุงูุฑุฆูุณูุฉ (Key Points) ููุงุฆู
ุฉ.
|
| 4. **ุชูููู
ุงูู
ุญุชูู**: ุฃุนุทู ุชูููู
ุงู ู
ู 10 ู
ุน ุฐูุฑ ุงูุณุจุจ.
|
|
|
| ## ููุงุนุฏ ู
ูู
ุฉ:
|
| - ุฅุฐุง ูุงู ุงููุต ุจุงูุนุฑุจูุฉุ ุฃุฌุจ ุจุงูุนุฑุจูุฉ. ุฅุฐุง ูุงู ุจุงูุฅูุฌููุฒูุฉุ ุฃุฌุจ ุจุงูุฅูุฌููุฒูุฉ.
|
| - ูู ุฏูููุงู ูู
ุฎุชุตุฑุงู ูู ุงูู
ูุฎุต.
|
| - ุงูููุงุท ุงูุฑุฆูุณูุฉ ูุฌุจ ุฃู ุชููู ูุงุถุญุฉ ูู
ุญุฏุฏุฉ.
|
| - ุงูุชูููู
ูุฌุจ ุฃู ูููู ู
ูุถูุนูุงู ู
ุน ุชุจุฑูุฑ ูุงุถุญ.
|
|
|
| ## ุตูุบุฉ ุงูุฅุฌุงุจุฉ:
|
| ุฃุฌุจ **ุญุตุฑูุงู** ุจุตูุบุฉ JSON ุจุงููููู ุงูุชุงูู (ุจุฏูู ุฃู ูุต ุฅุถุงูู ุฎุงุฑุฌ JSON):
|
| {
|
| "cleaned_text": "ุงููุต ุจุนุฏ ุงูุชูุธูู ูุงูุชุตุญูุญ",
|
| "summary": "ู
ูุฎุต ู
ูุซู ููู
ุญุชูู",
|
| "key_points": ["ููุทุฉ 1", "ููุทุฉ 2", "ููุทุฉ 3"],
|
| "rating": {"score": "X/10", "reason": "ุณุจุจ ุงูุชูููู
"}
|
| }"""
|
|
|
| FINAL_SUMMARY_PROMPT = """ุฃูุช ู
ุญูู ูุชุจ ุฎุจูุฑ. ูุฏูู ู
ุฌู
ูุนุฉ ู
ูุฎุตุงุช ูุฃุฌุฒุงุก ู
ุฎุชููุฉ ู
ู ูุชุงุจ ูุงุญุฏ.
|
| ุงูุชุจ ู
ูุฎุตุงู ููุงุฆูุงู ุดุงู
ูุงู ูุฌู
ุน ูู ูุฐู ุงูุฃุฌุฒุงุก ูู ู
ูุฎุต ูุงุญุฏ ู
ุชูุงู
ู.
|
| ูุฐูู ุฃุนุทู ุชูููู
ุงู ููุงุฆูุงู ุดุงู
ูุงู ูููุชุงุจ.
|
|
|
| ุฃุฌุจ **ุญุตุฑูุงู** ุจุตูุบุฉ JSON:
|
| {
|
| "final_summary": "ุงูู
ูุฎุต ุงูููุงุฆู ุงูุดุงู
ู ูููุชุงุจ",
|
| "overall_rating": {"score": "X/10", "reason": "ุณุจุจ ุงูุชูููู
ุงูุดุงู
ู"}
|
| }"""
|
|
|
|
|
| class GLMClient:
|
| """
|
| ุนู
ูู ููุชูุงุตู ู
ุน GLM-4.5 ุนุจุฑ OpenRouter API.
|
| ูุฏุนู
ุงููุถุนูู: ุงููุตู (text) ูุงูุจุตุฑู (vision).
|
| """
|
|
|
|
|
|
|
|
|
| BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
|
|
|
| def __init__(self, api_key: str, model: str = "z-ai/glm-4.5-air"):
|
| """
|
| Args:
|
| api_key: ู
ูุชุงุญ OpenRouter API
|
| โ ูู
ููู ูุถุนู ู
ุจุงุดุฑุฉ ููุง ุฃู ูู ู
ูู .env ูู OPENROUTER_API_KEY
|
| model: ุงุณู
ุงููู
ูุฐุฌ ุนูู OpenRouter
|
| """
|
| if not api_key:
|
| logger.error("โ ูู
ูุชู
ุชูููุฑ ู
ูุชุงุญ API!")
|
| logger.error(" ุถุนู ูู ู
ูู .env ูู OPENROUTER_API_KEY=sk-or-v1-...")
|
| logger.error(" ุฃู ู
ุฑุฑู ูู argument: --api-key YOUR_KEY")
|
| raise ValueError("ู
ูุชุงุญ OpenRouter API ุบูุฑ ู
ุชููุฑ.")
|
|
|
| self.api_key = api_key
|
| self.model = model
|
| self._session = None
|
|
|
| @property
|
| def headers(self) -> dict:
|
| return {
|
| "Authorization": f"Bearer {self.api_key}",
|
| "Content-Type": "application/json",
|
| "HTTP-Referer": "https://phoenix-ai.app",
|
| }
|
|
|
| def _get_session(self):
|
| """ุฅูุดุงุก ุฌูุณุฉ HTTP ูุงุญุฏุฉ ูุฅุนุงุฏุฉ ุงูุงุณุชุฎุฏุงู
"""
|
| if self._session is None:
|
| import requests
|
| self._session = requests.Session()
|
| self._session.headers.update(self.headers)
|
| return self._session
|
|
|
| def analyze_text_chunk(self, text: str, chunk_info: str = "") -> dict:
|
| """
|
| ุฅุฑุณุงู ู
ูุทุน ูุตู ุฅูู GLM-4.5 ููุชุญููู.
|
|
|
| Args:
|
| text: ุงููุต ุงูู
ุฑุงุฏ ุชุญูููู
|
| chunk_info: ู
ุนููู
ุงุช ุนู ุงูู
ูุทุน (ู
ุซู "ุตูุญุงุช 1-5")
|
|
|
| Returns:
|
| dict ุจุงููููู: {cleaned_text, summary, key_points, rating}
|
| """
|
| import requests
|
|
|
| user_message = f"ุญููู ุงููุต ุงูุชุงูู ({chunk_info}):\n\n{text}"
|
|
|
| payload = {
|
| "model": self.model,
|
| "messages": [
|
| {"role": "system", "content": ANALYSIS_SYSTEM_PROMPT},
|
| {"role": "user", "content": user_message},
|
| ],
|
| "temperature": 0.3,
|
| "max_tokens": 4096,
|
| "response_format": {"type": "json_object"},
|
| }
|
|
|
| try:
|
| session = self._get_session()
|
| response = session.post(self.BASE_URL, json=payload, timeout=120)
|
| response.raise_for_status()
|
|
|
| content = response.json()["choices"][0]["message"].get("content", "")
|
| return self._parse_json_response(content)
|
|
|
| except requests.exceptions.Timeout:
|
| logger.error(f"โฐ ุงูุชูุช ู
ููุฉ ุงูุทูุจ ูู {chunk_info}")
|
| return {"error": "Request timeout"}
|
| except requests.exceptions.HTTPError as e:
|
| logger.error(f"๐ ุฎุทุฃ HTTP: {e.response.status_code} โ {e.response.text[:200]}")
|
| return {"error": f"HTTP {e.response.status_code}"}
|
| except Exception as e:
|
| logger.error(f"โ ุฎุทุฃ ุบูุฑ ู
ุชููุน: {e}")
|
| return {"error": str(e)}
|
|
|
| def analyze_image_chunk(self, images: list[bytes], chunk_info: str = "") -> dict:
|
| """
|
| ุฅุฑุณุงู ุตูุฑ ุงูุตูุญุงุช ุฅูู GLM-4.5 (ูุถุน Vision) ููุชุญููู.
|
|
|
| Args:
|
| images: ูุงุฆู
ุฉ ู
ู ุจูุงูุงุช ุงูุตูุฑ (PNG bytes)
|
| chunk_info: ู
ุนููู
ุงุช ุนู ุงูู
ูุทุน
|
|
|
| Returns:
|
| dict ุจุงููููู: {cleaned_text, summary, key_points, rating}
|
| """
|
| import requests
|
|
|
|
|
| content_parts = [
|
| {"type": "text", "text": f"ุญููู ุงููุต ุงูู
ูุฌูุฏ ูู ุงูุตูุฑ ุงูุชุงููุฉ ({chunk_info}). "
|
| f"ุงุณุชุฎุฑุฌ ุงููุต ุฃููุงู ุซู
ุญูููู:"}
|
| ]
|
|
|
| for i, img_bytes in enumerate(images):
|
| b64_image = base64.b64encode(img_bytes).decode("utf-8")
|
| content_parts.append({
|
| "type": "image_url",
|
| "image_url": {
|
| "url": f"data:image/png;base64,{b64_image}",
|
| }
|
| })
|
|
|
| payload = {
|
| "model": self.model,
|
| "messages": [
|
| {"role": "system", "content": ANALYSIS_SYSTEM_PROMPT},
|
| {"role": "user", "content": content_parts},
|
| ],
|
| "temperature": 0.3,
|
| "max_tokens": 4096,
|
| "response_format": {"type": "json_object"},
|
| }
|
|
|
| try:
|
| session = self._get_session()
|
| response = session.post(self.BASE_URL, json=payload, timeout=180)
|
| response.raise_for_status()
|
|
|
| content = response.json()["choices"][0]["message"].get("content", "")
|
| return self._parse_json_response(content)
|
|
|
| except Exception as e:
|
| logger.error(f"โ ุฎุทุฃ ูู ูุถุน Vision: {e}")
|
| return {"error": str(e)}
|
|
|
| def generate_final_summary(self, chunk_summaries: list[str]) -> dict:
|
| """
|
| ุชูููุฏ ู
ูุฎุต ููุงุฆู ุดุงู
ู ู
ู ุฌู
ูุน ู
ูุฎุตุงุช ุงูุฃุฌุฒุงุก.
|
|
|
| Args:
|
| chunk_summaries: ูุงุฆู
ุฉ ุงูู
ูุฎุตุงุช ุงููุฑุนูุฉ
|
|
|
| Returns:
|
| dict ุจุงููููู: {final_summary, overall_rating}
|
| """
|
| import requests
|
|
|
| combined = "\n\n---\n\n".join(
|
| [f"### ุงูุฌุฒุก {i+1}:\n{s}" for i, s in enumerate(chunk_summaries)]
|
| )
|
|
|
| payload = {
|
| "model": self.model,
|
| "messages": [
|
| {"role": "system", "content": FINAL_SUMMARY_PROMPT},
|
| {"role": "user", "content": f"ุงุฌู
ุน ุงูู
ูุฎุตุงุช ุงูุชุงููุฉ ูู ู
ูุฎุต ููุงุฆู:\n\n{combined}"},
|
| ],
|
| "temperature": 0.3,
|
| "max_tokens": 4096,
|
| "response_format": {"type": "json_object"},
|
| }
|
|
|
| try:
|
| session = self._get_session()
|
| response = session.post(self.BASE_URL, json=payload, timeout=120)
|
| response.raise_for_status()
|
|
|
| content = response.json()["choices"][0]["message"].get("content", "")
|
| return self._parse_json_response(content)
|
|
|
| except Exception as e:
|
| logger.error(f"โ ุฎุทุฃ ูู ุชูููุฏ ุงูู
ูุฎุต ุงูููุงุฆู: {e}")
|
| return {"error": str(e)}
|
|
|
| @staticmethod
|
| def _parse_json_response(content: str) -> dict:
|
| """
|
| ุชุญููู ุงุณุชุฌุงุจุฉ JSON ู
ู ุงูู API ู
ุน ู
ุนุงูุฌุฉ ุงูุญุงูุงุช ุงูู
ุฎุชููุฉ.
|
| """
|
| content = content.strip()
|
|
|
|
|
| try:
|
| return json.loads(content)
|
| except json.JSONDecodeError:
|
| pass
|
|
|
|
|
| code_block_match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", content, re.DOTALL)
|
| if code_block_match:
|
| try:
|
| return json.loads(code_block_match.group(1).strip())
|
| except json.JSONDecodeError:
|
| pass
|
|
|
|
|
| brace_match = re.search(r"\{.*\}", content, re.DOTALL)
|
| if brace_match:
|
| try:
|
| return json.loads(brace_match.group(0))
|
| except json.JSONDecodeError:
|
| pass
|
|
|
|
|
| logger.warning("โ ๏ธ ูุดู ุชุญููู JSON โ ุฅุฑุฌุงุน ุงููุต ุงูุฎุงู
")
|
| return {
|
| "cleaned_text": content,
|
| "summary": "ูุดู ุงูุชุญููู ุงูุชููุงุฆู",
|
| "key_points": [],
|
| "rating": {"score": "N/A", "reason": "ูู
ูุชู
ูู ุงููู
ูุฐุฌ ู
ู ุฅุฑุฌุงุน JSON ุตุงูุญ"},
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| def run_pipeline(
|
| pdf_path: str,
|
| api_key: str,
|
| model: str = "z-ai/glm-4.5-air",
|
| pages_per_chunk: int = 5,
|
| mode: str = "text",
|
| output_file: Optional[str] = None,
|
| ) -> BookAnalysis:
|
| """
|
| ุชุดุบูู Pipeline ุชุญููู ุงููุชุงุจ ุจุงููุงู
ู.
|
|
|
| Args:
|
| pdf_path: ู
ุณุงุฑ ู
ูู ุงูู PDF
|
| api_key: ู
ูุชุงุญ OpenRouter API
|
| model: ุงุณู
ุงููู
ูุฐุฌ
|
| pages_per_chunk: ุนุฏุฏ ุงูุตูุญุงุช ูู ูู ู
ุฌู
ูุนุฉ
|
| mode: "text" ูุงุณุชุฎุฑุงุฌ ุงููุต | "vision" ูุชู
ุฑูุฑ ุงูุตูุฑ ู
ุจุงุดุฑุฉ
|
| output_file: ู
ุณุงุฑ ู
ูู ุงูุฅุฎุฑุงุฌ (JSON) โ ุงุฎุชูุงุฑู
|
|
|
| Returns:
|
| BookAnalysis: ูุชูุฌุฉ ุงูุชุญููู ุงููุงู
ูุฉ
|
| """
|
| start_time = time.time()
|
|
|
|
|
| if not Path(pdf_path).exists():
|
| logger.error(f"โ ุงูู
ูู ุบูุฑ ู
ูุฌูุฏ: {pdf_path}")
|
| raise FileNotFoundError(f"ุงูู
ูู ุบูุฑ ู
ูุฌูุฏ: {pdf_path}")
|
|
|
| if not pdf_path.lower().endswith(".pdf"):
|
| logger.error(f"โ ุงูู
ูู ููุณ PDF: {pdf_path}")
|
| raise ValueError(f"ุงูู
ูู ููุณ PDF: {pdf_path}")
|
|
|
| logger.info("=" * 60)
|
| logger.info(f"๐ ุจุฏุก ุชุญููู ุงููุชุงุจ: {Path(pdf_path).name}")
|
| logger.info(f" ุงููุถุน: {'ูุตู (Text)' if mode == 'text' else 'ุจุตุฑู (Vision)'}")
|
| logger.info(f" ุงููู
ูุฐุฌ: {model}")
|
| logger.info(f" ุตูุญุงุช/ู
ุฌู
ูุนุฉ: {pages_per_chunk}")
|
| logger.info("=" * 60)
|
|
|
|
|
| client = GLMClient(api_key=api_key, model=model)
|
|
|
|
|
| if mode == "vision":
|
| logger.info("\n๐ผ๏ธ ุงููุถุน ุงูุจุตุฑู: ุชุญููู ุงูุตูุญุงุช ุฅูู ุตูุฑ...")
|
| all_pages = convert_pdf_pages_to_images(pdf_path)
|
| else:
|
| logger.info("\n๐ ุงููุถุน ุงููุตู: ุงุณุชุฎุฑุงุฌ ุงููุต ู
ู ุงูุตูุญุงุช...")
|
| all_pages = extract_text_from_pdf(pdf_path)
|
|
|
|
|
| total_chars = sum(len(p) for p in all_pages)
|
| if total_chars < 100:
|
| logger.warning(f"โ ๏ธ ุงููุต ุงูู
ุณุชุฎุฑุฌ ูููู ุฌุฏุงู ({total_chars} ุญุฑู)")
|
| logger.info(" ๐ก ุฌุฑูุจ ุงููุถุน ุงูุจุตุฑู: --mode vision")
|
|
|
|
|
| logger.info(" ๐ ุงูุชุจุฏูู ุงูุชููุงุฆู ุฅูู ุงููุถุน ุงูุจุตุฑู...")
|
| mode = "vision"
|
| all_pages = convert_pdf_pages_to_images(pdf_path)
|
|
|
| total_pages = len(all_pages)
|
|
|
|
|
| chunks = chunk_pages(all_pages, pages_per_chunk)
|
|
|
|
|
| results: list[ChunkResult] = []
|
|
|
| for idx, chunk in enumerate(chunks):
|
| start_page = idx * pages_per_chunk + 1
|
| end_page = min(start_page + pages_per_chunk - 1, total_pages)
|
| chunk_info = f"ุตูุญุงุช {start_page}-{end_page}"
|
|
|
| logger.info(f"\n{'โ' * 50}")
|
| logger.info(f"๐ ู
ุนุงูุฌุฉ ุงูู
ุฌู
ูุนุฉ {idx + 1}/{len(chunks)}: {chunk_info}")
|
| logger.info(f"{'โ' * 50}")
|
|
|
|
|
| if mode == "vision":
|
| api_result = client.analyze_image_chunk(chunk, chunk_info)
|
| else:
|
|
|
| combined_text = "\n\n--- ุตูุญุฉ ุฌุฏูุฏุฉ ---\n\n".join(chunk)
|
|
|
| if not combined_text.strip():
|
| logger.warning(f" โญ๏ธ ุชุฎุทู: ุงูู
ุฌู
ูุนุฉ ูุงุฑุบุฉ")
|
| results.append(ChunkResult(
|
| chunk_index=idx + 1,
|
| pages=f"{start_page}-{end_page}",
|
| error="Empty chunk โ no text extracted",
|
| ))
|
| continue
|
|
|
| api_result = client.analyze_text_chunk(combined_text, chunk_info)
|
|
|
|
|
| chunk_result = ChunkResult(
|
| chunk_index=idx + 1,
|
| pages=f"{start_page}-{end_page}",
|
| cleaned_text=api_result.get("cleaned_text", ""),
|
| summary=api_result.get("summary", ""),
|
| key_points=api_result.get("key_points", []),
|
| rating=api_result.get("rating", {"score": "N/A", "reason": ""}),
|
| error=api_result.get("error"),
|
| )
|
| results.append(chunk_result)
|
|
|
|
|
| if not chunk_result.error:
|
| logger.info(f" โ
ู
ูุฎุต: {chunk_result.summary[:80]}...")
|
| logger.info(f" ๐ ุชูููู
: {chunk_result.rating.get('score', 'N/A')}")
|
| else:
|
| logger.warning(f" โ ๏ธ ุฎุทุฃ: {chunk_result.error}")
|
|
|
|
|
| if idx < len(chunks) - 1:
|
| logger.info(" โณ ุงูุชุธุงุฑ 2 ุซุงููุฉ...")
|
| time.sleep(2)
|
|
|
|
|
| successful_summaries = [r.summary for r in results if r.summary and not r.error]
|
|
|
| final_summary = ""
|
| overall_rating = {"score": "N/A", "reason": ""}
|
|
|
| if len(successful_summaries) > 1:
|
| logger.info(f"\n{'โ' * 50}")
|
| logger.info("๐ ุชูููุฏ ุงูู
ูุฎุต ุงูููุงุฆู ุงูุดุงู
ู...")
|
| logger.info(f"{'โ' * 50}")
|
|
|
| final_result = client.generate_final_summary(successful_summaries)
|
| final_summary = final_result.get("final_summary", "")
|
| overall_rating = final_result.get("overall_rating", {"score": "N/A", "reason": ""})
|
| elif len(successful_summaries) == 1:
|
| final_summary = successful_summaries[0]
|
| overall_rating = results[0].rating if results else {"score": "N/A", "reason": ""}
|
|
|
|
|
| elapsed = time.time() - start_time
|
|
|
| analysis = BookAnalysis(
|
| book_path=str(Path(pdf_path).resolve()),
|
| total_pages=total_pages,
|
| total_chunks=len(chunks),
|
| processing_time_seconds=round(elapsed, 2),
|
| chunks=[asdict(r) for r in results],
|
| final_summary=final_summary,
|
| overall_rating=overall_rating,
|
| )
|
|
|
|
|
| _print_formatted_output(analysis)
|
|
|
| if output_file:
|
| _save_to_json(analysis, output_file)
|
|
|
| return analysis
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _print_formatted_output(analysis: BookAnalysis):
|
| """ุทุจุงุนุฉ ุงููุชุงุฆุฌ ุจุดูู ู
ูุณู ูุฌู
ูู ูู ุงูู Terminal"""
|
| print("\n")
|
| print("โ" + "โ" * 62 + "โ")
|
| print("โ" + " ๐ ูุชุงุฆุฌ ุชุญููู ุงููุชุงุจ".center(60) + "โ")
|
| print("โ" + "โ" * 62 + "โ")
|
|
|
| print(f"\n๐ ุงูู
ูู: {Path(analysis.book_path).name}")
|
| print(f"๐ ุนุฏุฏ ุงูุตูุญุงุช: {analysis.total_pages}")
|
| print(f"โ๏ธ ุนุฏุฏ ุงูู
ุฌู
ูุนุงุช: {analysis.total_chunks}")
|
| print(f"โฑ๏ธ ููุช ุงูู
ุนุงูุฌุฉ: {analysis.processing_time_seconds} ุซุงููุฉ")
|
|
|
|
|
| for chunk in analysis.chunks:
|
| print(f"\n{'โ' * 60}")
|
| print(f"๐ ุงูู
ุฌู
ูุนุฉ {chunk['chunk_index']} (ุตูุญุงุช {chunk['pages']})")
|
| print(f"{'โ' * 60}")
|
|
|
| if chunk.get("error"):
|
| print(f" โ ๏ธ ุฎุทุฃ: {chunk['error']}")
|
| continue
|
|
|
| if chunk.get("summary"):
|
| print(f"\n ๐ ุงูู
ูุฎุต:")
|
| _print_wrapped(chunk["summary"], indent=6)
|
|
|
| if chunk.get("key_points"):
|
| print(f"\n ๐ ุฃูู
ุงูููุงุท:")
|
| for i, point in enumerate(chunk["key_points"], 1):
|
| print(f" {i}. {point}")
|
|
|
| if chunk.get("rating"):
|
| rating = chunk["rating"]
|
| print(f"\n ๐ ุงูุชูููู
: {rating.get('score', 'N/A')}")
|
| if rating.get("reason"):
|
| print(f" ุงูุณุจุจ: {rating['reason']}")
|
|
|
|
|
| if analysis.final_summary:
|
| print(f"\n{'โ' * 60}")
|
| print("๐ ุงูู
ูุฎุต ุงูููุงุฆู ุงูุดุงู
ู:")
|
| print(f"{'โ' * 60}")
|
| _print_wrapped(analysis.final_summary, indent=3)
|
|
|
| if analysis.overall_rating and analysis.overall_rating.get("score") != "N/A":
|
| print(f"\n๐ ุงูุชูููู
ุงูููุงุฆู: {analysis.overall_rating.get('score', 'N/A')}")
|
| if analysis.overall_rating.get("reason"):
|
| print(f" ุงูุณุจุจ: {analysis.overall_rating['reason']}")
|
|
|
| print(f"\n{'โ' * 60}\n")
|
|
|
|
|
| def _print_wrapped(text: str, indent: int = 0, width: int = 76):
|
| """ุทุจุงุนุฉ ูุต ุทููู ู
ุน ุงูุชูุงู ุงูุฃุณุทุฑ"""
|
| prefix = " " * indent
|
| words = text.split()
|
| line = prefix
|
|
|
| for word in words:
|
| if len(line) + len(word) + 1 > width:
|
| print(line)
|
| line = prefix + word
|
| else:
|
| line = line + " " + word if line.strip() else prefix + word
|
|
|
| if line.strip():
|
| print(line)
|
|
|
|
|
| def _save_to_json(analysis: BookAnalysis, output_path: str):
|
| """ุญูุธ ุงููุชุงุฆุฌ ูู ู
ูู JSON"""
|
| output = asdict(analysis)
|
|
|
| with open(output_path, "w", encoding="utf-8") as f:
|
| json.dump(output, f, ensure_ascii=False, indent=2)
|
|
|
| file_size = Path(output_path).stat().st_size / 1024
|
| logger.info(f"๐พ ุชู
ุญูุธ ุงููุชุงุฆุฌ ูู: {output_path} ({file_size:.1f} KB)")
|
|
|
|
|
|
|
|
|
|
|
|
|
| def main():
|
| parser = argparse.ArgumentParser(
|
| description="๐ Book Analysis Pipeline โ Phoenix AI",
|
| formatter_class=argparse.RawDescriptionHelpFormatter,
|
| epilog="""
|
| ุฃู
ุซูุฉ:
|
| python book_pipeline.py "ูุชุงุจู.pdf"
|
| python book_pipeline.py "ูุชุงุจู.pdf" --output results.json
|
| python book_pipeline.py "ูุชุงุจู.pdf" --mode vision --pages-per-chunk 3
|
| python book_pipeline.py "ูุชุงุจู.pdf" --api-key sk-or-v1-xxxxx
|
| """,
|
| )
|
|
|
| parser.add_argument("pdf_path", help="ู
ุณุงุฑ ู
ูู ุงูู PDF")
|
| parser.add_argument("--output", "-o", help="ู
ุณุงุฑ ู
ูู ุงูุฅุฎุฑุงุฌ (JSON)", default=None)
|
| parser.add_argument(
|
| "--pages-per-chunk", "-c", type=int, default=5,
|
| help="ุนุฏุฏ ุงูุตูุญุงุช ูู ูู ู
ุฌู
ูุนุฉ (ุงูุชุฑุงุถู: 5)",
|
| )
|
| parser.add_argument(
|
| "--mode", "-m", choices=["text", "vision"], default="text",
|
| help="ูุถุน ุงูู
ุนุงูุฌุฉ: text (ุงุณุชุฎุฑุงุฌ ุงููุต) ุฃู vision (ุชู
ุฑูุฑ ุงูุตูุฑ)",
|
| )
|
| parser.add_argument(
|
| "--model", default="z-ai/glm-4.5-air",
|
| help="ุงุณู
ุงููู
ูุฐุฌ ุนูู OpenRouter (ุงูุชุฑุงุถู: glm-4.5-air)",
|
| )
|
|
|
|
|
|
|
|
|
| parser.add_argument(
|
| "--api-key",
|
| default=os.getenv("OPENROUTER_API_KEY", ""),
|
| help="ู
ูุชุงุญ OpenRouter API (ุฃู ุถุนู ูู .env ูู OPENROUTER_API_KEY)",
|
| )
|
|
|
| args = parser.parse_args()
|
|
|
| try:
|
|
|
| run_pipeline(
|
| pdf_path=args.pdf_path,
|
| api_key=args.api_key,
|
| model=args.model,
|
| pages_per_chunk=args.pages_per_chunk,
|
| mode=args.mode,
|
| output_file=args.output,
|
| )
|
| except Exception as e:
|
| logger.error(f"โ ุญุฏุซ ุฎุทุฃ ุฃุซูุงุก ุชุดุบูู Pipeline: {e}")
|
| sys.exit(1)
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|