bep40 commited on
Commit
767f8ac
·
verified ·
1 Parent(s): 7b2b477

Update AI runtime to produce opinion posts instead of summaries

Browse files
Files changed (1) hide show
  1. ai_runtime.py +126 -27
ai_runtime.py CHANGED
@@ -86,6 +86,50 @@ def _scrape_url_with_images(url):
86
  return data
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def rich_context(topic, limit=5):
90
  try: ctx,sources=base.web_context(topic, limit=limit)
91
  except Exception: ctx,sources='',[]
@@ -122,35 +166,36 @@ _PATCH={('/api/topic_post','POST'),('/api/url_wall','POST'),('/api/rewrite_share
122
  app.router.routes=[r for r in app.router.routes if not any(getattr(r,'path',None)==p and m in getattr(r,'methods',set()) for p,m in _PATCH)]
123
 
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  @app.post('/api/url_wall')
126
  async def url_wall_only(request:Request):
127
  body=await request.json();url=base._clean_text(body.get('url',''))
 
128
  if not url.startswith('http'):return JSONResponse({'error':'missing url'},status_code=400)
129
  try:data=_scrape_url_with_images(url)
130
  except Exception as e:return JSONResponse({'error':'Không scrape được URL: '+str(e)[:180]},status_code=422)
131
  raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
132
- if len(raw)<120:return JSONResponse({'error':'URL không có đủ nội dung để tóm tắt'},status_code=422)
133
- prompt=f"""Tóm tắt bài viết nguồn dưới đây để đăng lên Tường AI VNEWS.
134
-
135
- Yêu cầu bắt buộc:
136
- - Chỉ tóm tắt nội dung chính, không viết lại toàn bộ bài.
137
- - Ngắn gọn, cụ thể, dễ hiểu.
138
- - Không lặp lại ý và không thêm chi tiết ngoài nguồn.
139
- - Tối đa 5 ý chính hoặc 2 đoạn ngắn.
140
- - Tránh dùng dấu đầu dòng nếu không thật cần thiết.
141
-
142
- Tiêu đề gốc: {data.get('title','')}
143
- Nguồn: {data.get('via','') or _domain(url)}
144
- Nội dung gốc:
145
- {raw[:16000]}"""
146
- text=await base.qwen_generate(prompt,image_url=(data.get('image') or None),max_tokens=900)
147
- if not text:text=old._fallback_summary_from_prompt(prompt,max_units=5) if hasattr(old,'_fallback_summary_from_prompt') else raw[:900]
148
- text=postprocess(text)
149
- src=[{'title':data.get('title'), 'url':url, 'excerpt':raw[:500], 'via':data.get('via') or _domain(url)}]
150
- if 'Nguồn tham khảo:' not in text:text+='\n\n'+source_line(src)
151
- images=_collect_all_images(data)
152
- post=base.make_post(data.get('title') or 'Bài viết',text,images[0] if images else (data.get('image') or ''),url,'url',sources=src)
153
- post['images']=images
154
  posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
155
  return JSONResponse({'post':post})
156
 
@@ -166,8 +211,62 @@ async def ai_url_compat(request:Request):
166
 
167
 
168
  @app.post('/api/topic_post')
169
- async def topic_disabled(request:Request):
170
- return JSONResponse({'error':'Đã tắt tạo bài theo chủ đề. Vui lòng dán URL bài viết để AI tóm tắt.'},status_code=410)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
 
173
  def split_segments(post,max_segments=8):
@@ -341,10 +440,10 @@ function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&l
341
  function hideTopicControls(){
342
  document.querySelectorAll('#ai-topic-input').forEach(e=>{let p=e.closest('.ai-compose,.ai-compose-topic,.topic-row,div'); if(p&&p.querySelector('#ai-url-input')) e.style.display='none'; else if(p) p.style.display='none';});
343
  document.querySelectorAll('button').forEach(b=>{let t=(b.textContent||'').toLowerCase();let oc=b.getAttribute('onclick')||'';if(oc.includes('createTopicPost')||t.includes('chủ đề'))b.style.display='none';});
344
- let url=document.getElementById('ai-url-input'); if(url&&!document.getElementById('ai-url-only-note')){let n=document.createElement('div');n.id='ai-url-only-note';n.className='ai-url-only-note';n.textContent='Dán URL bài viết để AI tóm tắt và lấy ảnh từ bài.';url.insertAdjacentElement('afterend',n);}
345
  }
346
- window.createTopicPost=function(){alert('Đã tắt ô nhập chủ đề. Vui lòng dán URL bài viết.');};
347
- window.createUrlPost=function(){let inp=document.getElementById('ai-url-input');let url=(inp&&inp.value||'').trim();if(!url)return alert('Dán URL trước');if(!/^https?:\/\//i.test(url))return alert('URL cần bắt đầu bằng http:// hoặc https://');fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})}).then(r=>r.json().then(j=>({ok:r.ok,j}))).then(({ok,j})=>{if(ok&&j.post){if(typeof prependWallPost==='function')prependWallPost(j.post);if(window.patchedWall)window.patchedWall=[j.post].concat(window.patchedWall||[]);if(inp)inp.value='';alert('Đã tóm tắt URL, lấy ảnh trong bài và đăng lên Tường AI');location.reload();}else alert(j.error||'Lỗi URL')}).catch(e=>alert(e.message||'Lỗi URL'));};
348
  function galleryHtml(p){let imgs=(p.images||[]).filter(Boolean);if(!imgs.length&&p.img)imgs=[p.img];if(!imgs.length)return '';return '<div class="ai-wall-gallery">'+imgs.slice(0,12).map(u=>`<img src="${esc(u)}" loading="lazy">`).join('')+'</div>';}
349
  function patchReaders(){
350
  let oldRead=window.aiReadWallPatched||window.aiReadWall;
 
86
  return data
87
 
88
 
89
+ def _opinion_template(title, source, raw):
90
+ return f"""Bạn là nhà bình luận tiếng Việt của VNEWS. Nhiệm vụ là viết một bài quan điểm cá nhân đăng lên Tường AI, không phải tóm tắt tin tức.
91
+
92
+ YÊU CẦU BẮT BUỘC:
93
+ - Viết theo quan điểm cá nhân rõ ràng, có lập trường/thesis ngay từ đầu.
94
+ - Không viết bài kiểu bản tin/tóm tắt, không liệt kê 5 ý chính, không recap nguồn.
95
+ - Nguồn bên dưới chỉ là bằng chứng/gợi ý để lập luận, không phải nội dung chính cần lặp lại.
96
+ - Nếu nguồn không đủ mạnh để bảo vệ quan điểm, hãy nói rõ giới hạn đó thay vì bịa thêm chi tiết.
97
+ - Không sao chép nguyên văn câu từ của nguồn.
98
+ - Giọng văn tự nhiên như một bài opinion trên mạng xã hội: trực diện, có góc nhìn, có lý lẽ.
99
+ - Kết quả cuối cùng chỉ gồm 1 tiêu đề ngắn và 2-4 đoạn văn. Không dùng gạch đầu dòng.
100
+
101
+ Tiêu đề nguồn: {title}
102
+ Nguồn tham khảo: {source}
103
+
104
+ Nội dung nguồn dùng làm bằng chứng:
105
+ {raw[:16000]}"""
106
+
107
+
108
+ def _opinion_title(text, fallback):
109
+ text = clean(text)
110
+ if not text:
111
+ return fallback
112
+ first = text.split('\n', 1)[0]
113
+ if len(first) <= 95 and not any(first.endswith(x) for x in ['.', '!', '?']):
114
+ return first[:95]
115
+ return fallback
116
+
117
+
118
+ def _postprocess_opinion(text):
119
+ text = clean(text)
120
+ if not text:
121
+ return text
122
+ text = re.sub(r'^(Tôi nghĩ|Theo tôi|Mình cho rằng|Mình tin|Rất cần nói lại|Cần nhìn lại|Không nên|Phải|Nên)\b.*$', '', text, flags=re.I).strip()
123
+ text = re.sub(r'\n{3,}', '\n\n', text)
124
+ text = re.sub(r'\n{2,}', '\n\n', text)
125
+ # Keep paragraphs but avoid bullet-style output.
126
+ paras = [p.strip() for p in text.split('\n\n') if p.strip()]
127
+ if len(paras) > 4:
128
+ paras = paras[:4]
129
+ text = '\n\n'.join(paras)
130
+ return text
131
+
132
+
133
  def rich_context(topic, limit=5):
134
  try: ctx,sources=base.web_context(topic, limit=limit)
135
  except Exception: ctx,sources='',[]
 
166
  app.router.routes=[r for r in app.router.routes if not any(getattr(r,'path',None)==p and m in getattr(r,'methods',set()) for p,m in _PATCH)]
167
 
168
 
169
+ async def _make_opinion_post(title, raw, url, source_url, source_name, image_url=None, opinion_text=''):
170
+ source = f"{source_name or _domain(source_url or url) or 'nguồn VNEWS'}"
171
+ prompt = _opinion_template(title, source, raw)
172
+ if opinion_text:
173
+ prompt += "\n\nQuan điểm ban đầu của người dùng:\n" + clean(opinion_text)
174
+ text = await base.qwen_generate(prompt, image_url=(image_url or None), max_tokens=1100)
175
+ if not text:
176
+ text = old._fallback_summary_from_prompt(prompt, max_units=5) if hasattr(old,'_fallback_summary_from_prompt') else raw[:900]
177
+ text = _postprocess_opinion(text)
178
+ if len(text) < 120:
179
+ text = clean(raw[:900])
180
+ src=[{'title':title, 'url':url, 'excerpt':clean(raw)[:500], 'via':source_name or _domain(url)}]
181
+ if 'Nguồn tham khảo:' not in text:
182
+ text += '\n\n' + source_line(src)
183
+ images=_collect_all_images({'image':image_url, 'images':[]})
184
+ post=base.make_post(_opinion_title(text, title or 'Bài quan điểm'), text, images[0] if images else (image_url or ''), url, 'opinion', sources=src)
185
+ post['images']=images
186
+ return post
187
+
188
+
189
  @app.post('/api/url_wall')
190
  async def url_wall_only(request:Request):
191
  body=await request.json();url=base._clean_text(body.get('url',''))
192
+ opinion_text=base._clean_text(body.get('opinion') or body.get('opinion_text') or '')
193
  if not url.startswith('http'):return JSONResponse({'error':'missing url'},status_code=400)
194
  try:data=_scrape_url_with_images(url)
195
  except Exception as e:return JSONResponse({'error':'Không scrape được URL: '+str(e)[:180]},status_code=422)
196
  raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
197
+ if len(raw)<120:return JSONResponse({'error':'URL không có đủ nội dung để viết quan điểm'},status_code=422)
198
+ post=await _make_opinion_post(data.get('title') or 'Bài viết', raw, url, url, data.get('via') or _domain(url), data.get('image') or None, opinion_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
200
  return JSONResponse({'post':post})
201
 
 
211
 
212
 
213
  @app.post('/api/topic_post')
214
+ async def topic_post(request:Request):
215
+ body=await request.json();topic=base._clean_text(body.get('topic',''))
216
+ opinion_text=base._clean_text(body.get('opinion') or body.get('opinion_text') or '')
217
+ if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
218
+ try:ctx,sources=rich_context(topic, limit=5)
219
+ except Exception:ctx,sources=('',[])
220
+ if not sources:return JSONResponse({'error':'Không lấy được bài viết nguồn cho chủ đề này.'},status_code=422)
221
+ raw_parts=[];src=[];seen=set()
222
+ for s in sources[:5]:
223
+ url=s.get('url') or ''
224
+ if not url.startswith('http') or url in seen:continue
225
+ seen.add(url)
226
+ try:
227
+ data=base.scrape_any_url(url)
228
+ raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
229
+ if len(raw)<120:continue
230
+ title=data.get('title') or s.get('title') or url
231
+ via=data.get('via') or s.get('via') or _domain(url)
232
+ raw_parts.append(f"### {title} ({via})\n{raw[:3000]}")
233
+ src.append({'title':title, 'url':url, 'excerpt':raw[:500], 'via':via})
234
+ except Exception:
235
+ continue
236
+ if not raw_parts:return JSONResponse({'error':'Không lấy được nội dung đủ mạnh cho chủ đề này.'},status_code=422)
237
+ post=await _make_opinion_post(topic, '\n\n'.join(raw_parts), '', '', 'chủ đề '+topic, opinion_text=opinion_text)
238
+ post['sources']=src
239
+ posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
240
+ return JSONResponse({'post':post})
241
+
242
+
243
+ async def _make_personal_opinion_post(topic, opinion_text, image_url=None):
244
+ if not opinion_text:
245
+ return None
246
+ opinion_text=clean(opinion_text)
247
+ if len(opinion_text)<20:return None
248
+ prompt=_opinion_template(topic, 'ý kiến người dùng và bối cảnh chung', opinion_text)
249
+ text=await base.qwen_generate(prompt, image_url=(image_url or None), max_tokens=1100)
250
+ if not text:text=opinion_text[:1100]
251
+ text=_postprocess_opinion(text)
252
+ if len(text)<120:text=opinion_text
253
+ src=[{'title':topic, 'url':'', 'excerpt':opinion_text[:500], 'via':'người dùng'}]
254
+ if 'Nguồn tham khảo:' not in text:text+='\n\n'+source_line(src)
255
+ post=base.make_post(_opinion_title(text, topic), text, image_url or '', '', 'personal_opinion', sources=src)
256
+ post['images']=[image_url] if image_url else []
257
+ return post
258
+
259
+
260
+ @app.post('/api/personal_opinion')
261
+ async def personal_opinion_post(request:Request):
262
+ body=await request.json();topic=base._clean_text(body.get('topic','')) or base._clean_text(body.get('title',''))
263
+ opinion_text=base._clean_text(body.get('opinion') or body.get('opinion_text') or '')
264
+ if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
265
+ if not opinion_text:return JSONResponse({'error':'missing opinion'},status_code=400)
266
+ post=await _make_personal_opinion_post(topic, opinion_text, body.get('image') or '')
267
+ if not post:return JSONResponse({'error':'Quan điểm chưa đủ dài'},status_code=422)
268
+ posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
269
+ return JSONResponse({'post':post})
270
 
271
 
272
  def split_segments(post,max_segments=8):
 
440
  function hideTopicControls(){
441
  document.querySelectorAll('#ai-topic-input').forEach(e=>{let p=e.closest('.ai-compose,.ai-compose-topic,.topic-row,div'); if(p&&p.querySelector('#ai-url-input')) e.style.display='none'; else if(p) p.style.display='none';});
442
  document.querySelectorAll('button').forEach(b=>{let t=(b.textContent||'').toLowerCase();let oc=b.getAttribute('onclick')||'';if(oc.includes('createTopicPost')||t.includes('chủ đề'))b.style.display='none';});
443
+ let url=document.getElementById('ai-url-input'); if(url&&!document.getElementById('ai-url-only-note')){let n=document.createElement('div');n.id='ai-url-only-note';n.className='ai-url-only-note';n.textContent='Dán URL bài viết để AI viết quan điểm cá nhân và lấy ảnh từ bài.';url.insertAdjacentElement('afterend',n);}
444
  }
445
+ window.createTopicPost=function(){alert('Đã tắt ô nhập chủ đề. Vui lòng dán URL bài viết để AI viết quan điểm cá nhân.');};
446
+ window.createUrlPost=function(){let inp=document.getElementById('ai-url-input');let url=(inp&&inp.value||'').trim();if(!url)return alert('Dán URL trước');if(!/^https?:\/\//i.test(url))return alert('URL cần bắt đầu bằng http:// hoặc https://');fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})}).then(r=>r.json().then(j=>({ok:r.ok,j}))).then(({ok,j})=>{if(ok&&j.post){if(typeof prependWallPost==='function')prependWallPost(j.post);if(window.patchedWall)window.patchedWall=[j.post].concat(window.patchedWall||[]);if(inp)inp.value='';alert('Đã viết bài quan điểm cá nhân, lấy ảnh trong bài và đăng lên Tường AI');location.reload();}else alert(j.error||'Lỗi URL')}).catch(e=>alert(e.message||'Lỗi URL'));};
447
  function galleryHtml(p){let imgs=(p.images||[]).filter(Boolean);if(!imgs.length&&p.img)imgs=[p.img];if(!imgs.length)return '';return '<div class="ai-wall-gallery">'+imgs.slice(0,12).map(u=>`<img src="${esc(u)}" loading="lazy">`).join('')+'</div>';}
448
  function patchReaders(){
449
  let oldRead=window.aiReadWallPatched||window.aiReadWall;