This page explains how the code implements each benchmark stage. The README is the user-facing command guide; this file is the implementation map for extending or debugging the pipeline.
Repo-local agent skills in skills/ mirror the same workflow at a higher level:
xslidesbench-setup, xslidesbench-probes, xslidesbench-deck-generation,
xslidesbench-evaluation, and xslidesbench-ablations. Use those skills for
agent handoffs; use this document when changing or debugging implementation
details.
Main code:
src/stages/s01_sources.pysrc/shared/source_text.py
The pipeline expects each case to have one source file and optional
source_metadata.json. Supported text extraction paths are:
- PDF: MinerU markdown extraction by default.
- HTML: script/style/navigation removal, tag stripping, and whitespace cleanup.
- Markdown/plain text: direct UTF-8 read.
For PDFs, extract_pdf_text() chooses XSLIDESBENCH_PDF_PARSER or mineru.
MINERU_API_KEY uses the hosted API, while MINERU_API_URL uses an offline or
compatible endpoint. MINERU_BASE_URL defaults to the hosted base URL when left
empty. Parsed markdown is cached by source-file hash under
XSLIDESBENCH_MINERU_CACHE_DIR. PyMuPDF remains available only through
XSLIDESBENCH_PDF_PARSER=pymupdf for explicit local debugging.
extract_source_text() applies max_chars after extraction. This limit controls
cost and latency; it is not a model context limit. Metadata supplies the scene,
display name, category, topic, and other source-side descriptors used by source
selection and result summaries.
Main code:
src/stages/s02_probes.py
Probes are generated before audience conditioning. A probe is a source-grounded
question-answer unit with evidence, level, modality, and theme. The generator is
called k times per source or source chunk, so the final bank is a union of
independent samples.
Long-source handling is in chunk_source_text(). The preferred setting for
large-context models is source_chars=1000000, chunk_chars=0, and
chunk_overlap=0, so it sends the whole retained source excerpt in one prompt:
- if
chunk_chars <= 0or the source is shorter thanchunk_chars, the source is sent as one chunk; - otherwise, character windows of length
chunk_charsare created; - the step size is
chunk_chars - max(0, chunk_overlap), with a minimum step of 1 character; - each chunk is prefixed as
[Source chunk chars start-end]; - generated probes receive
source_chunk_indexandsource_chunk_countin metadata.
source_chars is the maximum number of extracted source characters retained
after parsing. It controls runtime and API cost; it is not the model context
window. Chunking reduces request size but can remove cross-section context. For
models with large context windows, keep --chunk-chars 0 for high-quality final
probe generation. Use positive chunk sizes mainly when latency, budget, or
endpoint limits make full-source prompting unreliable.
After all chunks and all k generations are complete, deduplicate_probes()
runs hard deduplication over the full union. A pair is treated as duplicate when
either evidence token overlap is at least 0.80 and answer similarity is at
least 0.70, or question similarity is at least 0.90 and answer similarity is
at least 0.70. Citation-list and reference-only questions are filtered before
deduplication.
Per case, the stage writes:
source_context.txt: extracted source text used by later stages;raw/chunk_XX/*.json: raw generation requests and responses;probes_deduped.jsonl: final probe bank;probe_generation_status.json: counts, chunk settings, and model id.
Main code:
src/stages/s03_weights.py
The same probe bank is weighted separately for each audience profile. The
utility judge receives batches of probes, with batch_size=12 by default. For
every probe-audience pair, the utility judge runs k independent judgments and
snaps each raw value to {0.0, 0.3, 0.6, 1.0}. The stored utility weight is the
mean of these snapped values.
The metadata keeps all sampled values, rationales, judge model id, and standard
deviation. If the standard deviation is greater than 0.25, the weight row is
marked as high variance so it can be inspected or re-run.
This stage writes one JSONL file per audience:
probes/<case_label>/weights/<audience>.jsonl
Main code:
src/generators/xslidesbench.pysrc/generators/slidetailor.pysrc/generators/notebooklm.py
Deck rows are driven by run sheets. Each row describes the system, condition, audience, case label, source document, prompt path, and preferred output deck path. The normalized run layout stores generated decks under:
data/runs/<run_name>/decks/<system>/<condition>/<audience>/<case_label>/
The X+SlidesBench local adapter runner reads the run sheet, loads environment
variables, skips already completed outputs unless forced, and writes per-row
status plus batch summaries. The reusable batch entrypoints live under
src/tools and are intentionally thin wrappers around the public xslidesbench
stage commands.
The SlideTailor workflow has two steps. First, the preparation logic builds the
local generator dataset, configuration files, preference files, and
slidetailor_run_sheet.csv. It currently prepares PDF sources only.
Both audience-agnostic and audience-conditioned rows use
template_mode=source_template_generation: each deck is generated directly from
the source document and the reference template, with the row-specific preference
guidelines applied. Second, the runner executes rows. It supports row filters by
condition, audience, and case label. It also supports GPU parallelism through
stable case sharding:
md5(case_label) % num_shards == shard_index
NotebookLM is imported manually: exported PDF decks are copied into the normalized deck tree, and evaluation then treats them like any other PDF deck.
Main code:
src/stages/s05_evaluation.pysrc/shared/decks.pysrc/shared/rendering.py
The evaluation logic expands deck rows into evaluation jobs. A conditioned deck is evaluated for its target audience. An audience-agnostic deck is evaluated for every active audience, because the same generic deck can be scored under different utility weight vectors.
Before judging, the script ensures the required audience weights exist. If a
weight file is missing, it calls xslidesbench weights for that case and audience.
Deck text is extracted from PPTX or PDF. Probe answerability uses the
concatenated deck text when visible text is available. judge_probes() batches
multiple probes into one prompt with batch_size=12 by default. Each batch
contains:
- the full extracted deck text;
- the source context;
- a JSON list of probe ids, questions, expected answers, evidence, levels, modalities, and themes.
The judge must return one judgment per probe. Missing judgments are filled as
uncovered with zero confidence, preserving deterministic probe order. Existing
score files are reused only when their judge_input_mode matches the current
text-or-image mode.
Correctness is scored separately from answerability. The default scorer first
extracts atomic factual claims from the deck, retrieves local source snippets for
each claim, verifies the claims in batches, and aggregates the verification
labels into the public correctness scalar. This keeps the metric name stable
while avoiding the brittleness of a single all-or-nothing deck-level judgment.
Speaker notes are not used by the main scoring path.
Correctness judgments are cacheable because they depend on the deck, source
text, model, and claim-verification settings, but not on the audience weight
vector. The evaluate command therefore writes reusable entries under a
run-level correctness_cache/ directory unless --correctness-cache-dir
overrides the location.
Each evaluation job writes:
deck_stats.jsonand extracted visible text;- rendered assets when needed;
probe_scores.jsonl;correctness.json, including extracted claims and claim verification labels;metrics.json;run_manifest.jsonand status/log files.
Main code:
src/stages/s06_metrics.py
Metrics join three files by probe_id: source probes, audience weights, and
probe scores. Probes are included only when their audience weight is at least
tau_a. This implements the audience-relevant denominator:
included probes = {probe | utility_weight(audience, probe) >= tau_a}
Audience Coverage is recovered utility divided by available utility over the included probes. Recovered utility is the weighted sum of covered probes.
Efficiency is reported with three cost normalizations:
- time:
0.25 * slide_count + visible_word_count / 130; - slide:
max(slide_count, 1); - token/word proxy:
max(visible_word_count, 1).
If correctness is available, safe_efficiency_time multiplies time-normalized
efficiency by correctness. With the default claim-level scorer, contradictions
and unsupported claims reduce this multiplier, but one weak claim does not
automatically collapse the whole deck to zero.
Main code:
src/stages/s07_aggregation.py
The generic aggregator scans metric files, infers system/audience/case identity,
and groups rows by (system_id, audience). For each metric key it reports the
mean and bootstrap confidence intervals over row-level results.
Benchmark-specific summaries read probes, weights, scores, and deck statistics for each metric row, then recompute domain-wise utility recovery. Domain labels are inferred from probe theme, question, answer, modality, and level using a keyword-based classifier with the domains:
context, method, evidence, limitations, implementation, implications
This keeps Domain-wise Coverage derived from the same probe-score files as Audience Coverage while exposing which kinds of information each deck covers.
The pipeline uses JSON for per-case metadata and aggregate outputs, and JSONL for row-oriented probe, weight, and score files. The examples below show the stable fields that downstream tools should expect. Additional metadata fields may appear as the pipeline records model ids, chunk settings, prompts, or debugging information.
Source metadata identifies the input topic and its intended benchmark context.
It is usually stored as source_metadata.json beside the source file.
{
"case_id": "case_000",
"title": "Document title",
"source_path": "data/sources/case_000/source.pdf",
"source_type": "pdf",
"scene": "academic_research_talk",
"valid_audiences": ["specialists", "learners", "decision_makers"],
"metadata": {}
}Each line in probes_deduped.jsonl is one source-grounded probe. Probes are
audience-agnostic at generation time; audience conditioning is introduced by
the weighting stage.
{
"probe_id": "pb_0001",
"source_case_id": "case_000",
"scene": "academic_research_talk",
"question": "What is the main claim?",
"answer": "The source-supported answer.",
"evidence": [{"locator": "Section 1", "text": "Evidence text"}],
"level": 2,
"modality": "text",
"theme": "method",
"metadata": {}
}Each weight row assigns one probe a utility value for one audience profile.
{
"probe_id": "pb_0001",
"audience": "specialists",
"utility_weight": 1.0,
"reason": "Why this probe matters for the audience.",
"metadata": {}
}The deck stage stores generator outputs under system, condition, audience, and case directories. A prepared deck can be PPTX or PDF. Deck statistics are stored as JSON and point to the extracted visible text used by evaluation.
{
"deck_path": "data/runs/run/decks/case_000.pptx",
"slide_count": 8,
"visible_word_count": 700,
"extracted_text_path": "data/runs/run/decks/case_000.txt",
"parser": "pptx",
"warnings": []
}Each score row records whether the deck contains enough visible evidence to answer one probe.
{
"probe_id": "pb_0001",
"covered": true,
"answer": "Deck-supported answer.",
"cited_evidence": "Slide 3",
"metadata": {}
}Compact aggregate input uses metrics/<system>/<audience>/<case>.json. The
aggregator also accepts full xslidesbench evaluate output folders that contain a
metrics.json file next to run_manifest.json.
{
"audience": "specialists",
"tau_a": 0.7,
"included_probe_count": 42,
"available_utility": 39.5,
"recovered_utility": 24.0,
"coverage": 0.6076,
"efficiency_time": 3.2,
"efficiency_slide": 2.0,
"correctness": 0.9,
"safe_efficiency_time": 2.88
}