AIdetector / core /aggregator.py
mokshad
Fix model accuracy: correct BERT label, reduce temperature, add perplexity signal, fix TTR for short texts
e518cdb
Raw
History Blame Contribute Delete
2.59 kB
# core/aggregator.py
# Combines BERT score + stylometric score + perplexity into a final probability.
from core.stylometrics import compute_stylometric_score
from core.explainer import find_ai_phrases, generate_explanation
from core.perplexity_scorer import perplexity_scorer
from config.settings import settings
def aggregate_scores(
bert_result: dict,
text: str,
sentence_scores: list[dict],
) -> dict:
"""
Three-signal hybrid:
- BERT (60%): neural classifier fine-tuned on AI vs human text
- Perplexity (20%): GPT-2 perplexity — model-agnostic, works for any LLM
- Stylometrics (20%): structural writing features
Perplexity replaces pure BERT weight because it catches AI text that
slop-detector-bert wasn't trained on (modern GPT-4/Claude output).
"""
stylo_result = compute_stylometric_score(text)
stylo_score = stylo_result["stylometric_ai_score"]
bert_prob = bert_result["ai_probability"]
# Perplexity signal — lower perplexity = more predictable = more AI-like
raw_perplexity = perplexity_scorer.get_perplexity(text)
perplexity_score = perplexity_scorer.perplexity_to_ai_score(raw_perplexity)
bert_weight = 0.60
perplexity_weight = 0.20
stylo_weight = 0.20
final_probability = (
bert_prob * bert_weight +
perplexity_score * perplexity_weight +
stylo_score * stylo_weight
)
final_probability = round(max(0.0, min(1.0, final_probability)), 4)
ai_phrases = find_ai_phrases(text)
explanation = generate_explanation(
ai_probability=final_probability,
stylometric_data=stylo_result,
sentence_scores=sentence_scores,
ai_phrases=ai_phrases,
perplexity=raw_perplexity,
)
is_ai = final_probability >= settings.CONFIDENCE_THRESHOLD
return {
"ai_probability": final_probability,
"is_ai": is_ai,
"verdict": explanation["verdict"],
"confidence_threshold": settings.CONFIDENCE_THRESHOLD,
"scores": {
"bert_score": bert_prob,
"stylometric_score": round(stylo_score, 4),
"bert_weight": bert_weight,
"stylometric_weight": stylo_weight,
},
"stylometric_features": stylo_result["features"],
"sentence_scores": sentence_scores,
"explanation": explanation,
"chunks_analyzed": bert_result.get("chunks_analyzed", 1),
"perplexity": round(raw_perplexity, 2) if raw_perplexity is not None else None,
"perplexity_score": round(perplexity_score, 4),
}