sdk / model_setup.py
gijl's picture
Upload model_setup.py
c95b2ff verified
#!/usr/bin/env python3
"""
خادم واجهة إعداد النموذج — يعمل على المنفذ 7860 قبل Open WebUI
يتيح تحميل النماذج من Hugging Face وتشغيلها دون إعادة البناء
"""
import http.server, threading, subprocess, os, json, sys
MODELS_DIR = "/data/models"
os.makedirs(MODELS_DIR, exist_ok=True)
state = {"status": "waiting", "message": "في انتظار إدخال رابط النموذج"}
httpd = None # يُعيَّن لاحقاً
HTML = """<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>مدير النماذج</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',Tahoma,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem}
.wrap{width:100%;max-width:620px}
h1{font-size:1.75rem;color:#38bdf8;margin-bottom:.25rem}
.sub{color:#64748b;margin-bottom:2rem;font-size:.9rem}
.card{background:#1e293b;border:1px solid #334155;border-radius:14px;padding:1.5rem;margin-bottom:1.25rem}
.card-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:#64748b;margin-bottom:1rem}
label{display:block;font-size:.8rem;color:#94a3b8;margin-bottom:.3rem}
input{width:100%;padding:.6rem .9rem;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#e2e8f0;font-size:.85rem;margin-bottom:.9rem;direction:ltr}
input:focus{outline:none;border-color:#38bdf8}
.btn{width:100%;padding:.75rem;border:none;border-radius:8px;font-size:.875rem;font-weight:600;cursor:pointer;transition:background .2s}
.btn-blue{background:#0ea5e9;color:#fff}.btn-blue:hover{background:#0284c7}
.btn-green{background:#059669;color:#fff;width:auto;padding:.4rem .85rem;font-size:.75rem}.btn-green:hover{background:#047857}
.btn:disabled{background:#334155;color:#475569;cursor:not-allowed}
.status{padding:.75rem 1rem;border-radius:8px;font-size:.85rem;margin-top:.75rem;display:none}
.info{background:#0c4a6e;color:#38bdf8}
.success{background:#064e3b;color:#34d399}
.error{background:#4c0519;color:#fb7185}
.loading{background:#1e1b4b;color:#818cf8}
.model-row{display:flex;align-items:center;gap:.75rem;padding:.7rem;background:#0f172a;border:1px solid #334155;border-radius:8px;margin-bottom:.5rem}
.model-name{font-size:.8rem;color:#94a3b8;flex:1;word-break:break-all;direction:ltr}
.empty{text-align:center;color:#475569;padding:1.5rem;font-size:.85rem}
@keyframes spin{to{transform:rotate(360deg)}}
.spin{display:inline-block;width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;margin-right:.4rem;vertical-align:middle}
</style>
</head>
<body>
<div class="wrap">
<h1>🤖 مدير النماذج</h1>
<p class="sub">حمّل نموذجاً من Hugging Face ثم شغّله — لا شيء يُحمَّل تلقائياً</p>
<div class="card">
<div class="card-title">النماذج المحفوظة في /data/models</div>
<div id="model-list"><div class="empty">لا توجد نماذج بعد</div></div>
</div>
<div class="card">
<div class="card-title">تحميل نموذج جديد</div>
<label>معرّف المستودع (Repo ID)</label>
<input id="repo" placeholder="gijl/gemma-4-E2B-it-GGUF" value="gijl/gemma-4-E2B-it-GGUF"/>
<label>اسم ملف النموذج (.gguf)</label>
<input id="file" placeholder="model.gguf" value="gemma-4-E2B-it-UD-Q5_K_XL.gguf"/>
<label>ملف الرؤية mmproj (اختياري — للنماذج المتعددة الوسائط)</label>
<input id="mmproj" placeholder="mmproj-BF16.gguf ← اتركه فارغاً إن لم تحتجه"/>
<button class="btn btn-blue" id="dl-btn" onclick="startDownload()">⬇️ تحميل النموذج</button>
<div id="dl-status" class="status"></div>
</div>
</div>
<script>
async function loadModels(){
const {files}=await fetch('/api/models').then(r=>r.json());
const el=document.getElementById('model-list');
if(!files.length){el.innerHTML='<div class="empty">لا توجد نماذج بعد</div>';return;}
el.innerHTML=files.map(f=>`
<div class="model-row">
<span class="model-name">${f}</span>
<button class="btn btn-green" onclick="launchModel('${f}')">▶ تشغيل</button>
</div>`).join('');
}
async function startDownload(){
const repo=document.getElementById('repo').value.trim();
const file=document.getElementById('file').value.trim();
const mmproj=document.getElementById('mmproj').value.trim();
if(!repo||!file)return;
document.getElementById('dl-btn').disabled=true;
showStatus('loading','<span class="spin"></span> جارٍ بدء التحميل...');
await fetch('/api/download',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({repo,file,mmproj})});
poll();
}
async function launchModel(file){
showStatus('loading','<span class="spin"></span> جارٍ تشغيل '+file+'...');
await fetch('/api/launch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({model:file,mmproj:''})});
poll();
}
function poll(){
const iv=setInterval(async()=>{
const d=await fetch('/api/status').then(r=>r.json());
if(d.status==='downloading') showStatus('loading','<span class="spin"></span> '+d.message);
else if(d.status==='done'){showStatus('success','✅ '+d.message);document.getElementById('dl-btn').disabled=false;loadModels();clearInterval(iv);}
else if(d.status==='error'){showStatus('error','❌ '+d.message);document.getElementById('dl-btn').disabled=false;clearInterval(iv);}
else if(d.status==='launching'){showStatus('loading','<span class="spin"></span> 🚀 '+d.message);clearInterval(iv);}
},2000);
}
function showStatus(type,msg){
const el=document.getElementById('dl-status');
el.className='status '+type;el.style.display='block';el.innerHTML=msg;
}
loadModels();
setInterval(loadModels,8000);
</script>
</body>
</html>"""
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *a): pass # إخفاء السجلات
def do_GET(self):
if self.path == '/api/status':
self.json(state)
elif self.path == '/api/models':
files = sorted([f for f in os.listdir(MODELS_DIR) if f.endswith('.gguf')]) if os.path.exists(MODELS_DIR) else []
self.json({"files": files})
else:
self.send_response(200)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write(HTML.encode('utf-8'))
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))))
if self.path == '/api/download':
threading.Thread(target=do_download, args=(body,), daemon=True).start()
self.json({"ok": True})
elif self.path == '/api/launch':
threading.Thread(target=do_launch, args=(body,), daemon=True).start()
self.json({"ok": True})
def json(self, data):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def do_download(body):
state['status'] = 'downloading'
state['message'] = f"جارٍ تحميل {body['file']} ..."
os.makedirs(MODELS_DIR, exist_ok=True)
files_to_dl = [body['file']]
if body.get('mmproj'):
files_to_dl.append(body['mmproj'])
for fname in files_to_dl:
state['message'] = f"جارٍ تحميل {fname} ..."
r = subprocess.run(
['hf', 'download', body['repo'], fname, '--local-dir', MODELS_DIR],
capture_output=True, text=True
)
if r.returncode != 0:
state['status'] = 'error'
state['message'] = r.stderr.strip() or 'فشل التحميل'
return
state['status'] = 'done'
state['message'] = f"اكتمل تحميل {body['file']} — اضغط تشغيل"
def do_launch(body):
state['status'] = 'launching'
state['message'] = 'جارٍ تشغيل llama.cpp وOpen WebUI...'
with open('/tmp/selected_model', 'w') as f:
f.write(body.get('model', ''))
with open('/tmp/selected_mmproj', 'w') as f:
f.write(body.get('mmproj', ''))
open('/tmp/launch_signal', 'w').close()
# إيقاف الخادم بشكل آمن من thread مختلف
threading.Thread(target=httpd.shutdown, daemon=True).start()
if __name__ == '__main__':
httpd = http.server.HTTPServer(('0.0.0.0', 7860), Handler)
print(">>> واجهة إعداد النموذج تعمل على http://0.0.0.0:7860", flush=True)
httpd.serve_forever()
print(">>> واجهة الإعداد أُغلقت، جارٍ تسليم المنفذ لـ Open WebUI...", flush=True)