bep40 commited on
Commit
642471d
·
verified ·
1 Parent(s): 7a609ad

Upload rewrite_slide.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. rewrite_slide.py +185 -0
rewrite_slide.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fast rewrite as slides - no AI needed, extracts key points + images from article."""
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)
65
+ seen_imgs.add(og_img)
66
+
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
+
87
+ def _extract_key_points(paragraphs, max_points=5):
88
+ """Extract key points: take first sentence of each significant paragraph."""
89
+ points = []
90
+ for p in paragraphs:
91
+ if len(points) >= max_points: break
92
+ # Take first complete sentence (ends with . ! ?)
93
+ m = re.match(r'^(.+?[.!?])\s', p)
94
+ if m:
95
+ sentence = m.group(1)
96
+ else:
97
+ sentence = p[:150] + ('.' if not p.endswith('.') else '')
98
+
99
+ # Skip if too short or duplicate
100
+ if len(sentence) < 30: continue
101
+ if any(sentence[:50] in existing for existing in points): continue
102
+
103
+ points.append(sentence)
104
+
105
+ return points
106
+
107
+
108
+ @app.post("/api/rewrite_slide")
109
+ async def api_rewrite_slide(request: Request):
110
+ """
111
+ Fast rewrite as SLIDES:
112
+ - Extract key points from article (1 sentence each, full and complete)
113
+ - Pair each point with an image from the article
114
+ - Return as slides array for frontend to display
115
+ - Save to Tường AI
116
+ NO AI NEEDED - instant response.
117
+ """
118
+ body = await request.json()
119
+ url = _clean(body.get("url", ""))
120
+ context = body.get("context", "")
121
+
122
+ if not url and not context:
123
+ return JSONResponse({"error": "Cần URL hoặc nội dung"}, status_code=400)
124
+
125
+ # Scrape article
126
+ data = None
127
+ if url and url.startswith("http"):
128
+ data = _scrape_article_full(url)
129
+
130
+ if not data and context:
131
+ # Use context passed from frontend
132
+ paragraphs = [_clean(p) for p in context.split('\n') if len(_clean(p)) > 40]
133
+ data = {'title': paragraphs[0][:80] if paragraphs else 'Bài viết', 'paragraphs': paragraphs, 'images': [], 'og_img': ''}
134
+
135
+ if not data or not data.get('paragraphs'):
136
+ return JSONResponse({"error": "Không đọc được bài viết"}, status_code=422)
137
+
138
+ # Extract key points
139
+ points = _extract_key_points(data['paragraphs'], max_points=6)
140
+ if not points:
141
+ return JSONResponse({"error": "Không tìm được ý chính"}, status_code=422)
142
+
143
+ # Build slides: pair each point with an image
144
+ images = data.get('images', [])
145
+ slides = []
146
+ for i, point in enumerate(points):
147
+ img = images[i] if i < len(images) else (images[-1] if images else '')
148
+ # Proxy dantri images
149
+ if img and 'cdnphoto.dantri' in img:
150
+ img = '/api/proxy/img?url=' + quote(img, safe='')
151
+ slides.append({
152
+ 'text': point,
153
+ 'image': img,
154
+ 'index': i + 1
155
+ })
156
+
157
+ # Create post for Tường AI
158
+ summary_text = '\n\n'.join([f"• {s['text']}" for s in slides])
159
+ # Auto voice + emotion based on topic (reuse ai_ext detector if available)
160
+ try:
161
+ from ai_ext import _detect_voice_emotion
162
+ _voice, _emotion = _detect_voice_emotion(data['title'], summary_text)
163
+ except Exception:
164
+ _voice, _emotion = "hoaimy", "trung_tinh"
165
+ post = {
166
+ "id": str(int(time.time() * 1000)) + str(random.randint(100, 999)),
167
+ "title": data['title'],
168
+ "text": summary_text,
169
+ "img": images[0] if images else '',
170
+ "url": url,
171
+ "kind": "slide_summary",
172
+ "slides": slides,
173
+ "images": images[:10],
174
+ "video": "",
175
+ "voice": _voice,
176
+ "emotion": _emotion,
177
+ "ts": int(time.time())
178
+ }
179
+
180
+ # Save to wall
181
+ posts = _load_wall()
182
+ posts.insert(0, post)
183
+ _save_wall(posts)
184
+
185
+ return JSONResponse({"post": post, "slides": slides})