factlogic's picture
Update publication links for Phoenix model name
af3342d verified
Raw
History Blame
31.3 kB
#!/usr/bin/env python3
"""
โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
โ•‘ ๐Ÿ“š 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
# โ”€โ”€โ”€ ุฅุตู„ุงุญ ุชุฑู…ูŠุฒ Windows Console โ”€โ”€โ”€
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")
# โ”€โ”€โ”€ ุชุญู…ูŠู„ ุงู„ู…ุชุบูŠุฑุงุช ู…ู† ู…ู„ู .env โ”€โ”€โ”€
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / ".env")
# โ”€โ”€โ”€ ุฅุนุฏุงุฏ ู†ุธุงู… ุงู„ุณุฌู„ุงุช (Logging) โ”€โ”€โ”€
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s โ”‚ %(levelname)-7s โ”‚ %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("book_pipeline")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ“ฆ ู‡ูŠุงูƒู„ ุงู„ุจูŠุงู†ุงุช (Data Models)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@dataclass
class ChunkResult:
"""ู†ุชูŠุฌุฉ ุชุญู„ูŠู„ ู…ู‚ุทุน ูˆุงุญุฏ ู…ู† ุงู„ูƒุชุงุจ"""
chunk_index: int
pages: str # ู…ุซุงู„: "1-5"
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": ""})
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ“„ ุงู„ุฎุทูˆุฉ 1: ุชู‚ุณูŠู… ุงู„ูƒุชุงุจ (PDF Chunking)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def extract_text_from_pdf(pdf_path: str) -> list[str]:
"""
ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต ู…ู† ู…ู„ู PDF โ€” ุตูุญุฉ ุจุตูุญุฉ.
ูŠูุฑุฌุน ู‚ุงุฆู…ุฉ ู…ู† ุงู„ู†ุตูˆุต (ูƒู„ ุนู†ุตุฑ = ุตูุญุฉ ูˆุงุญุฏุฉ).
ูŠุณุชุฎุฏู… PyMuPDF (fitz) ู„ุฃู†ู‡ ุณุฑูŠุน ูˆุฏู‚ูŠู‚.
"""
try:
import fitz # PyMuPDF
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 # PyMuPDF
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 # 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
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿค– ุงู„ุฎุทูˆุฉ 2: ุงู„ุงุชุตุงู„ ุจู€ GLM-4.5 API
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โ”€โ”€โ”€ Prompt ุงู„ุชุญู„ูŠู„ ุงู„ุฏุงุฎู„ูŠ โ”€โ”€โ”€
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).
"""
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โฌ‡๏ธ ุถุน ู…ูุชุงุญ ุงู„ู€ API ุงู„ุฎุงุต ุจูƒ ู‡ู†ุง ุฃูˆ ููŠ ู…ู„ู .env
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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"}, # ุฅุฌุจุงุฑ JSON
}
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()
# ู…ุญุงูˆู„ุฉ 1: ุชุญู„ูŠู„ ู…ุจุงุดุฑ
try:
return json.loads(content)
except json.JSONDecodeError:
pass
# ู…ุญุงูˆู„ุฉ 2: ุงุณุชุฎุฑุงุฌ JSON ู…ู† ุจูŠู† ูƒุชู„ุฉ ูƒูˆุฏ
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
# ู…ุญุงูˆู„ุฉ 3: ุงู„ุจุญุซ ุนู† ุฃูˆู„ { ... } ููŠ ุงู„ู†ุต
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 ุตุงู„ุญ"},
}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”„ ุงู„ุฎุทูˆุฉ 3: ุชุดุบูŠู„ ุงู„ู€ Pipeline ุงู„ูƒุงู…ู„
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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)
# โ”€โ”€ ุฅู†ุดุงุก ุนู…ูŠู„ GLM โ”€โ”€
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}")
# ุฅุฑุณุงู„ ุฅู„ู‰ GLM-4.5
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}")
# ุชุฃุฎูŠุฑ ุจุณูŠุท ู„ุชุฌู†ุจ rate limiting
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
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ–จ๏ธ ุงู„ุฎุทูˆุฉ 4: ุทุจุงุนุฉ ูˆุญูุธ ุงู„ู†ุชุงุฆุฌ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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)")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿš€ ู†ู‚ุทุฉ ุงู„ุฏุฎูˆู„ (Entry Point)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
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)",
)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โฌ‡๏ธ ูŠู…ูƒู†ูƒ ูˆุถุน ู…ูุชุงุญ API ู‡ู†ุง ุจุฏู„ุงู‹ ู…ู† ู…ู„ู .env
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
parser.add_argument(
"--api-key",
default=os.getenv("OPENROUTER_API_KEY", ""),
help="ู…ูุชุงุญ OpenRouter API (ุฃูˆ ุถุนู‡ ููŠ .env ูƒู€ OPENROUTER_API_KEY)",
)
args = parser.parse_args()
try:
# ุชุดุบูŠู„ ุงู„ู€ Pipeline
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()