Rahaf2001 commited on
Commit
5f56b43
·
verified ·
1 Parent(s): 0d2b1e7

تحميل ملفات المشروع

Browse files
Files changed (5) hide show
  1. 1FAzj3b99IwhOFapY2oeDCw.webp +0 -0
  2. Dockerfile +20 -0
  3. README.md +43 -8
  4. main.py +293 -0
  5. requirements.txt +10 -0
1FAzj3b99IwhOFapY2oeDCw.webp ADDED
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # System deps
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ build-essential curl \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ # Install Python deps first (layer cache)
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy app
15
+ COPY main.py .
16
+
17
+ # HF Spaces requires port 7860
18
+ EXPOSE 7860
19
+
20
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,47 @@
1
  ---
2
- title: Lassen
3
- emoji: 🦀
4
- colorFrom: purple
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 6.12.0
8
- app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Bayan API
3
+ emoji: 📜
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
+ # Bayan API مشروع بيان
11
+
12
+ FastAPI backend for the Bayan Arabic Poetry AI project.
13
+
14
+ ## Endpoints
15
+
16
+ | Method | Path | Description |
17
+ |--------|------|-------------|
18
+ | GET | `/health` | Health check |
19
+ | POST | `/fasserha` | فسّرها لي — classify + literary analysis |
20
+ | POST | `/generate` | ساعدني في الكتابة — generate verses |
21
+ | GET | `/meters` | List all supported meters |
22
+
23
+ ## /fasserha — Request
24
+ ```json
25
+ { "poem": "قفا نبك من ذكرى حبيب ومنزل..." }
26
+ ```
27
+
28
+ ## /fasserha — Response
29
+ ```json
30
+ {
31
+ "success": true,
32
+ "data": {
33
+ "meter": { "meter_ar": "الطويل", "meter_en": "taweel", "confidence": 0.95 },
34
+ "era": { "era": "قديم", "classical_probability": 0.98, "modern_probability": 0.02 },
35
+ "topic": { "topic": "غزل رومانسي", "confidence": 0.87, "top3": [...] },
36
+ "explanation": "..."
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## /generate — Request
42
+ ```json
43
+ { "idea": "الشوق إلى الوطن", "meter": "الطويل", "num_verses": 4 }
44
+ ```
45
+
46
+ ## Environment Variables
47
+ Set `OPENAI_API_KEY` in the Space secrets.
main.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, re, html, pickle
2
+ import numpy as np
3
+ import torch
4
+ from collections import Counter
5
+ from contextlib import asynccontextmanager
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel
9
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
10
+ from openai import OpenAI
11
+
12
+ # ── Config ────────────────────────────────────────────────────────────────────
13
+ METER_MODEL_ID = "Rahaf2001/Lassen-meter-classifier"
14
+ ERA_MODEL_ID = "Rahaf2001/LassenEraClassifier"
15
+ TOPIC_MODEL_ID = "Rahaf2001/Lassen-topic-classifier"
16
+
17
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
18
+
19
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
20
+
21
+ # ── Global model holders ──────────────────────────────────────────────────────
22
+ models = {}
23
+
24
+ # ── Arabic text cleaning ──────────────────────────────────────────────────────
25
+ ARABIC_DIACRITICS = re.compile(r'[\u0617-\u061A\u064B-\u0652\u0670\u06D6-\u06ED]')
26
+
27
+ def clean_arabic(text: str) -> str:
28
+ if not text:
29
+ return ""
30
+ text = html.unescape(str(text))
31
+ text = re.sub(r"<.*?>", " ", text)
32
+ text = text.replace("\u0640", "")
33
+ text = ARABIC_DIACRITICS.sub("", text)
34
+ text = re.sub(r'[\u0623\u0625\u0622\u0671]', '\u0627', text)
35
+ text = text.replace("\u0629", "\u0647")
36
+ text = re.sub(r"[0-9\u0660-\u0669]", " ", text)
37
+ text = re.sub(r"[^\u0600-\u06FF\s]", " ", text)
38
+ text = re.sub(r"\s+", " ", text).strip()
39
+ return text
40
+
41
+ # ── Meter labels ──────────────────────────────────────────────────────────────
42
+ LABELS_METER = ['saree', 'kamel', 'mutakareb', 'mutadarak', 'munsareh',
43
+ 'madeed', 'mujtath', 'ramal', 'baseet', 'khafeef',
44
+ 'taweel', 'wafer', 'hazaj', 'rajaz']
45
+
46
+ METER_ARABIC = {
47
+ 'saree': 'السريع', 'kamel': 'الكامل', 'mutakareb': 'المتقارب',
48
+ 'mutadarak': 'المتدارك', 'munsareh': 'المنسرح', 'madeed': 'المديد',
49
+ 'mujtath': 'المجتث', 'ramal': 'الرمل', 'baseet': 'البسيط',
50
+ 'khafeef': 'الخفيف', 'taweel': 'الطويل', 'wafer': 'الوافر',
51
+ 'hazaj': 'الهزج', 'rajaz': 'الرجز'
52
+ }
53
+
54
+ # ── Meter taf'ila patterns ────────────────────────────────────────────────────
55
+ METER_PATTERNS = {
56
+ 'الطويل': 'فَعُولُنْ مَفَاعِيلُنْ فَعُولُنْ مَفَاعِلُنْ',
57
+ 'الكامل': 'مُتَفَاعِلُنْ مُتَفَاعِلُنْ مُتَفَاعِلُنْ',
58
+ 'البسيط': 'مُسْتَفْعِلُنْ فَاعِلُنْ مُسْتَفْعِلُنْ فَاعِلُنْ',
59
+ 'الوافر': 'مُفَاعَلَتُنْ مُفَاعَلَتُنْ فَعُولُنْ',
60
+ 'الخفيف': 'فَاعِلَاتُنْ مُسْتَفْعِلُنْ فَاعِلَاتُنْ',
61
+ 'الرجز': 'مُسْتَفْعِلُنْ مُسْتَفْعِلُنْ مُسْتَفْعِلُنْ',
62
+ 'الرمل': 'فَاعِلَاتُنْ فَاعِلَاتُنْ فَاعِلَاتُنْ',
63
+ 'السريع': 'مُسْتَفْعِلُنْ مُسْتَفْعِلُنْ مَفْعُولَاتُ',
64
+ 'المنسرح': 'مُسْتَفْعِلُنْ مَفْعُولَاتُ مُسْتَفْعِلُنْ',
65
+ 'الهزج': 'مَفَاعِيلُنْ مَفَاعِيلُنْ',
66
+ 'المتقارب': 'فَعُولُنْ فَعُولُنْ فَعُولُنْ فَعُولُنْ',
67
+ 'المتدارك': 'فَاعِلُنْ فَاعِلُنْ فَاعِلُنْ فَاعِلُنْ',
68
+ 'المديد': 'فَاعِلَاتُنْ فَاعِلُنْ فَاعِلَاتُنْ',
69
+ }
70
+
71
+ # ── Lifespan: load models once at startup ────────────────────────────────────
72
+ @asynccontextmanager
73
+ async def lifespan(app: FastAPI):
74
+ print("Loading models...")
75
+
76
+ # Meter
77
+ models["meter_tokenizer"] = AutoTokenizer.from_pretrained(METER_MODEL_ID)
78
+ models["meter_model"] = AutoModelForSequenceClassification.from_pretrained(METER_MODEL_ID)
79
+ models["meter_model"].to(device).eval()
80
+
81
+ # Era
82
+ models["era_tokenizer"] = AutoTokenizer.from_pretrained(ERA_MODEL_ID)
83
+ models["era_model"] = AutoModelForSequenceClassification.from_pretrained(ERA_MODEL_ID)
84
+ models["era_model"].to(device).eval()
85
+
86
+ # Topic
87
+ models["topic_tokenizer"] = AutoTokenizer.from_pretrained(TOPIC_MODEL_ID)
88
+ models["topic_model"] = AutoModelForSequenceClassification.from_pretrained(TOPIC_MODEL_ID)
89
+ models["topic_model"].to(device).eval()
90
+
91
+ # Topic labels — loaded from HF model config
92
+ topic_cfg = models["topic_model"].config
93
+ if hasattr(topic_cfg, "id2label"):
94
+ models["id2label_topic"] = {int(k): v for k, v in topic_cfg.id2label.items()}
95
+ else:
96
+ models["id2label_topic"] = {i: str(i) for i in range(topic_cfg.num_labels)}
97
+
98
+ # OpenAI client
99
+ models["openai"] = OpenAI(api_key=OPENAI_API_KEY)
100
+
101
+ print(f"All models loaded on {device} ✓")
102
+ yield
103
+ models.clear()
104
+
105
+ # ── App ───────────────────────────────────────────────────────────────────────
106
+ app = FastAPI(title="Bayan API", lifespan=lifespan)
107
+
108
+ app.add_middleware(
109
+ CORSMiddleware,
110
+ allow_origins=["*"],
111
+ allow_methods=["*"],
112
+ allow_headers=["*"],
113
+ )
114
+
115
+ # ── Inference helpers ─────────────────────────────────────────────────────────
116
+ def predict_meter(poem_text: str) -> dict:
117
+ verses = [v.replace("#", " ").strip() for v in poem_text.strip().split("\n") if v.strip()]
118
+ if not verses:
119
+ raise ValueError("القصيدة فارغة")
120
+ predictions = []
121
+ for verse in verses:
122
+ inputs = models["meter_tokenizer"](verse, return_tensors="pt", truncation=True,
123
+ max_length=32, padding="max_length")
124
+ inputs = {k: v.to(device) for k, v in inputs.items()}
125
+ with torch.no_grad():
126
+ probs = torch.softmax(models["meter_model"](**inputs).logits, dim=-1)[0]
127
+ pred_id = torch.argmax(probs).item()
128
+ predictions.append((LABELS_METER[pred_id], probs[pred_id].item()))
129
+ top_meter = Counter(p[0] for p in predictions).most_common(1)[0][0]
130
+ avg_conf = sum(c for _, c in predictions) / len(predictions)
131
+ return {"meter_ar": METER_ARABIC[top_meter], "meter_en": top_meter,
132
+ "confidence": round(avg_conf, 3)}
133
+
134
+ def predict_era(poem_text: str) -> dict:
135
+ cleaned = clean_arabic(poem_text)
136
+ enc = models["era_tokenizer"](cleaned, padding="max_length", truncation=True,
137
+ max_length=256, return_tensors="pt")
138
+ enc = {k: v.to(device) for k, v in enc.items()}
139
+ with torch.no_grad():
140
+ probs = torch.softmax(models["era_model"](**enc).logits, dim=-1).cpu().numpy()[0]
141
+ label_names = ["قديم", "حديث"]
142
+ pred_idx = int(np.argmax(probs))
143
+ return {"era": label_names[pred_idx],
144
+ "classical_probability": round(float(probs[0]), 4),
145
+ "modern_probability": round(float(probs[1]), 4)}
146
+
147
+ def predict_topic(poem_text: str) -> dict:
148
+ cleaned = clean_arabic(poem_text)
149
+ inputs = models["topic_tokenizer"](cleaned, truncation=True, max_length=512,
150
+ return_tensors="pt", padding=True)
151
+ inputs = {k: v.to(device) for k, v in inputs.items()}
152
+ with torch.no_grad():
153
+ probs = torch.softmax(models["topic_model"](**inputs).logits, dim=-1)[0].cpu().numpy()
154
+ top3 = np.argsort(probs)[::-1][:3]
155
+ id2label = models["id2label_topic"]
156
+ return {"topic": id2label[int(top3[0])],
157
+ "confidence": round(float(probs[top3[0]]), 3),
158
+ "top3": [{"label": id2label[int(i)], "prob": round(float(probs[i]), 3)} for i in top3]}
159
+
160
+ # ── Request / Response schemas ────────────────────────────────────────────────
161
+ class PoemRequest(BaseModel):
162
+ poem: str
163
+
164
+ class GenerateRequest(BaseModel):
165
+ idea: str
166
+ meter: str
167
+ num_verses: int = 4
168
+
169
+ # ── Routes ────────────────────────────────────────────────────────────────────
170
+ @app.get("/")
171
+ def root():
172
+ return {"status": "ok", "service": "Bayan API"}
173
+
174
+ @app.get("/health")
175
+ def health():
176
+ return {"status": "healthy", "device": str(device)}
177
+
178
+ @app.post("/fasserha")
179
+ def fasserha(req: PoemRequest):
180
+ """فسّرها لي — classify meter, era, topic then generate literary analysis."""
181
+ if not req.poem.strip():
182
+ raise HTTPException(400, "القصيدة فارغة")
183
+ try:
184
+ meter = predict_meter(req.poem)
185
+ era = predict_era(req.poem)
186
+ topic = predict_topic(req.poem)
187
+ except Exception as e:
188
+ raise HTTPException(500, f"خطأ في التصنيف: {str(e)}")
189
+
190
+ system_prompt = """أنت ناقد أدبي متخصص في الشعر العربي الكلاسيكي والحديث.
191
+ تحلل القصائد بأسلوب أكاديمي راقٍ، وتستخدم المصطلحات البلاغية والعروضية بدقة.
192
+ ردك دائماً بالعربية الفصحى."""
193
+
194
+ user_prompt = f"""حلّل هذه القصيدة:
195
+
196
+ {req.poem}
197
+
198
+ معطيات النماذج (حقائق مؤكدة):
199
+ - البحر الشعري: {meter['meter_ar']} (ثقة: {meter['confidence']*100:.0f}%)
200
+ - العصر: {era['era']} (كلاسيكي: {era['classical_probability']*100:.0f}% | حديث: {era['modern_probability']*100:.0f}%)
201
+ - الموضوع: {topic['topic']} (ثقة: {topic['confidence']*100:.0f}%)
202
+
203
+ اكتب تحليلاً أدبياً شاملاً يتضمن:
204
+ 1. الفكرة العامة والمعنى الكلي
205
+ 2. المعنى التفصيلي للأبيات
206
+ 3. الجماليات البلاغية والأسلوبية
207
+ 4. البحر والإيقاع وأثرهما في المعنى
208
+ 5. لمسة نقدية تقييمية
209
+
210
+ التزم بالترتيب أعلاه. لا تكرر المعلومات."""
211
+
212
+ try:
213
+ response = models["openai"].chat.completions.create(
214
+ model="gpt-4o",
215
+ messages=[
216
+ {"role": "system", "content": system_prompt},
217
+ {"role": "user", "content": user_prompt}
218
+ ],
219
+ max_tokens=1200,
220
+ temperature=0.7
221
+ )
222
+ explanation = response.choices[0].message.content
223
+ except Exception as e:
224
+ raise HTTPException(500, f"خطأ في التفسير: {str(e)}")
225
+
226
+ return {
227
+ "success": True,
228
+ "data": {
229
+ "meter": meter,
230
+ "era": era,
231
+ "topic": topic,
232
+ "explanation": explanation
233
+ }
234
+ }
235
+
236
+
237
+ @app.post("/generate")
238
+ def generate(req: GenerateRequest):
239
+ """ساعدني في الكتابة — generate classical Arabic verses."""
240
+ if not req.idea.strip():
241
+ raise HTTPException(400, "الفكرة فارغة")
242
+ if req.meter not in METER_PATTERNS and req.meter not in METER_ARABIC.values():
243
+ raise HTTPException(400, f"البحر غير معروف: {req.meter}")
244
+ if not 1 <= req.num_verses <= 12:
245
+ raise HTTPException(400, "عدد الأبيات بين 1 و 12")
246
+
247
+ pattern = METER_PATTERNS.get(req.meter, "")
248
+ pattern_line = f"تفعيلة البحر: {pattern}" if pattern else ""
249
+
250
+ prompt = f"""أنت شاعر عربي متخصص في العروض الكلاسيكي.
251
+
252
+ الموضوع: "{req.idea}"
253
+ البحر: {req.meter}
254
+ {pattern_line}
255
+
256
+ المطلوب: اكتب {req.num_verses} أبيات شعرية بالفصحى الكلاسيكية.
257
+
258
+ القواعد الصارمة:
259
+ - كل بيت من شطرين صحيحين عروضياً
260
+ - قافية موحدة في جميع الأبيات
261
+ - فصحى كلاسيكية فقط
262
+ - الأبيات متصلة كقصيدة واحدة
263
+ - اكتب الأبيات فقط، بدون ترقيم أو شرح
264
+ - سطر واحد لكل بيت، {req.num_verses} سطور فقط"""
265
+
266
+ try:
267
+ response = models["openai"].chat.completions.create(
268
+ model="gpt-4o-mini",
269
+ messages=[
270
+ {"role": "system", "content": "أنت شاعر عربي كلاسيكي. اكتب الأبيات فقط، سطر لكل بيت."},
271
+ {"role": "user", "content": prompt}
272
+ ],
273
+ temperature=0.75,
274
+ max_tokens=600
275
+ )
276
+ raw = response.choices[0].message.content.strip()
277
+ verses = [l.strip() for l in raw.split("\n") if l.strip() and len(l.strip()) > 10]
278
+ return {
279
+ "success": True,
280
+ "data": {
281
+ "verses": verses[:req.num_verses],
282
+ "meter": req.meter,
283
+ "pattern": pattern
284
+ }
285
+ }
286
+ except Exception as e:
287
+ raise HTTPException(500, f"خطأ في التوليد: {str(e)}")
288
+
289
+
290
+ @app.get("/meters")
291
+ def list_meters():
292
+ """Return all supported meters."""
293
+ return {"meters": list(METER_PATTERNS.keys())}
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
3
+ transformers==4.44.2
4
+ torch==2.4.1
5
+ accelerate==0.34.2
6
+ safetensors==0.4.5
7
+ numpy==1.26.4
8
+ openai==1.51.0
9
+ pydantic==2.9.2
10
+ huggingface-hub==0.25.1