Commit ·
2df4f2d
1
Parent(s): 239c0bb
fix: Add wait_for_model, debug endpoint, and better HF API error handling
Browse files- src/app.py +18 -1
- 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 |
-
|
|
|
|
| 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 |
-
#
|
| 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=
|
| 42 |
"""
|
| 43 |
Call HuggingFace Inference API.
|
| 44 |
-
|
| 45 |
-
|
|
|
|
| 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 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 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
|
| 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
|
| 140 |
-
|
| 141 |
-
# Text2text models return: [{"generated_text": "..."}]
|
| 142 |
if isinstance(result, list) and len(result) > 0:
|
| 143 |
-
|
| 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
|
| 159 |
-
|
| 160 |
-
# Encoder-decoder models return: [{"generated_text": "..."}]
|
| 161 |
if isinstance(result, list) and len(result) > 0:
|
| 162 |
-
|
| 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
|
| 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 |
+
|