youssefreda9 commited on
Commit
dd64fe1
·
1 Parent(s): f69f6eb

fix: Load summarization model locally with float16 (HF free tier has no outbound DNS)

Browse files

- HF Spaces free tier blocks ALL outbound DNS - no external API calls possible
- Load summarization model locally with float16 (280MB vs 560MB)
- Install CPU-only PyTorch (~200MB vs ~2GB)
- Spelling/punctuation/autocomplete gracefully degrade (return input unchanged)
- Health check accurately reports model availability

Files changed (4) hide show
  1. Dockerfile +4 -2
  2. src/app.py +17 -16
  3. src/hf_inference.py +63 -191
  4. src/model_loader.py +4 -4
Dockerfile CHANGED
@@ -8,8 +8,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
  # Copy requirements and install Python dependencies
 
11
  COPY requirements.txt .
12
- RUN pip install --no-cache-dir -r requirements.txt
 
13
 
14
  # Copy application code
15
  COPY src/ ./src/
@@ -23,5 +25,5 @@ ENV PYTHONUNBUFFERED=1
23
  # Expose port
24
  EXPOSE 7860
25
 
26
- # Start the app with gunicorn
27
  CMD ["gunicorn", "--chdir", "src", "app:app", "--bind", "0.0.0.0:7860", "--timeout", "120", "--workers", "1"]
 
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
  # Copy requirements and install Python dependencies
11
+ # Install CPU-only PyTorch first (saves ~1.5GB vs full torch)
12
  COPY requirements.txt .
13
+ RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu && \
14
+ pip install --no-cache-dir -r requirements.txt
15
 
16
  # Copy application code
17
  COPY src/ ./src/
 
25
  # Expose port
26
  EXPOSE 7860
27
 
28
+ # Start the app with gunicorn (single worker to minimize RAM)
29
  CMD ["gunicorn", "--chdir", "src", "app:app", "--bind", "0.0.0.0:7860", "--timeout", "120", "--workers", "1"]
src/app.py CHANGED
@@ -81,13 +81,14 @@ punctuation_model = None
81
 
82
 
83
  def load_models():
84
- """Load models. In HF API mode, skip local loading entirely."""
85
  global summarization_model, spelling_model, autocomplete_model, grammar_model, punctuation_model
86
 
87
  if USE_HF_API:
88
- logger.info("HF_API_TOKEN is set — using HuggingFace Inference API (no local models loaded)")
89
- logger.info("Models will be called remotely: summarization, spelling, punctuation, autocomplete")
90
- return True
 
91
 
92
  loaded = []
93
  failed = []
@@ -139,20 +140,22 @@ def health_check():
139
  if USE_HF_API:
140
  health = {
141
  'status': 'healthy',
142
- 'mode': 'hf_inference_api',
143
  'models': {
144
- 'summarization': True,
145
- 'spelling': True,
146
- 'autocomplete': True,
147
- 'grammar': True,
148
- 'punctuation': True
149
  },
 
150
  'supabase': {
151
  'configured': bool(SUPABASE_URL and SUPABASE_ANON_KEY),
152
  },
153
  'environment': 'huggingface_spaces',
154
  }
155
- return jsonify(health), 200
 
156
 
157
  health = {
158
  'status': 'healthy',
@@ -201,7 +204,7 @@ def summarize():
201
  "full_text": true/false (whether to summarize full text or just first paragraph)
202
  }
