Yermek68 commited on
Commit
aadac55
·
verified ·
1 Parent(s): d9729dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -159
app.py CHANGED
@@ -1,31 +1,17 @@
1
  import os
2
  import sys
3
  import time
 
4
  import logging
5
  import traceback
6
  from contextlib import asynccontextmanager
7
- from typing import List, Dict, Any
8
 
9
  from fastapi import FastAPI, HTTPException
10
  from fastapi.middleware.cors import CORSMiddleware
11
- from pydantic import BaseModel, Field, validator
12
 
13
- # ==================== Safe optional imports ====================
14
-
15
- try:
16
- from transformers import pipeline # type: ignore
17
- except Exception as e: # pragma: no cover
18
- pipeline = None # type: ignore
19
- logging.error(f"[ImportError] transformers not available: {e}", file=sys.stderr) # type: ignore
20
-
21
- try:
22
- from langdetect import detect # type: ignore
23
- except Exception as e: # pragma: no cover
24
- detect = None # type: ignore
25
- logging.error(f"[ImportError] langdetect not available: {e}", file=sys.stderr) # type: ignore
26
-
27
-
28
- # ==================== Logging ====================
29
 
30
  logging.basicConfig(
31
  level=logging.INFO,
@@ -33,66 +19,65 @@ logging.basicConfig(
33
  handlers=[logging.StreamHandler(sys.stderr)],
34
  )
35
 
36
- logger = logging.getLogger("eroha")
 
 
37
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- # ==================== Environment / HF cache ====================
 
40
 
41
  HF_HOME = "/tmp/huggingface"
42
  os.environ["HF_HOME"] = HF_HOME
43
  os.makedirs(HF_HOME, exist_ok=True)
44
 
45
-
46
- # ==================== Pydantic models ====================
47
 
48
 
49
  class SummarizeRequest(BaseModel):
50
  text: str = Field(..., min_length=3, max_length=1_000_000)
51
 
52
- @validator("text")
53
- def not_empty(cls, v: str) -> str:
54
- if not v.strip():
55
- raise ValueError("Text cannot be empty or whitespace only")
56
- return v
57
 
58
 
59
  class CheckRequest(BaseModel):
60
- text: str = Field(..., min_length=1, max_length=500_000)
61
 
62
- @validator("text")
63
- def not_empty(cls, v: str) -> str:
64
- if not v.strip():
65
- raise ValueError("Text cannot be empty or whitespace only")
66
- return v
67
 
68
 
69
- class HealthResponse(BaseModel):
70
- status: str
71
- version: str
72
- endpoints: List[str]
73
- cached_models: List[str]
74
-
75
-
76
- # ==================== Model management ====================
77
 
78
  _model_cache: Dict[str, Any] = {}
79
 
80
 
81
- def _safe_detect_lang(text: str) -> str:
82
- if detect is None:
83
- return "en"
84
  try:
85
- return detect(text) # type: ignore[call-arg]
86
- except Exception:
 
87
  return "en"
88
 
89
 
90
- def _get_model(lang: str):
91
  if pipeline is None:
92
- raise RuntimeError("Transformers pipeline unavailable")
93
 
94
  if lang in _model_cache:
95
- logger.info("[ModelCache] using cached model for %s", lang)
96
  return _model_cache[lang]
97
 
98
  model_map = {
@@ -105,77 +90,43 @@ def _get_model(lang: str):
105
  }
106
  model_name = model_map.get(lang, "facebook/bart-large-cnn")
107
 
108
- logger.info("[ModelLoad] loading model for %s: %s", lang, model_name)
109
- try:
110
- # CPU-режим (device=-1) — безопасно для HF Spaces
111
- model = pipeline("summarization", model=model_name, device=-1) # type: ignore[arg-type]
112
- _model_cache[lang] = model
113
- logger.info("[ModelLoad] model for %s ready", lang)
114
- return model
115
- except Exception as e: # pragma: no cover
116
- logger.error("[ModelLoad] failed to load %s: %s", lang, e)
117
- raise
118
-
119
-
120
- def _summarize(text: str, lang: str) -> Dict[str, Any]:
121
- summarizer = _get_model(lang)
122
-
123
- max_input_length = 3000 # защита от слишком длинных текстов
124
- input_text = text[:max_input_length]
125
-
126
- result = summarizer(
127
- input_text,
128
- max_length=180,
129
- min_length=40,
130
- do_sample=False,
131
- )
132
-
133
- summary = result[0]["summary_text"].replace("▁", " ").strip()
134
 
135
- json_ld = {
136
- "@context": "https://schema.org",
137
- "@type": "NewsArticle",
138
- "headline": summary[:80],
139
- "inLanguage": lang,
140
- "publisher": {"@type": "Organization", "name": "Eroha AI Publisher"},
141
- }
142
-
143
- return {
144
- "summary": summary,
145
- "summary_length": len(summary),
146
- "original_length": len(text),
147
- "truncated": len(text) > max_input_length,
148
- "seo_json_ld": json_ld,
149
- }
150
-
151
-
152
- # ==================== Lifespan: warmup on startup ====================
153
 
 
154
 
155
  @asynccontextmanager
156
- async def lifespan(app: FastAPI): # type: ignore[override]
157
- logger.info("[Startup] application starting, warming models...")
158
  start = time.time()
159
- if pipeline is not None:
160
- for lang in ("en", "ru"):
161
- try:
162
- _get_model(lang)
163
- except Exception as e:
164
- logger.error("[Startup] warmup for %s failed: %s", lang, e)
165
- else:
166
- logger.warning("[Startup] transformers pipeline unavailable, skipping warmup")
167
 
168
- logger.info("[Startup] warmup finished in %.2fs", time.time() - start)
169
- yield
170
- logger.info("[Shutdown] application stopping")
 
 
 
 
 
 
 
 
 
 
 
171
 
 
172
 
173
- # ==================== FastAPI app ====================
174
 
175
 
176
  app = FastAPI(
177
  title="Eroha AI Summarizer PRO",
178
- version="v3.4",
179
  lifespan=lifespan,
180
  )
181
 
@@ -186,18 +137,17 @@ app.add_middleware(
186
  allow_headers=["*"],
187
  )
188
 
 
189
 
190
- # ==================== Routes ====================
191
 
192
-
193
- @app.get("/", response_model=HealthResponse)
194
- async def root() -> HealthResponse:
195
- return HealthResponse(
196
- status="ok",
197
- version="v3.4",
198
- endpoints=["/ping", "/check", "/summarize", "/warmup"],
199
- cached_models=list(_model_cache.keys()),
200
- )
201
 
202
 
203
  @app.get("/ping")
@@ -205,71 +155,96 @@ async def ping():
205
  return {
206
  "status": "healthy",
207
  "cached_models": list(_model_cache.keys()),
208
- "time": time.time(),
209
  }
210
 
211
 
212
  @app.get("/warmup")
213
- async def manual_warmup():
214
- if pipeline is None:
215
- return {
216
- "status": "skipped",
217
- "reason": "transformers pipeline unavailable",
218
- }
 
219
 
220
- loaded = []
221
- errors = {}
222
- for lang in ("en", "ru"):
223
- try:
224
- _get_model(lang)
225
- loaded.append(lang)
226
- except Exception as e: # pragma: no cover
227
- errors[lang] = str(e)
228
 
229
- return {
230
- "status": "ok" if not errors else "partial",
231
- "loaded": loaded,
232
- "errors": errors,
233
- "cached_models": list(_model_cache.keys()),
234
- }
 
 
235
 
236
 
237
  @app.post("/check")
238
  async def check_text(req: CheckRequest):
239
  try:
240
- lang = _safe_detect_lang(req.text)
 
241
  return {
242
  "status": "success",
243
- "preview": req.text[:150],
244
- "length": len(req.text),
245
  "detected_language": lang,
246
  }
247
- except Exception as e: # pragma: no cover
248
  logger.error("/check error: %s", traceback.format_exc())
249
- raise HTTPException(status_code=500, detail=str(e))
250
 
251
 
252
  @app.post("/summarize")
253
  async def summarize(req: SummarizeRequest):
254
- if pipeline is None:
255
- raise HTTPException(
256
- status_code=503,
257
- detail="transformers pipeline is not available in this environment",
258
- )
259
-
260
  try:
261
- lang = _safe_detect_lang(req.text)
262
- logger.info("[Summarize] language=%s, length=%d", lang, len(req.text))
263
- data = _summarize(req.text, lang)
264
- return {"status": "success", "language": lang, **data}
265
- except Exception as e: # pragma: no cover
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  logger.error("/summarize error: %s", traceback.format_exc())
267
- raise HTTPException(status_code=500, detail=str(e))
268
 
269
 
270
  if __name__ == "__main__":
271
  import uvicorn
272
 
273
- port = int(os.getenv("PORT", "7860"))
274
- uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
275
-
 
1
  import os
2
  import sys
3
  import time
4
+ import json
5
  import logging
6
  import traceback
7
  from contextlib import asynccontextmanager
8
+ from typing import Dict, Any
9
 
10
  from fastapi import FastAPI, HTTPException
11
  from fastapi.middleware.cors import CORSMiddleware
12
+ from pydantic import BaseModel, Field
13
 
14
+ # ===================== ЛОГИРОВАНИЕ =====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  logging.basicConfig(
17
  level=logging.INFO,
 
19
  handlers=[logging.StreamHandler(sys.stderr)],
20
  )
