Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| import argparse | |
| import logging | |
| import sys | |
| import time | |
| from config import Config | |
| from src.models import Article, NewsItem | |
| from src.extractor import ArticleExtractor | |
| from src.analyzer import NewsAnalyzer | |
| from src.aggregator import NewsAggregator | |
| from src.presenter import NewsPresenter | |
| logger = logging.getLogger(__name__) | |
| TRANSLATE_DOMAINS = { | |
| "nawaat.org", "www.nawaat.org", | |
| "tunisienumerique.com", "www.tunisienumerique.com", | |
| "lapresse.tn", "www.lapresse.tn", | |
| "webmanagercenter.com", "www.webmanagercenter.com", | |
| "directinfo.webmanagercenter.com", | |
| "tuniscope.com", "www.tuniscope.com", | |
| } | |
| _translator = None | |
| def _translate_text(text: str, target: str = "en") -> str: | |
| if not text or len(text.strip()) < 3: | |
| return text | |
| global _translator | |
| try: | |
| if _translator is None: | |
| from googletrans import Translator | |
| _translator = Translator() | |
| return _translator.translate(text[:2000], dest=target).text | |
| except Exception: | |
| return text | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="OneNews β RSS news aggregator") | |
| parser.add_argument("--demo", action="store_true", help="Use sample data (no internet)") | |
| parser.add_argument("--source", choices=["feeds", "hn", "reddit"], default=None, | |
| help="Data source (default: RSS feeds)") | |
| parser.add_argument("--models", action="store_true", help="Enable local ML models (slower)") | |
| parser.add_argument("--subreddits", nargs="+", default=None, help="Override subreddits") | |
| parser.add_argument("--limit", type=int, default=None, help="Posts per subreddit") | |
| return parser.parse_args() | |
| def run_pipeline(items: list[NewsItem], cfg: Config, skip_extraction: bool = False): | |
| if not skip_extraction: | |
| logger.info("βββ Extracting article content βββ") | |
| extractor = ArticleExtractor() | |
| for i, item in enumerate(items, 1): | |
| article = extractor.extract(item.post.url) | |
| if article: | |
| logger.info(" [%2d/%d] %-60s β (%s)", i, len(items), item.post.title[:60], article.source_domain) | |
| else: | |
| article = Article( | |
| url=item.post.url, | |
| title=item.post.title, | |
| text=item.post.title, | |
| source_domain=item.post.source_domain or "reddit.com", | |
| extraction_success=False, | |
| image_url=item.post.image_url, | |
| published=item.post.published, | |
| published_iso=item.post.published_iso, | |
| ) | |
| logger.info(" [%2d/%d] %-60s β (title only)", i, len(items), item.post.title[:60]) | |
| item.article = article | |
| else: | |
| logger.info("βββ Extraction skipped (articles already loaded) βββ") | |
| logger.info("βββ Translating Arab/Tunisian articles βββ") | |
| for item in items: | |
| art = item.article | |
| post = item.post | |
| if not art or not post or not post.source_domain: | |
| continue | |
| domain = post.source_domain.lower() | |
| if domain not in TRANSLATE_DOMAINS: | |
| continue | |
| t_title = _translate_text(art.title) | |
| if t_title and t_title != art.title: | |
| logger.info(" Title: %s β %s", art.title[:50], t_title[:50]) | |
| art.title = t_title | |
| t_text = _translate_text(art.text) | |
| if t_text and t_text != art.text: | |
| art.text = t_text | |
| logger.info("βββ Analysing articles βββ") | |
| analyzer = NewsAnalyzer(cfg) | |
| for i, item in enumerate(items, 1): | |
| item.analysis = analyzer.analyze(item.article) | |
| cat = item.analysis.category | |
| topics = ", ".join(item.analysis.topics) if item.analysis.topics else "(none)" | |
| logger.info(" [%2d/%d] %-14s topic: %-30s trust: %s", i, len(items), cat, topics, f"{item.analysis.trustworthiness_score:.0%}") | |
| logger.info("βββ Clustering & ranking βββ") | |
| aggregator = NewsAggregator(cfg) | |
| clusters = aggregator.cluster_news(items) | |
| logger.info(" β %d story clusters found", len(clusters)) | |
| return clusters | |
| def main(): | |
| logging.basicConfig(level=logging.INFO, format="%(levelname).1s %(message)s", stream=sys.stderr) | |
| args = parse_args() | |
| cfg = Config() | |
| if args.models: | |
| cfg.use_local_models = True | |
| logging.getLogger().setLevel(logging.DEBUG) | |
| if args.subreddits: | |
| cfg.news_subreddits = args.subreddits | |
| if args.limit: | |
| cfg.posts_per_subreddit = args.limit | |
| total_start = time.perf_counter() | |
| source = args.source or "feeds" | |
| if args.demo: | |
| from src.mockdata import generate_demo_items | |
| print("βββ Loading demo data βββ") | |
| items = generate_demo_items() | |
| print(f" β {len(items)} sample articles loaded\n") | |
| clusters = run_pipeline(items, cfg, skip_extraction=True) | |
| n_posts = len(items) | |
| elif source == "hn": | |
| from src.hn_scraper import HackerNewsScraper | |
| print("βββ Scraping Hacker News βββ") | |
| scraper = HackerNewsScraper(cfg) | |
| posts = scraper.fetch_posts() | |
| n_posts = len(posts) | |
| print(f" β {n_posts} posts collected\n") | |
| if not posts: | |
| sys.exit(1) | |
| items = [NewsItem(post=p) for p in posts] | |
| clusters = run_pipeline(items, cfg) | |
| elif source == "reddit": | |
| from src.scraper import RedditScraper | |
| print("βββ Scraping Reddit βββ") | |
| scraper = RedditScraper(cfg) | |
| posts = scraper.fetch_posts() | |
| n_posts = len(posts) | |
| print(f" β {n_posts} posts collected\n") | |
| if not posts: | |
| sys.exit(1) | |
| items = [NewsItem(post=p) for p in posts] | |
| clusters = run_pipeline(items, cfg) | |
| else: | |
| from src.rss_feed_scraper import RSSFeedScraper | |
| from src.html_scraper import HTMLSiteScraper | |
| print("βββ Fetching RSS news feeds βββ") | |
| rss = RSSFeedScraper(cfg).fetch_posts() | |
| print(f" β {len(rss)} RSS posts collected") | |
| print("βββ Scraping HTML sites βββ") | |
| html_posts = HTMLSiteScraper(cfg).fetch_posts() | |
| print(f" β {len(html_posts)} HTML posts collected") | |
| posts = rss + html_posts | |
| n_posts = len(posts) | |
| if not posts: | |
| sys.exit(1) | |
| items = [NewsItem(post=p) for p in posts] | |
| clusters = run_pipeline(items, cfg) | |
| elapsed = time.perf_counter() - total_start | |
| print(f"\n Done in {elapsed:.1f}s β {n_posts} posts, {len(clusters)} clusters\n") | |
| if __name__ == "__main__": | |
| main() | |