bep40 commited on
Commit
4390847
·
verified ·
1 Parent(s): f085d5e

Restore Space to revision d7f41b2

Browse files
Files changed (1) hide show
  1. restore_runner.py +5 -227
restore_runner.py CHANGED
@@ -1,10 +1,10 @@
1
  import os
2
  import sys
3
- from textwrap import dedent
4
  from huggingface_hub import snapshot_download
5
 
6
- REVISION = os.environ.get("VNEWS_RESTORE_REVISION", "b9b6ba4")
7
  REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
 
8
 
9
  snapshot_dir = snapshot_download(
10
  repo_id=REPO_ID,
@@ -17,228 +17,6 @@ snapshot_dir = snapshot_download(
17
  os.chdir(snapshot_dir)
18
  sys.path.insert(0, snapshot_dir)
19
 
20
- PATCHED = r'''
21
- import os, re, json, time, requests
22
- from urllib.parse import quote, urlparse
23
- import ai_runtime_final6 as base_runtime
24
- from ai_runtime_final6 import app, base, rt, HTMLResponse, JSONResponse, Request, Query
25
- try:
26
- import main as main_mod
27
- except Exception:
28
- main_mod = None
29
-
30
- DATA_DIR = "/data" if os.path.isdir('/data') else "/app/data"
31
- AI_INTERACTIONS_FILE = os.path.join(DATA_DIR, 'ai_interactions.json')
32
- _SHORTS_CACHE = {'t':0,'d':[]}
33
- CHANNELS = ['baodantri7941','baosuckhoedoisongboyte']
34
-
35
-
36
- def clean(s):
37
- import html as html_lib
38
- return re.sub(r'\s+', ' ', html_lib.unescape(s or '')).strip()
39
-
40
- def _load(path, default):
41
- try:
42
- if os.path.exists(path):
43
- with open(path,'r',encoding='utf-8') as f:return json.load(f)
44
- except Exception:pass
45
- return default
46
-
47
- def _save(path, data):
48
- try:
49
- os.makedirs(os.path.dirname(path), exist_ok=True)
50
- tmp=path+'.tmp'
51
- with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
52
- os.replace(tmp,path)
53
- except Exception:pass
54
-
55
- def _domain(u):
56
- try:return urlparse(u or '').netloc.replace('www.','')
57
- except Exception:return ''
58
-
59
- def _fallback_shorts():
60
- out=[];seen=set();c=[]
61
- try:c += (getattr(main_mod,'SHORTS_FALLBACK',[]) or [])
62
- except Exception:pass
63
- hard=[
64
- ('Lu_iCQ5YwNM','Công an lập hồ sơ xử lý người phụ nữ chửi bới, tát tài xế ô tô ở Hà Nội | Dân trí','baodantri7941'),
65
- ('CwWvijF8BOA','Chú rể bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước | Dân trí','baodantri7941'),
66
- ('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | Dân trí','baodantri7941'),
67
- ('b1Nxzv9ixlU','Y án 3 năm tù với nữ tài xế uống 8 lon bia lái xe tông chủ tịch xã tử vong | Dân trí','baodantri7941'),
68
- ('7Pd6vZ2Lz1M','Hành động ấm lòng trong tìm kiếm 5 học sinh tử vong ở sông Lô | SKĐS','baosuckhoedoisongboyte'),
69
- ('SlHLt_ZyPiE','Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc - Nam | SKĐS','baosuckhoedoisongboyte'),
70
- ('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte'),
71
- ('YY8ojFNE-AU','Quái xế tự quay clip nẹt pô, đánh võng đăng TikTok bị xử lý | SKĐS','baosuckhoedoisongboyte')]
72
- for vid,title,ch in hard:c.append({'id':vid,'title':title,'channel':ch})
73
- for v in c:
74
- vid=v.get('id')
75
- if vid and vid not in seen:
76
- seen.add(vid)
77
- out.append({'id':vid,'title':v.get('title') or 'YouTube Short','channel':v.get('channel',''),'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
78
- return out
79
-
80
- def _yt_html(handle, count=24):
81
- try:
82
- html=requests.get(f'https://www.youtube.com/@{handle}/shorts',headers=getattr(base,'HEADERS',{}),timeout=8).text
83
- ids=[];out=[]
84
- for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"', html):
85
- vid=m.group(1)
86
- if vid in ids:continue
87
- ids.append(vid)
88
- snip=html[max(0,m.start()-900):m.start()+1600]
89
- mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip)
90
- title=clean((mt.group(1) if mt else 'YouTube Short').replace('\\n',' '))
91
- out.append({'id':vid,'title':title,'channel':handle,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'})
92
- if len(out)>=count:break
93
- return out
94
- except Exception:return []
95
-
96
- def _fresh_shorts_fast():
97
- # Fast: HTML first; yt-dlp caused slow page. Always append fallback so slide appears instantly.
98
- seen=set();out=[]
99
- for ch in CHANNELS:
100
- for v in _yt_html(ch,20):
101
- if v['id'] not in seen:
102
- seen.add(v['id']);out.append(v)
103
- for v in _fallback_shorts():
104
- if v['id'] not in seen:
105
- seen.add(v['id']);out.append(v)
106
- return out[:60]
107
-
108
- # Remove routes to override.
109
- _PATCH={('/api/shorts','GET'),('/api/ai/interact','POST'),('/api/topic_post','POST'),('/api/article/ask','POST'),('/','GET')}
110
- 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)]
111
-
112
- @app.get('/api/shorts')
113
- def api_shorts_fast(refresh:int=Query(default=0)):
114
- now=time.time()
115
- if not refresh and _SHORTS_CACHE['d'] and now-_SHORTS_CACHE['t']<300:return JSONResponse(_SHORTS_CACHE['d'])
116
- data=_fresh_shorts_fast();_SHORTS_CACHE.update({'t':now,'d':data});return JSONResponse(data)
117
-
118
- @app.post('/api/ai/interact')
119
- async def ai_interact_fixed(request:Request):
120
- body=await request.json();pid=str(body.get('id','')).strip();kind=str(body.get('kind','wall')).strip();action=str(body.get('action','')).strip();text=clean(body.get('text',''));title=clean(body.get('title',''));context=clean(body.get('context',''))
121
- if not pid:return JSONResponse({'error':'missing id'},status_code=400)
122
- db=_load(AI_INTERACTIONS_FILE,{})
123
- key=kind+':'+pid
124
- st=db.get(key) or {'views':0,'likes':0,'comments':[],'asks':[]}
125
- if action=='view':st['views']=int(st.get('views',0))+1
126
- elif action=='like':st['likes']=int(st.get('likes',0))+1
127
- elif action=='comment' and text:
128
- st.setdefault('comments',[]).insert(0,{'text':text[:300],'ts':int(time.time())});st['comments']=st['comments'][:100]
129
- elif action=='ask' and text:
130
- if kind in ('ai','short','wall'):
131
- posts=base._load_ai_wall();p=next((x for x in posts if str(x.get('id'))==pid),{})
132
- title=title or p.get('title','');context=context or p.get('text','')
133
- if not context:context=title or pid
134
- prompt=f"""Bạn là trợ lý VNEWS. Hãy trả lời đúng trọng tâm câu hỏi dựa trên nội dung/mô tả short hoặc bài viết.
135
-
136
- Tiêu đề: {title}
137
- Ngữ cảnh/mô tả short: {context[:6000]}
138
- Câu hỏi: {text}
139
-
140
- Yêu cầu:
141
- - Trả lời bằng tiếng Việt, cụ thể, có giải thích.
142
- - Nếu là short YouTube và chỉ có tiêu đề, hãy phân tích đúng trọng tâm tiêu đề; nói rõ bạn đang suy luận từ tiêu đề/mô tả, không khẳng định đã xem toàn bộ video.
143
- - Không trả lời chung chung.
144
- """
145
- ans=await base.qwen_generate(prompt,max_tokens=900)
146
- if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi cụ thể hơn.'
147
- st.setdefault('asks',[]).insert(0,{'q':text[:300],'a':ans[:1800],'ts':int(time.time())});st['asks']=st['asks'][:60]
148
- db[key]=st;_save(AI_INTERACTIONS_FILE,db);return JSONResponse({'stats':st})
149
-
150
- @app.post('/api/topic_post')
151
- async def topic_post_clean(request:Request):
152
- body=await request.json();topic=clean(body.get('topic',''))
153
- if not topic:return JSONResponse({'error':'missing topic'},status_code=400)
154
- try:img=base.pollinations_image_url(topic)
155
- except Exception:img='https://image.pollinations.ai/prompt/'+quote('Vietnamese editorial illustration '+topic)+'?width=1024&height=576&nologo=true'
156
- prompt=f"""Viết một bài báo tiếng Việt hoàn chỉnh, chất lượng cao về chủ đề: {topic}
157
-
158
- Chỉ xuất bản nội dung cuối cùng, KHÔNG nhắc lại yêu cầu, KHÔNG liệt kê chỉ dẫn, KHÔNG mở đầu bằng các câu như 'Nhiệm vụ', 'Dưới đây', 'Bài viết này sẽ'.
159
-
160
- Yêu cầu nội dung:
161
- - Tiêu đề hấp dẫn, cụ thể.
162
- - Sapo 2-3 câu đi thẳng vào chủ đề.
163
- - Các đoạn phân tích có kiến thức thực chất: bối cảnh, nguyên nhân, tác động, ví dụ, nhận định.
164
- - Nếu chủ đề là World Cup/thể thao: nói về lịch sử, ý nghĩa giải đấu, các yếu tố chuyên môn, văn hóa người hâm mộ, tác động kinh tế - xã hội.
165
- - Nếu không chắc thông tin thời sự mới, diễn đạt thận trọng.
166
- - Không sao chép nguyên văn, hãy tổng hợp và diễn đạt lại.
167
- - Cuối bài có mục 'Nguồn tham khảo' ngắn: Qwen2.5-VL / kiến thức tổng hợp.
168
- """
169
- text=await base.qwen_generate(prompt,image_url=img,max_tokens=1600)
170
- if not text:text=f"{topic}\n\n{topic} là một chủ đề có nhiều lớp ý nghĩa, từ bối cảnh hình thành đến tác động thực tế đối với xã hội, văn hóa và người quan tâm. Nội dung này tổng hợp các khía cạnh quan trọng để người đọc có cái nhìn rõ hơn về vấn đề.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
171
- # Clean leaked instructions if model echoes them.
172
- bad_patterns=[r'Chỉ xuất bản bài viết cuối cùng.*?Nguồn tham khảo ngắn\.?',r'Nhiệm vụ:\s*viết.*?nguồn bên dưới\.?',r'Không sao chép nguyên văn.*?diễn đạt lại\.?']
173
- for pat in bad_patterns:text=re.sub(pat,'',text,flags=re.I|re.S).strip()
174
- post=base.make_post(topic,text,img,'','topic_qwen',sources=[{'title':'Qwen2.5-VL / kiến thức tổng hợp','url':'','via':'Qwen2.5-VL'}]);post['images']=[img]
175
- posts=base._load_ai_wall();posts.insert(0,post);base._save_ai_wall(posts)
176
- return JSONResponse({'post':post})
177
-
178
- @app.post('/api/article/ask')
179
- async def article_ask_fixed(request:Request):
180
- body=await request.json();url=clean(body.get('url',''));question=clean(body.get('question',''));context=clean(body.get('context',''))
181
- if not question:return JSONResponse({'error':'missing question'},status_code=400)
182
- title='';raw=''
183
- try:
184
- if url:
185
- data=base.scrape_any_url(url)
186
- title=data.get('title','');raw=(data.get('summary','')+'\n'+data.get('text','')).strip()
187
- except Exception:pass
188
- ctx=(raw or context)[:12000]
189
- prompt=f"""Trả lời chi tiết câu hỏi của người dùng dựa trên bài viết.
190
-
191
- Tiêu đề: {title}
192
- Nội dung bài:
193
- {ctx}
194
-
195
- Câu hỏi: {question}
196
-
197
- Trả lời bằng tiếng Việt, bám sát nội dung. Nếu bài không có thông tin thì nói rõ.
198
- """
199
- ans=await base.qwen_generate(prompt,max_tokens=1200)
200
- if not ans:ans='AI chưa trả lời được lúc này. Bạn thử hỏi lại hoặc rút gọn câu hỏi.'
201
- return JSONResponse({'answer':ans,'title':title})
202
-
203
- PATCH_JS=r'''
204
- <style>.short-modal{position:fixed;inset:auto 0 0 0;max-height:60vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow:auto}.short-modal.active{display:block}.comment-item{background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px}</style>
205
- <div id="short-modal" class="short-modal"></div>
206
- <script>
207
- (function(){
208
- function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));}
209
- let shorts=[];
210
- async function loadShorts(){shorts=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);return shorts;}
211
- function actionPanel(kind,id){return `<div class="short-action-panel"><button class="short-action-btn" onclick="shortAct('${kind}','${id}','view')"><div class="ico">👁</div><span id="v-${kind}-${id}">0</span></button><button class="short-action-btn" onclick="shortAct('${kind}','${id}','like')"><div class="ico">❤️</div><span id="l-${kind}-${id}">0</span></button><button class="short-action-btn" onclick="openComments('${kind}','${id}')"><div class="ico">💬</div><span>BL</span></button><button class="short-action-btn" onclick="openAsk('${kind}','${id}')"><div class="ico">🤖</div><span>Hỏi</span></button><button class="short-action-btn" onclick="shareShortCtx('${kind}','${id}')"><div class="ico">📤</div><span>Share</span></button></div>`}
212
- window.openShortsFixed=async function(start){let arr=shorts.length?shorts:await loadShorts();if(!arr.length)return alert('Không tải được Shorts');let ordered=start>0?arr.slice(start).concat(arr.slice(0,start)):arr;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((v,i)=>{let id=v.id;let src='https://www.youtube.com/embed/'+id+'?autoplay=1&rel=0&playsinline=1';h+=`<div class="tiktok-slide" data-kind="yt" data-id="${id}" data-title="${esc(v.title)}" data-context="${esc('Video Shorts YouTube từ kênh '+(v.channel||'')+'. Tiêu đề: '+v.title)}"><iframe data-yt-src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">YT</span><p class="tiktok-title">${esc(v.title)}</p></div>${actionPanel('yt',id)}<span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;initFeed();}
213
- function initFeed(){let feed=document.getElementById('tiktok-feed');if(!feed)return;let slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{let fr=sl.querySelector('iframe'),v=sl.querySelector('video');if(idx===i){if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;if(v)v.play().catch(()=>{});shortAct(sl.dataset.kind,sl.dataset.id,'view').catch(()=>{})}else{if(fr&&fr.src)fr.src='';if(v)v.pause();}});cur=i}let t;feed.addEventListener('scroll',()=>{clearTimeout(t);t=setTimeout(()=>{let rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,b=-1,d=1e9;slides.forEach((sl,i)=>{let dd=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(dd<d){d=dd;b=i}});if(b>=0)act(b)},120)});setTimeout(()=>act(0),250)}
214
- window.shortAct=async function(kind,id,action,text=''){let sl=document.querySelector(`.tiktok-slide[data-id="${id}"]`);let body={id,kind,action,text,title:sl?.dataset.title||'',context:sl?.dataset.context||sl?.dataset.title||''};let r=await fetch('/api/ai/interact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});let j=await r.json();let st=j.stats||j;let v=document.getElementById(`v-${kind}-${id}`),l=document.getElementById(`l-${kind}-${id}`);if(v&&st.views!=null)v.textContent=st.views;if(l&&st.likes!=null)l.textContent=st.likes;return st;}
215
- window.openComments=function(kind,id){let m=document.getElementById('short-modal');m.innerHTML=`<h3>💬 Bình luận</h3><div id="comment-list">Đang tải...</div><textarea id="short-comment-text" style="width:100%;background:#222;color:#eee;border:1px solid #444;border-radius:10px;padding:8px" placeholder="Nhập bình luận..."></textarea><button onclick="submitComment('${kind}','${id}')">Gửi</button><button onclick="closeShortModal()">Đóng</button>`;m.classList.add('active');shortAct(kind,id,'noop').then(st=>{document.getElementById('comment-list').innerHTML=(st.comments||[]).map(c=>`<div class="comment-item">${esc(c.text)}</div>`).join('')||'<div class="comment-item">Chưa có bình luận</div>'})}
216
- window.submitComment=async function(kind,id){let t=document.getElementById('short-comment-text').value.trim();if(!t)return;let st=await shortAct(kind,id,'comment',t);document.getElementById('comment-list').innerHTML=(st.comments||[]).map(c=>`<div class="comment-item">${esc(c.text)}</div>`).join('')}
217
- window.openAsk=function(kind,id){let m=document.getElementById('short-modal');m.innerHTML=`<h3>🤖 Hỏi AI</h3><input id="short-ask-text" style="width:100%;background:#222;color:#eee;border:1px solid #444;border-radius:10px;padding:8px" placeholder="Bạn muốn hỏi gì?"><div id="short-answer"></div><button onclick="submitAsk('${kind}','${id}')">Hỏi</button><button onclick="closeShortModal()">Đóng</button>`;m.classList.add('active')}
218
- window.submitAsk=async function(kind,id){let t=document.getElementById('short-ask-text').value.trim();if(!t)return;let st=await shortAct(kind,id,'ask',t);let a=(st.asks&&st.asks[0]&&st.asks[0].a)||'Chưa có trả lời';document.getElementById('short-answer').innerHTML='<p style="white-space:pre-wrap;color:#ccc">'+esc(a)+'</p>'}
219
- window.closeShortModal=function(){document.getElementById('short-modal').classList.remove('active')}
220
- let oldOpen=window.openTikTok;window.openTikTok=function(type,start){if(type==='shorts')return openShortsFixed(start||0);return oldOpen?oldOpen(type,start):null;}
221
- setTimeout(async()=>{await loadShorts();document.querySelectorAll('.slider-label').forEach(label=>{if((label.textContent||'').includes('Shorts'))label.closest('.slider-wrap')?.querySelectorAll('.slider-item').forEach((el,i)=>el.setAttribute('onclick',`openShortsFixed(${i})`));});},800);
222
- })();
223
- </script>
224
- '''
225
-
226
- @app.get('/')
227
- async def index_patched_restore():
228
- html=base_runtime._load_index_html() if hasattr(base_runtime,'_load_index_html') else open('/app/static/index.html','r',encoding='utf-8').read()
229
- body=getattr(base_runtime.old,'PATCH_INJECT','')+getattr(base_runtime,'FINAL_INJECT','')+PATCH_JS
230
- return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
231
- '''
232
-
233
- with open(os.path.join(snapshot_dir, 'patched_restore.py'), 'w', encoding='utf-8') as f:
234
- f.write(PATCHED)
235
-
236
- cmd = [
237
- "uvicorn",
238
- "patched_restore:app",
239
- "--host",
240
- "0.0.0.0",
241
- "--port",
242
- "7860",
243
- ]
244
- os.execvp(cmd[0], cmd)
 
1
  import os
2
  import sys
 
3
  from huggingface_hub import snapshot_download
4
 
5
+ REVISION = os.environ.get("VNEWS_RESTORE_REVISION", "d7f41b2")
6
  REPO_ID = os.environ.get("VNEWS_REPO_ID", "bep40/vnews")
7
+ APP_MODULE = os.environ.get("VNEWS_RESTORE_APP", "restore_b9b6ba4_patch:app")
8
 
9
  snapshot_dir = snapshot_download(
10
  repo_id=REPO_ID,
 
17
  os.chdir(snapshot_dir)
18
  sys.path.insert(0, snapshot_dir)
19
 
20
+ import uvicorn
21
+ module, app_name = APP_MODULE.split(":", 1)
22
+ uvicorn.run(f"{module}:{app_name}", host="0.0.0.0", port=7860)