Spaces:
Running
Running
Auto scheduler: rewrite AI + short at 7/13/19 VN time from hot topics + latest articles
Browse files- app_v2_entry.py +202 -1
app_v2_entry.py
CHANGED
|
@@ -1282,4 +1282,205 @@ def _bg():
|
|
| 1282 |
time.sleep(90)
|
| 1283 |
threading.Thread(target=_bg,daemon=True).start()
|
| 1284 |
|
| 1285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1282 |
time.sleep(90)
|
| 1283 |
threading.Thread(target=_bg,daemon=True).start()
|
| 1284 |
|
| 1285 |
+
# ===== AUTO SCHEDULER: rewrite AI + short at 7/13/19 VN time =====
|
| 1286 |
+
_AUTO_SCHEDULE_TIMES = [(7, '07:00'), (13, '13:00'), (19, '19:00')]
|
| 1287 |
+
_AUTO_LOG = os.path.join(DATA_DIR, 'auto_rewrite_log.json')
|
| 1288 |
+
|
| 1289 |
+
def _load_auto_log():
|
| 1290 |
+
try:
|
| 1291 |
+
if os.path.exists(_AUTO_LOG):
|
| 1292 |
+
with open(_AUTO_LOG, 'r') as f:
|
| 1293 |
+
return json.load(f)
|
| 1294 |
+
except: pass
|
| 1295 |
+
return {}
|
| 1296 |
+
|
| 1297 |
+
def _save_auto_log(log):
|
| 1298 |
+
try:
|
| 1299 |
+
tmp = _AUTO_LOG + '.tmp'
|
| 1300 |
+
with open(tmp, 'w') as f:
|
| 1301 |
+
json.dump(log, f)
|
| 1302 |
+
os.replace(tmp, _AUTO_LOG)
|
| 1303 |
+
except: pass
|
| 1304 |
+
|
| 1305 |
+
async def _auto_fetch_short(post_id):
|
| 1306 |
+
"""Try to auto-generate a short for a post."""
|
| 1307 |
+
try:
|
| 1308 |
+
import httpx
|
| 1309 |
+
async with httpx.AsyncClient(timeout=180) as cl:
|
| 1310 |
+
r = await cl.post(
|
| 1311 |
+
f"http://localhost:7860/api/ai/short/{post_id}",
|
| 1312 |
+
json={"voice":"vi-VN-HoaiMyNeural","emotion":"neutral","speed":1.2},
|
| 1313 |
+
headers={"Content-Type":"application/json"}
|
| 1314 |
+
)
|
| 1315 |
+
if r.status_code < 300:
|
| 1316 |
+
sj = r.json()
|
| 1317 |
+
if sj.get('video'):
|
| 1318 |
+
posts = _load_wall_posts()
|
| 1319 |
+
for p in posts:
|
| 1320 |
+
if p.get('id') == post_id:
|
| 1321 |
+
p['video'] = sj['video']
|
| 1322 |
+
break
|
| 1323 |
+
_save_wall_posts(posts)
|
| 1324 |
+
return True
|
| 1325 |
+
except: pass
|
| 1326 |
+
return False
|
| 1327 |
+
|
| 1328 |
+
async def _auto_rewrite_one(topic, slot_label):
|
| 1329 |
+
"""Rewrite one topic: find articles, summarize, post to wall, trigger short."""
|
| 1330 |
+
from urllib.parse import quote as _q
|
| 1331 |
+
items = _search_all(topic, limit=6)
|
| 1332 |
+
if not items:
|
| 1333 |
+
# fallback: Google RSS
|
| 1334 |
+
try:
|
| 1335 |
+
r = req.get(f"https://news.google.com/rss/search?q={_q(topic)}&hl=vi&gl=VN&ceid=VN:vi",
|
| 1336 |
+
headers={'User-Agent':'Mozilla/5.0'}, timeout=8)
|
| 1337 |
+
r.encoding = 'utf-8'
|
| 1338 |
+
soup = BeautifulSoup(r.text, 'xml')
|
| 1339 |
+
for it in soup.find_all('item')[:5]:
|
| 1340 |
+
t = _clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 1341 |
+
lk = it.find('link').get_text(strip=True) if it.find('link') else ''
|
| 1342 |
+
if t and lk: items.append({'title':t,'url':lk,'via':'Google News'})
|
| 1343 |
+
except: pass
|
| 1344 |
+
if not items:
|
| 1345 |
+
return False
|
| 1346 |
+
|
| 1347 |
+
item = items[0] # best match
|
| 1348 |
+
url = item.get('url', '')
|
| 1349 |
+
title = item.get('title', topic)
|
| 1350 |
+
if not url.startswith('http'):
|
| 1351 |
+
return False
|
| 1352 |
+
|
| 1353 |
+
data = _scrape_article_for_rewrite(url)
|
| 1354 |
+
if not data or not data.get('paragraphs'):
|
| 1355 |
+
return False
|
| 1356 |
+
|
| 1357 |
+
raw_text = '\n'.join(data['paragraphs'])
|
| 1358 |
+
ai_text = None
|
| 1359 |
+
|
| 1360 |
+
# Try AI generation
|
| 1361 |
+
try:
|
| 1362 |
+
import ai_ext
|
| 1363 |
+
prompt = f"Tóm tắt tin tức (tự động {slot_label}):\nTiêu đề: {data['title']}\n{raw_text[:10000]}\n\n4-6 ý chính dạng bullet. Cuối ghi nguồn."
|
| 1364 |
+
ai_text = await ai_ext.qwen_generate(prompt, max_tokens=1000)
|
| 1365 |
+
except: pass
|
| 1366 |
+
|
| 1367 |
+
if not ai_text or len(ai_text) < 80:
|
| 1368 |
+
pts = data['paragraphs'][:6]
|
| 1369 |
+
ai_text = '\n\n'.join([f"• {p[:300]}" for p in pts])
|
| 1370 |
+
via = item.get('via', '') or urlparse(url).netloc.replace('www.', '')
|
| 1371 |
+
ai_text += f"\n\nNguồn tham khảo: {via}"
|
| 1372 |
+
|
| 1373 |
+
# Build slides
|
| 1374 |
+
images = data.get('images', [])
|
| 1375 |
+
pts = data['paragraphs'][:10]
|
| 1376 |
+
slides = []
|
| 1377 |
+
for i, p in enumerate(pts[:8]):
|
| 1378 |
+
img = images[i] if i < len(images) else (images[-1] if images else data.get('og_img', ''))
|
| 1379 |
+
slides.append({'text': p[:300], 'image': img, 'index': i + 1})
|
| 1380 |
+
|
| 1381 |
+
post_id = str(int(time.time() * 1000)) + str(random.randint(100, 999))
|
| 1382 |
+
post = {
|
| 1383 |
+
"id": post_id, "title": data.get('title', title)[:200],
|
| 1384 |
+
"text": ai_text, "img": images[0] if images else data.get('og_img', ''),
|
| 1385 |
+
"url": url, "kind": "auto_rewrite", "slides": slides,
|
| 1386 |
+
"images": images[:10], "video": "",
|
| 1387 |
+
"voice": "vi-VN-HoaiMyNeural", "emotion": "neutral",
|
| 1388 |
+
"language": "vietnamese", "ts": int(time.time()),
|
| 1389 |
+
"auto_scheduled": True, "slot": slot_label,
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
posts = _load_wall_posts()
|
| 1393 |
+
posts.insert(0, post)
|
| 1394 |
+
_save_wall_posts(posts)
|
| 1395 |
+
|
| 1396 |
+
# Trigger short generation async
|
| 1397 |
+
threading.Thread(target=lambda: asyncio.run(_auto_fetch_short(post_id)), daemon=True).start()
|
| 1398 |
+
return True
|
| 1399 |
+
|
| 1400 |
+
async def _do_scheduled_run(slot_label):
|
| 1401 |
+
"""Main scheduled run: top 3 hot topics + latest articles."""
|
| 1402 |
+
print(f"[auto] Starting scheduled rewrite for {slot_label}")
|
| 1403 |
+
|
| 1404 |
+
# 1) Get top 3 hot topics
|
| 1405 |
+
topics = _get_hot_topics()[:3]
|
| 1406 |
+
job_topics = [t['topic'] for t in topics if t.get('topic')]
|
| 1407 |
+
|
| 1408 |
+
# 2) Add latest articles from RSS
|
| 1409 |
+
for feed_url in ['https://vnexpress.net/rss/tin-moi-nhat.rss',
|
| 1410 |
+
'https://dantri.com.vn/rss/home.rss',
|
| 1411 |
+
'https://vietnamnet.vn/rss/tin-moi-nhat.rss']:
|
| 1412 |
+
try:
|
| 1413 |
+
r = req.get(feed_url, headers={'User-Agent':'Mozilla/5.0'}, timeout=5)
|
| 1414 |
+
r.encoding = 'utf-8'
|
| 1415 |
+
soup = BeautifulSoup(r.text, 'xml')
|
| 1416 |
+
for it in soup.find_all('item')[:3]:
|
| 1417 |
+
t = _clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 1418 |
+
if t and len(t) > 10 and t[:50] not in ' '.join(job_topics):
|
| 1419 |
+
job_topics.append(t[:60])
|
| 1420 |
+
except: pass
|
| 1421 |
+
|
| 1422 |
+
# 3) Execute jobs
|
| 1423 |
+
results = []
|
| 1424 |
+
for jt in job_topics[:6]:
|
| 1425 |
+
try:
|
| 1426 |
+
ok = await asyncio.wait_for(_auto_rewrite_one(jt, slot_label), timeout=90)
|
| 1427 |
+
results.append((jt, ok))
|
| 1428 |
+
except: results.append((jt, False))
|
| 1429 |
+
await asyncio.sleep(2)
|
| 1430 |
+
|
| 1431 |
+
# Log
|
| 1432 |
+
from datetime import datetime, timezone, timedelta
|
| 1433 |
+
VN_TZ_SCHED = timezone(timedelta(hours=7))
|
| 1434 |
+
today_str = datetime.now(VN_TZ_SCHED).strftime('%Y-%m-%d')
|
| 1435 |
+
log = _load_auto_log()
|
| 1436 |
+
if today_str not in log: log[today_str] = {}
|
| 1437 |
+
log[today_str][slot_label] = {
|
| 1438 |
+
'time': datetime.now(VN_TZ_SCHED).strftime('%H:%M:%S'),
|
| 1439 |
+
'count': sum(1 for _, ok in results if ok),
|
| 1440 |
+
'total': len(results),
|
| 1441 |
+
}
|
| 1442 |
+
_save_auto_log(log)
|
| 1443 |
+
print(f"[auto] Done {slot_label}: {sum(1 for _, ok in results if ok)}/{len(results)} posts")
|
| 1444 |
+
|
| 1445 |
+
def _scheduler_loop():
|
| 1446 |
+
"""Check every 60s; trigger at 7:00, 13:00, 19:00 VN time."""
|
| 1447 |
+
time.sleep(35)
|
| 1448 |
+
from datetime import datetime, timezone, timedelta
|
| 1449 |
+
VN_TZ_SCHED = timezone(timedelta(hours=7))
|
| 1450 |
+
|
| 1451 |
+
_last_run_date = ""
|
| 1452 |
+
_last_run_slots = set()
|
| 1453 |
+
|
| 1454 |
+
while True:
|
| 1455 |
+
try:
|
| 1456 |
+
now = datetime.now(VN_TZ_SCHED)
|
| 1457 |
+
today = now.strftime('%Y-%m-%d')
|
| 1458 |
+
hour = now.hour
|
| 1459 |
+
minute = now.minute
|
| 1460 |
+
|
| 1461 |
+
if today != _last_run_date:
|
| 1462 |
+
_last_run_date = today
|
| 1463 |
+
_last_run_slots = set()
|
| 1464 |
+
|
| 1465 |
+
slot = None
|
| 1466 |
+
for h, label in _AUTO_SCHEDULE_TIMES:
|
| 1467 |
+
if hour == h and 0 <= minute < 5:
|
| 1468 |
+
slot = label
|
| 1469 |
+
break
|
| 1470 |
+
|
| 1471 |
+
if slot and slot not in _last_run_slots:
|
| 1472 |
+
_last_run_slots.add(slot)
|
| 1473 |
+
loop = asyncio.new_event_loop()
|
| 1474 |
+
asyncio.set_event_loop(loop)
|
| 1475 |
+
try:
|
| 1476 |
+
loop.run_until_complete(_do_scheduled_run(slot))
|
| 1477 |
+
finally:
|
| 1478 |
+
loop.close()
|
| 1479 |
+
except Exception as e:
|
| 1480 |
+
print(f"[auto] Loop error: {e}")
|
| 1481 |
+
|
| 1482 |
+
time.sleep(60)
|
| 1483 |
+
|
| 1484 |
+
threading.Thread(target=_scheduler_loop, daemon=True, name='auto-rewrite-scheduler').start()
|
| 1485 |
+
|
| 1486 |
+
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|