21
 
22
+ logger = logging.getLogger("eroha-app")
23
+
24
+ # ===================== БЕЗОПАСНЫЕ ИМПОРТЫ =====================
25
 
26
+ try:
27
+ from transformers import pipeline
28
+ from langdetect import detect
29
+ except Exception as e: # noqa: BLE001
30
+ # ВАЖНО: без параметра file=...
31
+ logger.error("[ImportError] transformers/langdetect not available: %s", e, exc_info=True)
32
+
33
+ pipeline = None # type: ignore[assignment]
34
+ # запасной детектор языка
35
+ def detect(text: str) -> str: # type: ignore[no-redef]
36
+ return "en"
37
 
38
+
39
+ # ===================== НАСТРОЙКИ HF =====================
40
 
41
  HF_HOME = "/tmp/huggingface"
42
  os.environ["HF_HOME"] = HF_HOME
43
  os.makedirs(HF_HOME, exist_ok=True)
44
 
45
+ # ===================== Pydantic-модели =====================
 
46
 
47
 
48
  class SummarizeRequest(BaseModel):
49
  text: str = Field(..., min_length=3, max_length=1_000_000)
50
 
51
+ def clean_text(self) -> str:
52
+ return self.text.strip()
 
 
 
53
 
54
 
55
  class CheckRequest(BaseModel):
