bep40 commited on
Commit
2244b3e
·
1 Parent(s): 67a020a

Fix rewrite slide storage and JSON error handling (#46)

Browse files

- Fix rewrite slide storage and JSON error handling (c80226615706151e20325d12881a6a8763664df2)

Files changed (1) hide show
  1. rewrite_slide.py +62 -33
rewrite_slide.py CHANGED
@@ -2,63 +2,88 @@
2
  from main import app
3
  from fastapi import Request
4
  from fastapi.responses import JSONResponse
5
- import requests, re, time, random, json, os
6
  from bs4 import BeautifulSoup
7
- from urllib.parse import quote
8
 
9
  UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language': 'vi-VN,vi;q=0.9'}
10
 
11
- try:
12
- from main import _load_wall, _save_wall
13
- except:
14
- _data_dir = "/data" if os.path.isdir("/data") else "/app/data"
15
- _wall_file = os.path.join(_data_dir, "wall_posts.json")
16
- def _load_wall():
17
- try:
18
- if os.path.exists(_wall_file):
19
- with open(_wall_file, 'r', encoding='utf-8') as f: return json.load(f)
20
- except: pass
21
- return []
22
- def _save_wall(posts):
23
- try:
24
- os.makedirs(os.path.dirname(_wall_file), exist_ok=True)
25
- with open(_wall_file+'.tmp', 'w', encoding='utf-8') as f: json.dump(posts[:100], f, ensure_ascii=False)
26
- os.replace(_wall_file+'.tmp', _wall_file)
27
- except: pass
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
31
 
32
 
33
  def _scrape_article_full(url):
34
- """Scrape article: extract paragraphs + ALL images."""
35
  try:
 
 
 
36
  r = requests.get(url, headers=UA, timeout=15, allow_redirects=True)
37
  r.encoding = 'utf-8'
 
 
38
  soup = BeautifulSoup(r.text, 'lxml')
39
- for tag in soup.find_all(['script', 'style', 'nav', 'footer', 'aside', 'form']): tag.decompose()
 
40
 
41
  # Title
42
  h1 = soup.find('h1')
43
  ogt = soup.find('meta', property='og:title')
44
- title = (h1.get_text(strip=True) if h1 else '') or (ogt.get('content', '') if ogt else '')
45
 
46
  # OG image
47
  ogi = soup.find('meta', property='og:image')
48
- og_img = ogi.get('content', '') if ogi else ''
49
  if og_img and og_img.startswith('//'): og_img = 'https:' + og_img
50
 
51
  # Find content block
52
  block = None
53
- for sel in ['article', '.singular-content', '.detail-content', '.fck_detail', '.content-detail', '.knc-content', 'main', '.cms-body', '.article__body']:
54
  el = soup.select_one(sel)
55
- if el and len(el.find_all('p')) >= 2: block = el; break
56
- if not block: block = soup.body or soup
 
 
 
57
 
58
  # Extract paragraphs and images IN ORDER
59
  paragraphs = []
60
  images = []
61
  seen_imgs = set()
 
62
 
63
  if og_img and og_img not in seen_imgs:
64
  images.append(og_img)
@@ -67,20 +92,22 @@ def _scrape_article_full(url):
67
  for el in block.find_all(['p', 'h2', 'h3', 'figure', 'img'], recursive=True):
68
  if el.name == 'p':
69
  t = _clean(el.get_text(strip=True))
70
- if t and len(t) > 40:
71
  paragraphs.append(t)
 
72
  elif el.name in ('figure', 'img'):
73
  im = el if el.name == 'img' else el.find('img')
74
  if im:
75
- src = im.get('data-src') or im.get('src') or im.get('data-original') or ''
76
  if src and 'base64' not in src:
77
  if src.startswith('//'): src = 'https:' + src
 
78
  if src not in seen_imgs:
79
  images.append(src)
80
  seen_imgs.add(src)
81
 
82
- return {'title': _clean(title), 'paragraphs': paragraphs, 'images': images, 'og_img': og_img}
83
- except Exception as e:
84
  return None
85
 
86
 
@@ -123,7 +150,6 @@ def _extract_key_points(paragraphs, max_points=5):
123
 
124
  return points
125
 
126
-
127
  @app.post("/api/rewrite_slide")
128
  async def api_rewrite_slide(request: Request):
129
  """
@@ -131,10 +157,13 @@ async def api_rewrite_slide(request: Request):
131
  - Extract key points from article (1 sentence each, full and complete)
132
  - Pair each point with an image from the article
133
  - Return as slides array for frontend to display
134
- - Save to Tường AI
135
  NO AI NEEDED - instant response.
136
  """
137
- body = await request.json()
 
 
 
138
  url = _clean(body.get("url", ""))
139
  context = body.get("context", "")
140
 
 
2
  from main import app
3
  from fastapi import Request
4
  from fastapi.responses import JSONResponse
5
+ import requests, re, time, random, json, os, threading
6
  from bs4 import BeautifulSoup
7
+ from urllib.parse import quote, urlparse
8
 
9
  UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Language': 'vi-VN,vi;q=0.9'}
10
 
11
+ _DATA_DIR = "/data" if os.path.isdir("/data") else os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
12
+ _WALL_FILE = os.path.join(_DATA_DIR, "wall_posts.json")
13
+ _WALL_LOCK = threading.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ def _safe_json_load(path, default):
16
+ try:
17
+ path = str(path)
18
+ with open(path, 'r', encoding='utf-8') as f:
19
+ return json.load(f)
20
+ except Exception:
21
+ return default
22
+
23
+ def _safe_json_dump(path, data):
24
+ try:
25
+ path = str(path)
26
+ os.makedirs(os.path.dirname(path), exist_ok=True)
27
+ tmp = path + '.tmp'
28
+ with open(tmp, 'w', encoding='utf-8') as f:
29
+ json.dump(data, f, ensure_ascii=False)
30
+ os.replace(tmp, path)
31
+ except Exception:
32
+ pass
33
+
34
+ def _load_wall():
35
+ with _WALL_LOCK:
36
+ posts = _safe_json_load(_WALL_FILE, [])
37
+ return posts if isinstance(posts, list) else []
38
+
39
+ def _save_wall(posts):
40
+ with _WALL_LOCK:
41
+ if not isinstance(posts, list):
42
+ posts = []
43
+ _safe_json_dump(_WALL_FILE, posts[:100])
44
 
45
  def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
46
 
47
 
48
  def _scrape_article_full(url):
49
+ """Scrape article: extract paragraphs + images from common Vietnamese news layouts."""
50
  try:
51
+ parsed = urlparse(url)
52
+ if not parsed.netloc:
53
+ return None
54
  r = requests.get(url, headers=UA, timeout=15, allow_redirects=True)
55
  r.encoding = 'utf-8'
56
+ if r.status_code != 200 or not r.text:
57
+ return None
58
  soup = BeautifulSoup(r.text, 'lxml')
59
+ 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']):
60
+ tag.decompose()
61
 
62
  # Title
63
  h1 = soup.find('h1')
64
  ogt = soup.find('meta', property='og:title')
65
+ title = _clean((h1.get_text(strip=True) if h1 else '') or (ogt.get('content', '') if ogt else ''))
66
 
67
  # OG image
68
  ogi = soup.find('meta', property='og:image')
69
+ og_img = _clean(ogi.get('content', '') if ogi else '')
70
  if og_img and og_img.startswith('//'): og_img = 'https:' + og_img
71
 
72
  # Find content block
73
  block = None
74
+ for sel in ['article', '.singular-content', '.detail-content', '.fck_detail', '.content-detail', '.knc-content', 'main', '.cms-body', '.article__body', '.post-content', '.entry-content']:
75
  el = soup.select_one(sel)
76
+ if el and len(el.find_all('p')) >= 2:
77
+ block = el
78
+ break
79
+ if not block:
80
+ block = soup.body or soup
81
 
82
  # Extract paragraphs and images IN ORDER
83
  paragraphs = []
84
  images = []
85
  seen_imgs = set()
86
+ seen_text = set()
87
 
88
  if og_img and og_img not in seen_imgs:
89
  images.append(og_img)
 
92
  for el in block.find_all(['p', 'h2', 'h3', 'figure', 'img'], recursive=True):
93
  if el.name == 'p':
94
  t = _clean(el.get_text(strip=True))
95
+ if t and len(t) > 40 and t not in seen_text:
96
  paragraphs.append(t)
97
+ seen_text.add(t)
98
  elif el.name in ('figure', 'img'):
99
  im = el if el.name == 'img' else el.find('img')
100
  if im:
101
+ src = im.get('data-src') or im.get('src') or im.get('data-original') or im.get('data-lazy') or ''
102
  if src and 'base64' not in src:
103
  if src.startswith('//'): src = 'https:' + src
104
+ if src.startswith('/'): src = parsed.scheme + '://' + parsed.netloc + src
105
  if src not in seen_imgs:
106
  images.append(src)
107
  seen_imgs.add(src)
108
 
109
+ return {'title': title, 'paragraphs': paragraphs, 'images': images, 'og_img': og_img}
110
+ except Exception:
111
  return None
112
 
113
 
 
150
 
151
  return points
152
 
 
153
  @app.post("/api/rewrite_slide")
154
  async def api_rewrite_slide(request: Request):
155
  """
 
157
  - Extract key points from article (1 sentence each, full and complete)
158
  - Pair each point with an image from the article
159
  - Return as slides array for frontend to display
160
+ - Save to Tường AI using the same file as app_v2_entry.py
161
  NO AI NEEDED - instant response.
162
  """
163
+ try:
164
+ body = await request.json()
165
+ except Exception:
166
+ return JSONResponse({"error": "JSON request lỗi"}, status_code=400)
167
  url = _clean(body.get("url", ""))
168
  context = body.get("context", "")
169