File size: 20,400 Bytes
18573e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
package bg.bas.dcl.LLMs.IfGPTDataset;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import bg.bas.dcl.LLMs.BiasAnalyser;
import bg.bas.dcl.LLMs.BiasLexicon;
import bg.bas.dcl.LLMs.BulgarianSentenceSplitter;
import bg.bas.dcl.LLMs.PIIDetector;
import bg.bas.dcl.LLMs.SentenceBiasScore;
import bg.bas.dcl.general.FileHandler;
import bg.bas.dcl.general.JSONProcessor;

/**
 * IfGPTPipeline
 *
 * Pipeline for the ifGPT Bulgarian language dataset.
 *
 * -----------------------------------------------------------------------
-----------------------------------------------------------------------
 * PIPELINE STAGES (executed in order by {@link #run()})
 *
 *   1. EXTRACT  
 *   2. SPLIT   
 *   3. CLEAN  
 *   4. DEDUPLICATE  
 *   5. PII      
 *   6. BIAS    
 *   7. COUNTS    —  word / sentence / token counts are recomputed on the cleaned, deduplicated text
 *     FULL_DATA_DIR / FULL_META_DIR
 *
 * -----------------------------------------------------------------------
 
 */
@SuppressWarnings("unchecked")
public class IfGPTPipeline {

    // -----------------------------------------------------------------------
    // Fixed paths
    // -----------------------------------------------------------------------

    public static final String FULL_DATA_DIR =
            "/home/ivelina/WORK-DCL/IfGPT/IFGPT-DATASET-DATA/";
    public static final String FULL_META_DIR =
            "/home/ivelina/WORK-DCL/IfGPT/IFGPT-DATASET-METADATA/";

    // -----------------------------------------------------------------------
    // Configurable paths and options
    // -----------------------------------------------------------------------

    private SourceProcessor sourceProcessor;      // mandatory
    private String newDataDir;                    // mandatory: incoming texts
    private String sampleDir;                     // mandatory: boilerplate sample
    private String newMetaDir;                    // mandatory: output metadata
    private String blocklistFile;                 // boilerplate blocklist file
    private String dedupReport;                   // dedup TSV report path
    private String biasDictPath;                  // bias dictionary TSV
    private String openNlpModelPath = null;       // null = bundled JAR model
    private double boilerplateThreshold = 0.50;   // FileCleanProcessor threshold
    private double dedupThreshold       = 0.90;   // DeduplicationProcessor threshold
    private int    dedupShingleSize     = 5;
    private int    dedupNumHashes       = 200;
    private boolean removeDuplicates    = false;  // whether to strip dup sentences
    private boolean keepBackups         = true;   // keep .bak on file modification
    private boolean skipClean           = false;  // skip boilerplate cleaning
    private boolean skipDedup           = false;  // skip deduplication
    private boolean skipPii             = false;  // skip PII scoring
    private boolean skipBias            = false;  // skip bias scoring

    // -----------------------------------------------------------------------
    //  
    // -----------------------------------------------------------------------

    public IfGPTPipeline setSourceProcessor(SourceProcessor p)  { sourceProcessor = p; return this; }
    public IfGPTPipeline setNewDataDir(String p)                { newDataDir = p; return this; }
    public IfGPTPipeline setSampleDir(String p)                 { sampleDir = p; return this; }
    public IfGPTPipeline setNewMetaDir(String p)                { newMetaDir = p; return this; }
    public IfGPTPipeline setBlocklistFile(String p)             { blocklistFile = p; return this; }
    public IfGPTPipeline setDedupReport(String p)               { dedupReport = p; return this; }
    public IfGPTPipeline setBiasDictPath(String p)              { biasDictPath = p; return this; }
    public IfGPTPipeline setOpenNlpModelPath(String p)          { openNlpModelPath = p; return this; }
    public IfGPTPipeline setBoilerplateThreshold(double t)      { boilerplateThreshold = t; return this; }
    public IfGPTPipeline setDedupThreshold(double t)            { dedupThreshold = t; return this; }
    public IfGPTPipeline setDedupShingleSize(int n)             { dedupShingleSize = n; return this; }
    public IfGPTPipeline setDedupNumHashes(int n)               { dedupNumHashes = n; return this; }
    public IfGPTPipeline setRemoveDuplicates(boolean b)         { removeDuplicates = b; return this; }
    public IfGPTPipeline setKeepBackups(boolean b)              { keepBackups = b; return this; }
    public IfGPTPipeline setSkipClean(boolean b)                { skipClean = b; return this; }
    public IfGPTPipeline setSkipDedup(boolean b)                { skipDedup = b; return this; }
    public IfGPTPipeline setSkipPii(boolean b)                  { skipPii = b; return this; }
    public IfGPTPipeline setSkipBias(boolean b)                 { skipBias = b; return this; }

