File size: 16,025 Bytes
f63af94
35ff464
f63af94
 
8c3fd4f
 
 
 
 
 
35ff464
f63af94
8c3fd4f
 
 
35ff464
8c3fd4f
f63af94
35ff464
8c3fd4f
 
35ff464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c3fd4f
 
 
 
 
 
35ff464
 
 
 
 
 
 
 
8c3fd4f
 
35ff464
 
 
8c3fd4f
 
 
 
 
35ff464
8c3fd4f
 
 
35ff464
8c3fd4f
35ff464
8c3fd4f
 
 
 
 
 
35ff464
8c3fd4f
 
 
 
 
f63af94
8c3fd4f
35ff464
8c3fd4f
35ff464
 
 
 
 
 
8c3fd4f
 
 
 
35ff464
 
 
8c3fd4f
f63af94
8c3fd4f
 
f63af94
35ff464
8c3fd4f
 
 
35ff464
8c3fd4f
35ff464
8c3fd4f
 
35ff464
 
8c3fd4f
 
35ff464
8c3fd4f
35ff464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c3fd4f
 
 
35ff464
8c3fd4f
35ff464
8c3fd4f
 
 
 
f63af94
35ff464
 
 
 
 
 
8c3fd4f
65cfaf3
 
c2c7342
8c3fd4f
 
 
 
 
35ff464
8c3fd4f
 
 
 
 
f63af94
8c3fd4f
 
35ff464
 
8c3fd4f
 
 
35ff464
 
 
8c3fd4f
 
 
f63af94
35ff464
8c3fd4f
 
f63af94
35ff464
f63af94
8c3fd4f
 
 
 
 
 
 
 
 
35ff464
8c3fd4f
35ff464
8c3fd4f
 
 
 
35ff464
 
 
 
 
 
 
8c3fd4f
35ff464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c3fd4f
 
35ff464
 
 
 
 
 
8c3fd4f
35ff464
8c3fd4f
35ff464
8c3fd4f
 
35ff464
 
 
 
8c3fd4f
35ff464
f63af94
35ff464
 
 
 
 
 
8c3fd4f
35ff464
 
 
8c3fd4f
 
35ff464
 
 
 
f63af94
 
8c3fd4f
35ff464
 
 
 
 
 
8c3fd4f
f63af94
 
35ff464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f63af94
35ff464
 
 
 
 
 
 
 
 
 
 
 
 
f63af94
 
 
35ff464
 
8c3fd4f
35ff464
f63af94
 
8c3fd4f
35ff464
8c3fd4f
f63af94
35ff464
8c3fd4f
 
 
 
 
 
 
 
 
35ff464
8c3fd4f
35ff464
8c3fd4f
 
 
 
35ff464
 
 
 
8c3fd4f
 
 
35ff464
 
 
 
 
 
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# =============================================================================
# كود Ollama لـ Hugging Face Space مع Ngrok (نسخة محسّنة ضد Timeout)
# =============================================================================

import sys
import subprocess
import time
import os
import signal
import gradio as gr
import socket

# -----------------------------------------------------------------------------
# الجزء الأول: تثبيت المكتبات الضرورية و Ollama
# -----------------------------------------------------------------------------
print("✅ [الخطوة 1/6]: تثبيت المكتبات الضرورية و Ollama...")
sys.stdout.flush()

# تثبيت pyngrok بمحاولات متعددة
print("    - تثبيت pyngrok...", end="")
sys.stdout.flush()
max_retries = 3
for attempt in range(max_retries):
    try:
        subprocess.run(
            [sys.executable, '-m', 'pip', 'install', 'pyngrok', '-q', '--no-cache-dir'],
            check=True,
            timeout=120
        )
        print(" تم.")
        sys.stdout.flush()
        break
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
        if attempt < max_retries - 1:
            print(f" فشل (محاولة {attempt + 1}/{max_retries})، إعادة المحاولة...")
            time.sleep(5)
        else:
            print(f"\n[❌ خطأ فادح]: فشل تثبيت pyngrok بعد {max_retries} محاولات.")
            sys.exit(1)

# تثبيت Ollama
print("    - تثبيت Ollama بالطريقة الرسمية...")
sys.stdout.flush()
try:
    install_command = "curl -fsSL https://ollama.com/install.sh | sh"
    result = subprocess.run(
        install_command,
        shell=True,
        check=True,
        capture_output=True,
        text=True,
        timeout=300
    )
    print("    - تم تثبيت Ollama بنجاح.")
    sys.stdout.flush()
except subprocess.TimeoutExpired:
    print("\n[❌ خطأ فادح]: انتهت مهلة تثبيت Ollama (300 ثانية).")
    sys.exit(1)
