youssefreda9 commited on
Commit
2df4f2d
·
1 Parent(s): 239c0bb

fix: Add wait_for_model, debug endpoint, and better HF API error handling

Browse files
Files changed (2) hide show
  1. src/app.py +18 -1
  2. src/hf_inference.py +102 -97
src/app.py CHANGED
@@ -53,7 +53,8 @@ HUGGINGFACE_SUMMARIZATION_REPO = os.environ.get(
53
 
54
  # When HF_API_TOKEN is set, use remote HF Inference API instead of local models.
55
  # This avoids loading 500MB+ models into RAM on the free tier.
56
- USE_HF_API = bool(os.environ.get('HF_API_TOKEN', ''))
 
57
 
58
  # Configure logging
59
  logging.basicConfig(
@@ -172,6 +173,22 @@ def health_check():
172
  return jsonify(health), status_code
173
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  @app.route('/api/summarize', methods=['POST'])
176
  def summarize():
177
  """
 
53
 
54
  # When HF_API_TOKEN is set, use remote HF Inference API instead of local models.
55
  # This avoids loading 500MB+ models into RAM on the free tier.
56
+ HF_API_TOKEN = os.environ.get('HF_API_TOKEN', '')
57
+ USE_HF_API = bool(HF_API_TOKEN)
58
 
59
  # Configure logging
60
  logging.basicConfig(
 
173
  return jsonify(health), status_code
174
 
175
 
176
+ @app.route('/api/debug-models', methods=['GET'])
177
+ def debug_models():
178
+ """Debug endpoint: test all HF models and return actual errors."""
179
+ if not USE_HF_API:
180
+ return jsonify({'error': 'Not in HF API mode', 'mode': 'local'}), 400
181
+
182
+ from hf_inference import debug_test_all_models
183
+ results = debug_test_all_models()
184
+ return jsonify({
185
+ 'status': 'debug',
186
+ 'hf_api_token_set': bool(HF_API_TOKEN),
187
+ 'hf_api_token_prefix': HF_API_TOKEN[:10] + '...' if HF_API_TOKEN else 'NOT SET',
188
+ 'models': results,
189
+ }), 200
190
+
191
+
192
  @app.route('/api/summarize', methods=['POST'])
193
  def summarize():
194
  """
src/hf_inference.py CHANGED
@@ -25,9 +25,7 @@ HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
25
  HF_API_BASE = "https://api-inference.huggingface.co/models/"
26
 
27
  # Timeout for inference calls (seconds)
28
- # First call may be slow (cold start = model loading on HF servers)
29
- HF_TIMEOUT_FIRST = 120 # 2 min for cold start
30
- HF_TIMEOUT_NORMAL = 60 # 1 min for warm model
31
 
32
 
33
  def _build_headers():
@@ -38,65 +36,52 @@ def _build_headers():
38
  return headers
39
 
40
 
41
- def _call_hf_api(repo_id, payload, timeout=HF_TIMEOUT_NORMAL, retries=2):
42
  """
43
  Call HuggingFace Inference API.
44
-
45
- Handles cold starts by retrying when model is loading.
 
46
  Returns parsed JSON response or raises Exception.
47
  """
48
  url = HF_API_BASE + repo_id
49
  headers = _build_headers()
 
 
 
 
 
 
 
50
  data = json.dumps(payload).encode("utf-8")
51
-
52
  ctx = ssl.create_default_context()
53
-
54
- for attempt in range(retries + 1):
55
- try:
56
- req = urllib.request.Request(url, data=data, headers=headers, method="POST")
57
- resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
58
- result = json.loads(resp.read().decode("utf-8"))
59
- return result
60
-
61
- except urllib.error.HTTPError as e:
62
- body = e.read().decode("utf-8", errors="replace")
63
-
64
- # Model is loading (cold start) — wait and retry
65
- if e.code == 503 and "loading" in body.lower():
66
- try:
67
- wait_data = json.loads(body)
68
- wait_time = wait_data.get("estimated_time", 30)
69
- except (json.JSONDecodeError, KeyError):
70
- wait_time = 30
71
-
72
- logger.info(
73
- "Model %s is loading (attempt %d/%d), waiting %.0fs...",
74
- repo_id, attempt + 1, retries + 1, wait_time
75
- )
76
- time.sleep(min(wait_time, 60))
77
- continue
78
-
79
- # Other HTTP error
80
- logger.error("HF API error for %s: HTTP %d — %s", repo_id, e.code, body[:200])
81
- raise RuntimeError(
82
- "HF Inference API error (HTTP {}): {}".format(e.code, body[:200])
83
- )
84
-
85
- except Exception as e:
86
- if attempt < retries:
87
- logger.warning("HF API call failed (attempt %d): %s", attempt + 1, str(e))
88
- time.sleep(5)
89
- continue
90
- raise
91
-
92
- raise RuntimeError("HF Inference API: max retries exceeded for " + repo_id)
93
 
94
 
95
  # ============================================================
96
  # Model-specific wrappers
97
  # ============================================================
98
 
99
- # --- Repository IDs ---
100
  SUMMARIZATION_REPO = os.environ.get("SUMMARIZATION_REPO_ID", "bayan10/summarization-model")
101
  SPELLING_REPO = os.environ.get("SPELLING_REPO_ID", "bayan10/AraSpell-Model")
102
  PUNCTUATION_REPO = os.environ.get("PUNCTUATION_REPO_ID", "bayan10/PuncAra-v1")
@@ -104,10 +89,7 @@ AUTOCOMPLETE_REPO = os.environ.get("AUTOCOMPLETE_REPO_ID", "bayan10/AutoComplete
104
 
105
 
106
  def hf_summarize(text, max_length=128, min_length=30):
107
- """
108
- Summarize Arabic text via HF Inference API.
109
- Returns the summary string.
110
- """
111
  payload = {
112
  "inputs": text,
113
  "parameters": {
@@ -115,73 +97,67 @@ def hf_summarize(text, max_length=128, min_length=30):
115
  "min_length": min_length,
116
  }
117
  }
118
-
119
- result = _call_hf_api(SUMMARIZATION_REPO, payload, timeout=HF_TIMEOUT_FIRST)
120
-
121
  # HF summarization returns: [{"summary_text": "..."}]
122
  if isinstance(result, list) and len(result) > 0:
123
- return result[0].get("summary_text", "")
124
-
125
- # Fallback: might return {"generated_text": "..."}
126
  if isinstance(result, dict):
127
  return result.get("summary_text", result.get("generated_text", str(result)))
128
-
129
  return str(result)
130
 
131
 
132
  def hf_correct_spelling(text):
133
- """
134
- Correct spelling in Arabic text via HF Inference API.
135
- Returns the corrected string.
136
- """
137
  payload = {"inputs": text}
138
-
139
- result = _call_hf_api(SPELLING_REPO, payload, timeout=HF_TIMEOUT_FIRST)
140
-
141
- # Text2text models return: [{"generated_text": "..."}]
142
  if isinstance(result, list) and len(result) > 0:
143
- return result[0].get("generated_text", text)
144
-
 
 
 
145
  if isinstance(result, dict):
146
- return result.get("generated_text", text)
147
-
148
  return text
149
 
150
 
151
  def hf_add_punctuation(text):
152
- """
153
- Add punctuation to Arabic text via HF Inference API.
154
- Returns the punctuated string.
155
- """
156
  payload = {"inputs": text}
157
-
158
- result = _call_hf_api(PUNCTUATION_REPO, payload, timeout=HF_TIMEOUT_FIRST)
159
-
160
- # Encoder-decoder models return: [{"generated_text": "..."}]
161
  if isinstance(result, list) and len(result) > 0:
162
- return result[0].get("generated_text", text)
163
-
 
 
 
164
  if isinstance(result, dict):
165
- return result.get("generated_text", text)
166
-
167
  return text
168
 
169
 
170
  def hf_autocomplete(text, n=5):
171
- """
172
- Get autocomplete suggestions for Arabic text via HF Inference API.
173
- Returns a list of suggestion strings.
174
- """
175
  payload = {
176
  "inputs": text,
177
  "parameters": {
178
- "num_return_sequences": min(n, 5),
179
  "max_new_tokens": 20,
180
  }
181
  }
182
-
183
- result = _call_hf_api(AUTOCOMPLETE_REPO, payload, timeout=HF_TIMEOUT_FIRST)
184
-
185
  # Text generation returns: [{"generated_text": "..."}]
186
  if isinstance(result, list):
187
  suggestions = []
@@ -194,22 +170,51 @@ def hf_autocomplete(text, n=5):
194
  if gen:
195
  suggestions.append(gen)
196
  return suggestions if suggestions else [text]
197
-
198
  return [text]
199
 
200
 
201
  def check_hf_api_available():
202
- """
203
- Quick check if HF Inference API is reachable.
204
- Returns True if API responds, False otherwise.
205
- """
206
  try:
207
  url = HF_API_BASE + SUMMARIZATION_REPO
208
  headers = _build_headers()
209
- # Use GET to just check if model exists (won't trigger inference)
210
  req = urllib.request.Request(url, headers=headers, method="GET")
211
  ctx = ssl.create_default_context()
212
  resp = urllib.request.urlopen(req, timeout=10, context=ctx)
213
  return resp.status == 200
214
  except Exception:
215
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  HF_API_BASE = "https://api-inference.huggingface.co/models/"
26
 
27
  # Timeout for inference calls (seconds)
28
+ HF_TIMEOUT = 120 # 2 min accounts for cold starts
 
 
29
 
30
 
31
  def _build_headers():
 
36
  return headers
37
 
38
 
39
+ def _call_hf_api(repo_id, payload, timeout=HF_TIMEOUT):
40
  """
41
  Call HuggingFace Inference API.
42
+
43
+ Sends wait_for_model=true to handle cold starts automatically
44
+ (HF will wait up to ~2min for the model to load instead of 503).
45
  Returns parsed JSON response or raises Exception.
46
  """
47
  url = HF_API_BASE + repo_id
48
  headers = _build_headers()
49
+
50
+ # Tell HF to wait for the model to load instead of returning 503
51
+ if "options" not in payload:
52
+ payload["options"] = {"wait_for_model": True}
53
+ else:
54
+ payload["options"]["wait_for_model"] = True
55
+
56
  data = json.dumps(payload).encode("utf-8")
 
57
  ctx = ssl.create_default_context()
58
+
59
+ logger.info("Calling HF API: %s (payload keys: %s)", repo_id, list(payload.keys()))
60
+
61
+ try:
62
+ req = urllib.request.Request(url, data=data, headers=headers, method="POST")
63
+ resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
64
+ body = resp.read().decode("utf-8")
65
+ result = json.loads(body)
66
+ logger.info("HF API success for %s: response type=%s", repo_id, type(result).__name__)
67
+ return result
68
+
69
+ except urllib.error.HTTPError as e:
70
+ body = e.read().decode("utf-8", errors="replace")
71
+ logger.error("HF API error for %s: HTTP %d — %s", repo_id, e.code, body[:500])
72
+ raise RuntimeError(
73
+ "HF API error for {} (HTTP {}): {}".format(repo_id, e.code, body[:300])
74
+ )
75
+
76
+ except Exception as e:
77
+ logger.error("HF API exception for %s: %s", repo_id, str(e))
78
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
  # ============================================================
82
  # Model-specific wrappers
83
  # ============================================================
84
 
 
85
  SUMMARIZATION_REPO = os.environ.get("SUMMARIZATION_REPO_ID", "bayan10/summarization-model")
86
  SPELLING_REPO = os.environ.get("SPELLING_REPO_ID", "bayan10/AraSpell-Model")
87
  PUNCTUATION_REPO = os.environ.get("PUNCTUATION_REPO_ID", "bayan10/PuncAra-v1")
 
89
 
90
 
91
  def hf_summarize(text, max_length=128, min_length=30):
92
+ """Summarize Arabic text via HF Inference API."""
 
 
 
93
  payload = {
94
  "inputs": text,
95
  "parameters": {
 
97
  "min_length": min_length,
98
  }
99
  }
100
+
101
+ result = _call_hf_api(SUMMARIZATION_REPO, payload)
102
+
103
  # HF summarization returns: [{"summary_text": "..."}]
104
  if isinstance(result, list) and len(result) > 0:
105
+ return result[0].get("summary_text", result[0].get("generated_text", ""))
106
+
 
107
  if isinstance(result, dict):
108
  return result.get("summary_text", result.get("generated_text", str(result)))
109
+
110
  return str(result)
111
 
112
 
113
  def hf_correct_spelling(text):
114
+ """Correct spelling in Arabic text via HF Inference API."""
 
 
 
115
  payload = {"inputs": text}
116
+
117
+ result = _call_hf_api(SPELLING_REPO, payload)
118
+
119
+ # Text2text / seq2seq models return: [{"generated_text": "..."}]
120
  if isinstance(result, list) and len(result) > 0:
121
+ item = result[0]
122
+ if isinstance(item, dict):
123
+ return item.get("generated_text", item.get("translation_text", text))
124
+ return str(item)
125
+
126
  if isinstance(result, dict):
127
+ return result.get("generated_text", result.get("translation_text", text))
128
+
129
  return text
130
 
131
 
132
  def hf_add_punctuation(text):
133
+ """Add punctuation to Arabic text via HF Inference API."""
 
 
 
134
  payload = {"inputs": text}
135
+
136
+ result = _call_hf_api(PUNCTUATION_REPO, payload)
137
+
 
138
  if isinstance(result, list) and len(result) > 0:
139
+ item = result[0]
140
+ if isinstance(item, dict):
141
+ return item.get("generated_text", item.get("translation_text", text))
142
+ return str(item)
143
+
144
  if isinstance(result, dict):
145
+ return result.get("generated_text", result.get("translation_text", text))
146
+
147
  return text
148
 
149
 
150
  def hf_autocomplete(text, n=5):
151
+ """Get autocomplete suggestions for Arabic text via HF Inference API."""
 
 
 
152
  payload = {
153
  "inputs": text,
154
  "parameters": {
 
155
  "max_new_tokens": 20,
156
  }
157
  }
158
+
159
+ result = _call_hf_api(AUTOCOMPLETE_REPO, payload)
160
+
161
  # Text generation returns: [{"generated_text": "..."}]
162
  if isinstance(result, list):
163
  suggestions = []
 
170
  if gen:
171
  suggestions.append(gen)
172
  return suggestions if suggestions else [text]
173
+
174
  return [text]
175
 
176
 
177
  def check_hf_api_available():
178
+ """Quick check if HF Inference API is reachable."""
 
 
 
179
  try:
180
  url = HF_API_BASE + SUMMARIZATION_REPO
181
  headers = _build_headers()
 
182
  req = urllib.request.Request(url, headers=headers, method="GET")
183
  ctx = ssl.create_default_context()
184
  resp = urllib.request.urlopen(req, timeout=10, context=ctx)
185
  return resp.status == 200
186
  except Exception:
187
  return False
188
+
189
+
190
+ def debug_test_all_models():
191
+ """
192
+ Test all HF models and return results dict.
193
+ Used by /api/debug-models endpoint for troubleshooting.
194
+ """
195
+ results = {}
196
+ test_text = "هذا نص تجريبي للاختبار"
197
+
198
+ for name, fn, args in [
199
+ ("summarization", hf_summarize, (test_text + " " + test_text * 3, 30, 10)),
200
+ ("spelling", hf_correct_spelling, (test_text,)),
201
+ ("punctuation", hf_add_punctuation, (test_text,)),
202
+ ("autocomplete", hf_autocomplete, (test_text, 3)),
203
+ ]:
204
+ try:
205
+ t0 = time.time()
206
+ result = fn(*args)
207
+ elapsed = time.time() - t0
208
+ results[name] = {
209
+ "status": "ok",
210
+ "result": str(result)[:200],
211
+ "time_seconds": round(elapsed, 2),
212
+ }
213
+ except Exception as e:
214
+ results[name] = {
215
+ "status": "error",
216
+ "error": str(e)[:500],
217
+ }
218
+
219
+ return results
220
+