56
+ data: str = Field(..., min_length=1, max_length=500_000)
57
 
58
+ def clean_text(self) -> str:
59
+ return self.data.strip()
 
 
 
60
 
61
 
62
+ # ===================== КЭШ МОДЕЛЕЙ =====================
 
 
 
 
 
 
 
63
 
64
  _model_cache: Dict[str, Any] = {}
65
 
66
 
67
+ def safe_detect_lang(text: str) -> str:
 
 
68
  try:
69
+ lang = detect(text)
70
+ return lang or "en"
71
+ except Exception: # noqa: BLE001
72
  return "en"
73
 
74
 
75
+ def get_model(lang: str):
76
  if pipeline is None:
77
+ raise RuntimeError("Transformers pipeline is not available")
78
 
79
  if lang in _model_cache:
80
+ logger.info("[ModelCache] Using cached model for %s", lang)
81
  return _model_cache[lang]
82
 
83
  model_map = {
 
90
  }
91
  model_name = model_map.get(lang, "facebook/bart-large-cnn")
92
 
93
+ logger.info("[ModelLoad] Loading model for %s: %s", lang, model_name)
94
+ model = pipeline("summarization", model=model_name, device=-1)
95
+ _model_cache[lang] = model
96
+ logger.info("[ModelLoad] Cached model for %s", lang)
97
+ return model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ # ===================== LIFESPAN (WARMUP) =====================
101
 
102
  @asynccontextmanager
103
+ async def lifespan(app: FastAPI): # noqa: ARG001
 
104
  start = time.time()
105
+ logger.info("[Lifespan] Application startup warmup models...")
 
 
 
 
 
 
 
106
 
107
+ try:
108
+ if pipeline is not None:
109
+ for lang in ("en", "ru"):
110
+ try:
111
+ get_model(lang)
112
+ except Exception as e: # noqa: BLE001
113
+ logger.error("[Lifespan] Warmup failed for %s: %s", lang, e, exc_info=True)
114
+ else:
115
+ logger.warning("[Lifespan] transformers pipeline is None – warmup skipped")
116
+ except Exception as e: # noqa: BLE001
117
+ logger.error("[Lifespan] Warmup error: %s", e, exc_info=True)
118
+
119
+ elapsed = time.time() - start
120
+ logger.info("[Lifespan] Startup warmup finished in %.2f s", elapsed)
121
 
