Spaces:
Running
Running
| """Evaluare manuala pipeline TCS pe un set de articole cu ground truth. | |
| Rulare: python evaluation/run_benchmark.py | |
| Optiuni: | |
| --wikidata Activeaza verificare Wikidata (necesita retea) | |
| --threshold N Threshold TCS sub care articolul e prezis FAKE (default: 0.6) | |
| --pipeline P Pipeline de folosit: spacy sau llm (default: spacy) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import sys | |
| import time | |
| from datetime import datetime, date | |
| from pathlib import Path | |
| from typing import Optional | |
| # Adauga project root in sys.path (necesar cand scriptul e rulat direct) | |
| _PROJECT_ROOT = Path(__file__).parent.parent | |
| if str(_PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_PROJECT_ROOT)) | |
| logging.basicConfig( | |
| level=logging.WARNING, | |
| format="%(asctime)s %(name)-35s %(levelname)-5s %(message)s", | |
| datefmt="%H:%M:%S", | |
| ) | |
| logger = logging.getLogger("benchmark") | |
| EVAL_DIR = Path(__file__).parent | |
| BENCHMARK_FILE = EVAL_DIR / "benchmark_articles.json" | |
| RESULTS_DIR = EVAL_DIR / "results" | |
| FAKE_THRESHOLD = 0.70 | |
| # // section load | |
| def load_benchmark(path: Path) -> list[dict]: | |
| """Incarca articolele de benchmark din JSON.""" | |
| if not path.exists(): | |
| logger.error(f"Fisier benchmark negasit: {path}") | |
| sys.exit(1) | |
| with open(path, encoding="utf-8") as f: | |
| data = json.load(f) | |
| logger.info(f"Incarcat {len(data)} articole din {path}") | |
| return data | |
| # // section run | |
| def _resolve_spacy_model(requested: Optional[str]) -> Optional[str]: | |
| """Returneaza modelul spaCy de folosit: cel cerut (daca e instalat) sau primul disponibil.""" | |
| import spacy | |
| installed = spacy.util.get_installed_models() | |
| if not installed: | |
| return None | |
| if requested and requested in installed: | |
| return requested | |
| # Fallback: preferinta lg > sm > primul disponibil | |
| for preferred in ("en_core_web_trf", "en_core_web_lg", "en_core_web_sm"): | |
| if preferred in installed: | |
| return preferred | |
| return installed[0] | |
| def run_benchmark( | |
| articles: list[dict], | |
| pipeline: str = "spacy", | |
| use_wikidata: bool = False, | |
| threshold: float = FAKE_THRESHOLD, | |
| model_name: Optional[str] = None, | |
| use_rss: bool = False, | |
| ) -> list[dict]: | |
| """Ruleaza pipeline-ul TCS pe fiecare articol si colecteaza rezultatele.""" | |
| from backend.pipeline.graph.models import Article | |
| from backend.pipeline.orchestrator import PipelineOrchestrator | |
| resolved_model = _resolve_spacy_model(model_name) if pipeline == "spacy" else model_name | |
| if pipeline == "spacy" and resolved_model is None: | |
| logger.error("Niciun model spaCy instalat. Instaleaza cu: python -m spacy download en_core_web_sm") | |
| sys.exit(1) | |
| if resolved_model: | |
| logger.info(f"Model spaCy: {resolved_model}") | |
| orch = PipelineOrchestrator( | |
| use_wikidata=use_wikidata, | |
| extractor_name=pipeline, | |
| model_name=resolved_model, | |
| persistent_store=None, | |
| use_rss=use_rss, | |
| ) | |
| rows = [] | |
| for i, entry in enumerate(articles): | |
| pub_date: Optional[datetime] = None | |
| if entry.get("publication_date"): | |
| try: | |
| pub_date = datetime.strptime(entry["publication_date"], "%Y-%m-%d") | |
| except ValueError: | |
| pass | |
| article = Article( | |
| text=entry["text"], | |
| title=entry.get("title", f"Article {i+1}"), | |
| publication_date=pub_date, | |
| source=entry.get("source", "benchmark"), | |
| ) | |
| logger.info(f"[{i+1}/{len(articles)}] '{article.title[:60]}'") | |
| t0 = time.monotonic() | |
| try: | |
| result = orch.run(article) | |
| except Exception as e: | |
| logger.error(f"Eroare pipeline pe '{article.title}': {e}", exc_info=True) | |
| rows.append(_error_row(entry, i + 1)) | |
| continue | |
| elapsed_ms = (time.monotonic() - t0) * 1000 | |
| expected_fake: bool = entry.get("expected_fake", False) | |
| predicted_fake: bool = result.n_temporal_claims > 0 and result.score < threshold | |
| outcome = _classify_outcome(expected_fake, predicted_fake) | |
| rows.append({ | |
| "idx": i + 1, | |
| "title": article.title, | |
| "tcs": round(result.score, 4), | |
| "tcs_label": result.label, | |
| "n_claims": result.n_temporal_claims, | |
| "n_inconsistencies": result.n_inconsistencies, | |
| "coherence_factor": round(result.coherence_factor, 4), | |
| "expected_fake": expected_fake, | |
| "predicted_fake": predicted_fake, | |
| "outcome": outcome, | |
| "known_inconsistencies": entry.get("known_inconsistencies", []), | |
| "detected_inconsistencies": [ | |
| { | |
| "type": inc.inconsistency_type.value, | |
| "severity": inc.severity.value, | |
| "description": inc.description, | |
| } | |
| for inc in result.inconsistencies | |
| ], | |
| "processing_time_ms": round(elapsed_ms, 1), | |
| "source": entry.get("source", ""), | |
| }) | |
| return rows | |
| def _classify_outcome(expected_fake: bool, predicted_fake: bool) -> str: | |
| if expected_fake and predicted_fake: | |
| return "TP" | |
| if not expected_fake and not predicted_fake: | |
| return "TN" | |
| if not expected_fake and predicted_fake: | |
| return "FP" | |
| return "FN" | |
| def _error_row(entry: dict, idx: int) -> dict: | |
| return { | |
| "idx": idx, | |
| "title": entry.get("title", f"Article {idx}"), | |
| "tcs": 0.0, | |
| "tcs_label": "error", | |
| "n_claims": 0, | |
| "n_inconsistencies": 0, | |
| "coherence_factor": 0.0, | |
| "expected_fake": entry.get("expected_fake", False), | |
| "predicted_fake": False, | |
| "outcome": "ERROR", | |
| "known_inconsistencies": entry.get("known_inconsistencies", []), | |
| "detected_inconsistencies": [], | |
| "processing_time_ms": 0.0, | |
| "source": entry.get("source", ""), | |
| } | |
| # // section metrics | |
| def compute_metrics(rows: list[dict]) -> dict: | |
| """Calculeaza Precision, Recall, F1 pe baza clasificarilor TP/FP/FN/TN.""" | |
| tp = sum(1 for r in rows if r["outcome"] == "TP") | |
| tn = sum(1 for r in rows if r["outcome"] == "TN") | |
| fp = sum(1 for r in rows if r["outcome"] == "FP") | |
| fn = sum(1 for r in rows if r["outcome"] == "FN") | |
| precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 | |
| recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 | |
| f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0 | |
| accuracy = (tp + tn) / len(rows) if rows else 0.0 | |
| avg_tcs_true = _avg_tcs(rows, expected_fake=False) | |
| avg_tcs_fake = _avg_tcs(rows, expected_fake=True) | |
| return { | |
| "total": len(rows), | |
| "tp": tp, "tn": tn, "fp": fp, "fn": fn, | |
| "precision": round(precision, 4), | |
| "recall": round(recall, 4), | |
| "f1": round(f1, 4), | |
| "accuracy": round(accuracy, 4), | |
| "avg_tcs_true_articles": round(avg_tcs_true, 4) if avg_tcs_true is not None else None, | |
| "avg_tcs_fake_articles": round(avg_tcs_fake, 4) if avg_tcs_fake is not None else None, | |
| } | |
| def _avg_tcs(rows: list[dict], expected_fake: bool) -> Optional[float]: | |
| subset = [r["tcs"] for r in rows if r["expected_fake"] == expected_fake and r["outcome"] != "ERROR"] | |
| return sum(subset) / len(subset) if subset else None | |
| # // section print_table | |
| def print_table(rows: list[dict], threshold: float) -> None: | |
| """Afiseaza tabelul de rezultate in consola.""" | |
| title_w = 42 | |
| sep = "-" * (title_w + 62) | |
| print(f"\n{'='*(title_w + 62)}") | |
| print(f" TCS BENCHMARK | threshold = {threshold:.2f} | fake = TCS < {threshold:.2f}") | |
| print(f"{'='*(title_w + 62)}") | |
| print( | |
| f" {'#':>2} {'Title':<{title_w}} {'TCS':>6} " | |
| f"{'Exp':>4} {'Pred':>5} {'Out':>4} {'Inc':>4} {'ms':>6}" | |
| ) | |
| print(sep) | |
| for r in rows: | |
| exp_str = "FAKE" if r["expected_fake"] else "TRUE" | |
| pred_str = "FAKE" if r["predicted_fake"] else "TRUE" | |
| outcome = r["outcome"] | |
| marker = " " if outcome in ("TP", "TN") else "*" | |
| title_short = r["title"][:title_w] | |
| print( | |
| f"{marker} {r['idx']:>2} {title_short:<{title_w}} {r['tcs']:>6.3f} " | |
| f"{exp_str:>4} {pred_str:>5} {outcome:>4} " | |
| f"{r['n_inconsistencies']:>4} {r['processing_time_ms']:>6.0f}" | |
| ) | |
| print(sep) | |
| print(" * = misclassified\n") | |
| def print_metrics(metrics: dict, threshold: float) -> None: | |
| """Afiseaza metricile de evaluare.""" | |
| print(f" Metrics (threshold TCS < {threshold:.2f} = FAKE predicted):") | |
| print(f" {'-'*40}") | |
| print(f" TP={metrics['tp']} TN={metrics['tn']} FP={metrics['fp']} FN={metrics['fn']}") | |
| print(f" Precision : {metrics['precision']:.4f}") | |
| print(f" Recall : {metrics['recall']:.4f}") | |
| print(f" F1 : {metrics['f1']:.4f}") | |
| print(f" Accuracy : {metrics['accuracy']:.4f}") | |
| if metrics["avg_tcs_true_articles"] is not None: | |
| print(f" Avg TCS (TRUE articles) : {metrics['avg_tcs_true_articles']:.4f}") | |
| if metrics["avg_tcs_fake_articles"] is not None: | |
| print(f" Avg TCS (FAKE articles) : {metrics['avg_tcs_fake_articles']:.4f}") | |
| print() | |
| def print_inconsistency_details(rows: list[dict]) -> None: | |
| """Afiseaza detalii inconsistente detectate vs asteptate pentru fiecare articol.""" | |
| print(" Inconsistency details:") | |
| print(f" {'─'*60}") | |
| for r in rows: | |
| print(f" [{r['idx']}] {r['title'][:55]}") | |
| if r["known_inconsistencies"]: | |
| for k in r["known_inconsistencies"]: | |
| print(f" expected : {k}") | |
| if r["detected_inconsistencies"]: | |
| for d in r["detected_inconsistencies"]: | |
| print(f" detected : [{d['severity']}] {d['type']}: {d['description'][:70]}") | |
| if not r["known_inconsistencies"] and not r["detected_inconsistencies"]: | |
| print(" (no inconsistencies expected or found)") | |
| print() | |
| # // section save | |
| def save_results(rows: list[dict], metrics: dict, pipeline: str, threshold: float) -> Path: | |
| """Salveaza rezultatele in evaluation/results/results_YYYY-MM-DD.json.""" | |
| RESULTS_DIR.mkdir(parents=True, exist_ok=True) | |
| today = date.today().strftime("%Y-%m-%d") | |
| output_path = RESULTS_DIR / f"results_{today}.json" | |
| payload = { | |
| "generated_at": datetime.now().isoformat(), | |
| "pipeline": pipeline, | |
| "fake_threshold": threshold, | |
| "metrics": metrics, | |
| "articles": rows, | |
| } | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| json.dump(payload, f, indent=2, ensure_ascii=False, default=str) | |
| return output_path | |
| # // section main | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="TCS Pipeline Benchmark") | |
| parser.add_argument("--wikidata", action="store_true", help="Activeaza verificare Wikidata") | |
| parser.add_argument("--threshold", type=float, default=FAKE_THRESHOLD, help="Threshold TCS fake (default: 0.6)") | |
| parser.add_argument("--pipeline", choices=["spacy", "llm"], default="spacy", help="Pipeline extractor") | |
| parser.add_argument("--model", type=str, default=None, help="Model spaCy/LLM explicit (default: auto-detect)") | |
| parser.add_argument("--rss", action="store_true", help="Enable RSS Stream") | |
| args = parser.parse_args() | |
| print(f"\nBenchmark: pipeline={args.pipeline}, wikidata={args.wikidata}, threshold={args.threshold}") | |
| print(f"Fisier: {BENCHMARK_FILE}\n") | |
| articles = load_benchmark(BENCHMARK_FILE) | |
| rows = run_benchmark( | |
| articles, | |
| pipeline=args.pipeline, | |
| use_wikidata=args.wikidata, | |
| threshold=args.threshold, | |
| model_name=args.model, | |
| use_rss=args.rss, | |
| ) | |
| metrics = compute_metrics(rows) | |
| print_table(rows, args.threshold) | |
| print_metrics(metrics, args.threshold) | |
| print_inconsistency_details(rows) | |
| output_path = save_results(rows, metrics, args.pipeline, args.threshold) | |
| print(f" Rezultate salvate: {output_path}") | |
| if __name__ == "__main__": | |
| main() | |