// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors // // Spike B main: run the reconstruction pipeline over the synthetic corpus and // report paragraph-boundary F1. Exit 0 if F1 meets the §14 M0 target (≥ 0.85), // 1 otherwise. The release gate is ≥ 0.93 (§8.2) but that is for the real // labelled corpus; the M0 spike uses the synthetic corpus and the lower bar. #include "Corpus.h" #include "Reconstruct.h" #include "../common/SpikeRunner.h" #include #include int main(int argc, char** argv) { const std::size_t n_docs = (argc >= 2) ? std::size_t(std::atoi(argv[1])) : 200; const double target = (argc >= 3) ? std::stod(argv[2]) : 0.85; auto corpus = freepdfeditor::spike::b::generate_corpus(n_docs); if (corpus.empty()) { freepdfeditor::spike::SpikeResult r{}; r.spike = "B"; r.name = "glyph→Unicode + line/paragraph reconstruction"; r.metric_name = "paragraph_boundary_f1"; r.target = target; r.gate_met = false; r.notes = "empty corpus"; return freepdfeditor::spike::emit_json_report(r); } std::size_t total_true = 0, total_pred = 0, total_matched = 0; std::size_t docs_perfect = 0; for (auto& doc : corpus) { auto result = freepdfeditor::spike::b::reconstruct(doc.runs); // Boundary tolerance: 0.4 × the leading (14.4pt) ≈ 5.76pt. Generous // enough that a paragraph gap is matched even if the pipeline's // paragraph bottom is off by a line; tight enough that distinct // paragraphs aren't conflated. auto score = freepdfeditor::spike::b::score_boundaries( result.paragraphs, doc.truth, 5.76f); total_true += score.true_boundaries; total_pred += score.predicted_boundaries; total_matched += score.matched; if (score.true_boundaries == score.predicted_boundaries && score.matched == score.true_boundaries) { ++docs_perfect; } } double precision = total_pred ? double(total_matched) / double(total_pred) : 0.0; double recall = total_true ? double(total_matched) / double(total_true) : 0.0; double f1 = (precision + recall) > 0 ? 2.0 * precision * recall / (precision + recall) : 0.0; freepdfeditor::spike::SpikeResult r{}; r.spike = "B"; r.name = "glyph→Unicode + line/paragraph reconstruction"; r.total = corpus.size(); r.passed = docs_perfect; r.failed = corpus.size() - docs_perfect; r.errored = 0; r.metric_name = "paragraph_boundary_f1"; r.metric_value = f1; r.target = target; r.gate_met = f1 >= target; r.notes = "synthetic single-column LTR corpus; " "precision=" + std::to_string(precision) + " recall=" + std::to_string(recall) + " perfect=" + std::to_string(docs_perfect) + "/" + std::to_string(corpus.size()) + "; real labelled corpus (§8.1) needed for the 0.93 release gate"; return freepdfeditor::spike::emit_json_report(r); }