bep40 commited on
Commit
e90849c
·
verified ·
1 Parent(s): f73f710

Delete patch_runtime.py, restore_runner.py, rewrite_slide.py

Browse files
Files changed (3) hide show
  1. patch_runtime.py +0 -274
  2. restore_runner.py +0 -31
  3. rewrite_slide.py +0 -185
patch_runtime.py DELETED
@@ -1,274 +0,0 @@
1
- """Runtime patch layer for VNEWS.
2
- Keeps the current large app intact, but replaces fragile AI wall endpoints with
3
- stable JSON endpoints and injects frontend safeJson wrappers.
4
- """
5
- import hashlib
6
- import time
7
- import os
8
- from urllib.parse import quote
9
-
10
- import requests
11
- from bs4 import BeautifulSoup
12
- from fastapi import Request
13
- from fastapi.responses import JSONResponse, HTMLResponse
14
-
15
- import main as _main
16
-
17
- app = _main.app
18
- DEFAULT_IMG = "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
19
-
20
-
21
- def _remove_routes(paths):
22
- app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) not in set(paths)]
23
-
24
-
25
- def _safe_text(v):
26
- return (v or "").strip()
27
-
28
-
29
- def _ensure_article(url: str):
30
- data = None
31
- try:
32
- if hasattr(_main, "_article_by_url"):
33
- data = _main._article_by_url(url)
34
- except Exception:
35
- data = None
36
- if not data:
37
- try:
38
- data = _main._scrape_generic_article(url) if hasattr(_main, "_scrape_generic_article") else None
39
- except Exception:
40
- data = None
41
- if not data:
42
- data = {"title": "", "summary": "", "og_image": "", "body": [], "url": url, "source": "generic"}
43
- title = _safe_text(data.get("title"))
44
- summary = _safe_text(data.get("summary"))
45
- img = _safe_text(data.get("og_image"))
46
- body = data.get("body") or []
47
- if not title or not summary or not img or not body:
48
- try:
49
- r = requests.get(url, headers=getattr(_main, "HEADERS", {}), timeout=15)
50
- r.encoding = "utf-8"
51
- soup = BeautifulSoup(r.text, "lxml")
52
- if not title:
53
- tag = soup.find("meta", property="og:title") or soup.find("title")
54
- title = tag.get("content", "").strip() if tag and tag.name == "meta" else (tag.get_text(strip=True) if tag else "")
55
- if not summary:
56
- tag = soup.find("meta", property="og:description") or soup.find("meta", attrs={"name": "description"})
57
- summary = tag.get("content", "").strip() if tag else ""
58
- if not img:
59
- tag = soup.find("meta", property="og:image") or soup.find("meta", attrs={"name": "twitter:image"})
60
- img = tag.get("content", "").strip() if tag else ""
61
- if not body:
62
- ps = []
63
- for p in soup.find_all("p"):
64
- t = p.get_text(" ", strip=True)
65
- if len(t) > 40:
66
- ps.append({"type": "p", "text": t})
67
- if len(ps) >= 30:
68
- break
69
- body = ps
70
- except Exception:
71
- pass
72
- if not summary and body:
73
- first = next((b.get("text", "") for b in body if b.get("type") == "p" and b.get("text")), "")
74
- summary = first[:360]
75
- if not title:
76
- title = url
77
- if not img:
78
- img = DEFAULT_IMG
79
- if not body and summary:
80
- body = [{"type": "p", "text": summary}]
81
- data.update({"title": title, "summary": summary, "og_image": img, "body": body, "url": url})
82
- return data
83
-
84
-
85
- def _rewrite(data, tone="tu-nhien"):
86
- try:
87
- if hasattr(_main, "_ai_rewrite_article"):
88
- text = _main._ai_rewrite_article(data, tone=tone)
89
- if text and len(text.strip()) > 50:
90
- return text.strip()
91
- except Exception:
92
- pass
93
- title = data.get("title", "")
94
- summary = data.get("summary", "")
95
- ps = [b.get("text", "") for b in data.get("body", []) if b.get("type") == "p" and b.get("text")]
96
- lead = summary or (ps[0] if ps else "")
97
- points = "\n".join(["• " + p[:220] + ("..." if len(p) > 220 else "") for p in ps[:5]])
98
- body = "\n\n".join(ps[:10])
99
- return (f"Bản tin AI viết lại: {title}\n\n{lead}\n\n{body}\n\nĐiểm chính:\n{points}").strip()
100
-
101
-
102
- def _topic_image(topic):
103
- try:
104
- if hasattr(_main, "_image_for_topic"):
105
- return _main._image_for_topic(topic)
106
- except Exception:
107
- pass
108
- return "https://image.pollinations.ai/prompt/" + quote("editorial illustration Vietnamese news " + topic, safe="") + "?width=1024&height=576&nologo=true"
109
-
110
-
111
- def _save_post(post):
112
- try:
113
- posts = _main._load_wall() if hasattr(_main, "_load_wall") else []
114
- except Exception:
115
- posts = []
116
- posts.insert(0, post)
117
- try:
118
- if hasattr(_main, "_save_wall"):
119
- _main._save_wall(posts)
120
- except Exception:
121
- pass
122
- return post
123
-
124
-
125
- _remove_routes(["/api/url_wall", "/api/topic_post", "/api/rewrite_share", "/"])
126
-
127
-
128
- @app.post("/api/url_wall")
129
- async def patched_url_wall(request: Request):
130
- try:
131
- body = await request.json()
132
- except Exception:
133
- body = {}
134
- url = _safe_text(body.get("url"))
135
- tone = _safe_text(body.get("tone")) or "tu-nhien"
136
- if not url:
137
- return JSONResponse({"error": "missing url"}, status_code=400)
138
- try:
139
- data = _ensure_article(url)
140
- text = _rewrite(data, tone=tone)
141
- post = {
142
- "id": hashlib.md5((url + str(time.time())).encode()).hexdigest()[:12],
143
- "url": url,
144
- "title": data.get("title") or url,
145
- "summary": data.get("summary") or "",
146
- "img": data.get("og_image") or DEFAULT_IMG,
147
- "text": text or (data.get("summary") or data.get("title") or url),
148
- "source": data.get("source", "url"),
149
- "ts": int(time.time()),
150
- }
151
- _save_post(post)
152
- return JSONResponse({"post": post})
153
- except Exception as e:
154
- return JSONResponse({"error": "Không tạo được tóm tắt URL", "detail": str(e)[:300]}, status_code=500)
155
-
156
-
157
- @app.post("/api/rewrite_share")
158
- async def patched_rewrite_share(request: Request):
159
- return await patched_url_wall(request)
160
-
161
-
162
- @app.post("/api/topic_post")
163
- async def patched_topic_post(request: Request):
164
- try:
165
- body = await request.json()
166
- except Exception:
167
- body = {}
168
- topic = _safe_text(body.get("topic"))
169
- tone = _safe_text(body.get("tone")) or "tu-nhien"
170
- if not topic:
171
- return JSONResponse({"error": "missing topic"}, status_code=400)
172
- try:
173
- context = ""
174
- try:
175
- if hasattr(_main, "_topic_article_context"):
176
- context = _main._topic_article_context(topic)
177
- if not context and hasattr(_main, "_web_context"):
178
- context = _main._web_context(topic)
179
- except Exception:
180
- context = ""
181
- if not context:
182
- context = f"Chủ đề: {topic}"
183
- data = {"title": topic, "summary": context[:420], "og_image": _topic_image(topic), "body": [{"type": "p", "text": context}], "source": "topic", "url": ""}
184
- text = _rewrite(data, tone=tone)
185
- post = {
186
- "id": hashlib.md5((topic + str(time.time())).encode()).hexdigest()[:12],
187
- "url": "",
188
- "title": topic,
189
- "summary": data["summary"],
190
- "img": data["og_image"] or DEFAULT_IMG,
191
- "text": text or context,
192
- "source": "topic",
193
- "ts": int(time.time()),
194
- }
195
- _save_post(post)
196
- return JSONResponse({"post": post})
197
- except Exception as e:
198
- return JSONResponse({"error": "Không tạo được bài theo chủ đề", "detail": str(e)[:300]}, status_code=500)
199
-
200
-
201
- _FRONTEND_PATCH = r'''
202
- <script>
203
- (function(){
204
- async function safeJson(res){
205
- const text = await res.text();
206
- try { return JSON.parse(text); }
207
- catch(e){ return { error: (text || 'Server không trả JSON').slice(0,500) }; }
208
- }
209
- window.safeJson = safeJson;
210
- window.createUrlPost = function(){
211
- let inp=document.getElementById('ai-url-input');
212
- let url=(inp&&inp.value||'').trim();
213
- if(!url){ alert('Dán URL trước'); return; }
214
- fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})})
215
- .then(safeJson).then(j=>{
216
- if(j&&j.post){
217
- if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg';
218
- if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung tóm tắt.';
219
- if(typeof prependWallPost==='function') prependWallPost(j.post);
220
- alert('Đã tóm tắt URL và đăng lên tường');
221
- if(inp) inp.value='';
222
- } else alert((j&&j.error)||'Lỗi URL');
223
- }).catch(e=>alert('Lỗi URL: '+e.message));
224
- };
225
- window.createTopicPost = function(){
226
- let inp=document.getElementById('ai-topic-input');
227
- let topic=(inp&&inp.value||'').trim();
228
- if(!topic){ alert('Nhập chủ đề trước'); return; }
229
- fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})})
230
- .then(safeJson).then(j=>{
231
- if(j&&j.post){
232
- if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg';
233
- if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung.';
234
- if(typeof prependWallPost==='function') prependWallPost(j.post);
235
- alert('Đã tạo bài và đăng lên tường');
236
- if(inp) inp.value='';
237
- } else alert((j&&j.error)||'Lỗi tạo bài');
238
- }).catch(e=>alert('Lỗi tạo bài: '+e.message));
239
- };
240
- window.rewriteCurrentArticle = function(){
241
- if(!window._currentArticle && typeof _currentArticle!=='undefined') window._currentArticle=_currentArticle;
242
- let ca = (typeof _currentArticle!=='undefined') ? _currentArticle : window._currentArticle;
243
- if(!ca || !ca.url){ alert('Chưa có bài viết để rewrite'); return; }
244
- let tone=document.getElementById('rewrite-tone')?.value||'nghiem-tuc';
245
- let btn=document.querySelector('.article-actions button.primary');
246
- if(btn){btn.textContent='Đang rewrite...';btn.disabled=true;}
247
- fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:ca.url,tone})})
248
- .then(safeJson).then(j=>{
249
- if(j&&j.post){
250
- if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg';
251
- if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung.';
252
- let box=document.getElementById('rewrite-result');
253
- if(box) box.innerHTML='<div class="rewrite-box"><div class="rewrite-title">Đã rewrite và đăng lên Tường AI</div><div class="rewrite-text">'+(j.post.text||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))+'</div></div>';
254
- if(typeof prependWallPost==='function') prependWallPost(j.post);
255
- alert('Đã đăng lên Tường AI');
256
- } else alert((j&&j.error)||'Không tạo được bài AI');
257
- }).catch(e=>alert('Lỗi tạo bài AI: '+e.message))
258
- .finally(()=>{if(btn){btn.textContent='🤖 AI viết lại & đăng tường';btn.disabled=false;}});
259
- };
260
- })();
261
- </script>
262
- '''
263
-
264
-
265
- @app.get("/")
266
- async def patched_index():
267
- try:
268
- with open("/app/static/index.html", "r", encoding="utf-8") as f:
269
- html = f.read()
270
- if "window.safeJson" not in html:
271
- html = html.replace("</body>", _FRONTEND_PATCH + "</body>")
272
- return HTMLResponse(content=html)
273
- except Exception as e:
274
- return HTMLResponse(content=f"<pre>Index error: {str(e)}</pre>", status_code=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
restore_runner.py DELETED
@@ -1,31 +0,0 @@
1
- import os
2
- import sys
3
- import subprocess
4
- from huggingface_hub import snapshot_download
5
-
6
- REVISION = os.environ.get("VNEWS_RESTORE_REVISION", "bcaa2dc")
7
- REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
8
-
9
- # Download exact Space snapshot from Hugging Face Hub.
10
- # This avoids manually copying huge files from an old commit.
11
- snapshot_dir = snapshot_download(
12
- repo_id=REPO_ID,
13
- repo_type="space",
14
- revision=REVISION,
15
- local_dir="/tmp/vnews_restore",
16
- local_dir_use_symlinks=False,
17
- )
18
-
19
- os.chdir(snapshot_dir)
20
- sys.path.insert(0, snapshot_dir)
21
-
22
- # Commit bcaa2dc Dockerfile ran ai_patch:app.
23
- cmd = [
24
- "uvicorn",
25
- "ai_patch:app",
26
- "--host",
27
- "0.0.0.0",
28
- "--port",
29
- "7860",
30
- ]
31
- os.execvp(cmd[0], cmd)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
rewrite_slide.py DELETED
@@ -1,185 +0,0 @@
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})