except subprocess.CalledProcessError as e:
    print(f"\n[❌ خطأ فادح]: فشل تثبيت Ollama. رمز الخروج: {e.returncode}")
    print(e.stderr)
    sys.exit(1)

print("✅ [الخطوة 1/6]: تم تثبيت المكتبات و Ollama بنجاح!\n")
sys.stdout.flush()

# -----------------------------------------------------------------------------
# الجزء الثاني: تشغيل خادم Ollama أولاً
# -----------------------------------------------------------------------------
print("✅ [الخطوة 2/6]: تشغيل خادم Ollama...")
sys.stdout.flush()

os.environ['OLLAMA_HOST'] = '0.0.0.0:11434'

ollama_serve_process = None
try:
    print("    - تشغيل خادم Ollama في الخلفية...", end="")
    ollama_serve_process = subprocess.Popen(
        ['ollama', 'serve'],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        preexec_fn=os.setsid
    )
    print(" جاري البدء...", end="")
    time.sleep(15)

    check_process = subprocess.run(
        ['ollama', 'list'],
        capture_output=True,
        text=True,
        timeout=30
    )
    if check_process.returncode == 0:
        print(" يعمل بنجاح.")
    else:
        print(f"\n    - [❌] فشل تشغيل خادم Ollama بشكل صحيح.")
        if ollama_serve_process.stderr:
            stderr_output = ollama_serve_process.stderr.read().decode('utf-8')
            print(f"      الخطأ من الخادم: {stderr_output.strip()}")
        raise Exception("الخادم لم يبدأ بشكل صحيح.")
except Exception as e:
    print(f"\n[❌ خطأ فادح]: فشل تشغيل خادم Ollama. الخطأ: {e}")
    sys.exit(1)

print("✅ [الخطوة 2/6]: تم تشغيل خادم Ollama بنجاح.\n")
sys.stdout.flush()

# -----------------------------------------------------------------------------
# الجزء الثالث: إعداد Ngrok مع معالجة محسّنة للـ Timeout
# -----------------------------------------------------------------------------
print("✅ [الخطوة 3/6]: إعداد Ngrok مع حماية من Timeout...")
sys.stdout.flush()

NGROK_AUTH_TOKEN = os.getenv("NGROK_AUTH_TOKEN", "")

ngrok_tunnel = None
public_url_str = None
ngrok_setup_success = False

if not NGROK_AUTH_TOKEN:
    print("⚠️ [تحذير]: لم يتم العثور على NGROK_AUTH_TOKEN في Secrets")
    print("   سيتم تخطي إعداد Ngrok. يمكنك استخدام الخادم محلياً فقط.")
    print("   لإضافة التوكن: Settings → Repository secrets → NGROK_AUTH_TOKEN\n")
else:
    try:
        print("    - استيراد pyngrok...", end="")
        import pyngrok
        from pyngrok import ngrok, conf
        print(" تم.")
        
        # زيادة timeout لـ ngrok
        print("    - تكوين إعدادات ngrok...", end="")
        pyngrok_config = conf.get_default()
        pyngrok_config.start_timeout = 180  # 3 دقائق
        pyngrok_config.request_timeout = 30
        print(" تم.")
        
        print("    - تعيين Auth Token...", end="")
        ngrok.set_auth_token(NGROK_AUTH_TOKEN)
        print(" تم.")
        
        print("    - إنشاء نفق ngrok (قد يستغرق دقائق)...", end="")
        sys.stdout.flush()
        
        # محاولة الاتصال مع retry
        for attempt in range(3):
            try:
                ngrok_tunnel = ngrok.connect(
                    11434,
                    "http",
                    bind_tls=True
                )
                public_url_str = ngrok_tunnel.public_url
                ngrok_setup_success = True
                print(" تم بنجاح!")
                break
            except Exception as conn_error:
                if attempt < 2:
                    print(f"\n    محاولة {attempt + 1} فشلت، إعادة المحاولة بعد 10 ثواني...")
                    time.sleep(10)
                else:
                    raise conn_error
        
        if ngrok_setup_success:
            print(f"\n🔗 الـ API متاح الآن على الرابط العام التالي:")
            print(f"   {public_url_str}\n")
            sys.stdout.flush()
            
    except ImportError as e:
        print(f"\n⚠️ [تحذير]: فشل استيراد pyngrok: {e}")
        print("   سيتم تشغيل الخادم محلياً فقط.\n")
    except Exception as e:
        error_msg = str(e)
        if "timeout" in error_msg.lower() or "timed out" in error_msg.lower():
            print(f"\n⚠️ [تحذير]: انتهت مهلة الاتصال بـ ngrok: {error_msg}")
            print("   الأسباب المحتملة:")
            print("   1. اتصال الإنترنت بطيء في Hugging Face Space")
            print("   2. خوادم ngrok مشغولة")
            print("   3. جدار حماية يمنع الاتصال")
            print("\n   الحلول:")
            print("   ✓ أعد تشغيل Space")
            print("   ✓ استخدم Cloudflare Tunnel بدلاً من ngrok")
            print("   ✓ استخدم الخادم محلياً داخل Hugging Face\n")
        else:
            print(f"\n⚠️ [تحذير]: فشل إعداد Ngrok: {e}")
            print("   سيتم تشغيل الخادم محلياً فقط.\n")

