Deepmind / app.py
bh-ab's picture
Update app.py
5fcc4a8 verified
Raw History Blame Contribute Delete
16.5 kB
import os, io, re, gc, json, time, base64, threading
from fnmatch import fnmatch
import numpy as np
import torch
import soundfile as sf
from PIL import Image
from gradio import Server
from fastapi.responses import HTMLResponse
from huggingface_hub import snapshot_download, HfApi, constants
from transformers import (AutoTokenizer, AutoModelForCausalLM, AutoModelForSeq2SeqLM,
WhisperProcessor, WhisperForConditionalGeneration,
VisionEncoderDecoderModel, ViTImageProcessor,
TextIteratorStreamer, StoppingCriteria, StoppingCriteriaList)
try: # ZeroGPU على Hugging Face (اختياري)
import spaces
GPU = spaces.GPU
except Exception:
def GPU(duration=60):
return lambda f: f
app = Server()
HERE = os.path.dirname(os.path.abspath(__file__))
# ---- إعدادات الستريمينج: نجمع التوكنات ثم نرسلها دفعة واحدة ليبدو الرد سلساً وسريعاً ----
BATCH_TOKENS = int(os.environ.get("BATCH_TOKENS", 6)) # عدد القطع قبل كل إرسال
BATCH_SECONDS = float(os.environ.get("BATCH_SECONDS", 0.25)) # أو أقصى انتظار بين إرسالين
MAX_HISTORY = 12 # آخر N رسالة تُرسل للنموذج
@GPU(duration=10)
def _gpu_marker(): # ZeroGPU يشترط وجود دالة واحدة على الأقل؛ النماذج تعمل على CPU
return 0
def M(repo, kind, size, group, bf16=False):
return {"repo": repo, "kind": kind, "size": size, "group": group, "bf16": bf16}
SM, MID, BIG = "💬 دردشة صغيرة (أقل من 1B)", "🧠 متوسطة (1–2B)", "🚀 كبيرة (2–4B) — بطيئة على CPU"
CODE, T5, CP, CU = "👨‍💻 برمجة", "⚡ سريعة (سؤال وجواب)", "🐣 إكمال نصي", "🧩 مخصّص"
MODELS = {
# --- صغيرة ---
"qwen25": M("Qwen/Qwen2.5-0.5B-Instruct", "chat", "~1GB", SM),
"qwen3": M("Qwen/Qwen3-0.6B", "chat", "~1.2GB", SM),
"qwen2": M("Qwen/Qwen2-0.5B-Instruct", "chat", "~1GB", SM),
"qwen": M("Qwen/Qwen1.5-0.5B-Chat", "chat", "~1GB", SM),
"lfm2": M("LiquidAI/LFM2-350M", "chat", "~700MB", SM),
"lfm2_700": M("LiquidAI/LFM2-700M", "chat", "~1.4GB", SM),
"smol360": M("HuggingFaceTB/SmolLM2-360M-Instruct", "chat", "~720MB", SM),
"smol135": M("HuggingFaceTB/SmolLM2-135M-Instruct", "chat", "~270MB", SM),
"qwen15_18": M("Qwen/Qwen1.5-1.8B-Chat", "chat", "~3.7GB", MID),
# --- متوسطة ---
"qwen25_15": M("Qwen/Qwen2.5-1.5B-Instruct", "chat", "~3GB", MID),
"qwen3_17": M("Qwen/Qwen3-1.7B", "chat", "~3.4GB", MID),
"smol17": M("HuggingFaceTB/SmolLM2-1.7B-Instruct", "chat", "~3.4GB", MID),
"lfm2_12": M("LiquidAI/LFM2-1.2B", "chat", "~2.4GB", MID),
"tinyllama": M("TinyLlama/TinyLlama-1.1B-Chat-v1.0", "chat", "~2.2GB", MID),
"r1_15": M("deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B", "chat", "~3.5GB", MID),
"granite2": M("ibm-granite/granite-3.3-2b-instruct", "chat", "~5GB", MID, True),
# --- كبيرة (bfloat16 لتوفير الذاكرة) ---
"qwen25_3": M("Qwen/Qwen2.5-3B-Instruct", "chat", "~6GB", BIG, True),
"smol3": M("HuggingFaceTB/SmolLM3-3B", "chat", "~6GB", BIG, True),
"phi35": M("microsoft/Phi-3.5-mini-instruct", "chat", "~7.6GB", BIG, True),
"phi4mini": M("microsoft/Phi-4-mini-instruct", "chat", "~7.6GB", BIG, True),
"qwen3_4": M("Qwen/Qwen3-4B-Instruct-2507", "chat", "~8GB", BIG, True),
"lfm2_26": M("LiquidAI/LFM2-2.6B", "chat", "~5.2GB", BIG, True),
# --- برمجة ---
"qcoder05": M("Qwen/Qwen2.5-Coder-0.5B-Instruct", "chat", "~1GB", CODE),
"qcoder15": M("Qwen/Qwen2.5-Coder-1.5B-Instruct", "chat", "~3GB", CODE),
"qcoder3": M("Qwen/Qwen2.5-Coder-3B-Instruct", "chat", "~6GB", CODE, True),
# --- سؤال وجواب ---
"lamini": M("MBZUAI/LaMini-Flan-T5-77M", "t5", "~300MB", T5),
"lamini248": M("MBZUAI/LaMini-Flan-T5-248M", "t5", "~1GB", T5),
"lamini783": M("MBZUAI/LaMini-Flan-T5-783M", "t5", "~3GB", T5),
"flansmall": M("google/flan-t5-small", "t5", "~300MB", T5),
"flanbase": M("google/flan-t5-base", "t5", "~1GB", T5),
"flanlarge": M("google/flan-t5-large", "t5", "~3GB", T5),
"t5small": M("google/t5-small", "t5", "~240MB", T5),
# --- إكمال نصي ---
"distilgpt2": M("distilbert/distilgpt2", "alpaca", "~350MB", CP),
"gpt2": M("openai-community/gpt2", "alpaca", "~500MB", CP),
"opt125": M("facebook/opt-125m", "alpaca", "~250MB", CP),
"opt350": M("facebook/opt-350m", "alpaca", "~650MB", CP),
"lgpt": M("MBZUAI/LaMini-GPT-124M", "alpaca", "~250MB", CP),
"lgpt774": M("MBZUAI/LaMini-GPT-774M", "alpaca", "~1.5GB", CP),
# --- صوت وصور ---
"whisper": M("openai/whisper-tiny.en", "asr", "~150MB", "aux"),
"caption": M("nlpconnect/vit-gpt2-image-captioning", "caption", "~950MB", "aux"),
}
TEXT_KINDS = ("chat", "t5", "alpaca")
_pipes, _state, _cancels = {}, {}, {}
_lock = threading.Lock()
F32 = torch.float32
def _st(mid):
return _state.setdefault(mid, {"state": "idle", "pct": 0, "error": "", "t0": 0, "took": 0,
"done": 0, "total": 0, "speed": 0, "eta": 0})
def _plan(repo):
"""الملفات المطلوبة وحجمها الإجمالي (لحساب النسبة)."""
sib = HfApi().model_info(repo, files_metadata=True).siblings
ign = ["*.h5", "*.msgpack", "*.ot", "*.onnx", "*.onnx_data", "*.tflite",
"*.gguf", "onnx/*", "openvino/*", "coreml/*", "original/*", "consolidated*"]
if any(s.rfilename.endswith(".safetensors") for s in sib):
ign.append("*.bin")
keep = [s for s in sib if not any(fnmatch(s.rfilename, p) for p in ign)]
return ign, sum(s.size or 0 for s in keep)
def _watch(mid, repo, total, stop):
"""يقيس حجم ما نزل في الكاش: النسبة + السرعة + الوقت المتبقي."""
d = os.path.join(constants.HF_HUB_CACHE, "models--" + repo.replace("/", "--"))
s = _st(mid)
t_start, n0 = time.time(), None
while not stop.is_set():
n = 0
for r, _, fs in os.walk(d):
for f in fs:
p = os.path.join(r, f)
try:
if not os.path.islink(p):
n += os.path.getsize(p)
except OSError:
pass
if n0 is None:
n0 = n # ما كان موجوداً مسبقاً في الكاش لا يدخل في حساب السرعة
dt = time.time() - t_start
speed = (n - n0) / dt if dt > 1 else 0
s["done"], s["total"], s["speed"] = n, total, speed
s["eta"] = max(0, (total - n) / speed) if speed > 0 and total > n else 0
if total:
s["pct"] = min(99.0, n * 100 / total)
stop.wait(0.7)
def _build(info):
k, r = info["kind"], info["repo"]
if k == "asr":
return {"proc": WhisperProcessor.from_pretrained(r),
"model": WhisperForConditionalGeneration.from_pretrained(r, dtype=F32).eval()}
if k == "caption":
return {"proc": ViTImageProcessor.from_pretrained(r), "tok": AutoTokenizer.from_pretrained(r),
"model": VisionEncoderDecoderModel.from_pretrained(r, dtype=F32).eval()}
cls = AutoModelForSeq2SeqLM if k == "t5" else AutoModelForCausalLM
dt = torch.bfloat16 if info.get("bf16") else F32
return {"tok": AutoTokenizer.from_pretrained(r), "model": cls.from_pretrained(r, dtype=dt).eval()}
def _free_text(keep):
"""نموذج نصي واحد في الذاكرة فقط، لتجنّب نفاد RAM مع النماذج الكبيرة."""
for k in list(_pipes):
if k != keep and MODELS.get(k, {}).get("kind") in TEXT_KINDS:
_pipes.pop(k, None)
_st(k).update(state="idle", pct=0)
gc.collect()
def _load_sync(mid):
info, s, stop = MODELS[mid], _st(mid), threading.Event()
s.update(state="downloading", pct=0, error="", t0=time.time(), took=0,
done=0, total=0, speed=0, eta=0)
try:
ign, total = _plan(info["repo"])
s["total"] = total
threading.Thread(target=_watch, args=(mid, info["repo"], total, stop), daemon=True).start()
snapshot_download(info["repo"], ignore_patterns=ign)
stop.set()
s.update(state="loading", pct=100, eta=0)
if info["kind"] in TEXT_KINDS:
_free_text(mid)
_pipes[mid] = _build(info)
s.update(state="ready", took=time.time() - s["t0"])
except Exception as e:
stop.set()
s.update(state="error", error=str(e)[:300])
class StopOnCancel(StoppingCriteria):
def __init__(self, rid):
self.rid = rid
def __call__(self, input_ids, scores, **kw):
return torch.full((input_ids.shape[0],), bool(_cancels.get(self.rid)),
dtype=torch.bool, device=input_ids.device)
def _alpaca(msgs):
q = next((m["content"] for m in reversed(msgs) if m["role"] == "user"), "")
return ("Below is an instruction that describes a task. Write a response that "
f"appropriately completes the request.\n\n### Instruction:\n{q}\n\n### Response:\n")
# ============ API ============
@app.api(name="models")
def api_models() -> dict:
return {"models": [{"id": k, **v} for k, v in MODELS.items()]}
@app.api(name="register")
def api_register(repo: str, kind: str = "chat") -> dict:
repo = repo.strip()
if not re.fullmatch(r"[\w.\-]+/[\w.\-]+", repo) or kind not in TEXT_KINDS:
return {"ok": False, "error": "صيغة غير صحيحة. مثال: Qwen/Qwen2.5-3B-Instruct"}
mid = "c_" + re.sub(r"\W", "_", repo)
MODELS.setdefault(mid, M(repo, kind, "؟", CU, True))
return {"ok": True, "model": {"id": mid, **MODELS[mid]}}
@app.api(name="status")
def api_status() -> dict:
out, now = {}, time.time()
for mid in MODELS:
s = _st(mid)
if mid in _pipes:
s["state"] = "ready"
busy = s["state"] in ("downloading", "loading")
out[mid] = {"state": s["state"], "pct": s["pct"], "error": s["error"],
"elapsed": (now - s["t0"]) if busy and s["t0"] else s["took"],
"took": s["took"], "done": s["done"], "total": s["total"],
"speed": s["speed"], "eta": s["eta"]}
return out
@app.api(name="load")
def api_load(model_id: str) -> dict:
if model_id not in MODELS:
return {"ok": False, "error": "نموذج غير معروف"}
with _lock:
if model_id not in _pipes and _st(model_id)["state"] not in ("downloading", "loading"):
_st(model_id).update(state="downloading", t0=time.time())
threading.Thread(target=_load_sync, args=(model_id,), daemon=True).start()
return {"ok": True}
@app.api(name="unload")
def api_unload(model_id: str = "") -> dict:
with _lock:
for mid in ([model_id] if model_id else list(_pipes)):
_pipes.pop(mid, None)
if mid in _state:
_state[mid].update(state="idle", pct=0)
gc.collect()
return {"ok": True}
@app.api(name="cancel")
def api_cancel(req_id: str) -> dict:
_cancels[req_id] = True
return {"ok": True}
@app.api(name="generate")
def api_generate(model_id: str, messages: str, req_id: str,
max_tokens: int = 256, temperature: float = 0.7, top_p: float = 0.9) -> dict:
_cancels[req_id] = False
try:
p = _pipes.get(model_id)
if p is None:
yield {"ok": False, "error": "النموذج غير محمّل"}
return
kind, tok, model = MODELS[model_id]["kind"], p["tok"], p["model"]
msgs = json.loads(messages)
if len(msgs) > MAX_HISTORY + 1: # نحتفظ بتعليمات النظام + آخر الرسائل
head = msgs[:1] if msgs and msgs[0].get("role") == "system" else []
msgs = head + msgs[-MAX_HISTORY:]
special = True
if kind == "chat":
try:
prompt = tok.apply_chat_template(msgs, tokenize=False,
add_generation_prompt=True, enable_thinking=False)
special = False
except Exception:
prompt = _alpaca(msgs)
elif kind == "t5":
prompt = next((m["content"] for m in reversed(msgs) if m["role"] == "user"), "")
else:
prompt = _alpaca(msgs)
enc = tok(prompt, return_tensors="pt", add_special_tokens=special)
streamer = TextIteratorStreamer(tok, skip_prompt=(kind != "t5"), skip_special_tokens=True)
kw = dict(**enc, max_new_tokens=int(max_tokens), streamer=streamer,
stopping_criteria=StoppingCriteriaList([StopOnCancel(req_id)]))
if temperature > 0:
kw.update(do_sample=True, temperature=float(temperature), top_p=float(top_p or 0.9))
else:
kw["do_sample"] = False
if kind != "t5":
kw.update(repetition_penalty=1.1, pad_token_id=tok.pad_token_id or tok.eos_token_id)
errs = []
def run():
try:
with torch.inference_mode():
model.generate(**kw)
except Exception as e:
errs.append(str(e))
streamer.end()
th = threading.Thread(target=run, daemon=True)
t_start = time.time()
th.start()
out, pend, last = "", 0, t_start
for piece in streamer:
if _cancels.get(req_id):
break
out += piece
pend += 1
now = time.time()
# تجميع التوكنات: نرسل فقط عند اكتمال دفعة أو مرور مهلة قصيرة
if pend >= BATCH_TOKENS or now - last >= BATCH_SECONDS:
yield {"ok": True, "text": out, "done": False}
pend, last = 0, now
th.join(timeout=2)
if errs:
yield {"ok": False, "error": errs[0]}
else:
secs = max(time.time() - t_start, 0.001)
try:
n_tok = len(tok.encode(out, add_special_tokens=False))
except Exception:
n_tok = 0
yield {"ok": True, "text": out.strip(), "done": True,
"secs": round(secs, 2), "tokens": n_tok}
except Exception as e:
yield {"ok": False, "error": str(e)}
finally:
_cancels.pop(req_id, None)
@app.api(name="asr")
def api_asr(audio_b64: str) -> dict:
try:
p = _pipes.get("whisper")
if p is None:
return {"ok": False, "error": "نموذج الصوت غير محمّل"}
data, sr = sf.read(io.BytesIO(base64.b64decode(audio_b64)), dtype="float32")
if data.ndim > 1:
data = data.mean(axis=1)
if sr != 16000:
data = np.interp(np.linspace(0, len(data) - 1, int(len(data) * 16000 / sr)),
np.arange(len(data)), data).astype("float32")
feats = p["proc"](data[:16000 * 30], sampling_rate=16000, return_tensors="pt").input_features
with torch.inference_mode():
ids = p["model"].generate(feats, max_new_tokens=220)
return {"ok": True, "text": p["proc"].batch_decode(ids, skip_special_tokens=True)[0].strip()}
except Exception as e:
return {"ok": False, "error": str(e)}
@app.api(name="caption")
def api_caption(image_b64: str) -> dict:
try:
p = _pipes.get("caption")
if p is None:
return {"ok": False, "error": "نموذج الصور غير محمّل"}
img = Image.open(io.BytesIO(base64.b64decode(image_b64))).convert("RGB")
pix = p["proc"](images=img, return_tensors="pt").pixel_values
with torch.inference_mode():
ids = p["model"].generate(pix, max_new_tokens=32, num_beams=4)
return {"ok": True, "text": p["tok"].decode(ids[0], skip_special_tokens=True).strip()}
except Exception as e:
return {"ok": False, "error": str(e)}
@app.get("/", response_class=HTMLResponse)
async def serve_ui():
with open(os.path.join(HERE, "index.html"), "r", encoding="utf-8") as f:
return f.read()
app.launch(server_name="0.0.0.0",
server_port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)),
ssr_mode=False, show_error=True)