trungngnthanh commited on
Commit
e0c8dcf
·
verified ·
1 Parent(s): 3ff2625

Upload 9 files

Browse files
Files changed (9) hide show
  1. .dockerignore +8 -0
  2. .gitignore +15 -0
  3. Dockerfile +18 -0
  4. README.md +18 -11
  5. app/__init__.py +0 -0
  6. app/app.py +79 -0
  7. app/static/style.css +51 -0
  8. app/templates/index.html +47 -0
  9. requirements.txt +5 -0
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ .venv
2
+ __pycache__
3
+ *.pyc
4
+ .git
5
+ .gitignore
6
+ Dockerfile
7
+ README.md
8
+ poetry.lock
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # Virtual environments
6
+ venv/
7
+ env/
8
+
9
+ # IDE files (example)
10
+ .vscode/
11
+ .idea/
12
+
13
+ # OS files
14
+ .DS_Store
15
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a lightweight Python image
2
+ FROM python:3.12.10-slim
3
+
4
+ # Set a working directory
5
+ WORKDIR /app
6
+
7
+ # Copy requirements and install
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Copy the entire repo into /app
12
+ COPY . .
13
+
14
+ # Expose the Flask port
15
+ EXPOSE 5000
16
+
17
+ # Default command
18
+ CMD ["python", "app/app.py"]
README.md CHANGED
@@ -1,11 +1,18 @@
1
- ---
2
- title: Mlops Project Webapp
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
1
+ # MLOps Project WebApp
2
+
3
+ This repository contains a Flask web application that downloads a model from the Hugging Face Hub and serves predictions through a simple user interface.
4
+
5
+ ## Project Structure
6
+
7
+ - `app/`: Contains the Flask application code, including:
8
+ - `app.py`: The main Flask entry point.
9
+ - `templates/`: Directory for HTML templates.
10
+ - `static/`: Directory for CSS, JS, and other static files.
11
+ - `.github/workflows/`: Contains GitHub Actions workflow for CI/CD (`main.yml`).
12
+ - `Dockerfile`: Used to build the Docker image.
13
+
14
+ ## Getting Started
15
+
16
+ 1. **Install Dependencies** (Locally):
17
+ ```bash
18
+ pip install -r requirements.txt
app/__init__.py ADDED
File without changes
app/app.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import joblib
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import pandas as pd
7
+ from flask import Flask, request, jsonify, render_template, Response
8
+ from flask.typing import ResponseReturnValue
9
+ from huggingface_hub import hf_hub_download
10
+ from sklearn.base import BaseEstimator
11
+
12
+ app: Flask = Flask(__name__)
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger: logging.Logger = logging.getLogger(__name__)
17
+
18
+ # Environment variables for configuration
19
+ HF_REPO_ID: str = os.getenv("HF_REPO_ID", "trungngnthanh/mlops-project")
20
+ MODEL_FILENAME: str = os.getenv("MODEL_FILENAME", "model.joblib")
21
+ HF_REVISION: str = os.getenv("HF_REVISION", "main")
22
+
23
+ # Global model variable, initially None.
24
+ model: Optional[BaseEstimator] = None
25
+
26
+ try:
27
+ logger.info("Downloading model from Hugging Face Hub...")
28
+ model_path: str = hf_hub_download(
29
+ repo_id=HF_REPO_ID,
30
+ filename=MODEL_FILENAME,
31
+ revision=HF_REVISION
32
+ )
33
+ model = joblib.load(model_path)
34
+ logger.info("Model loaded successfully from Hugging Face Hub.")
35
+ except Exception as e:
36
+ logger.error(f"Error loading model: {e}")
37
+ model = None
38
+
39
+ @app.route("/")
40
+ def index() -> str:
41
+ """Render the home page."""
42
+ return render_template("index.html")
43
+
44
+ @app.route("/predict", methods=["POST"])
45
+ def predict() -> ResponseReturnValue:
46
+ """
47
+ Predict endpoint that accepts a JSON payload with a "data" key.
48
+ Converts the received data to a DataFrame with valid feature names if needed
49
+ and returns the prediction result as JSON.
50
+ """
51
+ if model is None:
52
+ return jsonify({"error": "Model not available."}), 500
53
+
54
+ data: Optional[Dict[str, Any]] = request.get_json()
55
+ if not data or "data" not in data:
56
+ return jsonify({"error": "Missing 'data' in request payload."}), 400
57
+
58
+ try:
59
+ # Expected input: a list of samples (list of lists of feature values)
60
+ input_data: List[Any] = data["data"]
61
+
62
+ # If model was trained with feature names, convert input_data to a DataFrame.
63
+ if hasattr(model, "feature_names_in_"):
64
+ feature_names = model.feature_names_in_
65
+ # Assume input_data is list of samples. Convert using pandas DataFrame.
66
+ input_df = pd.DataFrame(input_data, columns=feature_names)
67
+ predictions: List[Any] = model.predict(input_df).tolist()
68
+ else:
69
+ predictions = model.predict(input_data).tolist()
70
+
71
+ return jsonify({"predictions": predictions})
72
+ except Exception as e:
73
+ logger.error(f"Prediction error: {e}")
74
+ return jsonify({"error": str(e)}), 500
75
+
76
+ if __name__ == "__main__":
77
+ port: int = int(os.getenv("PORT", "5000"))
78
+ app.run(host="0.0.0.0", port=port)
79
+
app/static/style.css ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ margin: 0;
4
+ padding: 0;
5
+ background: #f8f9fa;
6
+ }
7
+
8
+ .container {
9
+ width: 600px;
10
+ margin: 50px auto;
11
+ background: #ffffff;
12
+ padding: 20px;
13
+ box-shadow: 2px 2px 12px #aaaaaa;
14
+ }
15
+
16
+ h1 {
17
+ text-align: center;
18
+ margin-bottom: 20px;
19
+ }
20
+
21
+ form {
22
+ display: flex;
23
+ flex-direction: column;
24
+ }
25
+
26
+ label {
27
+ margin-bottom: 5px;
28
+ }
29
+
30
+ input[type="text"] {
31
+ padding: 10px;
32
+ margin-bottom: 10px;
33
+ }
34
+
35
+ button {
36
+ background-color: #4285f4;
37
+ color: white;
38
+ padding: 10px;
39
+ border: none;
40
+ cursor: pointer;
41
+ width: 150px;
42
+ }
43
+
44
+ button:hover {
45
+ background-color: #3367d6;
46
+ }
47
+
48
+ #result {
49
+ margin-top: 20px;
50
+ font-size: 18px;
51
+ }
app/templates/index.html ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>MLOps Project WebApp</title>
6
+ <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
7
+ </head>
8
+ <body>
9
+ <div class="container">
10
+ <h1>Welcome to the MLOps Project Web App</h1>
11
+ <form id="predictionForm">
12
+ <label for="features">Enter features (comma-separated):</label>
13
+ <input type="text" id="features" name="features" placeholder="0.1, 0.2, 0.3, ..." />
14
+ <button type="submit">Predict</button>
15
+ </form>
16
+ <div id="result"></div>
17
+ </div>
18
+
19
+ <script>
20
+ document.getElementById("predictionForm").addEventListener("submit", async (event) => {
21
+ event.preventDefault();
22
+ const featuresInput = document.getElementById("features").value;
23
+ const features = featuresInput.split(",").map(v => parseFloat(v.trim()));
24
+ const payload = { data: [features] };
25
+
26
+ const responseDiv = document.getElementById("result");
27
+ responseDiv.innerHTML = "Predicting...";
28
+
29
+ try {
30
+ const res = await fetch("/predict", {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify(payload)
34
+ });
35
+ const data = await res.json();
36
+ if (res.ok) {
37
+ responseDiv.innerHTML = `<h2>Prediction: ${data.predictions}</h2>`;
38
+ } else {
39
+ responseDiv.innerHTML = `<h2>Error: ${data.error}</h2>`;
40
+ }
41
+ } catch (err) {
42
+ responseDiv.innerHTML = `<h2>Error: ${err}</h2>`;
43
+ }
44
+ });
45
+ </script>
46
+ </body>
47
+ </html>
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask
2
+ scikit-learn
3
+ joblib
4
+ huggingface-hub
5
+ pandas