Firmansyah-Ibrahim commited on
Commit
0350da9
Β·
verified Β·
1 Parent(s): ec08541

Create codekaggle/ .extractor_part_pdf

Browse files
Files changed (1) hide show
  1. codekaggle/ .extractor_part_pdf +385 -0
codekaggle/ .extractor_part_pdf ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # IBEX v2.4 β€” Indo-Bloom Context Extractor
3
+ # Kaggle Notebook (CPU, tidak butuh GPU)
4
+ #
5
+ # CARA PAKAI:
6
+ # 1. Upload PDF BSE ke Kaggle Input (+ Add Input β†’ klik titik 3 β†’ Upload)
7
+ # 2. Sesuaikan CONFIG di Cell 2
8
+ # 3. Run All β†’ unduh CSV di bagian bawah
9
+ #
10
+ # Output: CSV dengan kolom:
11
+ # chunk_id | source_file | source_type | page_range |
12
+ # word_count | noise_score | kata_dibuang_l2 | context
13
+ #
14
+ # Requirements: pymupdf pandas (sudah pre-installed di Kaggle)
15
+ # =============================================================================
16
+
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+ # CELL 1 β€” Install (jika belum ada)
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ import subprocess, sys
21
+ subprocess.run([sys.executable, "-m", "pip", "install", "-q", "pymupdf"], check=False)
22
+
23
+ import os, re, tempfile
24
+ import pandas as pd
25
+ import fitz # PyMuPDF
26
+
27
+ print("βœ… Library siap.")
28
+
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # CELL 2 β€” KONFIGURASI (SESUAIKAN DI SINI)
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ CONFIG = {
34
+ # Path ke PDF β€” setelah upload ke Kaggle Input, path-nya biasanya:
35
+ # /kaggle/input/<nama-dataset>/<nama-file>.pdf
36
+ "pdf_path" : "/kaggle/input/datasets/baimfirmansyah/sosiologi-bs-kls-x11/Sosiologi_BS_KLS_XII.pdf",
37
+
38
+ # Tipe sumber: "BSE" atau "Wikipedia"
39
+ "source_type" : "BSE",
40
+
41
+ # ── Pembagian part ────────────────────────────────────────────────────────
42
+ # Jalankan notebook ini sekali per part, ubah hal_mulai & hal_selesai
43
+ # Rekomendasi untuk BSE Sosiologi XII:
44
+ # Part 1: 15–35 | Part 2: 36–55 | Part 3: 56–75
45
+ # Part 4: 76–95 | Part 5: 96–115 | Part 6: 116–135
46
+ # Part 7: 136–155 (sesuaikan dengan halaman terakhir konten)
47
+ "hal_mulai" : 36,
48
+ "hal_selesai" : 55,
49
+
50
+ # ── Parameter ekstraksi ───────────────────────────────────────────────────
51
+ # Kata per chunk: 100–200 (rekomendasi 150 untuk BSE)
52
+ "chunk_size" : 150,
53
+
54
+ # Toleransi noise: 1–6
55
+ # BSE β†’ 2 (ketat, banyak instruksional)
56
+ # Wikipedia β†’ 4 (lebih longgar, teks lebih bersih)
57
+ "batas_noise" : 2,
58
+
59
+ # Folder output CSV
60
+ "output_dir" : "/kaggle/working",
61
+ }
62
+
63
+ print(f"πŸ“‹ Konfigurasi:")
64
+ print(f" PDF : {CONFIG['pdf_path']}")
65
+ print(f" Sumber : {CONFIG['source_type']}")
66
+ print(f" Halaman : {CONFIG['hal_mulai']} – {CONFIG['hal_selesai']}")
67
+ print(f" Chunk size : {CONFIG['chunk_size']} kata")
68
+ print(f" Batas noise: {CONFIG['batas_noise']}")
69
+
70
+ # ─────────────────────────────────────────────────────────────────────────────
71
+ # CELL 3 β€” KONSTANTA & POLA FILTER
72
+ # ─────────────────────────────────────────────────────────────────────────────
73
+
74
+ NOISE_BSE = [
75
+ r'tujuan pembelajaran', r'setelah mempelajari',
76
+ r'diharapkan (mampu|dapat)', r'kata kunci', r'pemetaan pikiran',
77
+ r'isbn\s*:', r'penulis\s*:', r'kementerian pendidikan',
78
+ r'latihan\s+\d+\.\d+', r'tugas\s+\d+\.\d+', r'uji pengetahuan',
79
+ r'apersepsi', r'simak(lah)?\s+(kutipan|artikel|infografik|gambar)',
80
+ r'amatilah\s+gambar', r'salin\s+tabel', r'berilah\s+tanda',
81
+ r'buku\s+tugas', r'presentasikan',
82
+ r'jawablah\s+(beberapa\s+)?pertanyaan',
83
+ r'kemukakan\s+pendapat\s+kalian',
84
+ r'coba\s+(deskripsikan|identifikasi|kemukakan)',
85
+ r'diskusikan\s+(jawabannya|beberapa)', r'ajukan\s+pertanyaan',
86
+ r'gambar\s+\d+\.\d+', r'tabel\s+\d+\.\d+',
87
+ r'sumber\s*:\s*(kemendikbud|bank|indonesiabaik)',
88
+ r'bab\s+\d+\s+\|\s+\w+', r'no\.\s+faktor\s+pendorong',
89
+ r'benar\s+salah', r'sosiologi\s+untuk\s+sma',
90
+ ]
91
+ NOISE_PATTERNS = [re.compile(p, re.IGNORECASE) for p in NOISE_BSE]
92
+
93
+ KALIMAT_INSTRUKSIONAL = [
94
+ r'(setelah|sesudah)\s+(membaca|menyimak|mengamati).{0,60}(jawab|diskusi|kemukakan|sajikan)',
95
+ r'^(simak|amati|perhatikan)(lah)?\s+',
96
+ r'(masih\s+ingatkah|tahukah|ingatkah)\s+kalian',
97
+ r'^(pada\s+pembahasan\s+awal|kalian\s+telah\s+(mempelajari|mengetahui|memahami))',
98
+ r'^(sebelum\s+memahami|sebelum\s+mempelajari)',
99
+ r'^(mari\s+(kita\s+)?(simak|pelajari|bahas|amati))',
100
+ r'^apakah\s+(fenomena|peristiwa|kondisi).{0,80}\?$',
101
+ r'^menurut\s+kalian.{0,80}\?$',
102
+ r'^apa\s+yang\s+dimaksud.{0,60}\?\s*(kemukakan|diskusikan)',
103
+ r'^setujukah\s+kalian',
104
+ r'kemukakan\s+pendapat\s+(kalian|anda)',
105
+ r'(sajikan|presentasikan)\s+(hasilnya|hasil)',
106
+ r'(ajukan|ajukanlah)\s+pertanyaan',
107
+ r'(diskusikan|bahaslah)\s+(bersama|dengan\s+teman)',
108
+ r'jawab(lah)?\s+(pertanyaan|soal)\s+(berikut|di\s+bawah)',
109
+ r'(jelaskan|uraikan)\s+(pendapat|jawaban)\s+kalian',
110
+ r'^\d{1,3}\s+(sosiologi|matematika|fisika|kimia|biologi|sejarah|geografi|'
111
+ r'ekonomi|pkn|bahasa|pendidikan|prakarya|seni)\s+untuk\s+',
112
+ r'^dikutip\s+dari\s*:',
113
+ r'setelah\s+menyimak\s+penjelasan',
114
+ r'kalian\s+telah\s+(mengetahui|mempelajari|memahami)\s+bahwa',
115
+ r'apa\s+yang\s+dimaksud.{0,80}\?$',
116
+ ]
117
+ KALIMAT_PATTERNS = [re.compile(p, re.IGNORECASE) for p in KALIMAT_INSTRUKSIONAL]
118
+ HEADER_BAB_PAT = re.compile(r'bab\s+\d+\s*\|\s*[\w\s]+\d+', re.IGNORECASE)
119
+
120
+ _MAPEL = (r'sosiologi|matematika|fisika|kimia|biologi|sejarah|geografi|'
121
+ r'ekonomi|pkn|pendidikan|prakarya|seni|bahasa')
122
+
123
+ KATA_KUNCI_C2 = [
124
+ 'karena','menyebabkan','berdampak','sehingga','mengakibatkan','berakibat',
125
+ 'oleh karena','disebabkan','bertujuan','berfungsi','berperan','berguna',
126
+ 'tujuan','fungsi','manfaat','peran','kegunaan','mekanisme','tahapan',
127
+ 'langkah','alasan','dampak','pengaruh','akibat','sebab','hubungan',
128
+ 'keterkaitan','mengapa','bagaimana','jelaskan','uraikan',
129
+ ]
130
+
131
+ print("βœ… Pola filter dimuat.")
132
+
133
+ # ─────────────────────────────────────────────────────────────────────────────
134
+ # CELL 4 β€” FUNGSI IBEX v2.4
135
+ # ─────────────────────────────────────────────────────────────────────────────
136
+
137
+ def hitung_noise(t):
138
+ return sum(1 for p in NOISE_PATTERNS if p.search(t))
139
+
140
+ def pisah_nomor_halaman(t):
141
+ return re.sub(
142
+ rf'\s+(\d{{1,3}})\s+({_MAPEL})\s+untuk\s+sma',
143
+ r'. \1 \2 untuk sma', t, flags=re.IGNORECASE
144
+ )
145
+
146
+ def hapus_header_bab(t):
147
+ return HEADER_BAB_PAT.sub('', t)
148
+
149
+ def rescue_narasi(k):
150
+ m = re.search(
151
+ r'\d{1,3}\s+\S+\s+untuk\s+sma\S*\s+kelas\s+x{1,3}i{0,2}\s+(.*)',
152
+ k, re.IGNORECASE
153
+ )
154
+ if m:
155
+ sisa = m.group(1).strip()
156
+ if len(sisa.split()) >= 5:
157
+ return sisa
158
+ return ""
159
+
160
+ def bersihkan_kalimat(teks):
161
+ """Filter L2: buang kalimat instruksional, rescue narasi yang terselamatkan."""
162
+ teks = pisah_nomor_halaman(teks)
163
+ teks = hapus_header_bab(teks)
164
+ teks = re.sub(r'\s{2,}', ' ', teks).strip()
165
+ bersih, dibuang = [], 0
166
+ for k in re.split(r'(?<=[.!?])\s+', teks):
167
+ k = k.strip()
168
+ if len(k) < 10:
169
+ continue
170
+ if any(p.search(k) for p in KALIMAT_PATTERNS):
171
+ rescued = rescue_narasi(k)
172
+ if rescued:
173
+ bersih.append(rescued)
174
+ dibuang += len(k.split()) - len(rescued.split())
175
+ else:
176
+ dibuang += len(k.split())
177
+ else:
178
+ bersih.append(k)
179
+ return " ".join(bersih).strip(), dibuang
180
+
181
+
182
+ def ekstrak_ibex(pdf_path, hal_mulai, hal_selesai,
183
+ chunk_size, batas_noise, source_type="BSE"):
184
+ """
185
+ Core IBEX v2.4.
186
+ Return: (df_chunks, ringkasan_str)
187
+ """
188
+ doc = fitz.open(pdf_path)
189
+ nama = os.path.basename(pdf_path)
190
+ total = len(doc)
191
+ start = max(0, hal_mulai - 1)
192
+ end = min(total, hal_selesai)
193
+
194
+ if start >= end:
195
+ return None, f"❌ Rentang tidak valid. PDF punya {total} halaman."
196
+
197
+ print(f"πŸ“– Membaca hal {hal_mulai}–{hal_selesai} dari '{nama}' ({total} hal total)...")
198
+
199
+ # ── Ekstrak teks per halaman (crop margin 8%) ─────────────────────────
200
+ teks_per_hal = []
201
+ for num in range(start, end):
202
+ page = doc.load_page(num)
203
+ rect = page.rect
204
+ mg = rect.height * 0.08
205
+ clip = fitz.Rect(rect.x0, rect.y0 + mg, rect.x1, rect.y1 - mg)
206
+ teks_per_hal.append(page.get_text("text", clip=clip, sort=True))
207
+
208
+ def bersihkan_dasar(t):
209
+ t = re.sub(r'-\n\s*', '', t) # sambung kata yang terpotong baris
210
+ t = re.sub(r'\n+', ' ', t)
211
+ return re.sub(r'\s+', ' ', t).strip()
212
+
213
+ teks_gabung = bersihkan_dasar(" ".join(teks_per_hal))
214
+ kalimat_list = [
215
+ k.strip() for k in re.split(r'(?<=[.!?])\s+', teks_gabung)
216
+ if len(k.strip()) > 10
217
+ ]
218
+ print(f" Total kata input : {len(teks_gabung.split()):,}")
219
+ print(f" Total kalimat : {len(kalimat_list)}")
220
+
221
+ # ── Chunking dengan overlap 25% ───────────────────────────────────────
222
+ overlap = max(1, chunk_size // 4)
223
+ chunk_meta = []
224
+ chunk_id = 1
225
+ bng_l1 = bng_pendek = 0
226
+ i = 0
227
+
228
+ while i < len(kalimat_list):
229
+ buf, j = [], i
230
+ while j < len(kalimat_list) and len(buf) < chunk_size:
231
+ buf.extend(kalimat_list[j].split())
232
+ j += 1
233
+
234
+ pot = " ".join(buf)
235
+ wc = len(buf)
236
+ noise = hitung_noise(pot)
237
+ ada_c2 = any(k in pot.lower() for k in KATA_KUNCI_C2)
238
+
239
+ if wc >= 50 and ada_c2:
240
+ # ── Filter L1: buang jika noise β‰₯ batas ──────────────────────
241
+ if noise >= batas_noise:
242
+ bng_l1 += 1
243
+ else:
244
+ # ── Filter L2: bersihkan kalimat instruksional ────────────
245
+ bersih, dibuang = bersihkan_kalimat(pot)
246
+ wc2 = len(bersih.split())
247
+ noise2 = hitung_noise(bersih)
248
+ if wc2 >= 50 and any(k in bersih.lower() for k in KATA_KUNCI_C2):
249
+ chunk_meta.append({
250
+ "chunk_id" : f"chunk_{chunk_id:04d}",
251
+ "source_file" : nama,
252
+ "source_type" : source_type,
253
+ "page_range" : f"{hal_mulai}-{hal_selesai}",
254
+ "word_count" : wc2,
255
+ "noise_score" : noise2,
256
+ "kata_dibuang_l2" : dibuang,
257
+ "context" : bersih,
258
+ })
259
+ chunk_id += 1
260
+ else:
261
+ bng_pendek += 1
262
+
263
+ i = j - overlap if (j - overlap) > i else j
264
+
265
+ # ── Ringkasan ─────────────────────────────────────────────────────────
266
+ ringkasan = (
267
+ f"\n{'='*55}\n"
268
+ f"βœ… IBEX v2.4 SELESAI\n"
269
+ f"{'='*55}\n"
270
+ f" Halaman diproses : {end - start}\n"
271
+ f" Total kata input : {len(teks_gabung.split()):,}\n"
272
+ f" Chunk bersih : {len(chunk_meta)}\n"
273
+ f" Buang L1 (noise) : {bng_l1}\n"
274
+ f" Buang L2 (kata) : {sum(r['kata_dibuang_l2'] for r in chunk_meta)}\n"
275
+ f" Avg kata/chunk : {round(sum(r['word_count'] for r in chunk_meta)/max(1,len(chunk_meta)),1)}\n"
276
+ f"{'='*55}"
277
+ )
278
+
279
+ if not chunk_meta:
280
+ return None, (
281
+ "⚠️ Tidak ada context C2 bersih yang dihasilkan.\n"
282
+ f" Buang L1: {bng_l1} | Buang L2: {bng_pendek}\n"
283
+ " Coba: perluas rentang halaman ATAU naikkan batas_noise ke 3"
284
+ )
285
+
286
+ return pd.DataFrame(chunk_meta), ringkasan
287
+
288
+ print("βœ… Fungsi IBEX siap.")
289
+
290
+ # ─────────────────────────────────────────────────────────────────────────────
291
+ # CELL 5 β€” JALANKAN EKSTRAKSI
292
+ # ─────────────────────────────────────────────────────────────────────────────
293
+
294
+ df_result, ringkasan = ekstrak_ibex(
295
+ pdf_path = CONFIG["pdf_path"],
296
+ hal_mulai = CONFIG["hal_mulai"],
297
+ hal_selesai = CONFIG["hal_selesai"],
298
+ chunk_size = CONFIG["chunk_size"],
299
+ batas_noise = CONFIG["batas_noise"],
300
+ source_type = CONFIG["source_type"],
301
+ )
302
+
303
+ print(ringkasan)
304
+
305
+ if df_result is not None:
306
+ print("\nπŸ“‹ Preview 3 chunk pertama:")
307
+ print("-" * 55)
308
+ for _, row in df_result.head(3).iterrows():
309
+ print(f"[{row['chunk_id']}] wc={row['word_count']} "
310
+ f"noise={row['noise_score']} buang={row['kata_dibuang_l2']}")
311
+ print(f" {row['context'][:200]}...")
312
+ print()
313
+
314
+ # ─────────────────────────────────────────────────────────────────────────────
315
+ # CELL 6 β€” SIMPAN CSV
316
+ # ─────────────────────────────────────────────────────────────────────────────
317
+
318
+ if df_result is not None:
319
+ nama_pdf = os.path.splitext(os.path.basename(CONFIG["pdf_path"]))[0]
320
+ nama_file = (
321
+ f"IBEX_{nama_pdf}"
322
+ f"_hal{CONFIG['hal_mulai']}-{CONFIG['hal_selesai']}"
323
+ f"_chunk{CONFIG['chunk_size']}"
324
+ f"_noise{CONFIG['batas_noise']}"
325
+ f".csv"
326
+ )
327
+ out_path = os.path.join(CONFIG["output_dir"], nama_file)
328
+ df_result.to_csv(out_path, index=False, encoding="utf-8-sig")
329
+
330
+ print(f"πŸ’Ύ CSV tersimpan di: {out_path}")
331
+ print(f" {len(df_result)} chunk | "
332
+ f"{df_result['word_count'].sum():,} total kata")
333
+ print()
334
+ print("πŸ“₯ Cara unduh dari Kaggle:")
335
+ print(" Output (panel kanan) β†’ klik nama file β†’ Download")
336
+ print()
337
+ print("πŸ“€ Langkah berikutnya:")
338
+ print(" Upload CSV ini ke Space 2 QA Generator untuk generate soal C1+C2.")
339
+ else:
340
+ print("❌ Tidak ada output. Cek konfigurasi dan coba lagi.")
341
+
342
+ # ─────────────────────────────────────────────────────────────────────────────
343
+ # CELL 7 β€” ANALISIS KUALITAS (OPSIONAL)
344
+ # ─────────────────────────────────────────────────────────────────────────────
345
+
346
+ if df_result is not None and len(df_result) > 0:
347
+ print("πŸ“Š ANALISIS KUALITAS CHUNK")
348
+ print("=" * 55)
349
+ print(f"Total chunk : {len(df_result)}")
350
+ print(f"Rata-rata kata : {df_result['word_count'].mean():.1f}")
351
+ print(f"Min – Max kata : {df_result['word_count'].min()} – {df_result['word_count'].max()}")
352
+ print(f"Noise score = 0 : {(df_result['noise_score']==0).sum()} chunk βœ…")
353
+ print(f"Noise score > 0 : {(df_result['noise_score']>0).sum()} chunk ⚠️")
354
+ print(f"Total kata dibuang : {df_result['kata_dibuang_l2'].sum()}")
355
+ print()
356
+
357
+ # Estimasi QA yang akan dihasilkan
358
+ n_c1, n_c2 = 2, 2
359
+ est_qa = len(df_result) * (n_c1 + n_c2)
360
+ print(f"🎯 ESTIMASI OUTPUT QA GENERATOR")
361
+ print(f" Dengan setting {n_c1} C1 + {n_c2} C2 per chunk:")
362
+ print(f" β†’ ~{est_qa} QA pairs dari part ini")
363
+ print()
364
+
365
+ # Cek kata kunci C2
366
+ KATA_KUNCI_C2_check = [
367
+ 'karena','menyebabkan','berdampak','sehingga','mengakibatkan',
368
+ 'tujuan','fungsi','manfaat','peran','dampak','pengaruh','sebab',
369
+ ]
370
+ df_result['has_c2_keyword'] = df_result['context'].apply(
371
+ lambda t: any(k in t.lower() for k in KATA_KUNCI_C2_check)
372
+ )
373
+ print(f" Chunk dengan kata kunci C2 : "
374
+ f"{df_result['has_c2_keyword'].sum()}/{len(df_result)} βœ…")
375
+
376
+ print()
377
+ print("=" * 55)
378
+ print("Tampilkan semua chunk:")
379
+ print("=" * 55)
380
+ for _, row in df_result.iterrows():
381
+ print(f"\n[{row['chunk_id']}]")
382
+ print(f" word_count : {row['word_count']}")
383
+ print(f" noise_score : {row['noise_score']}")
384
+ print(f" kata_dibuang_l2 : {row['kata_dibuang_l2']}")
385
+ print(f" context preview : {row['context'][:150]}...")