122
+ yield
123
 
124
+ logger.info("[Lifespan] Application shutdown")
125
 
126
 
127
  app = FastAPI(
128
  title="Eroha AI Summarizer PRO",
129
+ version="3.4",
130
  lifespan=lifespan,
131
  )
132
 
 
137
  allow_headers=["*"],
138
  )
139
 
140
+ # ===================== ЭНДПОИНТЫ =====================
141
 
 
142
 
143
+ @app.get("/")
144
+ async def root():
145
+ return {
146
+ "status": "ok",
147
+ "version": "v3.4",
148
+ "cached_models": list(_model_cache.keys()),
149
+ "endpoints": ["/ping", "/check", "/summarize", "/warmup"],
150
+ }
 
151
 
152
 
153
  @app.get("/ping")
 
155
  return {
156
  "status": "healthy",
157
  "cached_models": list(_model_cache.keys()),
 
158
  }
159
 
160
 
161
  @app.get("/warmup")
162
+ async def warmup():
163
+ try:
164
+ if pipeline is None:
165
+ return {
166
+ "status": "skipped",
167
+ "reason": "transformers pipeline is not available",
168
+ }
169
 
170
+ loaded = []
171
+ for lang in ("en", "ru"):
172
+ try:
173
+ get_model(lang)
174
+ loaded.append(lang)
175
+ except Exception as e: # noqa: BLE001
176
+ logger.error("[Warmup] Failed for %s: %s", lang, e, exc_info=True)
 
177
 
178
+ return {
179
+ "status": "ok",
180
+ "preloaded": loaded,
181
+ "cache_size": len(_model_cache),
182
+ }
183
+ except Exception as e: # noqa: BLE001
184
+ logger.error("[Warmup] Error: %s", e, exc_info=True)
185
+ raise HTTPException(status_code=500, detail="Warmup failed") from e
186
 
187
 
188
  @app.post("/check")
189
  async def check_text(req: CheckRequest):
190
  try:
191
+ text = req.clean_text()
192
+ lang = safe_detect_lang(text)
193
  return {
194
  "status": "success",
195
+ "preview": text[:150],
196
+ "length": len(text),
197
  "detected_language": lang,
198
  }
199
+ except Exception as e: # noqa: BLE001
200
  logger.error("/check error: %s", traceback.format_exc())
201
+ raise HTTPException(status_code=500, detail=str(e)) from e
202
 
203
 
204
  @app.post("/summarize")
205
  async def summarize(req: SummarizeRequest):
 
 
 
 
 
 
206
  try:
207
+ text = req.clean_text()
208
+ if not text:
209
+ raise HTTPException(status_code=400, detail="Text cannot be empty")
210
+
211
+ lang = safe_detect_lang(text)
212
+ model = get_model(lang)
213
+
214
+ max_input_length = 3000
215
+ input_text = text[:max_input_length]
216
+
217
+ result = model(input_text, max_length=180, min_length=40, do_sample=False)
218
+ summary = result[0]["summary_text"].replace("▁", " ").strip()
219
+
220
+ seo_json_ld = {
221
+ "@context": "https://schema.org",
222
+ "@type": "NewsArticle",
223
+ "headline": summary[:80],
224
+ "inLanguage": lang,
225
+ "publisher": {
226
+ "@type": "Organization",
227
+ "name": "Eroha AI Publisher",
228
+ },
229
+ }
230
+
231
+ return {
232
+ "status": "success",
233
+ "language": lang,
234
+ "summary": summary,
235
+ "summary_length": len(summary),
236
+ "original_length": len(text),
237
+ "truncated": len(text) > max_input_length,
238
+ "seo_json_ld": seo_json_ld,
239
+ }
240
+ except HTTPException:
241
+ raise
242
+ except Exception as e: # noqa: BLE001
243
  logger.error("/summarize error: %s", traceback.format_exc())
244
+ raise HTTPException(status_code=500, detail=str(e)) from e
245
 
246
 
247
  if __name__ == "__main__":
248
  import uvicorn
249
 
250
+ uvicorn.run(app, host="0.0.0.0", port=7860)