bep40 commited on
Commit
6915b47
·
verified ·
1 Parent(s): f5d9053

Fix rewrite URL, short JSON error, add missing endpoints, wc2026 timeout

Browse files
Files changed (2) hide show
  1. ai_ext.py +133 -1
  2. app_v2_entry.py +26 -1
ai_ext.py CHANGED
@@ -197,4 +197,136 @@ def _fallback_summary_from_prompt(prompt: str, max_units: int = 6) -> str:
197
  break
198
  if chunks:
199
  return "\n".join("• " + c for c in chunks)
200
- return "• Không có đủ nội dung để tóm tắt."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  break
198
  if chunks:
199
  return "\n".join("• " + c for c in chunks)
200
+ return "• Không có đủ nội dung để tóm tắt."
201
+
202
+ # ===== URL scraping & article processing =====
203
+ HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8"}
204
+
205
+ try:
206
+ _shorts_base = "/data" if os.path.isdir("/data") else os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
207
+ except Exception:
208
+ _shorts_base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
209
+ SHORTS_DIR = os.path.join(_shorts_base, "ai_shorts")
210
+ os.makedirs(SHORTS_DIR, exist_ok=True)
211
+
212
+ import random as _random2
213
+ from datetime import datetime, timezone, timedelta
214
+ _VN_TZ = timezone(timedelta(hours=7))
215
+
216
+
217
+ def _safe_name(filename: str) -> str:
218
+ """Sanitize filename."""
219
+ return re.sub(r"[^a-zA-Z0-9_.-]", "_", filename)[:120]
220
+
221
+
222
+ def pollinations_image_url(topic: str) -> str:
223
+ """Generate a placeholder image URL via Pollinations."""
224
+ try:
225
+ return "https://image.pollinations.ai/prompt/" + quote("Vietnamese editorial illustration, " + topic, safe="") + "?width=1024&height=576&nologo=true"
226
+ except Exception:
227
+ return ""
228
+
229
+
230
+ def _download_image(url: str, fallback_title: str, out_path: str) -> str:
231
+ """Download an image from URL or create a placeholder."""
232
+ if url:
233
+ try:
234
+ r = requests.get(url, headers=HEADERS, timeout=15)
235
+ if r.status_code == 200 and len(r.content) > 1200:
236
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
237
+ with open(out_path, "wb") as f:
238
+ f.write(r.content)
239
+ return out_path
240
+ except Exception:
241
+ pass
242
+ # Fallback: create a placeholder image
243
+ try:
244
+ from PIL import Image, ImageDraw, ImageFont
245
+ img = Image.new("RGB", (1080, 760), (24, 24, 24))
246
+ draw = ImageDraw.Draw(img)
247
+ try:
248
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
249
+ except Exception:
250
+ font = None
251
+ text = (fallback_title or "VNEWS")[:40]
252
+ try:
253
+ bbox = draw.textbbox((0, 0), text, font=font)
254
+ tw = bbox[2] - bbox[0]
255
+ except Exception:
256
+ tw = len(text) * 24
257
+ draw.text(((1080 - tw) // 2, 330), text, fill=(255, 255, 255), font=font)
258
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
259
+ img.save(out_path, quality=90)
260
+ return out_path
261
+ except Exception:
262
+ return out_path
263
+
264
+
265
+ def scrape_any_url(url: str) -> dict:
266
+ """Scrape article content from any URL."""
267
+ if not url or not url.startswith("http"):
268
+ return {"title": "", "text": "", "summary": "", "image": "", "og_image": "", "via": ""}
269
+ try:
270
+ r = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
271
+ if r.status_code != 200 or not r.text:
272
+ return {"title": "", "text": "", "summary": "", "image": "", "og_image": "", "via": _domain(url)}
273
+ r.encoding = "utf-8"
274
+ soup = BeautifulSoup(r.text, "lxml")
275
+ for tag in soup.find_all(["script", "style", "nav", "footer", "aside", "form", "noscript", "iframe", ".ads", ".ad", ".banner-ads", ".fb-comments", ".fb-root", ".social-share", ".related-news", ".breadcrumb"]):
276
+ tag.decompose()
277
+ title = ""
278
+ ogt = soup.find("meta", property="og:title")
279
+ if ogt:
280
+ title = ogt.get("content", "")
281
+ h1 = soup.find("h1")
282
+ if not title and h1:
283
+ title = h1.get_text(strip=True)
284
+ if not title:
285
+ t = soup.find("title")
286
+ if t:
287
+ title = t.get_text(strip=True)
288
+ og_image = ""
289
+ ogi = soup.find("meta", property="og:image")
290
+ if ogi:
291
+ og_image = ogi.get("content", "")
292
+ if og_image.startswith("//"):
293
+ og_image = "https:" + og_image
294
+ summary = ""
295
+ ogd = soup.find("meta", property="og:description") or soup.find("meta", attrs={"name": "description"})
296
+ if ogd:
297
+ summary = ogd.get("content", "")[:500]
298
+ body_text = []
299
+ for sel in ["article", ".singular-content", ".detail-content", ".fck_detail", ".content-detail", ".knc-content", "main", ".cms-body", ".article__body", ".post-content", ".entry-content"]:
300
+ el = soup.select_one(sel)
301
+ if el and len(el.find_all("p")) >= 2:
302
+ for p in el.find_all("p"):
303
+ t = _clean_text(p.get_text(strip=True))
304
+ if t and len(t) > 30:
305
+ body_text.append(t)
306
+ break
307
+ if not body_text and soup.body:
308
+ for p in soup.body.find_all("p"):
309
+ t = _clean_text(p.get_text(strip=True))
310
+ if t and len(t) > 30:
311
+ body_text.append(t)
312
+ text = "\n".join(body_text)
313
+ return {"title": _clean_text(title), "text": text, "summary": _clean_text(summary), "image": og_image, "og_image": og_image, "via": _domain(url), "url": url}
314
+ except Exception as e:
315
+ return {"title": "", "text": "", "summary": "", "image": "", "og_image": "", "via": _domain(url)}
316
+
317
+
318
+ def make_post(title: str, text: str, img: str, url: str, kind: str = "auto", sources: list = None) -> dict:
319
+ """Create a wall post dict."""
320
+ import random as _r2
321
+ now = int(time.time() * 1000)
322
+ return {
323
+ "id": str(now) + str(_r2.randint(100, 999)),
324
+ "title": (title or "Bài viết")[:200],
325
+ "text": (text or "")[:5000],
326
+ "img": img or "",
327
+ "url": url or "",
328
+ "kind": kind or "auto",
329
+ "sources": sources or [],
330
+ "created": now,
331
+ "created_str": datetime.now(_VN_TZ).strftime("%H:%M %d/%m/%Y"),
332
+ }
app_v2_entry.py CHANGED
@@ -226,7 +226,7 @@ _STOP=set('và của các những một được trong với cho tại sau trư
226
  def _has_kw(topic,title):
227
  tl=topic.lower();tt=(title or'').lower()
228
  if tl in tt:return True
229
- words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',tl) if len(w)>2 and w not in _STOP]
230
  if not words:return True
231
  return any(w in tt for w in words)
232
 
@@ -859,6 +859,31 @@ def _save_wall_posts(posts):
859
  with _wl_lock:
860
  _sj(WALL_FILE, posts)
861
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
  @app.get('/api/wall')
863
  def api_wall():
864
  posts = _load_wall_posts()
 
226
  def _has_kw(topic,title):
227
  tl=topic.lower();tt=(title or'').lower()
228
  if tl in tt:return True
229
+ words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',tl) if len(w)>2 and w.lower() not in _STOP]
230
  if not words:return True
231
  return any(w in tt for w in words)
232
 
 
859
  with _wl_lock:
860
  _sj(WALL_FILE, posts)
861
 
862
+ @app.get("/api/ai/wall")
863
+ def api_ai_wall():
864
+ return JSONResponse({"posts": _load_wall_posts()})
865
+
866
+ @app.post("/api/ai/short/{post_id}")
867
+ def api_ai_short(post_id: str):
868
+ """Generate a short video for a wall post.
869
+ Returns clear JSON error when video not available yet.
870
+ """
871
+ post_id_s = str(post_id)
872
+ posts = _load_wall_posts()
873
+ if not isinstance(posts, list):
874
+ posts = []
875
+ post = None
876
+ for p in posts:
877
+ pid = str(p.get("id", ""))
878
+ if pid == post_id_s or pid.startswith(post_id_s):
879
+ post = p
880
+ break
881
+ if not post:
882
+ return JSONResponse({"error": "Không tìm thấy bài viết", "post_id": post_id}, status_code=404)
883
+ if post.get("video"):
884
+ return JSONResponse({"video": post["video"], "post": post})
885
+ return JSONResponse({"error": "Chưa có video cho bài này. Vui lòng upload video trước."}, status_code=409)
886
+
887
  @app.get('/api/wall')
888
  def api_wall():
889
  posts = _load_wall_posts()