if ngrok_setup_success:
    print("✅ [الخطوة 3/6]: تم تشغيل النفق بنجاح!\n")
else:
    print("⚠️ [الخطوة 3/6]: تم تخطي Ngrok، الخادم يعمل محلياً فقط.\n")
sys.stdout.flush()

# -----------------------------------------------------------------------------
# الجزء الرابع: فحص مساحة التخزين
# -----------------------------------------------------------------------------
print("✅ [الخطوة 4/6]: فحص مساحة التخزين...")
sys.stdout.flush()
subprocess.run(['df', '-h', '/'])
print("")
sys.stdout.flush()

# -----------------------------------------------------------------------------
# الجزء الخامس: سحب النماذج
# -----------------------------------------------------------------------------
print("✅ [الخطوة 5/6]: سحب النماذج المطلوبة...")
sys.stdout.flush()

models_to_pull = [
    # "hf.co/unsloth/Olmo-3-32B-Think-GGUF:Q4_0",
    # "hf.co/unsloth/Qwen3-VL-30B-A3B-Thinking-1M-GGUF:Q4_0",
    "seamon67/Ministral-3-Reasoning:14b"
]

successfully_pulled = []
failed_to_pull = []

print("    - بدء سحب النماذج بشكل تسلسلي...\n")
sys.stdout.flush()

for model_name in models_to_pull:
    print(f"--- [⏳] جاري سحب النموذج: {model_name} ---")
    sys.stdout.flush()
    try:
        subprocess.run(
            ['ollama', 'pull', model_name],
            check=True,
            timeout=1800  # 30 دقيقة لكل نموذج
        )
        print(f"--- [✔️] تم سحب النموذج {model_name} بنجاح ---\n")
        successfully_pulled.append(model_name)
    except subprocess.TimeoutExpired:
        print(f"--- [❌] انتهت مهلة سحب النموذج {model_name} ---\n")
        failed_to_pull.append(model_name)
    except subprocess.CalledProcessError as e:
        print(f"--- [❌] فشل سحب النموذج {model_name}. رمز الخروج: {e.returncode} ---\n")
        failed_to_pull.append(model_name)
    except Exception as e:
        print(f"--- [❌] خطأ استثنائي: {e} ---\n")
        failed_to_pull.append(model_name)
    sys.stdout.flush()

print("✅ [الخطوة 5/6]: انتهت عملية سحب النماذج!\n")

print("--- ملخص عملية السحب ---")
if successfully_pulled:
    print(f"✔️ نماذج تم سحبها بنجاح ({len(successfully_pulled)}): {', '.join(successfully_pulled)}")
if failed_to_pull:
    print(f"❌ نماذج فشل سحبها ({len(failed_to_pull)}): {', '.join(failed_to_pull)}")
print("-------------------------\n")
sys.stdout.flush()

# -----------------------------------------------------------------------------
# الجزء السادس: إنشاء واجهة Gradio
# -----------------------------------------------------------------------------
print("✅ [الخطوة 6/6]: إنشاء واجهة Gradio...")

def get_models_list():
    """الحصول على قائمة النماذج المثبتة"""
    try:
        result = subprocess.run(
            ['ollama', 'list'],
            capture_output=True,
            text=True,
            timeout=10
        )
        return result.stdout if result.returncode == 0 else "❌ خطأ في الحصول على القائمة"
    except Exception as e:
        return f"❌ خطأ: {e}"

def get_server_status():
    """الحصول على حالة الخادم"""
    status = "🟢 الخادم يعمل\n\n"
    
    # حالة Ollama
    try:
        result = subprocess.run(['ollama', 'list'], capture_output=True, timeout=5)
        if result.returncode == 0:
            status += "✅ Ollama: يعمل\n"
        else:
            status += "❌ Ollama: لا يعمل\n"
    except:
        status += "❌ Ollama: خطأ في الفحص\n"
    
    # حالة Ngrok
    if ngrok_setup_success and public_url_str:
        status += f"✅ Ngrok: متصل\n"
        status += f"🔗 الرابط العام: {public_url_str}\n"
    else:
        status += "⚠️ Ngrok: غير متصل (محلي فقط)\n"
    
    return status