203
  """
204
- if not USE_HF_API and summarization_model is None:
205
  return jsonify({
206
  'error': 'Summarization model not loaded. Please check server logs.',
207
  'status': 'error'
@@ -253,10 +256,8 @@ def summarize():
253
  # Generate summary
254
  logger.info(f"Generating summary: length={length}, max_length={max_length}, text_length={len(text)}")
255
 
256
- if USE_HF_API:
257
- summary = hf_summarize(text, max_length=max_length, min_length=max(10, max_length // 3))
258
- else:
259
- summary = summarization_model.summarize(text, max_length=max_length, min_length=max(10, max_length // 3))
260
 
261
  return jsonify({
262
  'summary': summary,
 
81
 
82
 
83
  def load_models():
84
+ """Load models. In HF API mode, load summarization locally; other models gracefully degrade."""
85
  global summarization_model, spelling_model, autocomplete_model, grammar_model, punctuation_model
86
 
87
  if USE_HF_API:
88
+ logger.info("HF_API_TOKEN is set — HF API mode enabled")
89
+ logger.info("NOTE: HF Spaces free tier has NO outbound DNS. Loading summarization model locally.")
90
+ logger.info("Spelling, punctuation, autocomplete will gracefully degrade (return input unchanged).")
91
+ # Fall through to load summarization model locally
92
 
93
  loaded = []
94
  failed = []
 
140
  if USE_HF_API:
141
  health = {
142
  'status': 'healthy',
143
+ 'mode': 'hf_spaces_local',
144
  'models': {
145
+ 'summarization': summarization_model is not None,
146
+ 'spelling': False,
147
+ 'autocomplete': False,
148
+ 'grammar': False,
149
+ 'punctuation': False
150
  },
151
+ 'note': 'Free tier: summarization local, other models return input unchanged',
152
  'supabase': {
153
  'configured': bool(SUPABASE_URL and SUPABASE_ANON_KEY),
154
  },
155
  'environment': 'huggingface_spaces',
156
  }
157
+ status_code = 200 if summarization_model is not None else 503
158
+ return jsonify(health), status_code
159
 
160
  health = {
161
  'status': 'healthy',
 
204
  "full_text": true/false (whether to summarize full text or just first paragraph)
205
  }
206
  """
