76 lines
3.0 KiB
C++
76 lines
3.0 KiB
C++
// 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);
|
||
} |