# إنشاء محتوى الواجهة
if ngrok_setup_success and public_url_str:
    api_url = public_url_str
    api_url_v1 = f"{public_url_str}/v1"
    connection_status = "🟢 متصل بالإنترنت عبر Ngrok"
else:
    api_url = "http://localhost:11434"
    api_url_v1 = "http://localhost:11434/v1"
    connection_status = "🟡 يعمل محلياً فقط (داخل Hugging Face Space)"

instructions = f"""
# ✨ خادم Ollama على Hugging Face Space ✨

## 📊 حالة الاتصال
{connection_status}

## 🔗 روابط API:

### الرابط الأساسي:
```
{api_url}
```

### رابط OpenAI Compatible:
```
{api_url_v1}
```

## 📋 تعليمات الاستخدام:

### في RikkaHub (إذا كان Ngrok يعمل):
1. انسخ الرابط: `{api_url_v1}`
2. اذهب إلى Provider Settings
3. اختر: OpenAI-Compatible
4. Base URL: الصق الرابط
5. Model: اسم النموذج (مثال: `hf.co/Mungert/VibeThinker-1.5B-GGUF:BF16`)

### اختبار باستخدام curl:
```bash
curl {api_url}/api/tags
```

## ⚠️ ملاحظات:
- إذا فشل Ngrok، يمكنك استخدام الخادم محلياً داخل HF فقط
- أعد تشغيل Space إذا واجهت مشاكل
- استخدم نماذج صغيرة للأداء الأفضل
"""

# إنشاء واجهة Gradio
with gr.Blocks(title="Ollama Server", theme=gr.themes.Soft(), css="""
    .status-box { padding: 15px; border-radius: 8px; background: #f0f0f0; }
    .warning-box { padding: 15px; border-radius: 8px; background: #fff3cd; border-left: 4px solid #ffc107; }
    .success-box { padding: 15px; border-radius: 8px; background: #d4edda; border-left: 4px solid #28a745; }
""") as demo:
    
    gr.Markdown(instructions)
    
    with gr.Row():
        with gr.Column(scale=1):
            status_btn = gr.Button("🔄 تحديث حالة الخادم", size="sm")
            status_output = gr.Textbox(
                label="حالة الخادم",
                value=get_server_status(),
                lines=6,
                interactive=False
            )
        
        with gr.Column(scale=1):
            models_btn = gr.Button("🔄 تحديث قائمة النماذج", size="sm")
            models_output = gr.Textbox(
                label="النماذج المثبتة",
                value=get_models_list(),
                lines=6,
                interactive=False
            )
    
    status_btn.click(fn=get_server_status, outputs=status_output)
    models_btn.click(fn=get_models_list, outputs=models_output)
    
    if not ngrok_setup_success:
        gr.Markdown("""
        <div class="warning-box">
        <h3>⚠️ تنبيه: Ngrok غير متصل</h3>
        <p>فشل الاتصال بـ Ngrok بسبب timeout. الخادم يعمل محلياً فقط.</p>
        <p><strong>الحلول الممكنة:</strong></p>
        <ul>
            <li>أعد تشغيل Space (قد ينجح في المحاولة الثانية)</li>
            <li>تحقق من إضافة NGROK_AUTH_TOKEN في Secrets بشكل صحيح</li>
            <li>استخدم بديل مثل Cloudflare Tunnel</li>
        </ul>
        </div>
        """)
    
    gr.Markdown("""
    ---
    ### 💡 نصائح:
    - النماذج الصغيرة (1B-3B) أسرع على CPU
    - راقب استخدام الذاكرة في Logs
    - أعد تشغيل Space إذا حدثت مشاكل
    """)

print("✅ تم إنشاء واجهة Gradio بنجاح!")
print("⏳ الخادم جاهز للاستخدام...\n")
sys.stdout.flush()

# إطلاق Gradio
if __name__ == "__main__":
    try:
        demo.launch(
            server_name="0.0.0.0",
            server_port=7860,
            share=False,
            show_error=True
        )
    except KeyboardInterrupt:
        print('\n⏳ إيقاف...')
    finally:
        # تنظيف العمليات
        if ollama_serve_process:
            try:
                pgid = os.getpgid(ollama_serve_process.pid)
                os.killpg(pgid, signal.SIGTERM)
                print("✓ تم إيقاف Ollama")
            except:
                pass
        
        if ngrok_tunnel:
            try:
                ngrok.disconnect(public_url_str)
                ngrok.kill()
                print("✓ تم إيقاف Ngrok")
            except:
                pass
        
        print('✅ تم الإيقاف بنجاح')