Spaces:
Running
Running
Upload app_v2_entry.py with huggingface_hub
Browse files- app_v2_entry.py +69 -31
app_v2_entry.py
CHANGED
|
@@ -26,6 +26,7 @@ import asyncio
|
|
| 26 |
HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
|
| 27 |
|
| 28 |
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
|
|
|
| 29 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
|
| 30 |
app.routes[:]=[r for r in app.routes if not isinstance(r, Mount)]
|
| 31 |
app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
|
|
@@ -328,7 +329,7 @@ def _search_all(topic,limit=36):
|
|
| 328 |
if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
|
| 329 |
return out[:limit]
|
| 330 |
|
| 331 |
-
for _path in ['/api/article', '/api/hot_topics', '/api/categories', '/api/storage_status'
|
| 332 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)==_path and 'GET' in getattr(r,'methods',set()))]
|
| 333 |
|
| 334 |
_article_cache = {}
|
|
@@ -504,8 +505,44 @@ def _ht(topic:str=Query(...),page:int=Query(default=0)):
|
|
| 504 |
def _cat():return JSONResponse([])
|
| 505 |
@app.get('/api/storage_status')
|
| 506 |
def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
@app.get('/s')
|
| 508 |
-
async def _sh(url:str='',title:str='',img:str=''):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
from wc2026_scraper import scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail
|
| 511 |
|
|
@@ -1075,7 +1112,7 @@ def _scrape_article_for_rewrite(url):
|
|
| 1075 |
|
| 1076 |
|
| 1077 |
def _extract_key_points_rw(paragraphs, max_points=5):
|
| 1078 |
-
"""Extract key points from paragraphs - extracts ALL sentences, not just first one.
|
| 1079 |
|
| 1080 |
Fixes: Original regex `^(.+?[.!?])\s` only captured first sentence per paragraph.
|
| 1081 |
Now splits on all sentence boundaries and takes valid sentences until max_points.
|
|
@@ -1326,32 +1363,23 @@ async def _auto_fetch_short(post_id):
|
|
| 1326 |
except: pass
|
| 1327 |
return False
|
| 1328 |
|
| 1329 |
-
async def _auto_rewrite_one(topic, slot_label, used_urls=None):
|
| 1330 |
"""Rewrite one topic: find articles, summarize, post to wall, trigger short.
|
| 1331 |
-
used_urls: shared set to avoid duplicate articles across topics.
|
|
|
|
| 1332 |
from urllib.parse import quote as _q
|
| 1333 |
-
items
|
|
|
|
| 1334 |
# Skip URLs already used by another topic
|
| 1335 |
if used_urls is not None:
|
| 1336 |
filtered = [it for it in items if it.get('url') not in used_urls]
|
| 1337 |
if filtered:
|
| 1338 |
items = filtered
|
| 1339 |
-
if not items:
|
| 1340 |
-
# fallback: Google RSS
|
| 1341 |
-
try:
|
| 1342 |
-
r = req.get(f"https://news.google.com/rss/search?q={_q(topic)}&hl=vi&gl=VN&ceid=VN:vi",
|
| 1343 |
-
headers={'User-Agent':'Mozilla/5.0'}, timeout=8)
|
| 1344 |
-
r.encoding = 'utf-8'
|
| 1345 |
-
soup = BeautifulSoup(r.text, 'xml')
|
| 1346 |
-
for it in soup.find_all('item')[:5]:
|
| 1347 |
-
t = _clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 1348 |
-
lk = it.find('link').get_text(strip=True) if it.find('link') else ''
|
| 1349 |
-
if t and lk: items.append({'title':t,'url':lk,'via':'Google News'})
|
| 1350 |
-
except: pass
|
| 1351 |
-
if not items:
|
| 1352 |
return False
|
| 1353 |
|
| 1354 |
-
|
|
|
|
| 1355 |
url = item.get('url', '')
|
| 1356 |
title = item.get('title', topic)
|
| 1357 |
if url and used_urls is not None:
|
|
@@ -1407,7 +1435,7 @@ async def _auto_rewrite_one(topic, slot_label, used_urls=None):
|
|
| 1407 |
return True
|
| 1408 |
|
| 1409 |
async def _do_scheduled_run(slot_label):
|
| 1410 |
-
"""Main scheduled run:
|
| 1411 |
print(f"[auto] Starting scheduled rewrite for {slot_label}")
|
| 1412 |
|
| 1413 |
# Get top hot topics, skip duplicates
|
|
@@ -1440,14 +1468,25 @@ async def _do_scheduled_run(slot_label):
|
|
| 1440 |
_used_urls = set()
|
| 1441 |
results = []
|
| 1442 |
|
|
|
|
| 1443 |
for jt in job_topics:
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1451 |
|
| 1452 |
# Log
|
| 1453 |
from datetime import datetime, timezone, timedelta
|
|
@@ -1457,11 +1496,10 @@ async def _do_scheduled_run(slot_label):
|
|
| 1457 |
if today_str not in log: log[today_str] = {}
|
| 1458 |
log[today_str][slot_label] = {
|
| 1459 |
'time': datetime.now(VN_TZ_SCHED).strftime('%H:%M:%S'),
|
| 1460 |
-
'count':
|
| 1461 |
-
'total': len(
|
| 1462 |
}
|
| 1463 |
_save_auto_log(log)
|
| 1464 |
-
print(f"[auto] Done {slot_label}: {sum(1 for _, ok in results if ok)}/{len(results)} posts")
|
| 1465 |
|
| 1466 |
def _scheduler_loop():
|
| 1467 |
"""Check every 60s; trigger at 7:00, 13:00, 19:00 VN time.
|
|
|
|
| 26 |
HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
|
| 27 |
|
| 28 |
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
| 29 |
+
SPACE = "https://bep40-vnews.hf.space" # SEO URL base for share links
|
| 30 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
|
| 31 |
app.routes[:]=[r for r in app.routes if not isinstance(r, Mount)]
|
| 32 |
app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
|
|
|
|
| 329 |
if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
|
| 330 |
return out[:limit]
|
| 331 |
|
| 332 |
+
for _path in ['/api/article', '/api/hot_topics', '/api/categories', '/api/storage_status']:
|
| 333 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)==_path and 'GET' in getattr(r,'methods',set()))]
|
| 334 |
|
| 335 |
_article_cache = {}
|
|
|
|
| 505 |
def _cat():return JSONResponse([])
|
| 506 |
@app.get('/api/storage_status')
|
| 507 |
def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
|
| 508 |
+
@app.get('/s/{slug}')
|
| 509 |
+
async def _sh_slug(slug: str, url: str = '', title: str = '', img: str = ''):
|
| 510 |
+
"""SEO-friendly share endpoint with slug in URL path."""
|
| 511 |
+
safe_title = _clean(title) if title else 'VNEWS - Tin tức'
|
| 512 |
+
safe_img = _clean(img) if img else ''
|
| 513 |
+
safe_url = _clean(url) if url else '/'
|
| 514 |
+
return HTMLResponse(f'''<!DOCTYPE html>
|
| 515 |
+
<html lang="vi">
|
| 516 |
+
<head>
|
| 517 |
+
<meta charset="utf-8">
|
| 518 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 519 |
+
<title>{safe_title}</title>
|
| 520 |
+
<meta property="og:title" content="{safe_title}">
|
| 521 |
+
<meta property="og:image" content="{safe_img}">
|
| 522 |
+
<meta property="og:description" content="Tin tức tóm tắt, AI rewrite, World Cup 2026">
|
| 523 |
+
<meta property="og:url" content="{SPACE}/s/{slug}">
|
| 524 |
+
<link rel="canonical" href="{SPACE}/s/{slug}">
|
| 525 |
+
<meta http-equiv="refresh" content="0;url={safe_url}">
|
| 526 |
+
</head><body></body></html>''')
|
| 527 |
+
|
| 528 |
@app.get('/s')
|
| 529 |
+
async def _sh(url:str='',title:str='',img:str=''):
|
| 530 |
+
safe_title = _clean(title) if title else 'VNEWS - Tin tức'
|
| 531 |
+
safe_img = _clean(img) if img else ''
|
| 532 |
+
safe_url = _clean(url) if url else '/'
|
| 533 |
+
return HTMLResponse(f'''<!DOCTYPE html>
|
| 534 |
+
<html lang="vi">
|
| 535 |
+
<head>
|
| 536 |
+
<meta charset="utf-8">
|
| 537 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 538 |
+
<title>{safe_title}</title>
|
| 539 |
+
<meta property="og:title" content="{safe_title}">
|
| 540 |
+
<meta property="og:image" content="{safe_img}">
|
| 541 |
+
<meta property="og:description" content="Tin tức tóm tắt, AI rewrite, World Cup 2026">
|
| 542 |
+
<meta property="og:url" content="{SPACE}/s">
|
| 543 |
+
<link rel="canonical" href="{SPACE}/s">
|
| 544 |
+
<meta http-equiv="refresh" content="0;url={safe_url}">
|
| 545 |
+
</head><body></body></html>''')
|
| 546 |
|
| 547 |
from wc2026_scraper import scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail
|
| 548 |
|
|
|
|
| 1112 |
|
| 1113 |
|
| 1114 |
def _extract_key_points_rw(paragraphs, max_points=5):
|
| 1115 |
+
r"""Extract key points from paragraphs - extracts ALL sentences, not just first one.
|
| 1116 |
|
| 1117 |
Fixes: Original regex `^(.+?[.!?])\s` only captured first sentence per paragraph.
|
| 1118 |
Now splits on all sentence boundaries and takes valid sentences until max_points.
|
|
|
|
| 1363 |
except: pass
|
| 1364 |
return False
|
| 1365 |
|
| 1366 |
+
async def _auto_rewrite_one(topic, slot_label, used_urls=None, post_index=0):
|
| 1367 |
"""Rewrite one topic: find articles, summarize, post to wall, trigger short.
|
| 1368 |
+
used_urls: shared set to avoid duplicate articles across topics.
|
| 1369 |
+
post_index: 0-based index to create multiple posts per topic (0,1,2 = up to 3 posts)."""
|
| 1370 |
from urllib.parse import quote as _q
|
| 1371 |
+
# Get MORE items to support 1-3 posts per topic
|
| 1372 |
+
items = _search_all(topic, limit=12)
|
| 1373 |
# Skip URLs already used by another topic
|
| 1374 |
if used_urls is not None:
|
| 1375 |
filtered = [it for it in items if it.get('url') not in used_urls]
|
| 1376 |
if filtered:
|
| 1377 |
items = filtered
|
| 1378 |
+
if not items or post_index >= len(items):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1379 |
return False
|
| 1380 |
|
| 1381 |
+
# Get article at post_index (0,1,2 for multiple posts)
|
| 1382 |
+
item = items[post_index] # post_index allows multiple articles per topic
|
| 1383 |
url = item.get('url', '')
|
| 1384 |
title = item.get('title', topic)
|
| 1385 |
if url and used_urls is not None:
|
|
|
|
| 1435 |
return True
|
| 1436 |
|
| 1437 |
async def _do_scheduled_run(slot_label):
|
| 1438 |
+
"""Main scheduled run: 1-3 posts from 3 different HOT topics (3-9 total), no duplicates."""
|
| 1439 |
print(f"[auto] Starting scheduled rewrite for {slot_label}")
|
| 1440 |
|
| 1441 |
# Get top hot topics, skip duplicates
|
|
|
|
| 1468 |
_used_urls = set()
|
| 1469 |
results = []
|
| 1470 |
|
| 1471 |
+
# Process each topic, create 1-3 posts per topic
|
| 1472 |
for jt in job_topics:
|
| 1473 |
+
for post_idx in range(3): # Try up to 3 posts per topic
|
| 1474 |
+
try:
|
| 1475 |
+
ok = await asyncio.wait_for(_auto_rewrite_one(jt, slot_label, _used_urls, post_idx), timeout=120)
|
| 1476 |
+
if ok:
|
| 1477 |
+
results.append((jt, post_idx, True))
|
| 1478 |
+
print(f"[auto] Created post {post_idx+1} for '{jt}'")
|
| 1479 |
+
else:
|
| 1480 |
+
# No more articles for this topic
|
| 1481 |
+
break
|
| 1482 |
+
except Exception as e:
|
| 1483 |
+
print(f"[auto] Error on '{jt}' post {post_idx}: {e}")
|
| 1484 |
+
results.append((jt, post_idx, False))
|
| 1485 |
+
await asyncio.sleep(1) # Small delay between posts
|
| 1486 |
+
|
| 1487 |
+
# Ensure at least 3 posts total (fallback if needed)
|
| 1488 |
+
successful_posts = sum(1 for _, _, ok in results if ok)
|
| 1489 |
+
print(f"[auto] Done {slot_label}: {successful_posts} posts created")
|
| 1490 |
|
| 1491 |
# Log
|
| 1492 |
from datetime import datetime, timezone, timedelta
|
|
|
|
| 1496 |
if today_str not in log: log[today_str] = {}
|
| 1497 |
log[today_str][slot_label] = {
|
| 1498 |
'time': datetime.now(VN_TZ_SCHED).strftime('%H:%M:%S'),
|
| 1499 |
+
'count': successful_posts,
|
| 1500 |
+
'total': len(job_topics),
|
| 1501 |
}
|
| 1502 |
_save_auto_log(log)
|
|
|
|
| 1503 |
|
| 1504 |
def _scheduler_loop():
|
| 1505 |
"""Check every 60s; trigger at 7:00, 13:00, 19:00 VN time.
|