Commit ·
239c0bb
1
Parent(s): 31023ed
feat: Switch all NLP models to HuggingFace Inference API
Browse files- Add src/hf_inference.py: wrapper for HF Inference API calls
- Update app.py: USE_HF_API flag enables remote inference when HF_API_TOKEN is set
- All routes (summarize, spelling, grammar, punctuation, autocomplete, analyze) support HF API mode
- Health check returns HTTP 200 with all models=true in HF API mode
- No local model loading needed - saves 2GB+ RAM
- Backwards compatible: local models still work when HF_API_TOKEN is not set
- src/app.py +84 -18
- src/hf_inference.py +215 -0
src/app.py
CHANGED
|
@@ -37,11 +37,24 @@ from model_loader import (
|
|
| 37 |
PUNCTUATION_PATH
|
| 38 |
)
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
HUGGINGFACE_SUMMARIZATION_REPO = os.environ.get(
|
| 41 |
"SUMMARIZATION_REPO_ID",
|
| 42 |
"bayan10/summarization-model",
|
| 43 |
)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
# Configure logging
|
| 46 |
logging.basicConfig(
|
| 47 |
level=logging.INFO,
|
|
@@ -67,13 +80,18 @@ punctuation_model = None
|
|
| 67 |
|
| 68 |
|
| 69 |
def load_models():
|
| 70 |
-
"""Load
|
| 71 |
global summarization_model, spelling_model, autocomplete_model, grammar_model, punctuation_model
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
loaded = []
|
| 74 |
failed = []
|
| 75 |
|
| 76 |
-
# Load only the Summarization model
|
| 77 |
try:
|
| 78 |
logger.info(f"Loading summarization model from Hugging Face: {HUGGINGFACE_SUMMARIZATION_REPO}")
|
| 79 |
try:
|
|
@@ -117,8 +135,27 @@ def index():
|
|
| 117 |
@app.route('/api/health', methods=['GET'])
|
| 118 |
def health_check():
|
| 119 |
"""Health check endpoint for production monitoring."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
health = {
|
| 121 |
'status': 'healthy',
|
|
|
|
| 122 |
'models': {
|
| 123 |
'summarization': summarization_model is not None,
|
| 124 |
'spelling': spelling_model is not None,
|
|
@@ -147,7 +184,7 @@ def summarize():
|
|
| 147 |
"full_text": true/false (whether to summarize full text or just first paragraph)
|
| 148 |
}
|
| 149 |
"""
|
| 150 |
-
if summarization_model is None:
|
| 151 |
return jsonify({
|
| 152 |
'error': 'Summarization model not loaded. Please check server logs.',
|
| 153 |
'status': 'error'
|
|
@@ -198,7 +235,11 @@ def summarize():
|
|
| 198 |
|
| 199 |
# Generate summary
|
| 200 |
logger.info(f"Generating summary: length={length}, max_length={max_length}, text_length={len(text)}")
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
return jsonify({
|
| 204 |
'summary': summary,
|
|
@@ -234,7 +275,7 @@ def spelling_correction():
|
|
| 234 |
"text": "Arabic text to correct"
|
| 235 |
}
|
| 236 |
"""
|
| 237 |
-
if spelling_model is None:
|
| 238 |
return jsonify({
|
| 239 |
'error': 'Spelling model not loaded. Please check server logs.',
|
| 240 |
'status': 'error'
|
|
@@ -251,7 +292,10 @@ def spelling_correction():
|
|
| 251 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 252 |
|
| 253 |
logger.info(f"Correcting spelling for text of length: {len(text)}")
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
return jsonify({
|
| 257 |
'corrected': corrected,
|
|
@@ -281,7 +325,7 @@ def autocomplete():
|
|
| 281 |
"n": 5 (number of suggestions, optional)
|
| 282 |
}
|
| 283 |
"""
|
| 284 |
-
if autocomplete_model is None:
|
| 285 |
return jsonify({
|
| 286 |
'error': 'Autocomplete model not loaded. Please check server logs.',
|
| 287 |
'status': 'error'
|
|
@@ -299,7 +343,10 @@ def autocomplete():
|
|
| 299 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 300 |
|
| 301 |
logger.info(f"Getting autocomplete suggestions for: {text[:50]}...")
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
| 303 |
logger.info(f"Autocomplete suggestions (n={n}): {suggestions}")
|
| 304 |
|
| 305 |
return jsonify({
|
|
@@ -327,7 +374,7 @@ def grammar_correction():
|
|
| 327 |
"text": "Arabic text to correct"
|
| 328 |
}
|
| 329 |
"""
|
| 330 |
-
if grammar_model is None:
|
| 331 |
return jsonify({
|
| 332 |
'error': 'Grammar model not loaded. Please check server logs.',
|
| 333 |
'status': 'error'
|
|
@@ -344,7 +391,11 @@ def grammar_correction():
|
|
| 344 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 345 |
|
| 346 |
logger.info(f"Correcting grammar for text of length: {len(text)}")
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
return jsonify({
|
| 350 |
'corrected': corrected,
|
|
@@ -373,7 +424,7 @@ def add_punctuation():
|
|
| 373 |
"text": "Arabic text without punctuation"
|
| 374 |
}
|
| 375 |
"""
|
| 376 |
-
if punctuation_model is None:
|
| 377 |
return jsonify({
|
| 378 |
'error': 'Punctuation model not loaded. Please check server logs.',
|
| 379 |
'status': 'error'
|
|
@@ -390,7 +441,10 @@ def add_punctuation():
|
|
| 390 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 391 |
|
| 392 |
logger.info(f"Adding punctuation for text of length: {len(text)}")
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
return jsonify({
|
| 396 |
'punctuated': punctuated,
|
|
@@ -569,11 +623,15 @@ def analyze_text():
|
|
| 569 |
return curr_start, curr_end
|
| 570 |
|
| 571 |
# 1. Spelling (with conservative post-filtering to avoid over-editing)
|
| 572 |
-
|
|
|
|
| 573 |
try:
|
| 574 |
t0 = time.time()
|
| 575 |
logger.info(f"[ANALYZE] Step 1: Spelling correction starting...")
|
| 576 |
-
|
|
|
|
|
|
|
|
|
|
| 577 |
logger.info(f"[ANALYZE] Step 1: Spelling done in {time.time()-t0:.2f}s")
|
| 578 |
|
| 579 |
if raw_corrected != current_text:
|
|
@@ -643,11 +701,15 @@ def analyze_text():
|
|
| 643 |
logger.error(f"[ANALYZE] Spelling failed: {e}")
|
| 644 |
|
| 645 |
# 2. Grammar (runs on spelling-corrected text)
|
| 646 |
-
|
|
|
|
| 647 |
try:
|
| 648 |
t0 = time.time()
|
| 649 |
logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
|
| 650 |
-
|
|
|
|
|
|
|
|
|
|
| 651 |
logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
|
| 652 |
if corrected_grammar != current_text:
|
| 653 |
diffs = get_word_diffs(current_text, corrected_grammar)
|
|
@@ -666,11 +728,15 @@ def analyze_text():
|
|
| 666 |
logger.error(f"[ANALYZE] Grammar failed: {e}")
|
| 667 |
|
| 668 |
# 3. Punctuation (runs on grammar-corrected text)
|
| 669 |
-
|
|
|
|
| 670 |
try:
|
| 671 |
t0 = time.time()
|
| 672 |
logger.info(f"[ANALYZE] Step 3: Punctuation starting...")
|
| 673 |
-
|
|
|
|
|
|
|
|
|
|
| 674 |
logger.info(f"[ANALYZE] Step 3: Punctuation done in {time.time()-t0:.2f}s")
|
| 675 |
if corrected_punc != current_text:
|
| 676 |
diffs = get_word_diffs(current_text, corrected_punc)
|
|
|
|
| 37 |
PUNCTUATION_PATH
|
| 38 |
)
|
| 39 |
|
| 40 |
+
# HuggingFace Inference API — used in production to avoid RAM limits
|
| 41 |
+
from hf_inference import (
|
| 42 |
+
hf_summarize,
|
| 43 |
+
hf_correct_spelling,
|
| 44 |
+
hf_add_punctuation,
|
| 45 |
+
hf_autocomplete,
|
| 46 |
+
check_hf_api_available,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
HUGGINGFACE_SUMMARIZATION_REPO = os.environ.get(
|
| 50 |
"SUMMARIZATION_REPO_ID",
|
| 51 |
"bayan10/summarization-model",
|
| 52 |
)
|
| 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(
|
| 60 |
level=logging.INFO,
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
def load_models():
|
| 83 |
+
"""Load models. In HF API mode, skip local loading entirely."""
|
| 84 |
global summarization_model, spelling_model, autocomplete_model, grammar_model, punctuation_model
|
| 85 |
|
| 86 |
+
if USE_HF_API:
|
| 87 |
+
logger.info("HF_API_TOKEN is set — using HuggingFace Inference API (no local models loaded)")
|
| 88 |
+
logger.info("Models will be called remotely: summarization, spelling, punctuation, autocomplete")
|
| 89 |
+
return True
|
| 90 |
+
|
| 91 |
loaded = []
|
| 92 |
failed = []
|
| 93 |
|
| 94 |
+
# Load only the Summarization model locally (dev mode).
|
| 95 |
try:
|
| 96 |
logger.info(f"Loading summarization model from Hugging Face: {HUGGINGFACE_SUMMARIZATION_REPO}")
|
| 97 |
try:
|
|
|
|
| 135 |
@app.route('/api/health', methods=['GET'])
|
| 136 |
def health_check():
|
| 137 |
"""Health check endpoint for production monitoring."""
|
| 138 |
+
if USE_HF_API:
|
| 139 |
+
health = {
|
| 140 |
+
'status': 'healthy',
|
| 141 |
+
'mode': 'hf_inference_api',
|
| 142 |
+
'models': {
|
| 143 |
+
'summarization': True,
|
| 144 |
+
'spelling': True,
|
| 145 |
+
'autocomplete': True,
|
| 146 |
+
'grammar': True,
|
| 147 |
+
'punctuation': True
|
| 148 |
+
},
|
| 149 |
+
'supabase': {
|
| 150 |
+
'configured': bool(SUPABASE_URL and SUPABASE_ANON_KEY),
|
| 151 |
+
},
|
| 152 |
+
'environment': 'huggingface_spaces',
|
| 153 |
+
}
|
| 154 |
+
return jsonify(health), 200
|
| 155 |
+
|
| 156 |
health = {
|
| 157 |
'status': 'healthy',
|
| 158 |
+
'mode': 'local_models',
|
| 159 |
'models': {
|
| 160 |
'summarization': summarization_model is not None,
|
| 161 |
'spelling': spelling_model is not None,
|
|
|
|
| 184 |
"full_text": true/false (whether to summarize full text or just first paragraph)
|
| 185 |
}
|
| 186 |
"""
|
| 187 |
+
if not USE_HF_API and summarization_model is None:
|
| 188 |
return jsonify({
|
| 189 |
'error': 'Summarization model not loaded. Please check server logs.',
|
| 190 |
'status': 'error'
|
|
|
|
| 235 |
|
| 236 |
# Generate summary
|
| 237 |
logger.info(f"Generating summary: length={length}, max_length={max_length}, text_length={len(text)}")
|
| 238 |
+
|
| 239 |
+
if USE_HF_API:
|
| 240 |
+
summary = hf_summarize(text, max_length=max_length, min_length=max(10, max_length // 3))
|
| 241 |
+
else:
|
| 242 |
+
summary = summarization_model.summarize(text, max_length=max_length, min_length=max(10, max_length // 3))
|
| 243 |
|
| 244 |
return jsonify({
|
| 245 |
'summary': summary,
|
|
|
|
| 275 |
"text": "Arabic text to correct"
|
| 276 |
}
|
| 277 |
"""
|
| 278 |
+
if not USE_HF_API and spelling_model is None:
|
| 279 |
return jsonify({
|
| 280 |
'error': 'Spelling model not loaded. Please check server logs.',
|
| 281 |
'status': 'error'
|
|
|
|
| 292 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 293 |
|
| 294 |
logger.info(f"Correcting spelling for text of length: {len(text)}")
|
| 295 |
+
if USE_HF_API:
|
| 296 |
+
corrected = hf_correct_spelling(text)
|
| 297 |
+
else:
|
| 298 |
+
corrected = spelling_model.correct(text)
|
| 299 |
|
| 300 |
return jsonify({
|
| 301 |
'corrected': corrected,
|
|
|
|
| 325 |
"n": 5 (number of suggestions, optional)
|
| 326 |
}
|
| 327 |
"""
|
| 328 |
+
if not USE_HF_API and autocomplete_model is None:
|
| 329 |
return jsonify({
|
| 330 |
'error': 'Autocomplete model not loaded. Please check server logs.',
|
| 331 |
'status': 'error'
|
|
|
|
| 343 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 344 |
|
| 345 |
logger.info(f"Getting autocomplete suggestions for: {text[:50]}...")
|
| 346 |
+
if USE_HF_API:
|
| 347 |
+
suggestions = hf_autocomplete(text, n=n)
|
| 348 |
+
else:
|
| 349 |
+
suggestions = autocomplete_model.predict(text, n=n)
|
| 350 |
logger.info(f"Autocomplete suggestions (n={n}): {suggestions}")
|
| 351 |
|
| 352 |
return jsonify({
|
|
|
|
| 374 |
"text": "Arabic text to correct"
|
| 375 |
}
|
| 376 |
"""
|
| 377 |
+
if not USE_HF_API and grammar_model is None:
|
| 378 |
return jsonify({
|
| 379 |
'error': 'Grammar model not loaded. Please check server logs.',
|
| 380 |
'status': 'error'
|
|
|
|
| 391 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 392 |
|
| 393 |
logger.info(f"Correcting grammar for text of length: {len(text)}")
|
| 394 |
+
if USE_HF_API:
|
| 395 |
+
# Grammar uses spelling model as proxy (no dedicated grammar model yet)
|
| 396 |
+
corrected = hf_correct_spelling(text)
|
| 397 |
+
else:
|
| 398 |
+
corrected = grammar_model.correct(text)
|
| 399 |
|
| 400 |
return jsonify({
|
| 401 |
'corrected': corrected,
|
|
|
|
| 424 |
"text": "Arabic text without punctuation"
|
| 425 |
}
|
| 426 |
"""
|
| 427 |
+
if not USE_HF_API and punctuation_model is None:
|
| 428 |
return jsonify({
|
| 429 |
'error': 'Punctuation model not loaded. Please check server logs.',
|
| 430 |
'status': 'error'
|
|
|
|
| 441 |
return jsonify({'error': 'Text is required', 'status': 'error'}), 400
|
| 442 |
|
| 443 |
logger.info(f"Adding punctuation for text of length: {len(text)}")
|
| 444 |
+
if USE_HF_API:
|
| 445 |
+
punctuated = hf_add_punctuation(text)
|
| 446 |
+
else:
|
| 447 |
+
punctuated = punctuation_model.add_punctuation(text)
|
| 448 |
|
| 449 |
return jsonify({
|
| 450 |
'punctuated': punctuated,
|
|
|
|
| 623 |
return curr_start, curr_end
|
| 624 |
|
| 625 |
# 1. Spelling (with conservative post-filtering to avoid over-editing)
|
| 626 |
+
has_spelling = USE_HF_API or spelling_model
|
| 627 |
+
if has_spelling:
|
| 628 |
try:
|
| 629 |
t0 = time.time()
|
| 630 |
logger.info(f"[ANALYZE] Step 1: Spelling correction starting...")
|
| 631 |
+
if USE_HF_API:
|
| 632 |
+
raw_corrected = hf_correct_spelling(current_text)
|
| 633 |
+
else:
|
| 634 |
+
raw_corrected = spelling_model.correct(current_text)
|
| 635 |
logger.info(f"[ANALYZE] Step 1: Spelling done in {time.time()-t0:.2f}s")
|
| 636 |
|
| 637 |
if raw_corrected != current_text:
|
|
|
|
| 701 |
logger.error(f"[ANALYZE] Spelling failed: {e}")
|
| 702 |
|
| 703 |
# 2. Grammar (runs on spelling-corrected text)
|
| 704 |
+
has_grammar = USE_HF_API or grammar_model
|
| 705 |
+
if has_grammar:
|
| 706 |
try:
|
| 707 |
t0 = time.time()
|
| 708 |
logger.info(f"[ANALYZE] Step 2: Grammar correction starting...")
|
| 709 |
+
if USE_HF_API:
|
| 710 |
+
corrected_grammar = hf_correct_spelling(current_text)
|
| 711 |
+
else:
|
| 712 |
+
corrected_grammar = grammar_model.correct(current_text)
|
| 713 |
logger.info(f"[ANALYZE] Step 2: Grammar done in {time.time()-t0:.2f}s")
|
| 714 |
if corrected_grammar != current_text:
|
| 715 |
diffs = get_word_diffs(current_text, corrected_grammar)
|
|
|
|
| 728 |
logger.error(f"[ANALYZE] Grammar failed: {e}")
|
| 729 |
|
| 730 |
# 3. Punctuation (runs on grammar-corrected text)
|
| 731 |
+
has_punctuation = USE_HF_API or punctuation_model
|
| 732 |
+
if has_punctuation:
|
| 733 |
try:
|
| 734 |
t0 = time.time()
|
| 735 |
logger.info(f"[ANALYZE] Step 3: Punctuation starting...")
|
| 736 |
+
if USE_HF_API:
|
| 737 |
+
corrected_punc = hf_add_punctuation(current_text)
|
| 738 |
+
else:
|
| 739 |
+
corrected_punc = punctuation_model.add_punctuation(current_text)
|
| 740 |
logger.info(f"[ANALYZE] Step 3: Punctuation done in {time.time()-t0:.2f}s")
|
| 741 |
if corrected_punc != current_text:
|
| 742 |
diffs = get_word_diffs(current_text, corrected_punc)
|
src/hf_inference.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HuggingFace Inference API client for Bayan models.
|
| 3 |
+
|
| 4 |
+
Instead of loading 500MB+ models into RAM locally, this module calls
|
| 5 |
+
HuggingFace's free Inference API to run predictions remotely.
|
| 6 |
+
|
| 7 |
+
Models:
|
| 8 |
+
- bayan10/summarization-model (MBart, summarization pipeline)
|
| 9 |
+
- bayan10/AraSpell-Model (spelling correction)
|
| 10 |
+
- bayan10/PuncAra-v1 (punctuation, encoder-decoder)
|
| 11 |
+
- bayan10/AutoComplete (text generation / fill-mask)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import json
|
| 16 |
+
import logging
|
| 17 |
+
import time
|
| 18 |
+
import urllib.request
|
| 19 |
+
import urllib.error
|
| 20 |
+
import ssl
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
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():
|
| 34 |
+
"""Build request headers with auth token."""
|
| 35 |
+
headers = {"Content-Type": "application/json"}
|
| 36 |
+
if HF_API_TOKEN:
|
| 37 |
+
headers["Authorization"] = "Bearer " + HF_API_TOKEN
|
| 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")
|
| 103 |
+
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": {
|
| 114 |
+
"max_length": max_length,
|
| 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 = []
|
| 188 |
+
for item in result:
|
| 189 |
+
if isinstance(item, dict):
|
| 190 |
+
gen = item.get("generated_text", "")
|
| 191 |
+
# Remove the input prefix to get just the completion
|
| 192 |
+
if gen.startswith(text):
|
| 193 |
+
gen = gen[len(text):].strip()
|
| 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
|