Spaces:
Running
Running
Add stable runtime entrypoint to recover Space from restore snapshot error
Browse files- ai_runtime_stable.py +214 -0
ai_runtime_stable.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stable entrypoint for VNEWS.
|
| 2 |
+
|
| 3 |
+
Purpose: avoid restore_runner snapshot import errors. It imports the most recent
|
| 4 |
+
available runtime that is known to exist in the current repo, then adds only safe
|
| 5 |
+
compatibility routes if needed.
|
| 6 |
+
"""
|
| 7 |
+
import os, re, json, time, requests
|
| 8 |
+
from urllib.parse import urlparse, quote
|
| 9 |
+
|
| 10 |
+
# ai_runtime_final5 exists in current repo and imports its dependency chain correctly.
|
| 11 |
+
# Later final6/final7 had cross-module import fragility, so use final5 as stable base.
|
| 12 |
+
try:
|
| 13 |
+
import ai_runtime_final5 as stable
|
| 14 |
+
except Exception:
|
| 15 |
+
import ai_runtime_final4 as stable
|
| 16 |
+
|
| 17 |
+
app = stable.app
|
| 18 |
+
base = stable.base
|
| 19 |
+
rt = stable.rt
|
| 20 |
+
HTMLResponse = stable.HTMLResponse
|
| 21 |
+
JSONResponse = stable.JSONResponse
|
| 22 |
+
Request = stable.Request
|
| 23 |
+
Query = stable.Query
|
| 24 |
+
|
| 25 |
+
DATA_DIR = "/data" if os.path.isdir('/data') else "/app/data"
|
| 26 |
+
AI_INTERACTIONS_FILE = os.path.join(DATA_DIR, 'ai_interactions.json')
|
| 27 |
+
AI_SHORT_INDEX_FILE = os.path.join(DATA_DIR, 'ai_short_index.json')
|
| 28 |
+
SHORTS_CACHE = {'t': 0, 'd': []}
|
| 29 |
+
CHANNELS = ['baodantri7941', 'baosuckhoedoisongboyte']
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def clean(s):
|
| 33 |
+
import html as html_lib
|
| 34 |
+
return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _load(path, default):
|
| 38 |
+
try:
|
| 39 |
+
if os.path.exists(path):
|
| 40 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 41 |
+
return json.load(f)
|
| 42 |
+
except Exception:
|
| 43 |
+
pass
|
| 44 |
+
return default
|
| 45 |
+
|
| 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:
|
| 52 |
+
json.dump(data, f, ensure_ascii=False)
|
| 53 |
+
os.replace(tmp, path)
|
| 54 |
+
except Exception:
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _domain(u):
|
| 59 |
+
try:
|
| 60 |
+
return urlparse(u or '').netloc.replace('www.', '')
|
| 61 |
+
except Exception:
|
| 62 |
+
return ''
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _fallback_shorts():
|
| 66 |
+
hard = [
|
| 67 |
+
('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ô | Dân trí','baodantri7941'),
|
| 68 |
+
('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'),
|
| 69 |
+
('tvPewsc2ph4','Tính năng ẩn trên iPhone giúp giảm mỏi mắt | Dân trí','baodantri7941'),
|
| 70 |
+
('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'),
|
| 71 |
+
('7Pd6vZ2Lz1M','Hành động ấm lòng trong tìm kiếm học sinh tử vong ở sông Lô | SKĐS','baosuckhoedoisongboyte'),
|
| 72 |
+
('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'),
|
| 73 |
+
('IUOprcJyYr4','Phụ nữ táo bón có phải do lười ăn rau? | SKĐS','baosuckhoedoisongboyte'),
|
| 74 |
+
('YY8ojFNE-AU','Quái xế tự quay clip nẹt pô, đánh võng đăng TikTok bị xử lý | SKĐS','baosuckhoedoisongboyte'),
|
| 75 |
+
]
|
| 76 |
+
return [{'id':vid,'title':title,'channel':ch,'link':'https://www.youtube.com/watch?v='+vid,'img':'https://i.ytimg.com/vi/'+vid+'/hqdefault.jpg','source':'yt'} for vid,title,ch in hard]
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _yt_html(handle, count=24):
|
| 80 |
+
try:
|
| 81 |
+
html = requests.get(f'https://www.youtube.com/@{handle}/shorts', headers=getattr(base,'HEADERS',{}), timeout=10).text
|
| 82 |
+
ids=[]; out=[]
|
| 83 |
+
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"', html):
|
| 84 |
+
vid=m.group(1)
|
| 85 |
+
if vid in ids: continue
|
| 86 |
+
ids.append(vid)
|
| 87 |
+
snip=html[max(0,m.start()-900):m.start()+1600]
|
| 88 |
+
mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip) or re.search(r'"accessibilityText":"([^"]+)"',snip)
|
| 89 |
+
title=clean((mt.group(1) if mt else 'YouTube Short').replace('\\n',' '))
|
| 90 |
+
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'})
|
| 91 |
+
if len(out)>=count: break
|
| 92 |
+
return out
|
| 93 |
+
except Exception:
|
| 94 |
+
return []
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _fresh_shorts():
|
| 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 selected routes from base runtime and replace with safe versions.
|
| 109 |
+
_PATCH={('/api/shorts','GET'),('/api/ai/interact','POST'),('/api/topic_post','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 |
+
|
| 113 |
+
@app.get('/api/shorts')
|
| 114 |
+
def api_shorts_stable(refresh:int=Query(default=0)):
|
| 115 |
+
now=time.time()
|
| 116 |
+
if not refresh and SHORTS_CACHE['d'] and now-SHORTS_CACHE['t']<300:
|
| 117 |
+
return JSONResponse(SHORTS_CACHE['d'])
|
| 118 |
+
data=_fresh_shorts(); SHORTS_CACHE.update({'t':now,'d':data})
|
| 119 |
+
return JSONResponse(data)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@app.post('/api/ai/interact')
|
| 123 |
+
async def ai_interact_stable(request:Request):
|
| 124 |
+
body=await request.json()
|
| 125 |
+
pid=str(body.get('id','')).strip(); kind=str(body.get('kind','wall')).strip(); action=str(body.get('action','')).strip()
|
| 126 |
+
text=clean(body.get('text','')); title=clean(body.get('title','')); context=clean(body.get('context',''))
|
| 127 |
+
if not pid: return JSONResponse({'error':'missing id'},status_code=400)
|
| 128 |
+
db=_load(AI_INTERACTIONS_FILE,{})
|
| 129 |
+
key=kind+':'+pid
|
| 130 |
+
st=db.get(key) or {'views':0,'likes':0,'comments':[],'asks':[]}
|
| 131 |
+
if action=='view': st['views']=int(st.get('views',0))+1
|
| 132 |
+
elif action=='like': st['likes']=int(st.get('likes',0))+1
|
| 133 |
+
elif action=='comment' and text:
|
| 134 |
+
st.setdefault('comments',[]).insert(0,{'text':text[:300],'ts':int(time.time())}); st['comments']=st['comments'][:100]
|
| 135 |
+
elif action=='ask' and text:
|
| 136 |
+
if kind in ('ai','short','wall'):
|
| 137 |
+
posts=base._load_ai_wall(); p=next((x for x in posts if str(x.get('id'))==pid),{})
|
| 138 |
+
title=title or p.get('title',''); context=context or p.get('text','')
|
| 139 |
+
if not context: context=title or pid
|
| 140 |
+
prompt=f"""Bạn là trợ lý VNEWS. 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.
|
| 141 |
+
|
| 142 |
+
Tiêu đề: {title}
|
| 143 |
+
Ngữ cảnh/mô tả: {context[:6000]}
|
| 144 |
+
Câu hỏi: {text}
|
| 145 |
+
|
| 146 |
+
Trả lời bằng tiếng Việt, cụ thể, có giải thích. Nếu là short YouTube chỉ có tiêu đề, hãy nói rõ bạn suy luận từ tiêu đề/mô tả và không giả vờ đã xem toàn bộ video.
|
| 147 |
+
"""
|
| 148 |
+
ans=await base.qwen_generate(prompt,max_tokens=900)
|
| 149 |
+
if not ans: ans='AI chưa trả lời được lúc này. Bạn thử hỏi cụ thể hơn.'
|
| 150 |
+
st.setdefault('asks',[]).insert(0,{'q':text[:300],'a':ans[:1800],'ts':int(time.time())}); st['asks']=st['asks'][:60]
|
| 151 |
+
db[key]=st; _save(AI_INTERACTIONS_FILE,db)
|
| 152 |
+
return JSONResponse({'stats':st})
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@app.post('/api/topic_post')
|
| 156 |
+
async def topic_post_stable(request:Request):
|
| 157 |
+
body=await request.json(); topic=clean(body.get('topic',''))
|
| 158 |
+
if not topic: return JSONResponse({'error':'missing topic'},status_code=400)
|
| 159 |
+
try: img=base.pollinations_image_url(topic)
|
| 160 |
+
except Exception: img='https://image.pollinations.ai/prompt/'+quote('Vietnamese editorial illustration '+topic)+'?width=1024&height=576&nologo=true'
|
| 161 |
+
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}
|
| 162 |
+
|
| 163 |
+
Chỉ xuất bản nội dung cuối cùng. Không lập dàn ý, không nói chung chung, không hướng dẫn cách viết.
|
| 164 |
+
|
| 165 |
+
Yêu cầu:
|
| 166 |
+
- Tiêu đề hấp dẫn, cụ thể.
|
| 167 |
+
- Sapo 2-3 câu đi thẳng vào chủ đề.
|
| 168 |
+
- 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.
|
| 169 |
+
- Nếu là thể thao: nói về nhân vật/đội bóng, chuyên môn, lịch sử, ý nghĩa.
|
| 170 |
+
- Nếu là công nghệ/xã hội/giáo dục: nói về cơ chế, ứng dụng, lợi ích/rủi ro.
|
| 171 |
+
- Tránh bịa số liệu thời sự mới; nếu không chắc, diễn đạt thận trọng.
|
| 172 |
+
- Cuối bài có mục Nguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp.
|
| 173 |
+
"""
|
| 174 |
+
text=await base.qwen_generate(prompt,image_url=img,max_tokens=1600)
|
| 175 |
+
if not text: text=f"{topic}\n\n{topic} là một chủ đề có nhiều lớp ý nghĩa, cần nhìn từ bối cảnh, tác động và các hiểu lầm thường gặp. Nội dung này tổng hợp các điểm quan trọng để người đọc có cái nhìn rõ hơn.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
|
| 176 |
+
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'}])
|
| 177 |
+
post['images']=[img]
|
| 178 |
+
posts=base._load_ai_wall(); posts.insert(0,post); base._save_ai_wall(posts)
|
| 179 |
+
return JSONResponse({'post':post})
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
PATCH_JS=r'''
|
| 183 |
+
<style>
|
| 184 |
+
#ai-wall-topic-live,#ai-wall-patched,#ai-shorts-patched{display:none!important}.topic-final3,.topic-final4{display:none!important}.topic-final5{display:flex!important}.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}
|
| 185 |
+
</style>
|
| 186 |
+
<div id="short-modal" class="short-modal"></div>
|
| 187 |
+
<script>
|
| 188 |
+
(function(){
|
| 189 |
+
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
| 190 |
+
let shorts=[];
|
| 191 |
+
async function loadShorts(){shorts=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);return shorts;}
|
| 192 |
+
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>`}
|
| 193 |
+
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();}
|
| 194 |
+
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)}
|
| 195 |
+
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;}
|
| 196 |
+
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>'})}
|
| 197 |
+
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('')}
|
| 198 |
+
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')}
|
| 199 |
+
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>'}
|
| 200 |
+
window.closeShortModal=function(){document.getElementById('short-modal').classList.remove('active')}
|
| 201 |
+
let oldOpen=window.openTikTok;window.openTikTok=function(type,start){if(type==='shorts')return openShortsFixed(start||0);return oldOpen?oldOpen(type,start):null;}
|
| 202 |
+
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);
|
| 203 |
+
})();
|
| 204 |
+
</script>
|
| 205 |
+
'''
|
| 206 |
+
|
| 207 |
+
@app.get('/')
|
| 208 |
+
async def index_stable():
|
| 209 |
+
try:
|
| 210 |
+
html=stable._load_index_html()
|
| 211 |
+
except Exception:
|
| 212 |
+
with open('/app/static/index.html','r',encoding='utf-8') as f: html=f.read()
|
| 213 |
+
body=getattr(stable.rt.old,'PATCH_INJECT','')+getattr(stable,'FINAL_INJECT','')+getattr(stable,'FINAL5_INJECT','')+PATCH_JS
|
| 214 |
+
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)
|