    // -----------------------------------------------------------------------
    //   -----------------------------------------------------------------------

    /**
     * Executes all  stages in order.
     * Throws {@link IllegalStateException} if mandatory configuration is missing.
     */
    public void run() {
        validateConfig();
        ensureDirs(newMetaDir, FULL_DATA_DIR, FULL_META_DIR);

        banner("STAGE 1 — SOURCE EXTRACTION");
        runExtraction();

        // Shared NLP components (initialised once, reused across stages)
        BulgarianSentenceSplitter splitter = new BulgarianSentenceSplitter(openNlpModelPath);

        banner("STAGE 2 — SENTENCE SPLITTING & INITIAL METADATA");
        runSentenceSplitting(splitter);

        if (!skipClean) {
            banner("STAGE 3 — BOILERPLATE CLEANING");
            runCleaning();
        } else {
            log("STAGE 3 skipped (skipClean=true)");
        }

        if (!skipDedup) {
            banner("STAGE 4 — DEDUPLICATION");
            runDeduplication();
        } else {
            log("STAGE 4 skipped (skipDedup=true)");
        }

        PIIDetector  piiDetector  = skipPii  ? null : new PIIDetector(splitter);
        BiasAnalyser biasAnalyser = skipBias ? null : buildBiasAnalyser(splitter);

        banner("STAGES 5-7 — PII, BIAS & FINAL COUNTS");
        runAnnotationAndCounts(splitter, piiDetector, biasAnalyser);

        banner("STAGE 8 — PERSIST TO FULL CORPUS");
        runPersist();

        banner("PIPELINE COMPLETE");
    }

    // -----------------------------------------------------------------------
    // Stage 1 — Extraction
    // -----------------------------------------------------------------------

    private void runExtraction() {
        // The source processor writes plain-text files to newDataDir and
        // seed metadata JSON to newMetaDir.
        sourceProcessor.process(newDataDir, newMetaDir);
        log("Extraction complete → " + newDataDir);
    }

    // -----------------------------------------------------------------------
    // Stage 2 — Sentence splitting
    // -----------------------------------------------------------------------

