Automated fact-checker for Russian-language claims. Takes a claim, gathers evidence from open sources (DuckDuckGo, Wikipedia, Wikidata), runs it through NLI + LLM, and returns a verdict with reasoning and source list.
Verdicts (returned verbatim in Russian): ΠΠ ΠΠΠΠ (true) Β· ΠΠΠΠ¬ (false) Β· ΠΠ Π£ΠΠΠ ΠΠΠ (unsure) Β· Π‘ΠΠΠ (scam).
The system is evaluated on 28 probe claims, split into three sets by falsification type. All runs are deterministic, on a single adapter (fact_checker_lora_v2).
|
10 canonical claims 90 % |
8 claims not seen in training 87.5 % |
10 conspiracies and misconceptions 80 % |
Total: 24 of 28 Β· 86 %
Canonical facts and their substitutions β probes the main falsification patterns:
- Name swaps. Who created Bitcoin? Who wrote Eugene Onegin?
- Location swaps. What's the capital of Australia? Where is the Eiffel Tower?
- Date swaps. When did World War II end?
- Directional facts. Does the Sun orbit the Earth?
- Numerical facts. How much oxygen is in the atmosphere?
Main mechanism β Wikidata lookup. If the claim says "Bitcoin creator = Vitalik Buterin" and Wikidata says P178 = Satoshi Nakamoto, the mismatch is detected automatically without calling the LLM judge.
Claims not seen in training or in the core set. Measures whether the mechanisms generalise to fresh claims.
Covers:
- Multi-word entities in quotes. Β«Π ΠΎΠΌΠ°Π½ βΠΠΎΠΉΠ½Π° ΠΈ ΠΌΠΈΡβ Π½Π°ΠΏΠΈΡΠ°Π» ΠΠΎΡΡΠΎΠ΅Π²ΡΠΊΠΈΠΉΒ» (claims Dostoevsky wrote War and Peace)
- Geographic swaps. Capital of Canada = Toronto
- Scientific authorship. Newton formulated the theory of relativity
- Event dates. Berlin Wall fell in 1979
- Common truths. Moscow is the capital of Russia; Everest is the tallest mountain
Claims with no clear structural contradiction in the knowledge graph:
- Moon landing hoax
- "We only use 10% of our brain"
- Vikings wore horned helmets
- Vaccines cause autism
- 5G radiation causes cancer
- Flat Earth
- Great Wall of China visible from the Moon
- COVID-19 engineered as a bioweapon
-
97% of climatologists agree on anthropogenic warming
- Napoleon was short
A separate myth probe runs here via peft.disable_adapter() against the base Mistral 7B β it classifies the claim as ΠΠΠ€ / Π€ΠΠΠ’ / ΠΠΠΠΠΠΠ‘Π’ΠΠ (myth / fact / unknown). Accuracy is lower because the base model doesn't parametrically know every named myth, and search doesn't always surface a debunk source for a given claim.
Search variability. DuckDuckGo and Wikipedia can return different source sets across runs β within a single run the result is deterministic, but cross-session flips on borderline claims are possible.
LLM run-to-run flakiness. The Mistral 7B parametric probe sometimes returns ΠΠΠΠΠΠΠ‘Π’ΠΠ (unknown) on a claim it answered confidently in another run β an artefact of 4-bit bitsandbytes quantisation. Sample size (28 claims) means 1 flip β Β±3.5 % in the metric.
Myths without structural signal. For niche myths the base Mistral doesn't know parametrically (horned Vikings, Napoleon's height), the system returns ΠΠ Π£ΠΠΠ ΠΠΠ (unsure) β an honest defer rather than a confidently wrong verdict.
Claim
β
βΌ
[PARSE] claim classification, extract numbers/dates, detect scam
β
βΌ
[DECOMPOSE] rule-based split on conjunctions (quote-aware); Mistral for complex cases
β
βΌ
[SEARCH] DDG (3 parallel frames) + Wikipedia entity lookup
β + verification queries + counter-search (debunk framing)
β + Wikipedia "common misconceptions" frame
β + quoted-number query (for claims with number+unit)
β + rate-limiter 0.35s/host + 429-aware retry
βΌ
[RANK] multilingual-e5-base (bi-encoder) + mmarco cross-encoder reranker
β + fact-checker boost + TRUSTED_SOURCES filter
βΌ
[EVIDENCE] 4 parallel signals:
β β’ Wikidata SPARQL β structured KG check
β structural entity-mismatch for
β single-value props (capital, country,
β continent, author, founder, developer)
β β’ NUM comparison β deterministic numeric check
β β’ NLI β mDeBERTa sentence-level +
β cross-encoder tiebreaker + doc-level CE fallback +
β subject-mention guard
β β’ LLM knowledge probe (Mistral parametric memory)
β β’ LLM myth probe (via disable_adapter β base Mistral,
β not SFT β classifies as ΠΠΠ€/Π€ΠΠΠ’ β myth/fact)
βΌ
[DECIDE] priority-based tree over the signals:
β TIER 1: WD hard-mismatch β ΠΠΠΠ¬ 90 (false, confidence 90)
β TIER 2: NUM Β±1 (with LLM/myth consensus override)
β TIER 3: debunk-aware stance gate (myth detection)
β TIER 4: NLI gap zones (strong / moderate / ambiguous)
β + subject-verification gates
β + LLM coherence overrides
β TIER 5: LLM parametric / myth fallback
βΌ
[EXPLAIN] LLM generates the reasoning text (the verdict is already
decided β the model only explains why)
βΌ
Verdict + credibility_score (0-100) + reasoning + sources
LLM is an explainer, not a judge β all verdicts are derived deterministically from signals. This keeps verdicts reproducible and debuggable.
| Component | Model |
|---|---|
| Base LLM | unsloth/mistral-7b-instruct-v0.3-bnb-4bit |
| SFT adapter | adapters/fact_checker_lora_v2 (custom QLoRA r=16) |
| Bi-encoder ranker | intfloat/multilingual-e5-base |
| Cross-encoder reranker | cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 |
| NLI (primary) | MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7 |
| NLI (cross-encoder fallback) | cross-encoder/nli-deberta-v3-base |
| Translator (RUβEN fallback) | facebook/nllb-200-distilled-600M |
| NER + lemmatisation | Natasha + pymorphy2 |
| Knowledge graph | Wikidata SPARQL endpoint |
| Search | DuckDuckGo (async) + MediaWiki API |
GPU: 4-bit Mistral fits in ~5 GB VRAM, NLI/reranker run on CPU, embeddings on CPU. Tested on RTX 5070 12 GB (Blackwell sm_120); xformers is disabled β incompatible with sm_120.
git clone https://github.com/sakenuGOD/antifake.git
cd antifake
python -m venv venv
source venv/bin/activate # Linux/Mac
# or:
venv\Scripts\activate # Windows
pip install -r requirements.txt| Variable | Required | Purpose |
|---|---|---|
SERPAPI_API_KEY |
no | Google Search fallback (without it β DDG only) |
HF_TOKEN |
no | higher HuggingFace rate limit when pulling weights |
ANTIFAKE_DETERMINISTIC=1 |
no | strict-reproducibility mode (cuDNN deterministic) |
Stored in .env at the project root.
streamlit run app.pyThe UI shows 13 pipeline stages in real time: parsing, decomposition, keywords, found sources, Wikidata facts, NUM comparisons, NLI signals, debunk count, LLM probes, decision, aggregation, and explanation.
from pipeline import FactCheckPipeline
pipeline = FactCheckPipeline(adapter_path="adapters/fact_checker_lora_v2")
result = pipeline.check("Π ΠΎΠΌΠ°Π½ Β«ΠΠΎΠΉΠ½Π° ΠΈ ΠΌΠΈΡΒ» Π½Π°ΠΏΠΈΡΠ°Π» Π€ΡΠ΄ΠΎΡ ΠΠΎΡΡΠΎΠ΅Π²ΡΠΊΠΈΠΉ")
# (claim: "the novel War and Peace was written by Fyodor Dostoevsky")
print(result["verdict"]) # "ΠΠΠΠ¬" (false)
print(result["credibility_score"]) # 10
print(result["reasoning"]) # reasoning text (in Russian)
for src in result["sources"][:3]:
print(src["link"], src["title"])Full result signature β see pipeline.py::check().
python main.py "ΠΠΎΡΠΊΠ²Π° β ΡΡΠΎΠ»ΠΈΡΠ° Π ΠΎΡΡΠΈΠΈ"
# (claim: "Moscow is the capital of Russia")antifake/
βββ app.py # Streamlit entry point
βββ main.py # CLI entry point
βββ pipeline.py # Main evidence-first pipeline
βββ search.py # DDG/Wiki/Wikidata + rate-limiter + frames
βββ nli_checker.py # mDeBERTa + cross-encoder + doc-level CE
βββ wikidata.py # SPARQL + structural entity-mismatch
βββ counter_search.py # Multi-frame debunk/verify queries
βββ model.py # Mistral + LoRA loader
βββ prompts.py # Knowledge + myth probe templates
βββ claim_parser.py # Numbers/dates/locations + scam patterns
βββ embeddings.py # Semantic ranker + CE reranker
βββ evidence_tiers.py # T1-T3 authority weighting
βββ nlp_russian.py # Natasha/pymorphy2 helpers
βββ source_credibility.py # Domain trust boosting
βββ cache.py # Disk-based search cache (24h TTL)
βββ fact_cache.py # Verified facts cache
βββ config.py # All constants and thresholds
βββ utils.py
β
βββ tests/ # Regression suites
β βββ _path.py # sys.path shim
β βββ test_hard10.py # 10 canonical claims
β βββ test_manipulative.py # 10 conspiracy/myth claims
β βββ test_ood_probe.py # 8 out-of-distribution claims
β βββ test_*.py (archive)
β
βββ eval/ # Metrics on larger datasets
β βββ evaluate.py
β βββ evaluate_universal.py
β βββ quick_eval.py
β
βββ scripts/ # Training / data-gen / housekeeping
β βββ train.py # SFT
β βββ train_grpo.py # Reward-based
β βββ generate_*.py # Training data
β βββ merge_training_data.py
β βββ audit_training_data.py
β βββ download_dataset.py
β βββ sanity_check.py
β βββ summarize_night.py
β βββ run_training.sh, setup_and_train.sh, night_run.{sh,ps1}
β
βββ data/ # Datasets + results + caches
β βββ train_*.jsonl # Training sets
β βββ hard10_results.json
β βββ manipulative_results.json
β βββ ood_probe_results.json
β βββ wikidata_cache.json
β
βββ adapters/ # LoRA / GRPO adapters (weights gitignored)
βββ logs/ # Pipeline runs (gitignored)
βββ requirements.txt
Files in tests/, eval/, scripts/ start with import _path # noqa: F401,E402. This shim adds the project root to sys.path so from pipeline import β¦ works without installing the project as a package.
python scripts/train.py \
--dataset data/train_v2_combined.jsonl \
--output adapters/fact_checker_lora_v3 \
--epochs 4 \
--learning-rate 2e-5 \
--resume adapters/fact_checker_lora_v2 # optionalpython scripts/train_grpo.py \
--load-adapter adapters/fact_checker_lora_v2 \
--steps 200python scripts/generate_russian_data.py --output data/train.jsonl
python scripts/generate_failure_patterns.py --output data/failure_patterns.jsonl
python scripts/merge_training_data.py \
data/train.jsonl data/failure_patterns.jsonl \
--output data/train_v2_combined.jsonl# Small regression suites (~15-25 min per suite)
python tests/test_hard10.py --adapter adapters/fact_checker_lora_v2
python tests/test_manipulative.py --adapter adapters/fact_checker_lora_v2
python tests/test_ood_probe.py --adapter adapters/fact_checker_lora_v2
# Debug a single claim
python tests/debug_single.py "Bitcoin ΡΠΎΠ·Π΄Π°Π» ΠΠΈΡΠ°Π»ΠΈΠΊ ΠΡΡΠ΅ΡΠΈΠ½"
# (claim: "Bitcoin was created by Vitalik Buterin")Results land in data/*_results.json with a detailed log in logs/.
All thresholds and settings live in config.py:
| Class | What it controls |
|---|---|
ModelConfig |
base model path, max_seq_length, load_in_4bit |
PipelineConfig |
NLI device, cross-encoder model, re-ranker on/off |
SearchConfig |
DDG timeout, num_results, SerpAPI |
DecisionThresholds |
strong_gap, moderate_gap, num_nli_override |
| Service | Rate limit | Fallback |
|---|---|---|
| DuckDuckGo | no formal limit, 429 under burst | retry + async parallel |
| Wikipedia / Wikidata | ~30 req/s anonymous | 0.35s/host token bucket + 429 retry |
| SerpAPI | per-plan key | DDG (primary channel) |
Rate-limiter lives in search.py::_wiki_rate_limit β global per-host token bucket with tenacity retry (2-30s exponential backoff).
See LICENSE (if present).