File size: 5,532 Bytes
52ca90a
 
 
 
2c2df71
52ca90a
 
2c2df71
52ca90a
 
 
 
 
 
 
 
 
 
2c2df71
 
 
52ca90a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c2df71
 
 
 
 
 
 
 
 
 
 
 
 
52ca90a
 
2c2df71
52ca90a
 
 
2c2df71
 
 
 
 
 
52ca90a
 
 
2c2df71
 
 
 
 
 
 
52ca90a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c2df71
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
import os
import subprocess
import time
import requests
import threading
from huggingface_hub import hf_hub_download
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, JSONResponse
import uvicorn

APP_DIR = "/app"
LLAMA_DIR = os.path.join(APP_DIR, "llama.cpp")
BIN = os.path.join(LLAMA_DIR, "build", "bin", "llama-server")
MODEL_DIR = os.path.join(APP_DIR, "models")
LLAMA_PORT = 8080

os.makedirs(MODEL_DIR, exist_ok=True)

# متغير عام لتتبع حالة الخادم الداخلي
backend_ready = False


def build_llama_cpp():
    """بناء llama.cpp من المصدر إن لم يكن الملف التنفيذي موجودًا مسبقًا."""
    if os.path.exists(BIN):
        print("[build] llama-server موجود مسبقًا، تخطي البناء.")
        return

    print("[build] استنساخ llama.cpp ...")
    subprocess.run(
        ["git", "clone", "--depth", "1",
         "https://github.com/ggml-org/llama.cpp", LLAMA_DIR],
        check=True,
    )

    print("[build] تهيئة CMake ...")
    subprocess.run(
        ["cmake", "-B", "build", "-S", ".", "-DGGML_NATIVE=OFF"],
        check=True, cwd=LLAMA_DIR,
    )

    print("[build] بناء llama-server (قد يستغرق عدة دقائق) ...")
    subprocess.run(
        ["cmake", "--build", "build", "--config", "Release",
         "-j", str(os.cpu_count() or 2), "--target", "llama-server"],
        check=True, cwd=LLAMA_DIR,
    )


def download_model():
    """تنزيل ملفات GGUF من Hugging Face Hub."""
    repo = "gijl/gemma-4-E4B-it-GGUF"
    print("[download] تنزيل النموذج الأساسي ...")
    model_path = hf_hub_download(
        repo_id=repo, filename="gemma-4-E4B-it-BF16.gguf", local_dir=MODEL_DIR
    )
    print("[download] تنزيل mmproj ...")
    mmproj_path = hf_hub_download(
        repo_id=repo, filename="mmproj-BF16.gguf", local_dir=MODEL_DIR
    )
    return model_path, mmproj_path


def start_llama_server(model_path: str, mmproj_path: str):
    """تشغيل llama-server محليًا على منفذ داخلي."""
    print("[run] تشغيل llama-server ...")
    proc = subprocess.Popen([
        BIN,
        "-m", model_path,
        "--mmproj", mmproj_path,
        "--host", "127.0.0.1",
        "--port", str(LLAMA_PORT),
        "-t", "2",
        "--cache-type-k", "q8_0",
        "--cache-type-v", "iq4_nl",
        "-c", "64800",
        "-n", "50912",
    ])
    return proc


def wait_for_server(timeout_seconds: int = 180):
    print("[run] بانتظار جاهزية الخادم ...")
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        try:
            r = requests.get(f"http://127.0.0.1:{LLAMA_PORT}/health", timeout=1)
            if r.status_code == 200:
                print("[run] الخادم جاهز.")
                return
        except Exception:
            pass
        time.sleep(1)
    print("[run] تحذير: انتهت مهلة الانتظار، سيتم المتابعة رغم ذلك.")


def setup_backend():
    """هذه الدالة تقوم بتجميع وتنزيل وتشغيل النموذج، وستعمل في الخلفية."""
    global backend_ready
    try:
        build_llama_cpp()
        model_path, mmproj_path = download_model()
        start_llama_server(model_path, mmproj_path)
        wait_for_server()
        backend_ready = True
        print("[setup] اكتمل إعداد النموذج وهو جاهز الآن لاستقبال الطلبات.")
    except Exception as e:
        print(f"[setup] حدث خطأ أثناء إعداد النموذج: {e}")


# ---------------------------------------------------------------------------
# إعداد الواجهة (تطبيق FastAPI)
# ---------------------------------------------------------------------------
app = FastAPI()

@app.on_event("startup")
def startup_event():
    # تشغيل عملية التنزيل والبناء في مسار منفصل (Thread)
    # هذا يضمن أن يعمل تطبيق FastAPI فوراً دون انتظار التنزيل
    thread = threading.Thread(target=setup_backend)
    thread.start()

@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
async def proxy(path: str, request: Request):
    # إذا لم يكتمل تنزيل النموذج بعد، يتم إرجاع رسالة توضيحية بدلاً من خطأ اتصال
    if not backend_ready:
        return JSONResponse(
            status_code=503,
            content={"error": "النموذج لا يزال قيد التنزيل والإعداد في الخلفية. يرجى المحاولة بعد قليل."}
        )

    url = f"http://127.0.0.1:{LLAMA_PORT}/{path}"
    body = await request.body()
    forward_headers = {
        k: v for k, v in request.headers.items()
        if k.lower() not in ("host", "content-length")
    }
    resp = requests.request(
        method=request.method,
        url=url,
        headers=forward_headers,
        params=request.query_params,
        data=body,
        stream=True,
    )
    response_headers = {
        k: v for k, v in resp.headers.items()
        if k.lower() != "content-encoding"
    }
    return StreamingResponse(
        resp.iter_content(chunk_size=1024),
        status_code=resp.status_code,
        headers=response_headers,
    )


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 7860))
    uvicorn.run(app, host="0.0.0.0", port=port)