Spaces:
Paused
Paused
lets run this
Browse files- .dockerignore +43 -0
- Dockerfile +53 -0
- app.py +61 -16
- src/textSummarizer/pipeline/prediction.py +30 -16
.dockerignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ Version control βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
|
| 5 |
+
# ββ Python bytecode βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.pyd
|
| 10 |
+
*.egg-info/
|
| 11 |
+
dist/
|
| 12 |
+
build/
|
| 13 |
+
|
| 14 |
+
# ββ Virtual environments ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
.venv/
|
| 16 |
+
venv/
|
| 17 |
+
env/
|
| 18 |
+
|
| 19 |
+
# ββ Notebooks / research ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
research/
|
| 21 |
+
*.ipynb
|
| 22 |
+
.ipynb_checkpoints/
|
| 23 |
+
|
| 24 |
+
# ββ Logs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
logs/
|
| 26 |
+
*.log
|
| 27 |
+
|
| 28 |
+
# ββ Training data (large β not needed at inference time) ββββββββββββββββββββββ
|
| 29 |
+
artifacts/data_ingestion/
|
| 30 |
+
artifacts/data_transformation/
|
| 31 |
+
artifacts/data_validation/
|
| 32 |
+
|
| 33 |
+
# ββ Intermediate training checkpoints (only final model is needed) ββββββββββββ
|
| 34 |
+
artifacts/model_trainer/checkpoint-*/
|
| 35 |
+
|
| 36 |
+
# ββ Evaluation outputs (optional, not needed to serve) ββββββββββββββββββββββββ
|
| 37 |
+
artifacts/model_evaluation/
|
| 38 |
+
|
| 39 |
+
# ββ Misc ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
.env
|
| 41 |
+
.env.*
|
| 42 |
+
*.DS_Store
|
| 43 |
+
Thumbs.db
|
Dockerfile
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ββ Base image ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# Use slim Python 3.11 image to keep the final image as small as possible.
|
| 3 |
+
FROM python:3.11-slim
|
| 4 |
+
|
| 5 |
+
# ββ System dependencies βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
build-essential \
|
| 8 |
+
git \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# ββ Working directory βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
# ββ Install Python dependencies βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
# Copy only the files needed for the package install first so Docker can cache
|
| 16 |
+
# this expensive layer and only rebuild it when requirements change.
|
| 17 |
+
COPY requirements.txt setup.py README.md ./
|
| 18 |
+
COPY src/ ./src/
|
| 19 |
+
|
| 20 |
+
RUN pip install --no-cache-dir --upgrade pip \
|
| 21 |
+
&& pip install --no-cache-dir -r requirements.txt
|
| 22 |
+
|
| 23 |
+
# ββ Copy the rest of the application βββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
COPY config/ ./config/
|
| 25 |
+
COPY params.yaml .
|
| 26 |
+
COPY app.py .
|
| 27 |
+
COPY main.py .
|
| 28 |
+
|
| 29 |
+
# ββ Copy trained model artifacts ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
# Only the inference artefacts are needed at runtime β skip training data.
|
| 31 |
+
COPY artifacts/model_trainer/pegasus-samsum-model/ \
|
| 32 |
+
./artifacts/model_trainer/pegasus-samsum-model/
|
| 33 |
+
COPY artifacts/model_trainer/tokenizer/ \
|
| 34 |
+
./artifacts/model_trainer/tokenizer/
|
| 35 |
+
|
| 36 |
+
# ββ Non-root user (security best practice) βββββββββββββββββββββββββββββββββββ
|
| 37 |
+
RUN useradd -m -u 1000 appuser \
|
| 38 |
+
&& chown -R appuser:appuser /app
|
| 39 |
+
USER appuser
|
| 40 |
+
|
| 41 |
+
# ββ Port ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
# β’ Hugging Face Spaces: must be 7860
|
| 43 |
+
# β’ Render / Railway / Fly.io: inject PORT env-var and the app picks it up
|
| 44 |
+
# β’ Oracle Cloud / local: default falls back to 7860 as well
|
| 45 |
+
EXPOSE 7860
|
| 46 |
+
|
| 47 |
+
# ββ Health check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
# Docker / orchestrators will mark the container unhealthy if /health fails.
|
| 49 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 50 |
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:${PORT:-7860}/health')"
|
| 51 |
+
|
| 52 |
+
# ββ Start server ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 53 |
+
CMD ["python", "app.py"]
|
app.py
CHANGED
|
@@ -1,20 +1,60 @@
|
|
| 1 |
-
from
|
|
|
|
|
|
|
| 2 |
import uvicorn
|
| 3 |
import subprocess
|
| 4 |
import sys
|
|
|
|
| 5 |
from starlette.responses import RedirectResponse
|
| 6 |
from fastapi.responses import Response
|
| 7 |
from textSummarizer.pipeline.prediction import PredictionPipeline
|
| 8 |
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
async def index():
|
| 13 |
return RedirectResponse(url="/docs")
|
| 14 |
|
| 15 |
|
| 16 |
-
@app.get("/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
async def training():
|
|
|
|
| 18 |
try:
|
| 19 |
result = subprocess.run(
|
| 20 |
[sys.executable, "main.py"],
|
|
@@ -26,18 +66,23 @@ async def training():
|
|
| 26 |
except subprocess.TimeoutExpired:
|
| 27 |
return Response("Training timed out!", status_code=504)
|
| 28 |
except Exception as e:
|
| 29 |
-
return Response(f"Error
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
@app.post("/predict")
|
| 33 |
-
async def predict_route(
|
|
|
|
|
|
|
|
|
|
| 34 |
try:
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
text = obj.predict(text)
|
| 38 |
-
return text
|
| 39 |
except Exception as e:
|
| 40 |
-
raise e
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
if __name__ == "__main__":
|
| 43 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import asynccontextmanager
|
| 2 |
+
from fastapi import FastAPI, HTTPException
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
import uvicorn
|
| 5 |
import subprocess
|
| 6 |
import sys
|
| 7 |
+
import os
|
| 8 |
from starlette.responses import RedirectResponse
|
| 9 |
from fastapi.responses import Response
|
| 10 |
from textSummarizer.pipeline.prediction import PredictionPipeline
|
| 11 |
|
| 12 |
+
# ββ Shared state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
+
_pipeline: PredictionPipeline | None = None
|
| 14 |
|
| 15 |
+
|
| 16 |
+
@asynccontextmanager
|
| 17 |
+
async def lifespan(app: FastAPI):
|
| 18 |
+
"""Load the model once at startup and keep it in memory."""
|
| 19 |
+
global _pipeline
|
| 20 |
+
_pipeline = PredictionPipeline()
|
| 21 |
+
_pipeline.load_model() # pre-warms tokenizer + model into RAM
|
| 22 |
+
yield
|
| 23 |
+
_pipeline = None # clean up on shutdown
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
app = FastAPI(
|
| 27 |
+
title="AI Text Summarizer",
|
| 28 |
+
version="1.0.0",
|
| 29 |
+
description="Abstractive text summarization powered by Pegasus/T5.",
|
| 30 |
+
lifespan=lifespan,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ββ Request / Response schemas ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
class SummarizeRequest(BaseModel):
|
| 36 |
+
text: str
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class SummarizeResponse(BaseModel):
|
| 40 |
+
summary: str
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ββ Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
+
@app.get("/", tags=["root"])
|
| 45 |
async def index():
|
| 46 |
return RedirectResponse(url="/docs")
|
| 47 |
|
| 48 |
|
| 49 |
+
@app.get("/health", tags=["health"])
|
| 50 |
+
async def health_check():
|
| 51 |
+
"""Lightweight endpoint for UptimeRobot / keep-alive pings."""
|
| 52 |
+
return {"status": "healthy", "model_loaded": _pipeline is not None}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@app.get("/train", tags=["training"])
|
| 56 |
async def training():
|
| 57 |
+
"""Kick off the full training pipeline (long-running)."""
|
| 58 |
try:
|
| 59 |
result = subprocess.run(
|
| 60 |
[sys.executable, "main.py"],
|
|
|
|
| 66 |
except subprocess.TimeoutExpired:
|
| 67 |
return Response("Training timed out!", status_code=504)
|
| 68 |
except Exception as e:
|
| 69 |
+
return Response(f"Error: {e}", status_code=500)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@app.post("/predict", response_model=SummarizeResponse, tags=["inference"])
|
| 73 |
+
async def predict_route(request: SummarizeRequest):
|
| 74 |
+
"""Summarize the provided text."""
|
| 75 |
+
if _pipeline is None:
|
| 76 |
+
raise HTTPException(status_code=503, detail="Model is not loaded yet.")
|
| 77 |
try:
|
| 78 |
+
summary = _pipeline.predict(request.text)
|
| 79 |
+
return SummarizeResponse(summary=summary)
|
|
|
|
|
|
|
| 80 |
except Exception as e:
|
| 81 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ββ Entry-point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
if __name__ == "__main__":
|
| 86 |
+
# PORT env-var lets each platform (HF Spaces=7860, Render=10000, etc.) inject its own port
|
| 87 |
+
port = int(os.environ.get("PORT", 7860))
|
| 88 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
src/textSummarizer/pipeline/prediction.py
CHANGED
|
@@ -1,23 +1,37 @@
|
|
| 1 |
from textSummarizer.config.configuration import ConfigurationManager
|
|
|
|
| 2 |
from transformers import AutoTokenizer
|
| 3 |
-
from transformers import pipeline
|
|
|
|
| 4 |
|
| 5 |
class PredictionPipeline:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
def __init__(self):
|
| 7 |
self.config = ConfigurationManager().get_model_evaluation_config()
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
return output
|
|
|
|
| 1 |
from textSummarizer.config.configuration import ConfigurationManager
|
| 2 |
+
from textSummarizer.logging import logger
|
| 3 |
from transformers import AutoTokenizer
|
| 4 |
+
from transformers import pipeline as hf_pipeline
|
| 5 |
+
|
| 6 |
|
| 7 |
class PredictionPipeline:
|
| 8 |
+
"""Wraps the summarisation model. Call load_model() once to pre-warm,
|
| 9 |
+
then call predict() for every inference request. The pipeline object is
|
| 10 |
+
cached on the instance so the model is only loaded from disk once."""
|
| 11 |
+
|
| 12 |
def __init__(self):
|
| 13 |
self.config = ConfigurationManager().get_model_evaluation_config()
|
| 14 |
+
self._pipe = None # lazy-loaded / pre-warmed via load_model()
|
| 15 |
+
|
| 16 |
+
# ------------------------------------------------------------------
|
| 17 |
+
def load_model(self) -> None:
|
| 18 |
+
"""Load the tokenizer and model into memory (call once at startup)."""
|
| 19 |
+
logger.info("Loading summarisation model from %s", self.config.model_path)
|
| 20 |
+
tokenizer = AutoTokenizer.from_pretrained(str(self.config.tokenizer_path))
|
| 21 |
+
self._pipe = hf_pipeline(
|
| 22 |
+
"summarization",
|
| 23 |
+
model=str(self.config.model_path),
|
| 24 |
+
tokenizer=tokenizer,
|
| 25 |
+
)
|
| 26 |
+
logger.info("Model loaded and ready.")
|
| 27 |
+
|
| 28 |
+
# ------------------------------------------------------------------
|
| 29 |
+
def predict(self, text: str) -> str:
|
| 30 |
+
"""Return the summary for *text*. Loads the model on first call if not
|
| 31 |
+
already pre-warmed (useful for standalone / testing usage)."""
|
| 32 |
+
if self._pipe is None:
|
| 33 |
+
self.load_model()
|
| 34 |
+
|
| 35 |
+
gen_kwargs = {"length_penalty": 0.8, "num_beams": 8, "max_length": 128}
|
| 36 |
+
output: str = self._pipe(text, **gen_kwargs)[0]["summary_text"]
|
| 37 |
return output
|