207
+ if summarization_model is None:
208
  return jsonify({
209
  'error': 'Summarization model not loaded. Please check server logs.',
210
  'status': 'error'
 
256
  # Generate summary
257
  logger.info(f"Generating summary: length={length}, max_length={max_length}, text_length={len(text)}")
258
 
259
+ # Always use local model (HF Spaces free tier has no outbound DNS for API calls)
260
+ summary = summarization_model.summarize(text, max_length=max_length, min_length=max(10, max_length // 3))
 
 
261
 
262
  return jsonify({
263
  'summary': summary,
src/hf_inference.py CHANGED
@@ -1,226 +1,98 @@
1
  """
2
  HuggingFace Inference API client for Bayan models.
3
 
4
- Dynamically discovers a reachable HF API endpoint from inside HF Spaces,
5
- since api-inference.huggingface.co is not DNS-resolvable from free-tier containers.
 
 
 
 
 
 
6
  """
7
 
8
  import os
9
- import json
10
  import logging
11
  import time
12
- import socket
13
 
14
  logger = logging.getLogger(__name__)
15
 
16
  HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
17
 
18
- _client = None
19
- _working_url = None
20
-
21
-
22
- def _get_client():
23
- global _client
24
- if _client is None:
25
- from huggingface_hub import InferenceClient
26
- _client = InferenceClient(token=HF_API_TOKEN if HF_API_TOKEN else None)
27
- return _client
28
-
29
-
30
- # Repository IDs
31
  SUMMARIZATION_REPO = os.environ.get("SUMMARIZATION_REPO_ID", "bayan10/summarization-model")
32
  SPELLING_REPO = os.environ.get("SPELLING_REPO_ID", "bayan10/AraSpell-Model")
33
  PUNCTUATION_REPO = os.environ.get("PUNCTUATION_REPO_ID", "bayan10/PuncAra-v1")
34
  AUTOCOMPLETE_REPO = os.environ.get("AUTOCOMPLETE_REPO_ID", "bayan10/AutoComplete")
35
 
36
 
37
- def _find_working_endpoint():
38
- """Try multiple HF API endpoints to find one that resolves."""
39
- global _working_url
40
-
41
- if _working_url:
42
- return _working_url
43
-
44
- # Candidate API endpoints
45
- candidates = [
46
- "https://router.huggingface.co/hf-inference/models/",
47
- "https://api-inference.huggingface.co/models/",
48
- "https://api.huggingface.co/models/",
49
- "https://huggingface.co/api/models/",
50
- ]
51
-
52
- for url in candidates:
53
- # Extract hostname from URL
54
- hostname = url.split("//")[1].split("/")[0]
55
- try:
56
- socket.getaddrinfo(hostname, 443)
57
- logger.info("DNS resolved for: %s", hostname)
58
- _working_url = url
59
- return url
60
- except socket.gaierror:
61
- logger.warning("DNS failed for: %s", hostname)
62
- continue
63
-
64
- logger.error("No HF API endpoint is reachable!")
65
- return None
66
-
67
-
68
- def _call_model_httpx(repo_id, payload, task=""):
69
- """Call HF model using httpx (same transport as InferenceClient)."""
70
- import httpx
71
-
72
- base_url = _find_working_endpoint()
73
- if not base_url:
74
- raise RuntimeError("No reachable HF API endpoint found")
75
-
76
- url = base_url + repo_id
77
-
78
- if "options" not in payload:
79
- payload["options"] = {"wait_for_model": True}
80
-
81
- headers = {"Content-Type": "application/json"}
82
- if HF_API_TOKEN:
83
- headers["Authorization"] = "Bearer " + HF_API_TOKEN
84
-
85
- logger.info("Calling HF model: %s at %s", repo_id, url)
86
-
87
- with httpx.Client(timeout=120) as client:
88
- resp = client.post(url, json=payload, headers=headers)
89
-
90
- if resp.status_code != 200:
91
- raise RuntimeError(f"HF API error (HTTP {resp.status_code}): {resp.text[:300]}")
92
-
93
- result = resp.json()
94
- logger.info("HF result for %s: type=%s preview=%s",
95
- repo_id, type(result).__name__, str(result)[:200])
96
-
97
- if isinstance(result, dict) and "error" in result:
98
- raise RuntimeError("HF API error: " + str(result["error"]))
99
-
100
- return result
101
-
102
-
103
- def _extract_text(result, fallback=""):
104
- """Extract text from various HF response formats."""
105
- if isinstance(result, list) and len(result) > 0:
106
- item = result[0]
107
- if isinstance(item, dict):
108
- return (item.get("summary_text")
109
- or item.get("generated_text")
110
- or item.get("translation_text")
111
- or fallback)
112
- return str(item) if str(item).strip() else fallback
113
- if isinstance(result, dict):
114
- return (result.get("summary_text")
115
- or result.get("generated_text")
116
- or result.get("translation_text")
117
- or fallback)
118
- return str(result) if result else fallback
119
-
120
-
121
- # ============================================================
122
- # Model wrappers
123
- # ============================================================
124
-
125
  def hf_summarize(text, max_length=128, min_length=30):
126
- result = _call_model_httpx(SUMMARIZATION_REPO, {
127
- "inputs": text,
128
- "parameters": {"max_length": max_length, "min_length": min_length},
129
- }, task="summarization")
130
- return _extract_text(result, text[:100])
 
 
 
 
 
 
131
 
132
 
133
  def hf_correct_spelling(text):
134
- result = _call_model_httpx(SPELLING_REPO, {"inputs": text})
135
- return _extract_text(result, text)
 
 
 
 
136
 
137
 
138
  def hf_add_punctuation(text):
139
- result = _call_model_httpx(PUNCTUATION_REPO, {"inputs": text})
140
- return _extract_text(result, text)
 
 
 
 
141
 
142
 
143
  def hf_autocomplete(text, n=5):
144
- result = _call_model_httpx(AUTOCOMPLETE_REPO, {
145
- "inputs": text,
146
- "parameters": {"max_new_tokens": 20},
147
- }, task="text-generation")
148
-
149
- if isinstance(result, str):
150
- c = result[len(text):].strip() if result.startswith(text) else result
151
- return [c] if c else [text]
152
- if isinstance(result, list):
153
- out = []
154
- for item in result:
155
- g = item.get("generated_text", "") if isinstance(item, dict) else str(item)
156
- if g.startswith(text):
157
- g = g[len(text):].strip()
158
- if g:
159
- out.append(g)
160
- return out if out else [text]
161
- return [text]
162
 
163
 
164
  def check_hf_api_available():
165
- try:
166
- return _find_working_endpoint() is not None
167
- except Exception:
168
- return False
169
 
170
 
171
  def debug_test_all_models():
172
- """Test DNS resolution + all HF models."""
173
- results = {}
174
- test_text = "هذا نص تجريبي للاختبار"
175
- long_text = (test_text + " ") * 5
176
-
177
- # DNS diagnostics
178
- dns_results = {}
179
- hostnames = [
180
- "router.huggingface.co",
181
- "api-inference.huggingface.co",
182
- "api.huggingface.co",
183
- "huggingface.co",
184
- "hf.co",
185
- "google.com",
186
- "pypi.org",
187
- ]
188
- for hostname in hostnames:
189
- try:
190
- addrs = socket.getaddrinfo(hostname, 443)
191
- dns_results[hostname] = "OK (" + addrs[0][4][0] + ")"
192
- except socket.gaierror as e:
193
- dns_results[hostname] = "FAIL: " + str(e)
194
-
195
- results["_dns"] = dns_results
196
- results["_working_endpoint"] = _find_working_endpoint() or "NONE"
197
-
198
- try:
199
- import huggingface_hub
200
- results["_info"] = {"hf_hub_version": huggingface_hub.__version__}
201
- except Exception as e:
202
- results["_info"] = {"error": repr(e)[:200]}
203
-
204
- for name, fn, args in [
205
- ("summarization", hf_summarize, (long_text, 30, 10)),
206
- ("spelling", hf_correct_spelling, (test_text,)),
207
- ("punctuation", hf_add_punctuation, (test_text,)),
208
- ("autocomplete", hf_autocomplete, (test_text, 3)),
209
- ]:
210
- try:
211
- t0 = time.time()
212
- result = fn(*args)
213
- elapsed = time.time() - t0
214
- results[name] = {
215
- "status": "ok",
216
- "result": str(result)[:300],
217
- "time_seconds": round(elapsed, 2),
218
- }
219
- except Exception as e:
220
- results[name] = {
221
- "status": "error",
222
- "error_type": type(e).__name__,
223
- "error": str(e)[:500] if str(e) else repr(e)[:500],
224
- }
225
-
226
- return results
 
1
  """
2
  HuggingFace Inference API client for Bayan models.
3
 
4
+ IMPORTANT: HF Spaces free tier has NO outbound DNS resolution.
5
+ Neither urllib, requests, httpx, nor InferenceClient can reach
6
+ external APIs from inside the container.
7
+
8
+ This module provides graceful fallbacks:
9
+ - Summarization: uses local model (loaded in model_loader.py / app.py)
10
+ - Spelling/Punctuation/Grammar/Autocomplete: return input unchanged (graceful degradation)
11
+ These features require either a paid HF Space tier or local model files.
12
  """
13
 
14
  import os
 
15
  import logging
16
  import time
 
17
 
18
  logger = logging.getLogger(__name__)
19
 
20
  HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "")
21
 
22
+ # Repository IDs (kept for reference)
 
 
 
 
 
 
 
 
 
 
 
 
23
  SUMMARIZATION_REPO = os.environ.get("SUMMARIZATION_REPO_ID", "bayan10/summarization-model")
24
  SPELLING_REPO = os.environ.get("SPELLING_REPO_ID", "bayan10/AraSpell-Model")
25
  PUNCTUATION_REPO = os.environ.get("PUNCTUATION_REPO_ID", "bayan10/PuncAra-v1")
26
  AUTOCOMPLETE_REPO = os.environ.get("AUTOCOMPLETE_REPO_ID", "bayan10/AutoComplete")
27
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  def hf_summarize(text, max_length=128, min_length=30):
30
+ """
31
+ Summarize Arabic text.
32
+ NOTE: In HF API mode, this should NOT be called — the local
33
+ summarization model is used instead (see app.py load_models).
34
+ This is a fallback that returns the first few sentences.
35
+ """
36
+ logger.warning("hf_summarize called but no external API available. Using extractive fallback.")
37
+ # Simple extractive fallback: first N words
38
+ words = text.split()
39
+ target = max(10, max_length // 4)
40
+ return " ".join(words[:target]).strip()
41
 
42
 
43
  def hf_correct_spelling(text):
44
+ """
45
+ Correct spelling — graceful degradation (returns input unchanged).
46
+ Spelling correction requires local model files or a paid tier with network access.
47
+ """
48
+ logger.info("Spelling correction unavailable (no network). Returning original text.")
49
+ return text
50
 
51
 
52
  def hf_add_punctuation(text):
53
+ """
54
+ Add punctuation — graceful degradation (returns input unchanged).
55
+ Punctuation requires local model files or a paid tier with network access.
56
+ """
57
+ logger.info("Punctuation unavailable (no network). Returning original text.")
58
+ return text
59
 
60
 
61
  def hf_autocomplete(text, n=5):
62
+ """
63
+ Autocomplete — graceful degradation (returns empty list).
64
+ Autocomplete requires local model files or a paid tier with network access.
65
+ """
66
+ logger.info("Autocomplete unavailable (no network). Returning empty.")
67
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
 
70
  def check_hf_api_available():
71
+ """HF Inference API is NOT available on free tier (no outbound DNS)."""
72
+ return False
 
 
73
 
74
 
75
  def debug_test_all_models():
76
+ """Return status of all models."""
77
+ return {
78
+ "_info": {
79
+ "note": "HF Spaces free tier has NO outbound DNS. External API calls are impossible.",
80
+ "recommendation": "Use local model loading for summarization. Other models require local files or paid tier.",
81
+ },
82
+ "summarization": {
83
+ "status": "fallback",
84
+ "note": "Using local model via model_loader.py (loaded from HF Hub at build time)",
85
+ },
86
+ "spelling": {
87
+ "status": "unavailable",
88
+ "note": "Returns input unchanged. Requires local model files.",
89
+ },
90
+ "punctuation": {
91
+ "status": "unavailable",
92
+ "note": "Returns input unchanged. Requires local model files.",
93
+ },
94
+ "autocomplete": {
95
+ "status": "unavailable",
96
+ "note": "Returns empty. Requires local model files.",
97
+ },
98
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/model_loader.py CHANGED
@@ -185,7 +185,7 @@ class SummarizationModel:
185
  config=config,
186
  local_files_only=True,
187
  trust_remote_code=False,
188
- torch_dtype=torch.float32
189
  )
190
  except Exception as e:
191
  logger.warning(f"Failed to load with config: {str(e)}")
@@ -194,7 +194,7 @@ class SummarizationModel:
194
  self.model_source,
195
  local_files_only=True,
196
  trust_remote_code=False,
197
- torch_dtype=torch.float32
198
  )
199
  except Exception as e:
200
  logger.warning(f"Failed to load config: {str(e)}")
@@ -204,7 +204,7 @@ class SummarizationModel:
204
  self.model_source,
205
  local_files_only=True,
206
  trust_remote_code=False,
207
- torch_dtype=torch.float32
208
  )
209
  except Exception as e2:
210
  logger.warning(f"Failed to load with local_files_only: {str(e2)}")
@@ -212,7 +212,7 @@ class SummarizationModel:
212
  self.model = MBartForConditionalGeneration.from_pretrained(
213
  self.model_source,
214
  trust_remote_code=False,
215
- torch_dtype=torch.float32
216
  )
217
 
218
  # Move model to device
 
185
  config=config,
186
  local_files_only=True,
187
  trust_remote_code=False,
188
+ torch_dtype=torch.float16
189
  )
190
  except Exception as e:
191
  logger.warning(f"Failed to load with config: {str(e)}")
 
194
  self.model_source,
195
  local_files_only=True,
196
  trust_remote_code=False,
197
+ torch_dtype=torch.float16
198
  )
199
  except Exception as e:
200
  logger.warning(f"Failed to load config: {str(e)}")
 
204
  self.model_source,
205
  local_files_only=True,
206
  trust_remote_code=False,
207
+ torch_dtype=torch.float16
208
  )
209
  except Exception as e2:
210
  logger.warning(f"Failed to load with local_files_only: {str(e2)}")
 
212
  self.model = MBartForConditionalGeneration.from_pretrained(
213
  self.model_source,
214
  trust_remote_code=False,
215
+ torch_dtype=torch.float16
216
  )
217
 
218
  # Move model to device