-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
176 lines (141 loc) · 6.7 KB
/
Copy pathmain.py
File metadata and controls
176 lines (141 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python3
"""
Round-1B | Persona-Aware PDF Pipeline
------------------------------------
CLI
python main.py <input_dir> <output_dir>
• Reads challenge1b_input.json from <input_dir>.
• Runs the original Round-1A extractor *in parallel* on every PDF
(each worker opens the file from bytes once).
• Embeds + ranks sections with Granite-107 M embeddings and BM25.
• Refines best sections (thread-pool) and writes challenge1b_output.json
to <output_dir>.
"""
import json
import os
import sys
from datetime import datetime, timezone
from typing import Dict, List
import multiprocessing
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
# Round-1B modules
from semantic_analyzer import SemanticAnalyzer
from relevance_scorer import RelevanceScorer
from subsection_extractor import SubsectionExtractor
# Disable oneDNN heuristics for small-CPU images (optional but safe)
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
# ───────────────────────── Helpers ──────────────────────────
def _extract_outline_blob(pdf_path: str) -> Dict:
"""Worker: run Round-1A on a PDF file path."""
# This import is intentionally local to the worker process
from r1a.enhanced_pdf_extractor import process_pdf_enhanced
return process_pdf_enhanced(pdf_path)
def _load_input(inp_dir: str) -> Dict:
with open(os.path.join(inp_dir, "challenge1b_input.json"), encoding="utf-8") as fh:
return json.load(fh)
def _write_output(out_dir: str, data: Dict) -> None:
os.makedirs(out_dir, exist_ok=True)
with open(os.path.join(out_dir, "challenge1b_output.json"), "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, ensure_ascii=False)
# ───────────────────────── Main ──────────────────────────
def run_pipeline(input_dir: str, output_dir: str) -> None:
req = _load_input(input_dir)
metadata = {
"input_documents": [d["filename"] for d in req["documents"]],
"persona": req["persona"]["role"],
"job_to_be_done": req["job_to_be_done"]["task"],
"processing_timestamp": datetime.now(timezone.utc).isoformat()
}
# 1. Parallel outline extraction (Round-1A)
pdf_dir = os.path.join(input_dir, "PDFs")
outlines: Dict[str, Dict] = {}
pdf_paths = [os.path.join(pdf_dir, d["filename"]) for d in req["documents"]]
max_w = min(len(req["documents"]), multiprocessing.cpu_count())
with ProcessPoolExecutor(max_workers=max_w) as pool:
fut2doc = {}
for pdf_path, d in zip(pdf_paths, req["documents"]):
fut2doc[pool.submit(_extract_outline_blob, pdf_path)] = d
for fut in as_completed(fut2doc):
doc = fut2doc[fut]
outlines[doc["filename"]] = fut.result()
# 2. Initialise semantic engine + ranker + subsection extractor
model_dir = os.path.abspath("models") # Models are here
sem = SemanticAnalyzer(model_dir=model_dir, model = "granite-embedding-107m-multilingual")
rank = RelevanceScorer()
subex = SubsectionExtractor()
sem.embed_persona_and_task(metadata["persona"], metadata["job_to_be_done"])
rank.extract_task_keywords(metadata["job_to_be_done"])
extracted_sections: List[Dict] = []
subsection_analysis: List[Dict] = []
# 3. Process each PDF
for d in req["documents"]:
fname = d["filename"]
outline_data = outlines.get(fname, {})
outline = outline_data.get("outline", [])
# --- FIX START: Handle title separately and aggressively find one if needed ---
title_txt = outline_data.get("title", "").strip()
# Aggressive title finding: If the extractor fails, assume the first heading
# on page 1 is the title. This is a workaround for a potentially strict
# title extraction logic in the upstream `enhanced_pdf_extractor`.
if not title_txt and outline and outline[0].get("page") == 1:
potential_title = outline.pop(0) # Remove from outline to avoid duplication
title_txt = potential_title.get("text", "").strip()
title_was_added = False
if title_txt:
# Add the main title with the highest importance rank (1)
extracted_sections.append({
"document": fname,
"section_title": title_txt,
"importance_rank": 1,
"page_number": 1
})
# For subsection analysis, we can use the title itself as the "refined text"
subsection_analysis.append({
"document": fname,
"refined_text": title_txt,
"page_number": 1
})
title_was_added = True
# --- FIX END ---
if not outline:
continue
sections = sem.embed_sections(outline, fname)
for s in sections:
s["semantic_analyzer"] = sem
top5 = rank.rank_sections(sections)
pdf_path = os.path.join(pdf_dir, fname)
# Thread-pool: refine five sections in parallel
def _ref(sec):
return subex.refine_section(
pdf_path, sec.get("start_page", sec["page"]), sec.get("end_page", sec["page"]),
sec["title"], sem, rank
)
with ThreadPoolExecutor(max_workers=4) as tpool:
refined_list = list(tpool.map(_ref, top5))
for sec, refined in zip(top5, refined_list):
# Adjust rank if a title was already added for this document to maintain hierarchy
rank_offset = 1 if title_was_added else 0
extracted_sections.append({
"document": fname,
"section_title": sec["title"],
"importance_rank": sec["rank"] + rank_offset,
"page_number": sec["page"]
})
subsection_analysis.append({
"document": fname,
"refined_text": refined["text"],
"page_number": refined["page"]
})
# 4. Write output
extracted_sections.sort(key=lambda x: x["importance_rank"])
_write_output(output_dir, {
"metadata": metadata,
"extracted_sections": extracted_sections,
"subsection_analysis": subsection_analysis
})
# ───────────────────────── Entrypoint ──────────────────────────
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python main.py <input_dir> <output_dir>")
sys.exit(1)
run_pipeline(sys.argv[1], sys.argv[2])