Emaglp commited on
Commit
c41e59f
·
1 Parent(s): 36d1800

Step 7 Complete: Docker image with Flask API and Gradio UI working locally

Browse files
.ipynb_checkpoints/Dockerfile-checkpoint ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Utiliser Python 3.10 (stable pour PyTorch et Gradio)
2
+ FROM python:3.10-slim
3
+
4
+ # 2. Définir le dossier de travail dans le conteneur
5
+ WORKDIR /app
6
+
7
+ # 3. Installer les dépendances système nécessaires (OpenCV, Pillow, etc.)
8
+ RUN apt-get update && apt-get install -y \
9
+ libgl1 \
10
+ libglib2.0-0 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # 4. Copier et installer les bibliothèques Python
14
+ COPY requirements-api.txt .
15
+ RUN pip install --no-cache-dir -r requirements-api.txt
16
+ # On force l'installation de Gradio et Requests au cas où
17
+ RUN pip install --no-cache-dir gradio requests
18
+
19
+ # 5. Copier tout ton code (le modèle .pth doit être dans un dossier 'weights/')
20
+ COPY . .
21
+
22
+ # 6. Exposer les ports pour l'extérieur
23
+ # 5075 = Flask (API) | 7860 = Gradio (Interface)
24
+ EXPOSE 5075 7860
25
+
26
+ # 7. La commande de lancement
27
+ # On utilise 'sh -c' pour lancer deux processus en même temps :
28
+ # 'python movieposter_api.py &' lance l'API en tâche de fond.
29
+ # 'python app_gradio.py' lance l'interface au premier plan.
30
+ CMD sh -c "python movieposter_api.py --model_path weights/movieposter_net.pth & python app_gradio.py"
.ipynb_checkpoints/README-checkpoint.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # projet_AIF
2
+ projet avancé jusqu' au point 3. de la partie 1. Il faut faire le 4. Build Gradio interface
3
+ Docker commun
4
+ #commandes dans terminal :
5
+ docker build -t movie-poster-app .
6
+ docker run -p 5075:5075 -p 7860:7860 movie-poster-app
.ipynb_checkpoints/app_gradio-checkpoint.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import io
4
+ from PIL import Image
5
+
6
+ def predict_movie_genre(image):
7
+ # 1. Configuration (basée sur le test de ta collègue)
8
+ API_URL = "http://127.0.0.1:5075/predict"
9
+
10
+ # 2. Conversion de l'image Gradio (PIL) en bytes pour l'API
11
+ img_byte_arr = io.BytesIO()
12
+ image.save(img_byte_arr, format='JPEG')
13
+ img_data = img_byte_arr.getvalue()
14
+
15
+ try:
16
+ # 3. Envoi de la requête (format data brut comme dans son test)
17
+ response = requests.post(API_URL, data=img_data)
18
+
19
+ if response.status_code == 200:
20
+ prediction = response.json().get('label', 'Genre inconnu')
21
+ return f"🎬 Genre prédit : {prediction}"
22
+ else:
23
+ return f"⚠️ Erreur API : Code {response.status_code}"
24
+
25
+ except Exception as e:
26
+ return f"Impossible de contacter l'API. Est-elle lancée sur le port 5075 ? ({e})"
27
+
28
+ # 4. Création de l'interface visuelle
29
+ demo = gr.Interface(
30
+ fn=predict_movie_genre,
31
+ inputs=gr.Image(type="pil", label="Déposez un poster ici"),
32
+ outputs=gr.Text(label="Résultat de l'analyse"),
33
+ title="Analyseur de Posters de Films",
34
+ description="Cette interface utilise une API Flask et un modèle Deep Learning pour prédire le genre d'un film."
35
+ )
36
+
37
+ if __name__ == "__main__":
38
+ #CRUCIAL pour Docker : server_name="0.0.0.0" permet l'accès extérieur
39
+ demo.launch(server_name="0.0.0.0", server_port=7860)
.ipynb_checkpoints/movieposter_api-checkpoint.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import torch
3
+ import torchvision.transforms as transforms
4
+ from flask import Flask, jsonify, request
5
+ from PIL import Image
6
+ import io
7
+ from model import MovieposterNet
8
+
9
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
10
+
11
+ app = Flask(__name__)
12
+
13
+ # Liste des classes pour le mapping
14
+ CLASSES = ['action', 'animation', 'comedy', 'documentary', 'drama', 'fantasy', 'horror', 'romance', 'science Fiction', 'thriller']
15
+
16
+
17
+
18
+ parser = argparse.ArgumentParser()
19
+ parser.add_argument('--model_path', type=str, default = 'weights/movieposter_net.pth', help='model path')
20
+ args = parser.parse_args()
21
+ model_path = args.model_path
22
+
23
+ model = MovieposterNet().to(device)
24
+ model.load_state_dict(torch.load(model_path, map_location=device))
25
+ model.eval()
26
+
27
+ # Resizing des images
28
+ transform = transforms.Compose([
29
+ transforms.Resize((224, 224)),
30
+ transforms.ToTensor(),
31
+ transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
32
+ ])
33
+
34
+ @app.route('/predict', methods=['POST'])
35
+ def predict():
36
+ img_binary = request.data
37
+ img_pil = Image.open(io.BytesIO(img_binary))
38
+
39
+ # Transform the PIL image
40
+ tensor = transform(img_pil).to(device)
41
+ tensor = tensor.unsqueeze(0) # Add batch dimension
42
+
43
+ # Make prediction
44
+ with torch.no_grad():
45
+ outputs = model(tensor)
46
+ _, predicted = outputs.max(1)
47
+
48
+ return jsonify({"prediction": int(predicted[0]), "label": CLASSES[int(predicted[0])]})
49
+
50
+ @app.route('/batch_predict', methods=['POST'])
51
+ def batch_predict():
52
+ # Get the image data from the request
53
+ images_binary = request.files.getlist("images[]")
54
+
55
+ tensors = []
56
+
57
+ for img_binary in images_binary:
58
+ img_pil = Image.open(img_binary.stream)
59
+ tensor = transform(img_pil)
60
+ tensors.append(tensor)
61
+
62
+ # Stack tensors to form a batch tensor
63
+ batch_tensor = torch.stack(tensors, dim=0).to(device)
64
+
65
+ # Make prediction
66
+ with torch.no_grad():
67
+ outputs = model(batch_tensor)
68
+ _, predictions = outputs.max(1)
69
+
70
+ res = [CLASSES[idx] for idx in predictions.tolist()]
71
+ return jsonify({"predictions": res})
72
+
73
+ if __name__ == "__main__":
74
+ app.run(host='0.0.0.0', port=5075, debug=True)
.ipynb_checkpoints/requirements-api-checkpoint.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch==2.0.1
2
+ torchvision==0.15.2
3
+ flask==2.3.2
4
+ pillow==10.0.0
5
+ numpy==1.24.4
6
+ gradio
7
+ requests
.ipynb_checkpoints/test_api-checkpoint.ipynb ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "cc94cf7f",
7
+ "metadata": {},
8
+ "outputs": [
9
+ {
10
+ "ename": "ValueError",
11
+ "evalue": "Sample larger than population or is negative",
12
+ "output_type": "error",
13
+ "traceback": [
14
+ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
15
+ "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)",
16
+ "Cell \u001b[1;32mIn[1], line 23\u001b[0m\n\u001b[0;32m 20\u001b[0m all_images\u001b[38;5;241m.\u001b[39mappend((full_path, genre_reel))\n\u001b[0;32m 22\u001b[0m \u001b[38;5;66;03m# 3. Sélection de 10 posters au hasard\u001b[39;00m\n\u001b[1;32m---> 23\u001b[0m test_samples \u001b[38;5;241m=\u001b[39m random\u001b[38;5;241m.\u001b[39msample(all_images, \u001b[38;5;241m10\u001b[39m)\n\u001b[0;32m 25\u001b[0m \u001b[38;5;66;03m# 4. Affichage des résultats\u001b[39;00m\n\u001b[0;32m 26\u001b[0m plt\u001b[38;5;241m.\u001b[39mfigure(figsize\u001b[38;5;241m=\u001b[39m(\u001b[38;5;241m20\u001b[39m, \u001b[38;5;241m10\u001b[39m))\n",
17
+ "File \u001b[1;32m~\\anaconda3\\Lib\\random.py:430\u001b[0m, in \u001b[0;36mRandom.sample\u001b[1;34m(self, population, k, counts)\u001b[0m\n\u001b[0;32m 428\u001b[0m randbelow \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_randbelow\n\u001b[0;32m 429\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;241m0\u001b[39m \u001b[38;5;241m<\u001b[39m\u001b[38;5;241m=\u001b[39m k \u001b[38;5;241m<\u001b[39m\u001b[38;5;241m=\u001b[39m n:\n\u001b[1;32m--> 430\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSample larger than population or is negative\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 431\u001b[0m result \u001b[38;5;241m=\u001b[39m [\u001b[38;5;28;01mNone\u001b[39;00m] \u001b[38;5;241m*\u001b[39m k\n\u001b[0;32m 432\u001b[0m setsize \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m21\u001b[39m \u001b[38;5;66;03m# size of a small set minus size of an empty list\u001b[39;00m\n",
18
+ "\u001b[1;31mValueError\u001b[0m: Sample larger than population or is negative"
19
+ ]
20
+ }
21
+ ],
22
+ "source": [
23
+ "import requests\n",
24
+ "import os\n",
25
+ "import random\n",
26
+ "import matplotlib.pyplot as plt\n",
27
+ "from PIL import Image\n",
28
+ "import io\n",
29
+ "\n",
30
+ "# 1. Configuration\n",
31
+ "API_URL = \"http://localhost:5075/predict\"\n",
32
+ "DATASET_PATH = \"../sorted_movie_posters_paligema\"\n",
33
+ "\n",
34
+ "# 2. Récupération de tous les chemins d'images\n",
35
+ "all_images = []\n",
36
+ "for root, dirs, files in os.walk(DATASET_PATH):\n",
37
+ " for file in files:\n",
38
+ " if file.lower().endswith(('.png', '.jpg', '.jpeg')):\n",
39
+ " full_path = os.path.join(root, file)\n",
40
+ " # On extrait le genre à partir du nom du dossier parent\n",
41
+ " genre_reel = os.path.basename(root)\n",
42
+ " all_images.append((full_path, genre_reel))\n",
43
+ "\n",
44
+ "# 3. Sélection de 10 posters au hasard\n",
45
+ "test_samples = random.sample(all_images, 10)\n",
46
+ "\n",
47
+ "# 4. Affichage des résultats\n",
48
+ "plt.figure(figsize=(20, 10))\n",
49
+ "\n",
50
+ "for i, (img_path, ground_truth) in enumerate(test_samples):\n",
51
+ " # Lecture et envoi de l'image à l'API\n",
52
+ " with open(img_path, \"rb\") as f:\n",
53
+ " img_data = f.read()\n",
54
+ " \n",
55
+ " try:\n",
56
+ " response = requests.post(API_URL, data=img_data)\n",
57
+ " prediction = response.json().get('label', 'Erreur')\n",
58
+ " except Exception as e:\n",
59
+ " prediction = \"API Down\"\n",
60
+ "\n",
61
+ " # Affichage\n",
62
+ " img = Image.open(img_path)\n",
63
+ " plt.subplot(2, 5, i + 1)\n",
64
+ " plt.imshow(img)\n",
65
+ " \n",
66
+ " # Couleur du titre : vert si correct, rouge si erreur\n",
67
+ " color = 'green' if prediction == ground_truth else 'red'\n",
68
+ " \n",
69
+ " plt.title(f\"Réel: {ground_truth}\\nPred: {prediction}\", color=color, fontsize=10)\n",
70
+ " plt.axis('off')\n",
71
+ "\n",
72
+ "plt.tight_layout()\n",
73
+ "plt.show()"
74
+ ]
75
+ },
76
+ {
77
+ "cell_type": "code",
78
+ "execution_count": null,
79
+ "id": "7ab72703",
80
+ "metadata": {},
81
+ "outputs": [],
82
+ "source": []
83
+ }
84
+ ],
85
+ "metadata": {
86
+ "kernelspec": {
87
+ "display_name": "Python 3 (ipykernel)",
88
+ "language": "python",
89
+ "name": "python3"
90
+ },
91
+ "language_info": {
92
+ "codemirror_mode": {
93
+ "name": "ipython",
94
+ "version": 3
95
+ },
96
+ "file_extension": ".py",
97
+ "mimetype": "text/x-python",
98
+ "name": "python",
99
+ "nbconvert_exporter": "python",
100
+ "pygments_lexer": "ipython3",
101
+ "version": "3.12.3"
102
+ }
103
+ },
104
+ "nbformat": 4,
105
+ "nbformat_minor": 5
106
+ }
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Utiliser Python 3.10 (stable pour PyTorch et Gradio)
2
+ FROM python:3.10-slim
3
+
4
+ # 2. Définir le dossier de travail dans le conteneur
5
+ WORKDIR /app
6
+
7
+ # 3. Installer les dépendances système nécessaires (OpenCV, Pillow, etc.)
8
+ RUN apt-get update && apt-get install -y \
9
+ libgl1 \
10
+ libglib2.0-0 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # 4. Copier et installer les bibliothèques Python
14
+ COPY requirements-api.txt .
15
+ RUN pip install --no-cache-dir -r requirements-api.txt
16
+ # On force l'installation de Gradio et Requests au cas où
17
+ RUN pip install --no-cache-dir gradio requests
18
+
19
+ # 5. Copier tout ton code (le modèle .pth doit être dans un dossier 'weights/')
20
+ COPY . .
21
+
22
+ # 6. Exposer les ports pour l'extérieur
23
+ # 5075 = Flask (API) | 7860 = Gradio (Interface)
24
+ EXPOSE 5075 7860
25
+
26
+ # 7. La commande de lancement
27
+ # On utilise 'sh -c' pour lancer deux processus en même temps :
28
+ # 'python movieposter_api.py &' lance l'API en tâche de fond.
29
+ # 'python app_gradio.py' lance l'interface au premier plan.
30
+ CMD sh -c "python movieposter_api.py --model_path weights/movieposter_net.pth & python app_gradio.py"
Dockerfile-api DELETED
@@ -1,20 +0,0 @@
1
- # Use an official Python runtime as the parent image
2
- FROM python:3.10-slim
3
-
4
- # Set the working directory in the container to /app
5
- WORKDIR /app
6
-
7
- # Copy the current directory contents into the container at /app
8
- COPY . /app
9
-
10
- # Install any needed packages specified in requirements.txt
11
- RUN pip install --trusted-host pypi.python.org -r requirements-api.txt
12
-
13
- # Make port 5075 available to the world outside this container
14
- EXPOSE 5075
15
-
16
- # Define environment variable for Flask to run in production mode
17
- ENV FLASK_ENV=production
18
-
19
- # Run mnist_api.py when the container launches
20
- CMD ["python", "movieposter_api.py", "--model_path", "weights/movieposter_net.pth"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,3 +1,6 @@
1
  # projet_AIF
2
  projet avancé jusqu' au point 3. de la partie 1. Il faut faire le 4. Build Gradio interface
3
-
 
 
 
 
1
  # projet_AIF
2
  projet avancé jusqu' au point 3. de la partie 1. Il faut faire le 4. Build Gradio interface
3
+ Docker commun
4
+ #commandes dans terminal :
5
+ docker build -t movie-poster-app .
6
+ docker run -p 5075:5075 -p 7860:7860 movie-poster-app
app_gradio.py CHANGED
@@ -5,7 +5,7 @@ from PIL import Image
5
 
6
  def predict_movie_genre(image):
7
  # 1. Configuration (basée sur le test de ta collègue)
8
- API_URL = "http://localhost:5075/predict"
9
 
10
  # 2. Conversion de l'image Gradio (PIL) en bytes pour l'API
11
  img_byte_arr = io.BytesIO()
@@ -35,4 +35,5 @@ demo = gr.Interface(
35
  )
36
 
37
  if __name__ == "__main__":
38
- demo.launch()
 
 
5
 
6
  def predict_movie_genre(image):
7
  # 1. Configuration (basée sur le test de ta collègue)
8
+ API_URL = "http://127.0.0.1:5075/predict"
9
 
10
  # 2. Conversion de l'image Gradio (PIL) en bytes pour l'API
11
  img_byte_arr = io.BytesIO()
 
35
  )
36
 
37
  if __name__ == "__main__":
38
+ #CRUCIAL pour Docker : server_name="0.0.0.0" permet l'accès extérieur
39
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements-api.txt CHANGED
@@ -2,4 +2,6 @@ torch==2.0.1
2
  torchvision==0.15.2
3
  flask==2.3.2
4
  pillow==10.0.0
5
- numpy==1.24.4
 
 
 
2
  torchvision==0.15.2
3
  flask==2.3.2
4
  pillow==10.0.0
5
+ numpy==1.24.4
6
+ gradio
7
+ requests
test_api.ipynb CHANGED
The diff for this file is too large to render. See raw diff