    /**
     * Reads each metadata JSON produced by the source processor, then for
     * each document text file counts sentences properly using the OpenNLP
     * splitter and writes the sentence list to a parallel .sentences file
     * (one sentence per line) used by later stages.
     */
    private void runSentenceSplitting(BulgarianSentenceSplitter splitter) {
        try {
            FileHandler fh = new FileHandler();
            int docs = 0;

            for (File txtFile : fh.getFileListing(new File(newDataDir))) {
                if (!txtFile.isFile() || !txtFile.getName().endsWith(".txt")) continue;

                // Read document text
                StringBuilder sb = new StringBuilder();
                try (Scanner sc = new Scanner(txtFile, StandardCharsets.UTF_8)) {
                    while (sc.hasNextLine()) sb.append(sc.nextLine()).append('\n');
                }
                String text = sb.toString().trim();

                // Split into sentences and persist to .sentences sidecar file
                String[] sentences = splitter.split(text);
                File sentFile = new File(newDataDir, txtFile.getName()
                        .replace(".txt", ".sentences"));

                try (Writer w = new OutputStreamWriter(
                        new FileOutputStream(sentFile), StandardCharsets.UTF_8)) {
                    for (String sent : sentences) {
                        if (!sent.isBlank()) {
                            w.write(sent.trim());
                            w.write('\n');
                        }
                    }
                }
                docs++;
            }
            log("Sentence splitting complete.  Documents: " + docs);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // -----------------------------------------------------------------------
    // Stage 3 — Boilerplate cleaning
    // -----------------------------------------------------------------------

    private void runCleaning() {
        FileCleanProcessor fcp = new FileCleanProcessor(boilerplateThreshold);

        // Learn from sample
        fcp.learnFromSample(sampleDir);
        fcp.printTopCommonLines(20);

        // Save blocklist for audit / reproducibility
        if (blocklistFile != null && !blocklistFile.isBlank()) {
            fcp.saveBlocklist(blocklistFile);
        }

        // Clean the new data directory
        fcp.cleanDirectory(newDataDir, keepBackups);
        log("Boilerplate cleaning complete → " + newDataDir);
    }

    // -----------------------------------------------------------------------
    // Stage 4 — Deduplication
    // -----------------------------------------------------------------------

    private void runDeduplication() {
        DeduplicationProcessor dp = new DeduplicationProcessor(
                dedupThreshold, dedupShingleSize, dedupNumHashes);

        // Index the full existing corpus
        log("Indexing full corpus for deduplication…");
        dp.indexCorpus(FULL_DATA_DIR);
        log("Corpus indexed.  Sentences: " + dp.getCorpusSize());

        // Detect near-duplicates in new data
        String report = dedupReport != null
                ? dedupReport
                : newMetaDir + "dedup_report.tsv";
        dp.detectDuplicates(newDataDir, report);

        if (removeDuplicates) {
            dp.removeDuplicatesFromNewFolder(newDataDir, keepBackups);
        }
    }

    // -----------------------------------------------------------------------
    // Stages 5-7 — PII, Bias annotation + final counts
    // -----------------------------------------------------------------------

    /**
     * For each document:
     *   a) reads the (cleaned, deduplicated) .sentences sidecar file,
     *   b) runs PII and/or Bias scoring per sentence,
     *   c) recomputes word/sentence/token counts on the surviving text,
     *   d) merges all computed values into a DocumentMetadata and writes
     *      the final metadata JSON to newMetaDir.
     */
    private void runAnnotationAndCounts(BulgarianSentenceSplitter splitter,
                                        PIIDetector  piiDetector,
                                        BiasAnalyser biasAnalyser) {
        try {
            FileHandler   fh = new FileHandler();
            JSONProcessor jp = new JSONProcessor();
            int docs = 0, errors = 0;

            for (File sentFile : fh.getFileListing(new File(newDataDir))) {
                if (!sentFile.isFile()
                        || !sentFile.getName().endsWith(".sentences")) continue;

                String stem = sentFile.getName().replace(".sentences", "");

                // --- Load sentences ---
                List<String> sentences = new ArrayList<>();
                try (Scanner sc = new Scanner(sentFile, StandardCharsets.UTF_8)) {
                    while (sc.hasNextLine()) {
                        String s = sc.nextLine().trim();
                        if (!s.isBlank()) sentences.add(s);
                    }
                }

                if (sentences.isEmpty()) {
                    log("[WARN] No sentences for: " + stem);
                    errors++;
                    continue;
                }

                // --- Load or create DocumentMetadata ---
                DocumentMetadata meta = loadOrCreateMetadata(jp, stem);

                // --- PII per sentence ---
                List<Double> piiVec = new ArrayList<>();
                if (piiDetector != null) {
                    int sentIdx = 0;
                    for (String sent : sentences) {
                        PIIDetector.SentencePIIScore score =
                                piiDetector.analyseSentence(sent, stem + "-" + sentIdx++);
                        piiVec.add(score.getPiiCoverage());
                    }
                }
                meta.setPiiVector(piiVec);

                // --- Bias per sentence ---
                List<Double> biasVec = new ArrayList<>();
                if (biasAnalyser != null) {
                    for (String sent : sentences) {
                        SentenceBiasScore score = biasAnalyser.analyseSentence(sent);
                        biasVec.add(score.totalCoverage());
                    }
                }
                meta.setBiasVector(biasVec);

                // --- Recompute counts from surviving sentences ---
                int nSentences  = sentences.size();
                int nWords      = 0;
                int nTokens     = 0;

                for (String sent : sentences) {
                    String[] toks = sent.split("\\s+");
                    nWords  += toks.length;
                    // estimate tokens: words + punctuation characters
                    nTokens += toks.length + sent.length()
                             - sent.replaceAll("[.,;:!?()\\-]", "").length();
                }

                // Paragraphs: count blank-line groups in the original text file
                int nParagraphs = countParagraphs(new File(newDataDir, stem + ".txt"));

                meta.setNumberSentences(nSentences)
                    .setNumberWords(nWords)
                    .setNumberTokens(nTokens)
                    .setNumberParagraphs(nParagraphs);

                // --- Persist metadata JSON ---
                writeMetadata(meta, newMetaDir + stem + "_meta.json");
                docs++;
            }

            log("Annotation & counts complete.  Documents: " + docs
                    + "  Errors: " + errors);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // -----------------------------------------------------------------------
    // Stage 8 
    // -----------------------------------------------------------------------

    /** 
     */
    private void runPersist() {
        try {
            FileHandler fh = new FileHandler();
            int dataCopied = 0, metaCopied = 0;

            // Copy text files
            for (File f : fh.getFileListing(new File(newDataDir))) {
                if (!f.isFile() || !f.getName().endsWith(".txt")) continue;
                File dest = new File(FULL_DATA_DIR, f.getName());
                if (!dest.exists()) {
                    Files.copy(f.toPath(), dest.toPath(),
                            StandardCopyOption.REPLACE_EXISTING);
                    dataCopied++;
                }
            }

            // Copy metadata JSON files
            for (File f : fh.getFileListing(new File(newMetaDir))) {
                if (!f.isFile() || !f.getName().endsWith("_meta.json")) continue;
                File dest = new File(FULL_META_DIR, f.getName());
                if (!dest.exists()) {
                    Files.copy(f.toPath(), dest.toPath(),
                            StandardCopyOption.REPLACE_EXISTING);
                    metaCopied++;
                }
            }

            log("Persist complete.  Text files copied: " + dataCopied
                    + "  Metadata files copied: " + metaCopied);
            log("FULL_DATA_DIR : " + FULL_DATA_DIR);
            log("FULL_META_DIR : " + FULL_META_DIR);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    private DocumentMetadata loadOrCreateMetadata(JSONProcessor jp, String stem) {
        // Try to find a seed metadata JSON written by the source processor
        // Filename conventions: stem + ".json" or stem + "_meta.json"
        String[] candidates = {
            newMetaDir + stem + "_meta.json",
            newMetaDir + stem + ".json"
        };
        for (String path : candidates) {
            File f = new File(path);
            if (f.exists()) {
                try {
                    JSONObject raw = jp.readJSON(f);
                    // First try full schema, then legacy format
                    if (raw.containsKey("Identifier")) {
                        return DocumentMetadata.fromJson(raw);
                    } else {
                        DocumentMetadata m = new DocumentMetadata(stem);
                        m.mergeLegacy(raw);
                        return m;
                    }
                } catch (Exception e) {
                    log("[WARN] Could not parse metadata JSON for " + stem + ": " + e.getMessage());
                }
            }
        }
        // Fall back to empty skeleton
        return new DocumentMetadata(stem);
    }

    private void writeMetadata(DocumentMetadata meta, String outPath) throws Exception {
        JSONObject json = meta.toJson();
        try (Writer w = new OutputStreamWriter(
                new FileOutputStream(outPath), StandardCharsets.UTF_8)) {
            json.writeJSONString(w);
        }
    }

    private int countParagraphs(File txtFile) {
        if (!txtFile.exists()) return 0;
        int count = 0;
        boolean inPara = false;
        try (Scanner sc = new Scanner(txtFile, StandardCharsets.UTF_8)) {
            while (sc.hasNextLine()) {
                String line = sc.nextLine();
                if (line.isBlank()) {
                    inPara = false;
                } else {
                    if (!inPara) { count++; inPara = true; }
                }
            }
        } catch (Exception e) { /* ignored */ }
        return Math.max(count, 1);
    }

    private BiasAnalyser buildBiasAnalyser(BulgarianSentenceSplitter splitter) {
        if (biasDictPath == null || biasDictPath.isBlank()) {
            log("[WARN] No bias dictionary path set — bias scoring disabled.");
            return null;
        }
        BiasLexicon lexicon = new BiasLexicon(biasDictPath);
        return new BiasAnalyser(lexicon, splitter);
    }

    private void validateConfig() {
        List<String> missing = new ArrayList<>();
        if (sourceProcessor == null) missing.add("sourceProcessor");
        if (newDataDir  == null || newDataDir.isBlank())  missing.add("newDataDir");
        if (sampleDir   == null || sampleDir.isBlank())   missing.add("sampleDir");
        if (newMetaDir  == null || newMetaDir.isBlank())  missing.add("newMetaDir");
        if (!missing.isEmpty())
            throw new IllegalStateException(
                "Pipeline configuration missing: " + missing);
    }

    private void ensureDirs(String... paths) {
        for (String p : paths) {
            if (p != null) new File(p).mkdirs();
        }
    }

    private void banner(String msg) {
        System.out.println("\n" + "=".repeat(60));
        System.out.println("  " + msg);
        System.out.println("=".repeat(60));
    }

    private void log(String msg) {
        System.out.println("[Pipeline] " + msg);
    }
}