bep40 commited on
Commit
4969f05
·
verified ·
1 Parent(s): dcee409

Delete auto_poster.py

Browse files
Files changed (1) hide show
  1. auto_poster.py +0 -319
auto_poster.py DELETED
@@ -1,319 +0,0 @@
1
- """
2
- VNEWS Auto Poster — Tự động đăng bài rewrite và short lên Tường AI.
3
-
4
- Chạy nền trong runtime ai_patch:app (được app_v2_entry import). Mỗi ngày vào
5
- các khung giờ 7h, 13h, 19h (GIỜ VIỆT NAM, UTC+7 — Space chạy UTC nên tự cộng
6
- bù múi giờ):
7
- 1. Chọn một chủ đề nóng từ RSS báo Việt Nam.
8
- 2. Lấy bài nguồn, tóm tắt bằng AI (bài rewrite) và lưu lên Tường AI.
9
- 3. Tự động sinh video short (ffmpeg + edge-tts) cho bài vừa tạo.
10
-
11
- Tận dụng lại toàn bộ logic có sẵn trong ai_patch.py / ai_ext.py để đảm bảo
12
- định dạng bài và short giống hệt khi người dùng bấm thủ công.
13
- """
14
- import os
15
- import re
16
- import time
17
- import json
18
- import html as html_lib
19
- import threading
20
- import asyncio
21
- import traceback
22
-
23
- import ai_ext as base
24
- import ai_patch
25
-
26
- from urllib.parse import quote_plus
27
- from bs4 import BeautifulSoup
28
-
29
- UA = {
30
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
31
- "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
32
- }
33
-
34
- # Khung giờ đăng tự động THEO GIỜ VIỆT NAM (UTC+7).
35
- SCHEDULE_HOURS = [7, 13, 19]
36
- SCHEDULE_MINUTE = 5 # đăng lệch 5 phút để tránh chạm giờ làm tròn
37
- TZ_OFFSET = 7 # múi giờ Việt Nam so với UTC (Space chạy UTC)
38
-
39
-
40
- def _vn_now():
41
- """Trả về struct_time theo giờ Việt Nam (UTC+7)."""
42
- return time.gmtime(time.time() + TZ_OFFSET * 3600)
43
-
44
-
45
- # RSS nóng từ các báo VN (dùng để chọn chủ đề).
46
- VN_HOT_RSS = [
47
- ("VnExpress", "https://vnexpress.net/rss/tin-moi-nhat.rss"),
48
- ("Dân trí", "https://dantri.com.vn/rss/home.rss"),
49
- ("Vietnamnet", "https://vietnamnet.vn/rss/tin-moi-nhat.rss"),
50
- ("Thanh Niên", "https://thanhnien.vn/rss/home.rss"),
51
- ("Tuổi Trẻ", "https://tuoitre.vn/rss/tin-moi-nhat.rss"),
52
- ("VnExpress Thời sự", "https://vnexpress.net/rss/thoi-su.rss"),
53
- ("VnExpress Thế giới", "https://vnexpress.net/rss/the-gioi.rss"),
54
- ("VnExpress Kinh doanh", "https://vnexpress.net/rss/kinh-doanh.rss"),
55
- ("VnExpress Công nghệ", "https://vnexpress.net/rss/so-hoa.rss"),
56
- ("VnExpress Thể thao", "https://vnexpress.net/rss/the-thao.rss"),
57
- ]
58
-
59
- _STOP = set(
60
- "và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để".split()
61
- )
62
-
63
- _lock = threading.Lock()
64
- _state = {
65
- "running": False,
66
- "thread": None,
67
- "last_run": 0, # epoch của lần chạy gần nhất
68
- "last_slot": "", # slot "YYYY-MM-DD-HH" (giờ VN) đã chạy gần nhất
69
- "runs": [], # lịch sử 20 lần chạy gần nhất
70
- "errors": [], # lịch sử lỗi gần nhất
71
- }
72
-
73
-
74
- def _log(kind, msg, extra=None):
75
- entry = {"ts": int(time.time()), "kind": kind, "msg": msg, "extra": extra}
76
- with _lock:
77
- if kind == "error":
78
- _state["errors"].insert(0, entry)
79
- _state["errors"] = _state["errors"][:20]
80
- else:
81
- _state["runs"].insert(0, entry)
82
- _state["runs"] = _state["runs"][:20]
83
- print(f"[auto_poster:{kind}] {msg}" + (f" :: {extra}" if extra else ""))
84
-
85
-
86
- # ============================================================
87
- # CHỌN CHỦ ĐỀ NÓNG
88
- # ============================================================
89
- def _clean(s):
90
- return re.sub(r"\s+", " ", html_lib.unescape(str(s or ""))).strip()
91
-
92
-
93
- def _hot_candidates(limit=24):
94
- """Trả về list chủ đề nóng dạng {'topic':..., 'label':...} từ RSS VN."""
95
- freq = {}
96
- display = {}
97
- try:
98
- for name, url in VN_HOT_RSS:
99
- try:
100
- r = base.requests.get(url, headers=UA, timeout=6)
101
- r.encoding = "utf-8"
102
- soup = BeautifulSoup(r.text, "xml")
103
- for it in soup.find_all("item")[:12]:
104
- title = _clean(it.find("title").get_text(" ", strip=True) if it.find("title") else "")
105
- if not title:
106
- continue
107
- title = re.sub(r"\s*[-|].*$", "", title)
108
- words = [w for w in re.findall(r"[A-Za-zÀ-ỹ0-9]+", title)
109
- if len(w) > 2 and w.lower() not in _STOP]
110
- if len(words) < 2:
111
- continue
112
- for n in (3, 4, 2):
113
- for i in range(max(0, len(words) - n + 1)):
114
- phrase = " ".join(words[i:i + n])
115
- if 8 <= len(phrase) <= 50:
116
- key = phrase.lower()
117
- freq[key] = freq.get(key, 0) + 1
118
- display[key] = phrase
119
- except Exception as e:
120
- _log("warn", f"RSS {name} lỗi: {e}")
121
- except Exception as e:
122
- _log("error", "hot_candidates fail", str(e)[:200])
123
- ranked = sorted(freq.items(), key=lambda x: x[1], reverse=True)
124
- out = []
125
- seen = set()
126
- for key, _ in ranked:
127
- if key in seen:
128
- continue
129
- seen.add(key)
130
- out.append({"topic": display[key], "label": "#" + re.sub(r"\s+", "", display[key].title())})
131
- if len(out) >= limit:
132
- break
133
- # Fallback chủ đề VN cố định nếu RSS trống.
134
- for kw in ["World Cup 2026", "Kinh tế Việt Nam", "Bóng đá Việt Nam", "Công nghệ AI", "Giá vàng", "Thời tiết"]:
135
- if len(out) >= 24:
136
- break
137
- if kw.lower() not in seen:
138
- seen.add(kw.lower())
139
- out.append({"topic": kw, "label": "#" + re.sub(r"\s+", "", kw.title())})
140
- return out
141
-
142
-
143
- def pick_topic():
144
- """Chọn một chủ đề chưa được tự động đăng gần đây."""
145
- cands = _hot_candidates()
146
- if not cands:
147
- return "Tin tức Việt Nam hôm nay"
148
- recent_titles = set()
149
- with _lock:
150
- for run in _state["runs"][:12]:
151
- extra = run.get("extra") or {}
152
- if extra.get("topic"):
153
- recent_titles.add(extra["topic"].lower())
154
- # Ưu tiên chủ đề chưa đăng gần đây.
155
- for c in cands:
156
- if c["topic"].lower() not in recent_titles:
157
- return c["topic"]
158
- return cands[0]["topic"]
159
-
160
-
161
- # ============================================================
162
- # TẠO BÀI REWRITE (tái sử dụng logic ai_patch)
163
- # ============================================================
164
- async def _create_rewrite_post(topic, limit=4):
165
- """Tạo 1 bài rewrite từ chủ đề, trả về post dict (đã lưu wall)."""
166
- articles = ai_patch._topic_source_articles(topic, limit=limit)
167
- if not articles:
168
- raise RuntimeError("Không lấy được bài nguồn cho chủ đề: " + topic)
169
- art = articles[0] # 1 bài/đợt để Tường AI không bị ngập
170
- prompt = f"""Tóm tắt RIÊNG bài viết nguồn sau để đăng Tường AI.
171
-
172
- Chủ đề lọc: {topic}
173
- Tiêu đề bài nguồn: {art['title']}
174
- Nguồn: {art['via']}
175
-
176
- Yêu cầu bắt buộc:
177
- - Tóm tắt nội dung trong BÀI VIẾT này, không chỉ tiêu đề.
178
- - Không trộn với bài khác.
179
- - Không viết lại toàn bộ bài.
180
- - Không lặp ý.
181
- - 4-6 gạch đầu dòng, mỗi dòng 1 câu rõ ràng.
182
- - Giữ số liệu/nhân vật/thời điểm quan trọng nếu có.
183
-
184
- Nội dung bài:
185
- {art['raw'][:14000]}"""
186
- text = await base.qwen_generate(prompt, image_url=art.get("image") or None, max_tokens=1500)
187
- text = ai_patch._postprocess_ai_text(text, max_units=20)
188
- src = [art["source"]]
189
- if "Nguồn tham khảo:" not in text:
190
- text += "\n\n" + ai_patch._source_line(src)
191
- post = base.make_post(
192
- art["title"],
193
- text,
194
- art.get("image") or base.pollinations_image_url(art["title"]),
195
- art.get("url") or "",
196
- "auto_rewrite",
197
- sources=src,
198
- )
199
- # Slides để hiển thị sau reload.
200
- try:
201
- page_data = ai_patch._scrape_article_images(art.get("url", ""))
202
- if page_data and page_data.get("paragraphs"):
203
- key_points = ai_patch._extract_key_points_for_slides(page_data["paragraphs"], max_points=12)
204
- if key_points:
205
- relevant_imgs = page_data.get("images", []) or ([page_data["og_img"]] if page_data.get("og_img") else [])
206
- slides = []
207
- for i, point in enumerate(key_points):
208
- img = relevant_imgs[i] if i < len(relevant_imgs) else (relevant_imgs[-1] if relevant_imgs else "")
209
- slides.append({"text": point, "image": img, "index": i + 1})
210
- post["slides"] = slides
211
- except Exception:
212
- pass
213
- posts = base._load_ai_wall()
214
- posts.insert(0, post)
215
- base._save_ai_wall(posts)
216
- return post
217
-
218
-
219
- # ============================================================
220
- # SINH SHORT (tái sử dụng logic video của ai_patch)
221
- # ============================================================
222
- def _make_short_for_post(post_id, voice="nu", emotion="neutral", speed=1.2):
223
- """Sinh video short cho post (dùng lại hàm đã tách từ route /api/ai/short)."""
224
- if hasattr(ai_patch, "_generate_short_for_post"):
225
- return ai_patch._generate_short_for_post(post_id, voice=voice, emotion=emotion, speed=speed)
226
- raise RuntimeError("thiếu ai_patch._generate_short_for_post")
227
-
228
-
229
- # ============================================================
230
- # MỘT CHU KỲ CHẠY
231
- # ============================================================
232
- def run_once(topic=None, with_short=True):
233
- """Chạy 1 chu kỳ: tạo rewrite + (tùy chọn) sinh short. Trả về dict kết quả."""
234
- topic = topic or pick_topic()
235
- try:
236
- post = asyncio.run(_create_rewrite_post(topic, limit=4))
237
- pid = str(post.get("id"))
238
- out = {"topic": topic, "post_id": pid, "title": post.get("title"), "short": None}
239
- if with_short and base.gTTS is not None:
240
- try:
241
- res = _make_short_for_post(pid, voice="nu", emotion="neutral", speed=1.2)
242
- out["short"] = res.get("video") if isinstance(res, dict) else str(res)
243
- except Exception as e:
244
- _log("error", "short gen fail", f"{pid}: {e}")
245
- out["short_error"] = str(e)[:200]
246
- _log("run", f"Đã đăng tự động: {topic}", {"topic": topic, "post_id": pid, "short": bool(out.get("short"))})
247
- with _lock:
248
- _state["last_run"] = int(time.time())
249
- return {"ok": True, **out}
250
- except Exception as e:
251
- _log("error", "run_once fail", f"{topic}: {e}\n{traceback.format_exc()[-600:]}")
252
- with _lock:
253
- _state["errors"].insert(0, {"ts": int(time.time()), "kind": "error", "msg": str(e)[:300]})
254
- _state["errors"] = _state["errors"][:20]
255
- return {"ok": False, "topic": topic, "error": str(e)[:300]}
256
-
257
-
258
- # ============================================================
259
- # SCHEDULER LOOP (theo giờ Việt Nam UTC+7)
260
- # ============================================================
261
- def _slot_key():
262
- t = _vn_now()
263
- return f"{t.tm_year:04d}-{t.tm_mon:02d}-{t.tm_mday:02d}-{t.tm_hour:02d}"
264
-
265
-
266
- def _should_fire():
267
- t = _vn_now()
268
- return t.tm_hour in SCHEDULE_HOURS and t.tm_min >= SCHEDULE_MINUTE
269
-
270
-
271
- def _loop():
272
- _log("start", "Auto poster scheduler started", {"hours": SCHEDULE_HOURS, "tz": f"UTC+{TZ_OFFSET}"})
273
- while True:
274
- try:
275
- if _should_fire():
276
- slot = _slot_key()
277
- with _lock:
278
- fired = _state.get("last_slot") == slot
279
- if not fired:
280
- try:
281
- run_once()
282
- except Exception as e:
283
- _log("error", "scheduler cycle failed", str(e)[:300])
284
- with _lock:
285
- _state["last_slot"] = slot
286
- else:
287
- # reset slot khi ra khỏi khung giờ để lần sau bắn lại
288
- slot = _slot_key()
289
- with _lock:
290
- if _state.get("last_slot") and _state["last_slot"] != slot:
291
- _state["last_slot"] = ""
292
- except Exception as e:
293
- _log("error", "loop error", str(e)[:200])
294
- time.sleep(55)
295
-
296
-
297
- def start():
298
- with _lock:
299
- if _state["running"] and _state["thread"] and _state["thread"].is_alive():
300
- return False
301
- _state["running"] = True
302
- _state["thread"] = threading.Thread(target=_loop, name="vnews-auto-poster", daemon=True)
303
- _state["thread"].start()
304
- _log("start", "Auto poster thread launched")
305
- return True
306
-
307
-
308
- def status():
309
- with _lock:
310
- return {
311
- "running": bool(_state["running"] and _state["thread"] and _state["thread"].is_alive()),
312
- "schedule_hours": SCHEDULE_HOURS,
313
- "schedule_minute": SCHEDULE_MINUTE,
314
- "tz_offset": TZ_OFFSET,
315
- "last_run": _state["last_run"],
316
- "last_slot": _state["last_slot"],
317
- "runs": _state["runs"][:10],
318
- "errors": _state["errors"][:5],
319
- }