freepdfeditor/spike/B_reconstruction/main.cpp

76 lines
3.0 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <cstdio>
#include <vector>
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);
}