| import re |
| from urllib.parse import urlparse |
| from datetime import datetime |
| from typing import List, Dict |
|
|
| import gradio as gr |
| import plotly.graph_objects as go |
| import numpy as np |
|
|
|
|
| |
|
|
| SENTENCE_SPLIT_PATTERN = re.compile(r'(?<=[.!?])\s+') |
| URL_PATTERN = re.compile(r'(https?://[^\s]+)') |
| YEAR_PATTERN = re.compile(r'\b(19[5-9]\d|20[0-4]\d|2050)\b') |
|
|
| BIAS_WORDS = [ |
| "obviously", "definitely", "certainly", "undeniably", |
| "everyone knows", "no doubt", "clearly", "always", "never" |
| ] |
|
|
|
|
| def domain_quality(domain: str) -> int: |
| """Very rough heuristic for domain quality.""" |
| if not domain: |
| return 50 |
| domain = domain.lower() |
| if domain.endswith(".edu") or domain.endswith(".ac.uk"): |
| return 90 |
| if domain.endswith(".gov") or domain.endswith(".gov.uk"): |
| return 90 |
| if domain.endswith(".org"): |
| return 75 |
| if "wikipedia.org" in domain: |
| return 70 |
| if "blog" in domain or "medium.com" in domain: |
| return 55 |
| return 60 |
|
|
|
|
| def label_from_score(score: int) -> str: |
| if score >= 80: |
| return "High Trust" |
| if score >= 60: |
| return "Medium Trust" |
| if score >= 40: |
| return "LowβMedium Trust" |
| return "Low Trust" |
|
|
| def make_gauge(score: int): |
| |
| score = max(0, min(100, score)) |
|
|
| fig = go.Figure( |
| go.Indicator( |
| mode="gauge+number", |
| value=score, |
| number={"suffix": "%", "font": {"size": 34}}, |
| gauge={ |
| "shape": "angular", |
| "axis": {"range": [0, 100], "visible": False}, |
| "bar": { |
| "color": "#2563eb", |
| "thickness": 0.35 |
| }, |
| "bgcolor": "white", |
| "borderwidth": 0, |
|
|
| |
| "steps": [ |
| {"range": [0, 33], "color": "#c7d2fe"}, |
| {"range": [33, 66], "color": "#60a5fa"}, |
| {"range": [66, 100], "color": "#1d4ed8"} |
| ], |
| }, |
| domain={"x": [0, 1], "y": [0, 1]}, |
| ) |
| ) |
|
|
| fig.update_layout( |
| margin=dict(t=30, b=0, l=20, r=20), |
| height=220, |
| ) |
|
|
| return fig |
|
|
|
|
| def analyze_text(text: str, is_ai_generated: bool): |
| text = text.strip() |
| if not text: |
| return ( |
| "β", |
| "No input", |
| "Please paste some content to analyze.", |
| "- No issues (no text)\n", |
| "- Add some content to see suggestions.\n", |
| "_No sentences to analyze._", |
| ) |
|
|
| |
| sentences = [s.strip() for s in SENTENCE_SPLIT_PATTERN.split(text) if s.strip()] |
|
|
| urls = URL_PATTERN.findall(text) |
| domains = [] |
| for url in urls: |
| try: |
| parsed = urlparse(url) |
| domains.append(parsed.netloc) |
| except Exception: |
| continue |
| unique_domains = set(domains) |
|
|
| |
| sentence_data: List[Dict] = [] |
| sentences_with_citation = 0 |
| sentences_with_bias = 0 |
|
|
| for s in sentences: |
| lower_s = s.lower() |
| has_citation = bool( |
| URL_PATTERN.search(s) |
| or re.search(r'\[\d+\]', s) |
| or "according to" in lower_s |
| ) |
| has_bias = any(bias in lower_s for bias in BIAS_WORDS) |
| years = YEAR_PATTERN.findall(s) |
|
|
| sentence_data.append( |
| { |
| "text": s, |
| "has_citation": has_citation, |
| "has_bias_indicator": has_bias, |
| "years": years, |
| } |
| ) |
|
|
| if has_citation: |
| sentences_with_citation += 1 |
| if has_bias: |
| sentences_with_bias += 1 |
|
|
| total_sentences = len(sentences) |
|
|
| |
| if urls: |
| domain_scores = [domain_quality(d) for d in unique_domains] |
| avg_domain_score = sum(domain_scores) / len(domain_scores) |
| sqs = int(avg_domain_score) |
| sqs_rationale = f"Based on {len(unique_domains)} cited domain(s); average domain quality β {avg_domain_score:.1f}." |
| else: |
| sqs = 55 |
| sqs_rationale = "No explicit sources detected; defaulting to moderate source quality." |
|
|
| |
| citation_ratio = sentences_with_citation / total_sentences if total_sentences else 0 |
| if citation_ratio == 0: |
| css = 25 |
| css_rationale = "No citations detected for any sentences." |
| else: |
| css = int(40 + 60 * citation_ratio) |
| css_rationale = f"{sentences_with_citation}/{total_sentences} sentence(s) appear to have citations or references." |
|
|
| |
| bias_ratio = sentences_with_bias / total_sentences if total_sentences else 0 |
| diversity_bonus = min(len(unique_domains) * 5, 20) |
|
|
| base_bds = 70 + diversity_bonus |
| bias_penalty = int(bias_ratio * 40) |
| bds = max(20, min(100, base_bds - bias_penalty)) |
|
|
| bds_rationale_parts = [] |
| if unique_domains: |
| bds_rationale_parts.append(f"{len(unique_domains)} unique source domain(s) detected.") |
| if sentences_with_bias: |
| bds_rationale_parts.append( |
| f"{sentences_with_bias}/{total_sentences} sentence(s) with bias-indicating phrases." |
| ) |
| if not bds_rationale_parts: |
| bds_rationale_parts.append("No citations or explicit bias indicators detected.") |
| bds_rationale = " ".join(bds_rationale_parts) |
|
|
| |
| all_years = YEAR_PATTERN.findall(text) |
| current_year = datetime.now().year |
| if not all_years: |
| rcs = 70 |
| rcs_rationale = "No specific years detected; assuming moderate recency for educational content." |
| else: |
| years_int = [int(y) for y in all_years] |
| newest = max(years_int) |
| age = current_year - newest |
| if age <= 5: |
| rcs = 90 |
| rcs_rationale = f"Newest year mentioned is {newest}, which is recent." |
| elif age <= 15: |
| rcs = 70 |
| rcs_rationale = f"Newest year mentioned is {newest}, which is moderately old." |
| else: |
| rcs = 50 |
| rcs_rationale = f"Newest year mentioned is {newest}, which appears quite old for some topics." |
|
|
| |
| mrs = 80 if not is_ai_generated else 75 |
| mrs_rationale = "Assuming competent human or general-purpose AI author; can be refined later." |
|
|
| |
| weight_sqs = 0.30 |
| weight_css = 0.30 |
| weight_bds = 0.15 |
| weight_mrs = 0.15 |
| weight_rcs = 0.10 |
|
|
| overall = ( |
| sqs * weight_sqs |
| + css * weight_css |
| + bds * weight_bds |
| + mrs * weight_mrs |
| + rcs * weight_rcs |
| ) |
| overall_int = int(round(overall)) |
| label = label_from_score(overall_int) |
|
|
| |
|
|
| component_md = f""" |
| ### Component Breakdown |
| | Component | Weight | Score | Rationale | |
| |----------|--------|-------|-----------| |
| | SQS (Source Quality) | 30% | {sqs} | {sqs_rationale} | |
| | CSS (Claim Support) | 30% | {css} | {css_rationale} | |
| | BDS (Bias & Diversity) | 15% | {bds} | {bds_rationale} | |
| | MRS (Model Reliability) | 15% | {mrs} | {mrs_rationale} | |
| | RCS (Recency & Context) | 10% | {rcs} | {rcs_rationale} | |
| """ |
|
|
| issues: List[str] = [] |
| suggestions: List[str] = [] |
|
|
| if sentences_with_citation == 0: |
| issues.append("No explicit citations or references detected.") |
| suggestions.append("Add at least 2β3 citations from reliable educational or academic sources.") |
| elif citation_ratio < 0.3: |
| issues.append("Only a small portion of statements appear to be supported by citations.") |
| suggestions.append("Support more factual claims with references to textbooks, encyclopedias, or academic articles.") |
|
|
| if sentences_with_bias > 0: |
| issues.append("Some sentences use strong or absolute language that may indicate bias.") |
| suggestions.append("Rephrase strongly worded claims into more neutral, evidence-based statements.") |
|
|
| if rcs <= 60: |
| issues.append("The information relies on relatively old dates, which may be outdated for some topics.") |
| suggestions.append("Check if there are more recent sources or data to support your claims.") |
|
|
| if not issues: |
| issues.append("No major credibility issues detected by the heuristic checks.") |
| suggestions.append("You can still improve credibility by adding more high-quality sources and clarifying complex claims.") |
|
|
| issues_md = "\n".join(f"- {i}" for i in issues) |
| suggestions_md = "\n".join(f"- {s}" for s in suggestions) |
|
|
| sentence_lines = [] |
| for s in sentence_data: |
| tags = [] |
| if s["has_citation"]: |
| tags.append("π citation") |
| if s["has_bias_indicator"]: |
| tags.append("β οΈ possible bias") |
| if s["years"]: |
| tags.append("π
years: " + ", ".join(s["years"])) |
| tag_str = " β " + ", ".join(tags) if tags else "" |
| sentence_lines.append(f"- {s['text']}{tag_str}") |
|
|
| sentences_md = "\n".join(sentence_lines) if sentence_lines else "_No sentences found._" |
|
|
| overall_md = f""" |
| # Trust Score: **{overall_int}/100** |
| Label: **{label}** |
| """ |
|
|
| return overall_int, overall_md, label, component_md, issues_md, suggestions_md, sentences_md |
|
|
|
|
| |
|
|
|
|
| with gr.Blocks(title="Educational Content Credibility Checker") as demo: |
| gr.Markdown( |
| """ |
| # π Educational Content Credibility Checker (MVP) |
| |
| Paste any student essay or AI-generated explanation. |
| This tool estimates a **Trust Score (0β100)** based on: |
| - Source quality |
| - Claim support (citations) |
| - Bias & diversity |
| - Model reliability |
| - Recency & context |
| |
| > β οΈ MVP: This is a **heuristic prototype**, not a perfect fact-checker. |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(): |
| text_input = gr.Textbox( |
| label="1. Paste content here", |
| lines=15, |
| placeholder="Paste student content or AI-generated text...", |
| ) |
| is_ai_checkbox = gr.Checkbox( |
| label="Content is AI-generated", value=False |
| ) |
| analyze_btn = gr.Button("Analyze Credibility", variant="primary") |
|
|
| with gr.Column(): |
| gauge_plot = gr.Plot(label="trust.AI Score") |
| overall_out = gr.Markdown(label="Overall Trust Score") |
| components_out = gr.Markdown(label="Component Breakdown") |
| issues_out = gr.Markdown(label="Issues Detected") |
| suggestions_out = gr.Markdown(label="Suggestions for Improvement") |
| sentences_out = gr.Markdown(label="Sentence-level Analysis") |
|
|
| def gradio_wrapper(text, is_ai_generated): |
| overall_int, overall_md, label, component_md, issues_md, suggestions_md, sentences_md = analyze_text( |
| text, is_ai_generated |
| ) |
| fig = make_gauge(overall_int) |
| return fig, overall_md, component_md, issues_md, suggestions_md, sentences_md |
|
|
| analyze_btn.click( |
| fn=gradio_wrapper, |
| inputs=[text_input, is_ai_checkbox], |
| outputs=[gauge_plot, overall_out, components_out, issues_out, suggestions_out, sentences_out], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|