File size: 9,020 Bytes
c95b2ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/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)