| |
| """ |
| خادم واجهة إعداد النموذج — يعمل على المنفذ 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() |
|
|
| |
| 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) |
|
|