JeyBii's picture
Sync updated source code checker/external/core.py including partner integration
d4d82a3 verified
Raw
History Blame Contribute Delete
8.4 kB
import sys
import os
import logging
import threading
import time
# Add project root to path
sys.path.insert(
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
from checker.external.local_search import find_related_articles
from checker.external.web_search import (
search_google_news_multilingual,
extract_search_query,
)
from checker.external.scraper import fetch_all_news
from db.database import insert_articles, init_db, get_article_count
logger = logging.getLogger(__name__)
# ── Auto-scrape configuration ───────────────────────────────────────
SCRAPE_INTERVAL_HOURS = int(os.environ.get("SCRAPE_INTERVAL_HOURS", 6))
_scrape_thread = None
_scrape_lock = threading.Lock()
def _background_scrape(repeat=True):
"""Run the scraper in the background. Optionally repeats on a schedule."""
while True:
try:
logger.info("🔄 Background scrape starting...")
news = fetch_all_news()
if news:
new_count = insert_articles(news)
total = get_article_count()
logger.info(
"✅ Background scrape complete: %d fetched, %d new, %d total in DB",
len(news), new_count, total,
)
else:
logger.info("Background scrape: no articles fetched")
except Exception as exc:
logger.error("Background scrape failed: %s", exc, exc_info=True)
if not repeat:
break
# Sleep until next scrape
logger.info("Next scrape in %d hours", SCRAPE_INTERVAL_HOURS)
time.sleep(SCRAPE_INTERVAL_HOURS * 3600)
def start_background_scraper():
"""Start the periodic background scraper thread (if not already running)."""
global _scrape_thread
with _scrape_lock:
if _scrape_thread is not None and _scrape_thread.is_alive():
logger.info("Background scraper already running")
return
_scrape_thread = threading.Thread(
target=_background_scrape,
kwargs={"repeat": True},
daemon=True,
name="news-scraper",
)
_scrape_thread.start()
logger.info("🚀 Background scraper thread started (every %dh)", SCRAPE_INTERVAL_HOURS)
def trigger_scrape_once():
"""Trigger a one-off scrape in a background thread (non-blocking)."""
t = threading.Thread(
target=_background_scrape,
kwargs={"repeat": False},
daemon=True,
name="news-scraper-oneoff",
)
t.start()
return t
# ── Language detection + translation ─────────────────────────────────
_translator = None
_detector_available = False
try:
from langdetect import detect as _detect_lang
_detector_available = True
except ImportError:
logger.warning("langdetect not installed — language detection disabled")
_detect_lang = None
try:
from deep_translator import GoogleTranslator
_translator_available = True
except ImportError:
logger.warning("deep-translator not installed — translation disabled")
GoogleTranslator = None
_translator_available = False
def detect_language(text: str) -> str:
"""Detect the language of the input text. Returns ISO 639-1 code."""
if not _detector_available or not text or len(text.strip()) < 20:
return "en"
try:
lang = _detect_lang(text)
# langdetect returns 'tl' for Tagalog/Filipino, 'ceb' for Cebuano
return lang
except Exception:
return "en"
def translate_to_english(text: str, source_lang: str) -> str:
"""Translate text to English for ML processing."""
if source_lang == "en" or not _translator_available or not GoogleTranslator:
return text
import sys, io
try:
# Suppress deep_translator's "Backend with N concurrent workers." progress
# messages so they don't interleave with our print() output in the console.
_old_stdout = sys.stdout
_old_stderr = sys.stderr
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
try:
translated = GoogleTranslator(source=source_lang, target="en").translate(text)
finally:
sys.stdout = _old_stdout
sys.stderr = _old_stderr
if translated:
logger.info("Translated %s → en (%d chars)", source_lang, len(text))
return translated
return text
except Exception as exc:
logger.warning("Translation failed (%s→en): %s", source_lang, exc)
return text
class ExternalChecker:
def __init__(self):
"""Initialize the external checker, ensure DBs are ready, and start auto-scraper."""
init_db()
# Start periodic background scraping
start_background_scraper()
def check_claim(self, claim_text, translated_text=None):
"""
Check a claim against the local database and external web search.
Args:
claim_text (str): The claim or article text to verify (original language).
translated_text (str, optional): English translation for search boost.
Returns:
dict: Structured results containing:
- "db_results": List of dicts from local DB search.
- "web_results": List of dicts from Google News search.
- "verdict": Preliminary verdict string.
- "top_score": Highest semantic similarity score (float) or 0.0.
"""
if not claim_text or not claim_text.strip():
return {
"db_results": [],
"web_results": [],
"verdict": "NO INPUT",
"top_score": 0.0,
}
# 1. Search local database (use translated text if available for better matching)
search_text = translated_text if translated_text else claim_text
db_results = find_related_articles(search_text)
top_score = db_results[0]["similarity"] if db_results else 0.0
has_strong_db_match = top_score >= 0.55
# 2. Multilingual web search — searches in English + Filipino + Cebuano editions.
# When we have an English translation, make it the PRIMARY query: Google News
# indexes primarily in English, so an English query produces far more relevant
# hits for Philippine news stories even when the input is Cebuano/Tagalog.
# The original-language query is passed as `translated_query` (i.e. supplementary).
if translated_text:
# Primary query: from English translation (better Google News coverage)
query = extract_search_query(translated_text)
# Supplementary query: from original language (catches Cebuano sources)
translated_query = extract_search_query(claim_text)
else:
query = extract_search_query(claim_text)
translated_query = None
web_results = search_google_news_multilingual(
query,
translated_query=translated_query,
max_results=10,
)
# 3. Determine verdict
if has_strong_db_match:
verdict = "VERIFIED"
elif web_results:
verdict = "COVERAGE_FOUND_ONLINE"
elif db_results: # Weak match in DB
verdict = "WEAK_MATCH"
else:
verdict = "UNVERIFIED"
return {
"db_results": db_results,
"web_results": web_results,
"verdict": verdict,
"top_score": top_score,
}
def update_database(self):
"""
Trigger the scraper to fetch new articles and update the local database.
Returns:
int: Number of new articles added.
"""
logger.info("Manual database update triggered")
news = fetch_all_news()
if news:
new_count = insert_articles(news)
logger.info("Added %d new articles.", new_count)
return new_count
return 0