rinogeek commited on
Commit
fc3299e
·
1 Parent(s): 34a3ce5

Add application file

Browse files
.dockerignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ env
7
+ venv
8
+ .venv
9
+ pip-log.txt
10
+ pip-delete-this-directory.txt
11
+ .tox
12
+ .coverage
13
+ .coverage.*
14
+ .cache
15
+ nosetests.xml
16
+ coverage.xml
17
+ *.cover
18
+ *.log
19
+ .git
20
+ .gitignore
21
+ .idea
22
+ .vscode
23
+ .DS_Store
24
+ *.sqlite3
25
+ !db.sqlite3
26
+ media/uploads/*
27
+ media/results/*
28
+ !media/.gitkeep
29
+ staticfiles
.gitignore CHANGED
@@ -166,7 +166,7 @@ temp/
166
  *.backup
167
 
168
  # Docker
169
- .dockerignore
170
 
171
  # Certificats SSL
172
  ssl/
 
166
  *.backup
167
 
168
  # Docker
169
+
170
 
171
  # Certificats SSL
172
  ssl/
Dockerfile CHANGED
@@ -1,17 +1,18 @@
1
- # Dockerfile pour FireWatch AI
2
- # Créé par Marino ATOHOUN
3
 
4
  FROM python:3.11-slim
5
 
6
  # Métadonnées
7
- LABEL maintainer="Marino ATOHOUN"
8
  LABEL description="FireWatch AI - Système de détection d'incendie et d'intrusion"
9
- LABEL version="1.0"
10
 
11
  # Variables d'environnement
12
  ENV PYTHONDONTWRITEBYTECODE=1
13
  ENV PYTHONUNBUFFERED=1
14
  ENV DEBIAN_FRONTEND=noninteractive
 
15
 
16
  # Répertoire de travail
17
  WORKDIR /app
@@ -20,7 +21,7 @@ WORKDIR /app
20
  RUN apt-get update && apt-get install -y \
21
  build-essential \
22
  libpq-dev \
23
- libgl1-mesa-glx \
24
  libglib2.0-0 \
25
  libsm6 \
26
  libxext6 \
@@ -39,32 +40,32 @@ COPY requirements.txt .
39
  RUN pip install --no-cache-dir --upgrade pip
40
  RUN pip install --no-cache-dir -r requirements.txt
41
 
42
- # Copier le code de l'application
43
- COPY . .
44
 
45
- # Créer les répertoires nécessaires
46
  RUN mkdir -p media/uploads/images media/uploads/videos media/results models logs staticfiles
47
-
48
- # Collecter les fichiers statiques
49
- RUN python manage.py collectstatic --noinput
50
-
51
- # Créer un utilisateur non-root pour la sécurité
52
- RUN adduser --disabled-password --gecos '' appuser
53
  RUN chown -R appuser:appuser /app
 
 
54
  USER appuser
55
 
56
- # Exposer le port
57
- EXPOSE 8000
 
 
 
 
 
 
58
 
59
  # Commande de santé
60
  HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
61
- CMD curl -f http://localhost:8000/ || exit 1
62
 
63
  # Script de démarrage
64
- COPY docker-entrypoint.sh /app/
65
  RUN chmod +x /app/docker-entrypoint.sh
66
 
67
  # Commande par défaut
68
  ENTRYPOINT ["/app/docker-entrypoint.sh"]
69
- CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--timeout", "120", "firewatch_project.wsgi:application"]
70
-
 
1
+ # Dockerfile pour FireWatch AI sur Hugging Face Spaces
2
+ # Modifié par BlackBenAI Team
3
 
4
  FROM python:3.11-slim
5
 
6
  # Métadonnées
7
+ LABEL maintainer="BlackBenAI Team"
8
  LABEL description="FireWatch AI - Système de détection d'incendie et d'intrusion"
9
+ LABEL version="1.1"
10
 
11
  # Variables d'environnement
12
  ENV PYTHONDONTWRITEBYTECODE=1
13
  ENV PYTHONUNBUFFERED=1
14
  ENV DEBIAN_FRONTEND=noninteractive
15
+ ENV PORT=7860
16
 
17
  # Répertoire de travail
18
  WORKDIR /app
 
21
  RUN apt-get update && apt-get install -y \
22
  build-essential \
23
  libpq-dev \
24
+ libgl1 \
25
  libglib2.0-0 \
26
  libsm6 \
27
  libxext6 \
 
40
  RUN pip install --no-cache-dir --upgrade pip
41
  RUN pip install --no-cache-dir -r requirements.txt
42
 
43
+ # Créer un utilisateur non-root (user 1000 pour HF Spaces)
44
+ RUN useradd -m -u 1000 appuser
45
 
46
+ # Créer les répertoires nécessaires et donner les permissions
47
  RUN mkdir -p media/uploads/images media/uploads/videos media/results models logs staticfiles
 
 
 
 
 
 
48
  RUN chown -R appuser:appuser /app
49
+
50
+ # Passer à l'utilisateur non-root
51
  USER appuser
52
 
53
+ # Copier le code de l'application avec les bonnes permissions
54
+ COPY --chown=appuser:appuser . .
55
+
56
+ # Collecter les fichiers statiques (en tant qu'utilisateur appuser)
57
+ RUN python manage.py collectstatic --noinput
58
+
59
+ # Exposer le port 7860 (Hugging Face Spaces)
60
+ EXPOSE 7860
61
 
62
  # Commande de santé
63
  HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
64
+ CMD curl -f http://localhost:7860/ || exit 1
65
 
66
  # Script de démarrage
 
67
  RUN chmod +x /app/docker-entrypoint.sh
68
 
69
  # Commande par défaut
70
  ENTRYPOINT ["/app/docker-entrypoint.sh"]
71
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--timeout", "120", "firewatch_project.wsgi:application"]
 
detection/admin.py CHANGED
@@ -1,21 +1,35 @@
1
  """
2
  Configuration de l'interface d'administration Django
3
  Créé par Marino ATOHOUN - FireWatch AI Project
 
4
  """
5
  from django.contrib import admin
6
  from django.utils.html import format_html
 
 
7
  from .models import Contact, DetectionSession, Detection, AIModelStatus
8
 
 
 
 
 
 
 
 
 
 
9
 
10
  @admin.register(Contact)
11
  class ContactAdmin(admin.ModelAdmin):
12
- """Administration des contacts - Par Marino ATOHOUN"""
13
  list_display = ('name', 'email', 'created_at', 'is_read', 'message_preview')
14
  list_filter = ('is_read', 'created_at')
15
  search_fields = ('name', 'email', 'message')
16
  readonly_fields = ('created_at',)
17
  list_editable = ('is_read',)
18
  ordering = ('-created_at',)
 
 
19
 
20
  def message_preview(self, obj):
21
  """Aperçu du message"""
@@ -39,32 +53,76 @@ class DetectionInline(admin.TabularInline):
39
  """Inline pour afficher les détections dans une session"""
40
  model = Detection
41
  extra = 0
42
- readonly_fields = ('class_name', 'confidence', 'bbox_x', 'bbox_y', 'bbox_width', 'bbox_height')
 
 
43
 
44
 
45
  @admin.register(DetectionSession)
46
  class DetectionSessionAdmin(admin.ModelAdmin):
47
- """Administration des sessions de détection - Par Marino ATOHOUN"""
48
- list_display = ('session_id', 'detection_type', 'created_at', 'is_processed', 'processing_time', 'detections_count')
49
  list_filter = ('detection_type', 'is_processed', 'created_at')
50
  search_fields = ('session_id',)
51
- readonly_fields = ('session_id', 'created_at', 'processing_time')
52
  inlines = [DetectionInline]
53
  ordering = ('-created_at',)
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  def detections_count(self, obj):
56
  """Nombre de détections dans la session"""
57
- return obj.detections.count()
58
- detections_count.short_description = "Nombre de détections"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  fieldsets = (
61
  ('Informations de session', {
62
  'fields': ('session_id', 'detection_type', 'created_at')
63
  }),
64
- ('Fichiers', {
65
- 'fields': ('original_file', 'result_file')
 
 
 
 
 
66
  }),
67
- ('Traitement', {
68
  'fields': ('is_processed', 'processing_time')
69
  }),
70
  )
@@ -72,61 +130,110 @@ class DetectionSessionAdmin(admin.ModelAdmin):
72
 
73
  @admin.register(Detection)
74
  class DetectionAdmin(admin.ModelAdmin):
75
- """Administration des détections - Par Marino ATOHOUN"""
76
- list_display = ('session', 'class_name', 'confidence_percent', 'frame_number', 'bbox_preview')
77
  list_filter = ('class_name', 'session__detection_type', 'session__created_at')
78
  search_fields = ('session__session_id', 'class_name')
79
- readonly_fields = ('session', 'class_name', 'confidence', 'bbox_x', 'bbox_y', 'bbox_width', 'bbox_height')
80
  ordering = ('-session__created_at', 'frame_number')
81
-
82
- def confidence_percent(self, obj):
83
- """Affichage de la confiance en pourcentage"""
84
- return f"{obj.confidence:.1%}"
85
- confidence_percent.short_description = "Confiance"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  def bbox_preview(self, obj):
88
- """Aperçu de la bounding box"""
89
- return f"({obj.bbox_x:.0f}, {obj.bbox_y:.0f}) {obj.bbox_width:.0f}x{obj.bbox_height:.0f}"
90
- bbox_preview.short_description = "Boîte englobante"
 
 
 
 
 
 
 
 
 
 
 
91
 
92
 
93
  @admin.register(AIModelStatus)
94
  class AIModelStatusAdmin(admin.ModelAdmin):
95
- """Administration du statut des modèles IA - Par Marino ATOHOUN"""
96
- list_display = ('model_type', 'is_loaded_display', 'model_version', 'accuracy_percent', 'last_loaded')
97
  list_filter = ('model_type', 'is_loaded')
98
  readonly_fields = ('created_at', 'updated_at', 'last_loaded')
99
  ordering = ('model_type',)
 
 
 
 
 
 
 
100
 
101
  def is_loaded_display(self, obj):
102
  """Affichage coloré du statut de chargement"""
103
  if obj.is_loaded:
104
- return format_html('<span style="color: green;">✓ Chargé</span>')
105
  else:
106
- return format_html('<span style="color: red;">✗ Non chargé</span>')
107
  is_loaded_display.short_description = "Statut"
108
 
109
  def accuracy_percent(self, obj):
110
  """Affichage de la précision en pourcentage"""
111
- return f"{obj.accuracy:.1%}" if obj.accuracy else "N/A"
 
 
 
 
 
 
 
112
  accuracy_percent.short_description = "Précision"
113
 
114
  fieldsets = (
115
  ('Configuration du modèle', {
116
  'fields': ('model_type', 'model_path', 'model_version')
117
  }),
118
- ('Statut', {
119
  'fields': ('is_loaded', 'last_loaded', 'accuracy')
120
  }),
121
- ('Dates', {
122
  'fields': ('created_at', 'updated_at'),
123
  'classes': ('collapse',)
124
  }),
125
  )
126
 
127
 
128
- # Par Marino ATOHOUN: Personnalisation de l'interface d'administration
129
  admin.site.site_header = "FireWatch AI - Administration"
130
  admin.site.site_title = "FireWatch AI Admin"
131
- admin.site.index_title = "Panneau d'administration - Créé par Marino ATOHOUN"
132
-
 
1
  """
2
  Configuration de l'interface d'administration Django
3
  Créé par Marino ATOHOUN - FireWatch AI Project
4
+ Amélioré par BlackBenAI Team pour un contrôle avancé
5
  """
6
  from django.contrib import admin
7
  from django.utils.html import format_html
8
+ from django.urls import reverse
9
+ from django.utils.safestring import mark_safe
10
  from .models import Contact, DetectionSession, Detection, AIModelStatus
11
 
12
+ # Actions personnalisées
13
+ @admin.action(description='Marquer comme lu')
14
+ def make_read(modeladmin, request, queryset):
15
+ queryset.update(is_read=True)
16
+
17
+ @admin.action(description='Marquer comme non lu')
18
+ def make_unread(modeladmin, request, queryset):
19
+ queryset.update(is_read=False)
20
+
21
 
22
  @admin.register(Contact)
23
  class ContactAdmin(admin.ModelAdmin):
24
+ """Administration des contacts - Par Marino ATOHOUN & BlackBenAI"""
25
  list_display = ('name', 'email', 'created_at', 'is_read', 'message_preview')
26
  list_filter = ('is_read', 'created_at')
27
  search_fields = ('name', 'email', 'message')
28
  readonly_fields = ('created_at',)
29
  list_editable = ('is_read',)
30
  ordering = ('-created_at',)
31
+ actions = [make_read, make_unread]
32
+ list_per_page = 20
33
 
34
  def message_preview(self, obj):
35
  """Aperçu du message"""
 
53
  """Inline pour afficher les détections dans une session"""
54
  model = Detection
55
  extra = 0
56
+ readonly_fields = ('class_name', 'confidence', 'bbox_x', 'bbox_y', 'bbox_width', 'bbox_height', 'frame_number', 'timestamp')
57
+ can_delete = False
58
+ show_change_link = True
59
 
60
 
61
  @admin.register(DetectionSession)
62
  class DetectionSessionAdmin(admin.ModelAdmin):
63
+ """Administration des sessions de détection - Par Marino ATOHOUN & BlackBenAI"""
64
+ list_display = ('session_id_short', 'detection_type', 'created_at', 'is_processed', 'processing_time_display', 'detections_count', 'preview_image')
65
  list_filter = ('detection_type', 'is_processed', 'created_at')
66
  search_fields = ('session_id',)
67
+ readonly_fields = ('session_id', 'created_at', 'processing_time', 'preview_original', 'preview_result')
68
  inlines = [DetectionInline]
69
  ordering = ('-created_at',)
70
+ list_per_page = 20
71
+ date_hierarchy = 'created_at'
72
+
73
+ def session_id_short(self, obj):
74
+ return str(obj.session_id)[:8] + "..."
75
+ session_id_short.short_description = "ID Session"
76
+
77
+ def processing_time_display(self, obj):
78
+ if obj.processing_time:
79
+ return f"{obj.processing_time:.2f}s"
80
+ return "-"
81
+ processing_time_display.short_description = "Temps"
82
 
83
  def detections_count(self, obj):
84
  """Nombre de détections dans la session"""
85
+ count = obj.detections.count()
86
+ color = "green" if count > 0 else "gray"
87
+ return format_html('<span style="color: {}; font-weight: bold;">{}</span>', color, count)
88
+ detections_count.short_description = "Détections"
89
+
90
+ def preview_image(self, obj):
91
+ if obj.result_file:
92
+ return format_html('<img src="{}" style="height: 50px; border-radius: 4px;" />', obj.result_file.url)
93
+ elif obj.original_file and obj.detection_type == 'image':
94
+ return format_html('<img src="{}" style="height: 50px; border-radius: 4px; opacity: 0.5;" />', obj.original_file.url)
95
+ return "-"
96
+ preview_image.short_description = "Aperçu"
97
+
98
+ def preview_original(self, obj):
99
+ if obj.original_file:
100
+ if obj.detection_type == 'image':
101
+ return format_html('<a href="{}" target="_blank"><img src="{}" style="max-height: 300px; max-width: 100%; border-radius: 8px;" /></a>', obj.original_file.url, obj.original_file.url)
102
+ elif obj.detection_type == 'video':
103
+ return format_html('<video src="{}" controls style="max-height: 300px; max-width: 100%; border-radius: 8px;"></video>', obj.original_file.url)
104
+ return "Aucun fichier"
105
+ preview_original.short_description = "Fichier original"
106
+
107
+ def preview_result(self, obj):
108
+ if obj.result_file:
109
+ return format_html('<a href="{}" target="_blank"><img src="{}" style="max-height: 300px; max-width: 100%; border-radius: 8px;" /></a>', obj.result_file.url, obj.result_file.url)
110
+ return "Aucun résultat"
111
+ preview_result.short_description = "Résultat de détection"
112
 
113
  fieldsets = (
114
  ('Informations de session', {
115
  'fields': ('session_id', 'detection_type', 'created_at')
116
  }),
117
+ ('Aperçu des fichiers', {
118
+ 'fields': ('preview_original', 'preview_result'),
119
+ 'description': 'Aperçu visuel des fichiers traités'
120
+ }),
121
+ ('Fichiers bruts', {
122
+ 'fields': ('original_file', 'result_file'),
123
+ 'classes': ('collapse',)
124
  }),
125
+ ('Métriques de traitement', {
126
  'fields': ('is_processed', 'processing_time')
127
  }),
128
  )
 
130
 
131
  @admin.register(Detection)
132
  class DetectionAdmin(admin.ModelAdmin):
133
+ """Administration des détections - Par Marino ATOHOUN & BlackBenAI"""
134
+ list_display = ('id', 'session_link', 'class_name_colored', 'confidence_bar', 'frame_number', 'bbox_preview')
135
  list_filter = ('class_name', 'session__detection_type', 'session__created_at')
136
  search_fields = ('session__session_id', 'class_name')
137
+ readonly_fields = ('session', 'class_name', 'confidence', 'bbox_x', 'bbox_y', 'bbox_width', 'bbox_height', 'bbox_visual')
138
  ordering = ('-session__created_at', 'frame_number')
139
+ list_per_page = 50
140
+
141
+ def session_link(self, obj):
142
+ url = reverse("admin:detection_detectionsession_change", args=[obj.session.id])
143
+ return format_html('<a href="{}">Session {}</a>', url, str(obj.session.session_id)[:8])
144
+ session_link.short_description = "Session"
145
+
146
+ def class_name_colored(self, obj):
147
+ colors = {
148
+ 'fire': 'red',
149
+ 'smoke': 'gray',
150
+ 'person': 'orange',
151
+ 'intrusion': 'orange',
152
+ 'vehicle': 'blue'
153
+ }
154
+ color = colors.get(obj.class_name, 'black')
155
+ return format_html('<span style="color: {}; font-weight: bold;">{}</span>', color, obj.get_class_name_display())
156
+ class_name_colored.short_description = "Classe"
157
+
158
+ def confidence_bar(self, obj):
159
+ """Barre de progression visuelle pour la confiance"""
160
+ percent = obj.confidence * 100
161
+ color = "green" if percent > 80 else "orange" if percent > 50 else "red"
162
+ return format_html(
163
+ '<div style="width: 100px; background-color: #ddd; border-radius: 4px;">'
164
+ '<div style="width: {}%; background-color: {}; height: 10px; border-radius: 4px;"></div>'
165
+ '</div><span style="font-size: 0.8em;">{}%</span>',
166
+ f"{percent:.1f}", color, f"{percent:.1f}"
167
+ )
168
+ confidence_bar.short_description = "Confiance"
169
 
170
  def bbox_preview(self, obj):
171
+ """Aperçu textuel de la bounding box"""
172
+ return f"x:{obj.bbox_x:.0f}, y:{obj.bbox_y:.0f} ({obj.bbox_width:.0f}x{obj.bbox_height:.0f})"
173
+ bbox_preview.short_description = "Position"
174
+
175
+ def bbox_visual(self, obj):
176
+ """Représentation visuelle de la bbox (conceptuel)"""
177
+ return format_html(
178
+ '<div style="position: relative; width: 200px; height: 150px; background: #f0f0f0; border: 1px solid #ccc;">'
179
+ '<div style="position: absolute; left: {}%; top: {}%; width: {}%; height: {}%; border: 2px solid red; background: rgba(255,0,0,0.2);"></div>'
180
+ '</div>',
181
+ f"{(obj.bbox_x / 640) * 100:.1f}", f"{(obj.bbox_y / 480) * 100:.1f}",
182
+ f"{(obj.bbox_width / 640) * 100:.1f}", f"{(obj.bbox_height / 480) * 100:.1f}"
183
+ )
184
+ bbox_visual.short_description = "Visualisation BBox (Approx)"
185
 
186
 
187
  @admin.register(AIModelStatus)
188
  class AIModelStatusAdmin(admin.ModelAdmin):
189
+ """Administration du statut des modèles IA - Par Marino ATOHOUN & BlackBenAI"""
190
+ list_display = ('model_type', 'is_loaded_display', 'model_version', 'accuracy_percent', 'last_loaded', 'updated_at')
191
  list_filter = ('model_type', 'is_loaded')
192
  readonly_fields = ('created_at', 'updated_at', 'last_loaded')
193
  ordering = ('model_type',)
194
+ actions = ['reload_models']
195
+
196
+ @admin.action(description='Recharger les modèles (Simulation)')
197
+ def reload_models(self, request, queryset):
198
+ # Ici on pourrait appeler une tâche Celery ou une fonction pour recharger le modèle
199
+ rows_updated = queryset.update(is_loaded=False) # Simuler un déchargement pour forcer le rechargement
200
+ self.message_user(request, f"{rows_updated} modèles marqués pour rechargement.")
201
 
202
  def is_loaded_display(self, obj):
203
  """Affichage coloré du statut de chargement"""
204
  if obj.is_loaded:
205
+ return format_html('<span style="color: green; font-weight: bold;">✓ Chargé</span>')
206
  else:
207
+ return format_html('<span style="color: red; font-weight: bold;">✗ Non chargé</span>')
208
  is_loaded_display.short_description = "Statut"
209
 
210
  def accuracy_percent(self, obj):
211
  """Affichage de la précision en pourcentage"""
212
+ if obj.accuracy:
213
+ return format_html(
214
+ '<div style="width: 100px; background-color: #ddd; border-radius: 4px;">'
215
+ '<div style="width: {}%; background-color: blue; height: 10px; border-radius: 4px;"></div>'
216
+ '</div><span style="font-size: 0.8em;">{}%</span>',
217
+ f"{obj.accuracy * 100:.1f}", f"{obj.accuracy * 100:.1f}"
218
+ )
219
+ return "N/A"
220
  accuracy_percent.short_description = "Précision"
221
 
222
  fieldsets = (
223
  ('Configuration du modèle', {
224
  'fields': ('model_type', 'model_path', 'model_version')
225
  }),
226
+ ('Statut opérationnel', {
227
  'fields': ('is_loaded', 'last_loaded', 'accuracy')
228
  }),
229
+ ('Métadonnées', {
230
  'fields': ('created_at', 'updated_at'),
231
  'classes': ('collapse',)
232
  }),
233
  )
234
 
235
 
236
+ # Par Marino ATOHOUN & BlackBenAI: Personnalisation de l'interface d'administration
237
  admin.site.site_header = "FireWatch AI - Administration"
238
  admin.site.site_title = "FireWatch AI Admin"
239
+ admin.site.index_title = "Gestion BlackBenAI"
 
detection/templates/detection/index.html CHANGED
@@ -1,337 +1,485 @@
1
  {% load static %}
2
  <!DOCTYPE html>
3
- <!-- Fichier adapté pour Django par Marino ATOHOUN - FireWatch AI Project -->
4
  <html lang="fr">
5
  <head>
6
  <meta charset="UTF-8">
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <title>FireWatch AI - Détection d'incendie et d'intrusion</title>
9
- <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
 
 
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
- <style>
12
- :root {
13
- --primary-dark: #0f172a;
14
- --primary-blue: #1e40af;
15
- --accent-violet: #7c3aed;
16
- --accent-pink: #ec4899;
17
- --light-gray: #f1f5f9;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  }
19
-
 
 
 
20
  body {
21
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
22
- background-color: var(--light-gray);
 
23
  }
24
-
25
- .gradient-bg {
26
- background: linear-gradient(135deg, var(--primary-blue) 0%, var(--accent-violet) 100%);
27
- }
28
-
29
- .card-glass {
30
- background: rgba(255, 255, 255, 0.1);
31
- backdrop-filter: blur(10px);
32
- -webkit-backdrop-filter: blur(10px);
33
- border-radius: 1rem;
34
- border: 1px solid rgba(255, 255, 255, 0.2);
35
  }
36
-
37
- .pulse-animation {
38
- animation: pulse 2s infinite;
 
 
 
39
  }
40
-
41
- @keyframes pulse {
42
- 0% { box-shadow: 0 0 0 0 rgba(236, 72, 153, 0.4); }
43
- 70% { box-shadow: 0 0 0 10px rgba(236, 72, 153, 0); }
44
- 100% { box-shadow: 0 0 0 0 rgba(236, 72, 153, 0); }
45
  }
46
-
47
- .tab-active {
48
- border-bottom: 3px solid var(--accent-pink);
49
- color: var(--accent-pink);
 
50
  }
51
 
52
- .loading {
53
- display: none;
54
  }
55
 
56
- .loading.show {
57
- display: block;
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
- .alert {
61
- padding: 1rem;
62
- margin: 1rem 0;
63
- border-radius: 0.5rem;
64
- display: none;
 
 
 
65
  }
66
 
67
- .alert.show {
68
- display: block;
 
 
 
 
 
69
  }
70
 
71
- .alert-success {
72
- background-color: #d1fae5;
73
- color: #065f46;
74
- border: 1px solid #a7f3d0;
 
 
 
 
 
75
  }
76
 
77
- .alert-error {
78
- background-color: #fee2e2;
79
- color: #991b1b;
80
- border: 1px solid #fca5a5;
 
 
 
81
  }
82
  </style>
83
  </head>
84
- <body class="min-h-screen">
85
- <!-- Header -->
86
- <header class="gradient-bg text-white shadow-xl">
87
- <div class="container mx-auto px-4 py-6">
88
- <div class="flex justify-between items-center">
89
- <div class="flex items-center space-x-2">
90
- <i class="fas fa-fire text-2xl text-pink-500"></i>
91
- <h1 class="text-2xl font-bold">FireWatch <span class="text-pink-400">AI</span></h1>
 
 
 
 
 
 
 
92
  </div>
93
- <nav class="hidden md:flex space-x-6">
94
- <a href="#about" class="hover:text-pink-300 transition">À propos</a>
95
- <a href="#demo" class="hover:text-pink-300 transition">Démonstration</a>
96
- <a href="#creator" class="hover:text-pink-300 transition">Créateur</a>
97
- <a href="#contact" class="hover:text-pink-300 transition">Contact</a>
98
- </nav>
99
- <button class="md:hidden text-xl">
100
- <i class="fas fa-bars"></i>
101
- </button>
102
  </div>
 
 
 
 
 
103
  </div>
104
- </header>
105
 
106
  <!-- Hero Section -->
107
- <section class="gradient-bg text-white py-20">
108
- <div class="container mx-auto px-4 flex flex-col md:flex-row items-center">
109
- <div class="md:w-1/2 mb-10 md:mb-0">
110
- <h2 class="text-4xl md:text-5xl font-bold mb-6">Détection intelligente d'incendie et d'intrusion</h2>
111
- <p class="text-xl mb-8 text-gray-200">Notre solution IA avancée utilise YOLOv8 pour identifier les risques en temps réel à partir d'images, de vidéos ou de flux caméra.</p>
112
- <a href="#demo" class="bg-pink-500 hover:bg-pink-600 text-white font-bold py-3 px-6 rounded-full transition duration-300 inline-flex items-center">
113
- Essayer maintenant <i class="fas fa-arrow-right ml-2"></i>
114
- </a>
115
  </div>
116
- <div class="md:w-1/2 flex justify-center">
117
- <div class="card-glass p-6 w-full max-w-md">
118
- <div class="bg-black rounded-lg overflow-hidden aspect-video flex items-center justify-center">
119
- <i class="fas fa-play text-pink-500 text-4xl"></i>
120
- </div>
121
- <p class="text-center mt-4 text-gray-300">Démonstration en direct de la détection</p>
122
- </div>
 
 
 
 
 
 
 
 
 
 
123
  </div>
124
  </div>
125
  </section>
126
 
127
- <!-- About Section -->
128
- <section id="about" class="py-16 bg-white">
129
- <div class="container mx-auto px-4">
130
- <h2 class="text-3xl font-bold text-center mb-12 text-gray-800">À propos du projet</h2>
131
- <div class="grid md:grid-cols-3 gap-8">
132
- <div class="bg-gray-50 p-6 rounded-xl shadow-md hover:shadow-lg transition">
133
- <div class="text-indigo-600 text-3xl mb-4">
134
- <i class="fas fa-fire-extinguisher"></i>
135
- </div>
136
- <h3 class="text-xl font-semibold mb-3 text-gray-800">Détection d'incendie</h3>
137
- <p class="text-gray-600">Notre modèle identifie avec précision les débuts d'incendie, même dans des conditions de faible visibilité.</p>
138
  </div>
139
- <div class="bg-gray-50 p-6 rounded-xl shadow-md hover:shadow-lg transition">
140
- <div class="text-violet-600 text-3xl mb-4">
141
- <i class="fas fa-user-secret"></i>
142
- </div>
143
- <h3 class="text-xl font-semibold mb-3 text-gray-800">Détection d'intrusion</h3>
144
- <p class="text-gray-600">Système de surveillance intelligent qui repère les intrusions et mouvements suspects en temps réel.</p>
145
  </div>
146
- <div class="bg-gray-50 p-6 rounded-xl shadow-md hover:shadow-lg transition">
147
- <div class="text-pink-600 text-3xl mb-4">
148
- <i class="fas fa-bolt"></i>
149
- </div>
150
- <h3 class="text-xl font-semibold mb-3 text-gray-800">Temps réel</h3>
151
- <p class="text-gray-600">Analyse instantanée des flux vidéo avec alertes en temps réel pour une intervention rapide.</p>
 
 
 
 
 
 
 
152
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  </div>
154
  </div>
155
  </section>
156
 
157
- <!-- Demo Section -->
158
- <section id="demo" class="py-16 bg-gray-50">
159
- <div class="container mx-auto px-4">
160
- <h2 class="text-3xl font-bold text-center mb-12 text-gray-800">Essayer la démonstration</h2>
161
-
162
- <div class="max-w-4xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden">
163
- <!-- Tabs -->
164
- <div class="flex border-b">
165
- <button onclick="switchTab('image')" class="tab-button py-4 px-6 font-medium text-gray-500 hover:text-pink-500 transition tab-active" id="image-tab">
166
- <i class="fas fa-image mr-2"></i> Image
 
 
 
 
 
 
167
  </button>
168
- <button onclick="switchTab('video')" class="tab-button py-4 px-6 font-medium text-gray-500 hover:text-pink-500 transition" id="video-tab">
169
- <i class="fas fa-video mr-2"></i> Vidéo
 
 
 
170
  </button>
171
- <button onclick="switchTab('camera')" class="tab-button py-4 px-6 font-medium text-gray-500 hover:text-pink-500 transition" id="camera-tab">
172
- <i class="fas fa-camera mr-2"></i> Caméra
 
 
 
173
  </button>
174
  </div>
175
-
176
- <!-- Tab Content -->
177
- <div class="p-6">
178
- <!-- Image Tab Content -->
179
- <div id="image-content" class="tab-content">
180
- <form id="image-form" method="post" enctype="multipart/form-data">
 
181
  {% csrf_token %}
182
- <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
183
- <i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-4"></i>
184
- <h3 class="text-xl font-semibold mb-2">Uploader une image</h3>
185
- <p class="text-gray-500 mb-4">Glissez-déposez une image ou cliquez pour sélectionner</p>
186
- <input type="file" name="image" id="image-upload" accept="image/*" class="hidden" required>
187
- <label for="image-upload" class="bg-indigo-500 hover:bg-indigo-600 text-white font-medium py-2 px-4 rounded cursor-pointer transition">
188
- Sélectionner une image
189
- </label>
190
- <div id="image-preview" class="mt-4 hidden">
191
- <img id="preview-img" src="" alt="Aperçu" class="max-w-full h-48 mx-auto rounded">
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  </div>
193
  </div>
194
- <div class="mt-6 flex justify-between">
195
- <button type="button" onclick="resetImageForm()" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition">
196
- <i class="fas fa-redo mr-2"></i> Réinitialiser
197
- </button>
198
- <button type="submit" class="bg-pink-500 hover:bg-pink-600 text-white font-medium py-2 px-4 rounded transition">
199
- <i class="fas fa-search mr-2"></i> Analyser
200
- </button>
201
- </div>
202
- <div class="loading mt-4 text-center">
203
- <i class="fas fa-spinner fa-spin text-2xl text-indigo-500"></i>
204
- <p class="mt-2 text-gray-600">Analyse en cours...</p>
 
 
 
 
 
 
 
205
  </div>
 
 
 
 
 
206
  </form>
207
  </div>
208
-
209
- <!-- Video Tab Content -->
210
- <div id="video-content" class="tab-content hidden">
211
- <form id="video-form" method="post" enctype="multipart/form-data">
212
  {% csrf_token %}
213
- <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
214
- <i class="fas fa-film text-4xl text-violet-500 mb-4"></i>
215
- <h3 class="text-xl font-semibold mb-2">Uploader une vidéo</h3>
216
- <p class="text-gray-500 mb-4">Glissez-déposez une vidéo ou cliquez pour sélectionner</p>
217
- <input type="file" name="video" id="video-upload" accept="video/*" class="hidden" required>
218
- <label for="video-upload" class="bg-violet-500 hover:bg-violet-600 text-white font-medium py-2 px-4 rounded cursor-pointer transition">
219
- Sélectionner une vidéo
220
- </label>
221
- <div id="video-preview" class="mt-4 hidden">
222
- <video id="preview-video" controls class="max-w-full h-48 mx-auto rounded">
223
- <source src="" type="video/mp4">
224
- Votre navigateur ne supporte pas la lecture vidéo.
225
- </video>
 
 
 
 
 
 
 
 
 
 
226
  </div>
227
  </div>
228
- <div class="mt-6 flex justify-between">
229
- <button type="button" onclick="resetVideoForm()" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition">
230
- <i class="fas fa-redo mr-2"></i> Réinitialiser
231
- </button>
232
- <button type="submit" class="bg-pink-500 hover:bg-pink-600 text-white font-medium py-2 px-4 rounded transition">
233
- <i class="fas fa-play mr-2"></i> Démarrer l'analyse
234
- </button>
235
- </div>
236
- <div class="loading mt-4 text-center">
237
- <i class="fas fa-spinner fa-spin text-2xl text-violet-500"></i>
238
- <p class="mt-2 text-gray-600">Analyse vidéo en cours...</p>
 
 
 
 
 
239
  </div>
 
 
 
 
 
240
  </form>
241
  </div>
242
-
243
- <!-- Camera Tab Content -->
244
- <div id="camera-content" class="tab-content hidden">
245
- <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
246
- <i class="fas fa-camera text-4xl text-pink-500 mb-4"></i>
247
- <h3 class="text-xl font-semibold mb-2">Accéder à votre caméra</h3>
248
- <p class="text-gray-500 mb-4">Autorisez l'accès à votre caméra pour une analyse en temps réel</p>
249
- <button id="start-camera" class="bg-pink-500 hover:bg-pink-600 text-white font-medium py-2 px-4 rounded cursor-pointer transition pulse-animation">
250
- <i class="fas fa-video mr-2"></i> Activer la caméra
251
- </button>
252
- <div id="camera-stream" class="mt-4 hidden">
253
- <video id="camera-video" autoplay muted class="max-w-full h-64 mx-auto rounded bg-black"></video>
254
- <canvas id="camera-canvas" class="hidden"></canvas>
255
  </div>
256
  </div>
257
- <div class="mt-6 flex justify-center">
258
- <button id="stop-camera" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition hidden">
259
- <i class="fas fa-stop mr-2"></i> Arrêter
 
260
  </button>
261
- <button id="capture-frame" class="bg-indigo-500 hover:bg-indigo-600 text-white font-medium py-2 px-4 rounded transition ml-4 hidden">
262
- <i class="fas fa-camera mr-2"></i> Capturer et analyser
 
 
 
263
  </button>
264
  </div>
265
  </div>
 
266
  </div>
267
  </div>
268
-
269
  <!-- Results Section -->
270
- <div id="results" class="max-w-4xl mx-auto mt-12 bg-white rounded-xl shadow-lg overflow-hidden hidden">
271
- <div class="p-6">
272
- <h3 class="text-xl font-semibold mb-4 text-gray-800">Résultats de l'analyse</h3>
273
- <div class="flex flex-col md:flex-row gap-6">
274
- <div class="md:w-1/2">
275
- <div id="result-image-container" class="bg-black rounded-lg overflow-hidden aspect-video flex items-center justify-center">
276
- <img id="result-image" src="" alt="Résultat de l'analyse" class="w-full h-full object-contain">
277
- </div>
 
278
  </div>
279
- <div class="md:w-1/2">
280
- <div class="bg-gray-50 p-4 rounded-lg">
281
- <h4 class="font-medium text-gray-800 mb-3">Détections:</h4>
282
- <ul id="detection-list" class="space-y-2">
283
- <!-- Les détections seront insérées ici par JavaScript -->
284
- </ul>
285
  </div>
286
- <div class="mt-4">
287
- <button id="download-results" class="bg-indigo-500 hover:bg-indigo-600 text-white font-medium py-2 px-4 rounded transition">
288
- <i class="fas fa-download mr-2"></i> Télécharger les résultats
289
- </button>
 
 
290
  </div>
291
  </div>
292
  </div>
293
  </div>
294
  </div>
295
-
296
- <!-- Alert Messages -->
297
- <div id="alert-container" class="max-w-4xl mx-auto mt-4">
298
- <div id="success-alert" class="alert alert-success">
299
- <i class="fas fa-check-circle mr-2"></i>
300
- <span id="success-message"></span>
301
- </div>
302
- <div id="error-alert" class="alert alert-error">
303
- <i class="fas fa-exclamation-circle mr-2"></i>
304
- <span id="error-message"></span>
305
- </div>
306
- </div>
307
  </div>
308
  </section>
309
 
310
  <!-- Creator Section -->
311
- <section id="creator" class="py-16 bg-gradient-to-r from-indigo-900 to-violet-900 text-white">
312
- <div class="container mx-auto px-4">
313
- <h2 class="text-3xl font-bold text-center mb-12">À propos du créateur</h2>
314
- <div class="max-w-4xl mx-auto flex flex-col md:flex-row items-center gap-8">
315
- <div class="md:w-1/3 flex justify-center">
316
- <div class="w-48 h-48 rounded-full bg-gray-300 overflow-hidden border-4 border-pink-400 shadow-lg">
317
- <img src="https://via.placeholder.com/200" alt="Marino ATOHOUN" class="w-full h-full object-cover">
318
- </div>
319
  </div>
320
- <div class="md:w-2/3">
321
- <h3 class="text-2xl font-bold mb-2">Marino ATOHOUN</h3>
322
- <p class="text-pink-300 font-medium mb-4">Data Scientist & Développeur Backend</p>
323
- <p class="mb-4 text-gray-300">Expert en intelligence artificielle et développement logiciel, Marino ATOHOUN a conçu cette solution innovante pour répondre aux besoins croissants de sécurité et de prévention des risques.</p>
324
- <p class="mb-4 text-gray-300">Avec une solide expérience dans le traitement d'images et l'analyse de données, il a développé ce système de détection basé sur YOLOv8 pour offrir une solution performante et accessible.</p>
325
- <div class="flex space-x-4">
326
- <a href="#" class="text-gray-300 hover:text-pink-300 transition">
327
- <i class="fab fa-github text-xl"></i>
328
- </a>
329
- <a href="#" class="text-gray-300 hover:text-pink-300 transition">
330
- <i class="fab fa-linkedin text-xl"></i>
331
- </a>
332
- <a href="#" class="text-gray-300 hover:text-pink-300 transition">
333
- <i class="fas fa-envelope text-xl"></i>
334
- </a>
335
  </div>
336
  </div>
337
  </div>
@@ -339,380 +487,334 @@
339
  </section>
340
 
341
  <!-- Contact Section -->
342
- <section id="contact" class="py-16 bg-white">
343
- <div class="container mx-auto px-4">
344
- <h2 class="text-3xl font-bold text-center mb-12 text-gray-800">Contactez-nous</h2>
345
- <div class="max-w-2xl mx-auto bg-gray-50 rounded-xl shadow-md p-8">
346
- <form id="contact-form" method="post">
347
- {% csrf_token %}
348
- <div id="contact-alert" class="alert">
349
- <span id="contact-message"></span>
350
- </div>
351
- <div class="mb-6">
352
- <label for="name" class="block text-gray-700 font-medium mb-2">Nom</label>
353
- <input type="text" id="name" name="name" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" required>
354
- </div>
355
- <div class="mb-6">
356
- <label for="email" class="block text-gray-700 font-medium mb-2">Email</label>
357
- <input type="email" id="email" name="email" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" required>
358
- </div>
359
- <div class="mb-6">
360
- <label for="message" class="block text-gray-700 font-medium mb-2">Message</label>
361
- <textarea id="message" name="message" rows="4" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" required></textarea>
362
- </div>
363
- <button type="submit" class="bg-indigo-500 hover:bg-indigo-600 text-white font-medium py-2 px-6 rounded-lg transition w-full">
364
- <i class="fas fa-paper-plane mr-2"></i> Envoyer le message
365
- </button>
366
- </form>
367
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  </div>
369
  </section>
370
 
371
  <!-- Footer -->
372
- <footer class="bg-gray-900 text-white py-8">
373
- <div class="container mx-auto px-4">
374
- <div class="flex flex-col md:flex-row justify-between items-center">
375
- <div class="mb-4 md:mb-0">
376
- <div class="flex items-center space-x-2">
377
- <i class="fas fa-fire text-xl text-pink-500"></i>
378
- <span class="text-xl font-bold">FireWatch <span class="text-pink-400">AI</span></span>
379
- </div>
380
- <p class="text-gray-400 mt-2">Solution intelligente de détection d'incendie et d'intrusion</p>
381
- </div>
382
- <div class="flex space-x-6">
383
- <a href="#" class="text-gray-400 hover:text-pink-400 transition">
384
- <i class="fab fa-facebook-f"></i>
385
- </a>
386
- <a href="#" class="text-gray-400 hover:text-pink-400 transition">
387
- <i class="fab fa-twitter"></i>
388
- </a>
389
- <a href="#" class="text-gray-400 hover:text-pink-400 transition">
390
- <i class="fab fa-instagram"></i>
391
- </a>
392
- <a href="#" class="text-gray-400 hover:text-pink-400 transition">
393
- <i class="fab fa-github"></i>
394
- </a>
395
- </div>
396
- </div>
397
- <div class="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
398
- <p>&copy; 2024 FireWatch AI. Tous droits réservés. Conçu avec passion par Marino ATOHOUN.</p>
399
  </div>
400
  </div>
401
  </footer>
402
 
 
403
  <script>
404
- // Par Marino ATOHOUN: JavaScript pour l'interaction avec le backend Django
405
-
406
- // Obtenir le token CSRF
407
  function getCSRFToken() {
408
  return document.querySelector('[name=csrfmiddlewaretoken]').value;
409
  }
410
 
411
- // Fonctions utilitaires pour les alertes
412
  function showAlert(type, message) {
413
- const alertContainer = document.getElementById('alert-container');
414
- const alert = document.getElementById(type + '-alert');
415
- const messageSpan = document.getElementById(type + '-message');
416
-
417
- messageSpan.textContent = message;
418
- alert.classList.add('show');
419
-
420
- setTimeout(() => {
421
- alert.classList.remove('show');
422
- }, 5000);
423
- }
424
-
425
- function showContactAlert(type, message) {
426
- const alert = document.getElementById('contact-alert');
427
- const messageSpan = document.getElementById('contact-message');
428
-
429
- alert.className = 'alert show alert-' + type;
430
- messageSpan.textContent = message;
431
-
432
- setTimeout(() => {
433
- alert.classList.remove('show');
434
- }, 5000);
435
  }
436
 
437
- // Tab switching functionality
438
  function switchTab(tabName) {
439
- // Hide all tab contents
440
- document.querySelectorAll('.tab-content').forEach(content => {
441
- content.classList.add('hidden');
442
- });
443
 
444
- // Remove active class from all tabs
445
- document.querySelectorAll('.tab-button').forEach(button => {
446
- button.classList.remove('tab-active');
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  });
448
-
449
- // Show selected tab content
450
- document.getElementById(tabName + '-content').classList.remove('hidden');
451
-
452
- // Add active class to selected tab
453
- document.getElementById(tabName + '-tab').classList.add('tab-active');
454
- }
455
-
456
- // Fonctions de réinitialisation des formulaires
457
- function resetImageForm() {
458
- document.getElementById('image-form').reset();
459
- document.getElementById('image-preview').classList.add('hidden');
460
- document.getElementById('results').classList.add('hidden');
461
  }
462
 
463
- function resetVideoForm() {
464
- document.getElementById('video-form').reset();
465
- document.getElementById('video-preview').classList.add('hidden');
466
- document.getElementById('results').classList.add('hidden');
467
- }
468
-
469
- // Aperçu des fichiers
470
- document.getElementById('image-upload').addEventListener('change', function(e) {
471
  const file = e.target.files[0];
472
- if (file) {
473
  const reader = new FileReader();
474
  reader.onload = function(e) {
475
- document.getElementById('preview-img').src = e.target.result;
476
- document.getElementById('image-preview').classList.remove('hidden');
477
- };
478
  reader.readAsDataURL(file);
479
  }
480
  });
 
 
 
 
481
 
482
- document.getElementById('video-upload').addEventListener('change', function(e) {
 
483
  const file = e.target.files[0];
484
- if (file) {
485
  const url = URL.createObjectURL(file);
486
- document.getElementById('preview-video').src = url;
487
- document.getElementById('video-preview').classList.remove('hidden');
488
  }
489
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
- // Gestion du formulaire d'image
492
  document.getElementById('image-form').addEventListener('submit', function(e) {
493
  e.preventDefault();
494
-
495
  const formData = new FormData(this);
496
- const loadingDiv = this.querySelector('.loading');
 
497
 
498
- loadingDiv.classList.add('show');
499
 
500
  fetch("{% url 'detection:analyze_image' %}", {
501
  method: 'POST',
502
  body: formData,
503
- headers: {
504
- 'X-CSRFToken': getCSRFToken()
505
- }
506
  })
507
- .then(response => response.json())
508
  .then(data => {
509
- loadingDiv.classList.remove('show');
510
-
511
- if (data.success) {
512
- displayResults(data);
513
- showAlert('success', 'Analyse terminée avec succès!');
514
- } else {
515
- showAlert('error', data.error || 'Erreur lors de l\'analyse');
516
- }
517
  })
518
- .catch(error => {
519
- loadingDiv.classList.remove('show');
520
- showAlert('error', 'Erreur de connexion au serveur');
521
- console.error('Error:', error);
522
  });
523
  });
524
 
525
- // Gestion du formulaire de vidéo
526
  document.getElementById('video-form').addEventListener('submit', function(e) {
527
  e.preventDefault();
528
-
529
  const formData = new FormData(this);
530
- const loadingDiv = this.querySelector('.loading');
 
531
 
532
- loadingDiv.classList.add('show');
533
 
534
  fetch("{% url 'detection:analyze_video' %}", {
535
  method: 'POST',
536
  body: formData,
537
- headers: {
538
- 'X-CSRFToken': getCSRFToken()
539
- }
540
  })
541
- .then(response => response.json())
542
  .then(data => {
543
- loadingDiv.classList.remove('show');
544
-
545
- if (data.success) {
546
- displayResults(data);
547
- showAlert('success', 'Analyse vidéo terminée avec succès!');
548
- } else {
549
- showAlert('error', data.error || 'Erreur lors de l\'analyse vidéo');
550
- }
551
  })
552
- .catch(error => {
553
- loadingDiv.classList.remove('show');
554
- showAlert('error', 'Erreur de connexion au serveur');
555
- console.error('Error:', error);
556
  });
557
  });
558
 
559
- // Gestion du formulaire de contact
560
  document.getElementById('contact-form').addEventListener('submit', function(e) {
561
  e.preventDefault();
562
-
563
  const formData = new FormData(this);
 
 
 
 
 
 
 
564
 
565
  fetch("{% url 'detection:contact' %}", {
566
  method: 'POST',
567
  body: formData,
568
- headers: {
569
- 'X-CSRFToken': getCSRFToken()
570
- }
571
  })
572
- .then(response => response.json())
573
  .then(data => {
574
- if (data.success) {
575
- showContactAlert('success', data.message);
 
 
 
 
 
 
576
  this.reset();
577
  } else {
578
- showContactAlert('error', data.error || 'Erreur lors de l\'envoi du message');
 
579
  }
 
 
 
580
  })
581
- .catch(error => {
582
- showContactAlert('error', 'Erreur de connexion au serveur');
583
- console.error('Error:', error);
 
584
  });
585
  });
586
 
587
- // Affichage des résultats
588
- function displayResults(data) {
589
- const resultsSection = document.getElementById('results');
590
- const resultImage = document.getElementById('result-image');
591
- const detectionList = document.getElementById('detection-list');
592
-
593
- // Afficher l'image de résultat
594
- if (data.result_image_url) {
595
- resultImage.src = data.result_image_url;
596
- }
597
-
598
- // Afficher les détections
599
- detectionList.innerHTML = '';
600
- if (data.detections && data.detections.length > 0) {
601
- data.detections.forEach(detection => {
602
- const li = document.createElement('li');
603
- li.className = 'flex items-center';
604
-
605
- // Couleur selon le type de détection
606
- let colorClass = 'bg-blue-500';
607
- if (detection.class_name === 'fire' || detection.class_name === 'smoke') {
608
- colorClass = 'bg-red-500';
609
- } else if (detection.class_name === 'person' || detection.class_name === 'intrusion') {
610
- colorClass = 'bg-orange-500';
611
- }
612
-
613
- li.innerHTML = `
614
- <span class="w-3 h-3 ${colorClass} rounded-full mr-2"></span>
615
- <span>${detection.label || detection.class_name}:
616
- <span class="font-medium">${(detection.confidence * 100).toFixed(1)}% de confiance</span></span>
617
- `;
618
- detectionList.appendChild(li);
619
- });
620
- } else {
621
- detectionList.innerHTML = '<li class="text-gray-500">Aucune détection trouvée</li>';
622
  }
623
-
624
- // Afficher la section des résultats
625
- resultsSection.classList.remove('hidden');
626
- resultsSection.scrollIntoView({ behavior: 'smooth' });
627
- }
628
-
629
- // Gestion de la caméra
630
- let cameraStream = null;
631
- let cameraVideo = document.getElementById('camera-video');
632
- let cameraCanvas = document.getElementById('camera-canvas');
633
-
634
- document.getElementById('start-camera').addEventListener('click', function() {
635
- navigator.mediaDevices.getUserMedia({ video: true })
636
- .then(function(stream) {
637
- cameraStream = stream;
638
- cameraVideo.srcObject = stream;
639
-
640
- document.getElementById('camera-stream').classList.remove('hidden');
641
- document.getElementById('stop-camera').classList.remove('hidden');
642
- document.getElementById('capture-frame').classList.remove('hidden');
643
- this.classList.remove('pulse-animation');
644
- })
645
- .catch(function(error) {
646
- showAlert('error', 'Impossible d\'accéder à la caméra: ' + error.message);
647
- });
648
  });
649
 
650
- document.getElementById('stop-camera').addEventListener('click', function() {
651
- if (cameraStream) {
652
- cameraStream.getTracks().forEach(track => track.stop());
653
- cameraStream = null;
654
- }
655
-
656
- document.getElementById('camera-stream').classList.add('hidden');
657
- document.getElementById('stop-camera').classList.add('hidden');
658
  document.getElementById('capture-frame').classList.add('hidden');
659
- document.getElementById('start-camera').classList.add('pulse-animation');
660
  });
661
 
662
- document.getElementById('capture-frame').addEventListener('click', function() {
663
- if (cameraVideo.videoWidth > 0) {
664
- // Capturer une frame de la vidéo
665
- cameraCanvas.width = cameraVideo.videoWidth;
666
- cameraCanvas.height = cameraVideo.videoHeight;
667
-
668
- const ctx = cameraCanvas.getContext('2d');
669
- ctx.drawImage(cameraVideo, 0, 0);
670
 
671
- // Convertir en blob et envoyer pour analyse
672
- cameraCanvas.toBlob(function(blob) {
673
- const formData = new FormData();
674
- formData.append('image', blob, 'camera_capture.jpg');
 
675
 
676
  fetch("{% url 'detection:analyze_image' %}", {
677
  method: 'POST',
678
- body: formData,
679
- headers: {
680
- 'X-CSRFToken': getCSRFToken()
681
- }
682
  })
683
- .then(response => response.json())
684
  .then(data => {
685
- if (data.success) {
686
- displayResults(data);
687
- showAlert('success', 'Capture analysée avec succès!');
688
- } else {
689
- showAlert('error', data.error || 'Erreur lors de l\'analyse');
690
- }
691
- })
692
- .catch(error => {
693
- showAlert('error', 'Erreur de connexion au serveur');
694
- console.error('Error:', error);
695
  });
696
- }, 'image/jpeg', 0.8);
697
  }
698
  });
699
-
700
- // Smooth scrolling for navigation links
701
- document.querySelectorAll('a[href^="#"]').forEach(anchor => {
702
- anchor.addEventListener('click', function(e) {
703
- e.preventDefault();
704
- const target = document.querySelector(this.getAttribute('href'));
705
- if (target) {
706
- target.scrollIntoView({
707
- behavior: 'smooth'
708
- });
709
- }
710
- });
711
- });
712
  </script>
713
-
714
- <!-- Signature Marino ATOHOUN -->
715
- <div style="display: none;">Code signé par Marino ATOHOUN - FireWatch AI Project</div>
716
  </body>
717
  </html>
718
-
 
1
  {% load static %}
2
  <!DOCTYPE html>
 
3
  <html lang="fr">
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>FireWatch AI - Détection Intelligente</title>
8
+
9
+ <!-- Fonts -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
13
+
14
+ <!-- Icons -->
15
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
16
+
17
+ <!-- Tailwind CSS -->
18
+ <script src="https://cdn.tailwindcss.com"></script>
19
+
20
+ <!-- Custom Config -->
21
+ <script>
22
+ tailwind.config = {
23
+ theme: {
24
+ extend: {
25
+ fontFamily: {
26
+ sans: ['"Plus Jakarta Sans"', 'sans-serif'],
27
+ display: ['"Outfit"', 'sans-serif'],
28
+ },
29
+ colors: {
30
+ brand: {
31
+ dark: '#0B0F19',
32
+ primary: '#3B82F6',
33
+ accent: '#8B5CF6',
34
+ glow: '#60A5FA',
35
+ surface: '#111827',
36
+ surfaceHighlight: '#1F2937'
37
+ }
38
+ },
39
+ animation: {
40
+ 'blob': 'blob 7s infinite',
41
+ 'pulse-glow': 'pulse-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
42
+ },
43
+ keyframes: {
44
+ blob: {
45
+ '0%': { transform: 'translate(0px, 0px) scale(1)' },
46
+ '33%': { transform: 'translate(30px, -50px) scale(1.1)' },
47
+ '66%': { transform: 'translate(-20px, 20px) scale(0.9)' },
48
+ '100%': { transform: 'translate(0px, 0px) scale(1)' },
49
+ },
50
+ 'pulse-glow': {
51
+ '0%, 100%': { opacity: 1, boxShadow: '0 0 20px rgba(59, 130, 246, 0.5)' },
52
+ '50%': { opacity: .5, boxShadow: '0 0 10px rgba(59, 130, 246, 0.2)' },
53
+ }
54
+ }
55
+ }
56
+ }
57
  }
58
+ </script>
59
+
60
+ <style>
61
+ /* Custom Styles for Premium Feel */
62
  body {
63
+ background-color: #0B0F19;
64
+ color: #F3F4F6;
65
+ overflow-x: hidden;
66
  }
67
+
68
+ .glass-panel {
69
+ background: rgba(17, 24, 39, 0.7);
70
+ backdrop-filter: blur(12px);
71
+ -webkit-backdrop-filter: blur(12px);
72
+ border: 1px solid rgba(255, 255, 255, 0.08);
73
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
 
 
 
 
74
  }
75
+
76
+ .glass-card {
77
+ background: rgba(31, 41, 55, 0.6);
78
+ backdrop-filter: blur(8px);
79
+ border: 1px solid rgba(255, 255, 255, 0.05);
80
+ transition: all 0.3s ease;
81
  }
82
+
83
+ .glass-card:hover {
84
+ transform: translateY(-5px);
85
+ border-color: rgba(59, 130, 246, 0.3);
86
+ box-shadow: 0 10px 40px -10px rgba(59, 130, 246, 0.2);
87
  }
88
+
89
+ .text-gradient {
90
+ background: linear-gradient(to right, #60A5FA, #A78BFA);
91
+ -webkit-background-clip: text;
92
+ -webkit-text-fill-color: transparent;
93
  }
94
 
95
+ .bg-gradient-brand {
96
+ background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
97
  }
98
 
99
+ /* Custom Scrollbar */
100
+ ::-webkit-scrollbar {
101
+ width: 8px;
102
+ }
103
+ ::-webkit-scrollbar-track {
104
+ background: #0B0F19;
105
+ }
106
+ ::-webkit-scrollbar-thumb {
107
+ background: #374151;
108
+ border-radius: 4px;
109
+ }
110
+ ::-webkit-scrollbar-thumb:hover {
111
+ background: #4B5563;
112
  }
113
 
114
+ /* Upload Zone */
115
+ .upload-zone {
116
+ background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%234B5563FF' stroke-width='2' stroke-dasharray='12%2c 12' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
117
+ transition: all 0.3s ease;
118
+ }
119
+ .upload-zone:hover, .upload-zone.dragover {
120
+ background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%2360A5FAFF' stroke-width='2' stroke-dasharray='12%2c 12' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
121
+ background-color: rgba(59, 130, 246, 0.05);
122
  }
123
 
124
+ /* Model Radio Button */
125
+ .model-radio:checked + div {
126
+ border-color: #3B82F6;
127
+ background-color: rgba(59, 130, 246, 0.1);
128
+ }
129
+ .model-radio:checked + div i {
130
+ transform: scale(1.1);
131
  }
132
 
133
+ /* Loader */
134
+ .loader {
135
+ border-top-color: #3B82F6;
136
+ -webkit-animation: spinner 1.5s linear infinite;
137
+ animation: spinner 1.5s linear infinite;
138
+ }
139
+ @keyframes spinner {
140
+ 0% { transform: rotate(0deg); }
141
+ 100% { transform: rotate(360deg); }
142
  }
143
 
144
+ /* Alert Animations */
145
+ .alert-enter {
146
+ animation: slideIn 0.5s ease-out forwards;
147
+ }
148
+ @keyframes slideIn {
149
+ from { opacity: 0; transform: translateY(-20px); }
150
+ to { opacity: 1; transform: translateY(0); }
151
  }
152
  </style>
153
  </head>
154
+ <body class="antialiased selection:bg-blue-500 selection:text-white">
155
+
156
+ <!-- Background Effects -->
157
+ <div class="fixed inset-0 z-[-1] overflow-hidden pointer-events-none">
158
+ <div class="absolute top-0 left-1/4 w-96 h-96 bg-blue-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob"></div>
159
+ <div class="absolute top-0 right-1/4 w-96 h-96 bg-purple-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob animation-delay-2000"></div>
160
+ <div class="absolute -bottom-32 left-1/3 w-96 h-96 bg-pink-600 rounded-full mix-blend-multiply filter blur-[128px] opacity-20 animate-blob animation-delay-4000"></div>
161
+ </div>
162
+
163
+ <!-- Navbar -->
164
+ <nav class="fixed w-full z-50 transition-all duration-300" id="navbar">
165
+ <div class="glass-panel mx-4 mt-4 rounded-2xl px-6 py-4 flex justify-between items-center max-w-7xl mx-auto">
166
+ <div class="flex items-center gap-3">
167
+ <div class="w-10 h-10 rounded-xl bg-gradient-brand flex items-center justify-center shadow-lg shadow-blue-500/30">
168
+ <i class="fas fa-fire text-white text-lg"></i>
169
  </div>
170
+ <span class="text-xl font-display font-bold tracking-tight">FireWatch <span class="text-blue-400">AI</span></span>
171
+ </div>
172
+
173
+ <div class="hidden md:flex items-center gap-8 text-sm font-medium text-gray-300">
174
+ <a href="#features" class="hover:text-white transition-colors">Fonctionnalités</a>
175
+ <a href="#demo" class="hover:text-white transition-colors">Démonstration</a>
176
+ <a href="#about" class="hover:text-white transition-colors">À propos</a>
 
 
177
  </div>
178
+
179
+ <a href="#contact" class="hidden md:flex items-center gap-2 px-5 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-sm font-medium">
180
+ <span>Contact</span>
181
+ <i class="fas fa-arrow-right text-xs"></i>
182
+ </a>
183
  </div>
184
+ </nav>
185
 
186
  <!-- Hero Section -->
187
+ <section class="relative pt-40 pb-20 px-4">
188
+ <div class="max-w-7xl mx-auto text-center">
189
+ <div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-sm font-medium mb-8 animate-fade-in-up">
190
+ <span class="relative flex h-2 w-2">
191
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
192
+ <span class="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
193
+ </span>
194
+ Système de Détection YOLOv8 Actif
195
  </div>
196
+
197
+ <h1 class="text-5xl md:text-7xl font-display font-bold mb-6 leading-tight">
198
+ La Sécurité Intelligente <br>
199
+ <span class="text-gradient">Nouvelle Génération</span>
200
+ </h1>
201
+
202
+ <p class="text-xl text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed">
203
+ Une solution de sécurité intelligente développée par <span class="text-white font-semibold">BlackBenAI</span>. Nous intégrons ces modèles de pointe dans vos infrastructures réelles (caméras, drones, serveurs) pour une protection sur mesure.
204
+ </p>
205
+
206
+ <div class="flex flex-col sm:flex-row items-center justify-center gap-4">
207
+ <a href="#demo" class="px-8 py-4 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-lg shadow-blue-600/30 transition-all transform hover:scale-105 flex items-center gap-2">
208
+ <i class="fas fa-play"></i> Essayer la Démo
209
+ </a>
210
+ <a href="#features" class="px-8 py-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-white font-semibold transition-all flex items-center gap-2">
211
+ <i class="fas fa-info-circle"></i> En savoir plus
212
+ </a>
213
  </div>
214
  </div>
215
  </section>
216
 
217
+ <!-- Stats/Features Grid -->
218
+ <section id="features" class="py-20 px-4">
219
+ <div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6">
220
+ <!-- Card 1 -->
221
+ <div class="glass-card p-8 rounded-2xl relative overflow-hidden group">
222
+ <div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
223
+ <i class="fas fa-fire text-8xl text-red-500"></i>
 
 
 
 
224
  </div>
225
+ <div class="w-12 h-12 rounded-lg bg-red-500/20 flex items-center justify-center mb-6 text-red-400">
226
+ <i class="fas fa-fire-extinguisher text-xl"></i>
 
 
 
 
227
  </div>
228
+ <h3 class="text-xl font-bold mb-3">Détection Incendie</h3>
229
+ <p class="text-gray-400 text-sm leading-relaxed">
230
+ Identification ultra-rapide des départs de feu et de fumée pour une intervention précoce et vitale.
231
+ </p>
232
+ </div>
233
+
234
+ <!-- Card 2 -->
235
+ <div class="glass-card p-8 rounded-2xl relative overflow-hidden group">
236
+ <div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
237
+ <i class="fas fa-user-shield text-8xl text-orange-500"></i>
238
+ </div>
239
+ <div class="w-12 h-12 rounded-lg bg-orange-500/20 flex items-center justify-center mb-6 text-orange-400">
240
+ <i class="fas fa-user-lock text-xl"></i>
241
  </div>
242
+ <h3 class="text-xl font-bold mb-3">Anti-Intrusion</h3>
243
+ <p class="text-gray-400 text-sm leading-relaxed">
244
+ Surveillance périmétrique avec détection de personnes non autorisées en temps réel.
245
+ </p>
246
+ </div>
247
+
248
+ <!-- Card 3 -->
249
+ <div class="glass-card p-8 rounded-2xl relative overflow-hidden group">
250
+ <div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
251
+ <i class="fas fa-bolt text-8xl text-blue-500"></i>
252
+ </div>
253
+ <div class="w-12 h-12 rounded-lg bg-blue-500/20 flex items-center justify-center mb-6 text-blue-400">
254
+ <i class="fas fa-microchip text-xl"></i>
255
+ </div>
256
+ <h3 class="text-xl font-bold mb-3">IA Temps Réel</h3>
257
+ <p class="text-gray-400 text-sm leading-relaxed">
258
+ Propulsé par YOLOv8 pour une latence minimale et une précision maximale sur tout support.
259
+ </p>
260
  </div>
261
  </div>
262
  </section>
263
 
264
+ <!-- Demo Section (The Core) -->
265
+ <section id="demo" class="py-20 px-4 relative">
266
+ <div class="max-w-5xl mx-auto">
267
+ <div class="text-center mb-12">
268
+ <h2 class="text-3xl md:text-4xl font-display font-bold mb-4">Démonstration Live</h2>
269
+ <p class="text-gray-400">Ceci est une démonstration technique. BlackBenAI peut déployer et adapter ces modèles pour vos cas d'usage spécifiques.</p>
270
+ </div>
271
+
272
+ <div class="glass-panel rounded-3xl overflow-hidden shadow-2xl border border-white/10">
273
+ <!-- Tabs Header -->
274
+ <div class="flex border-b border-white/10 bg-black/20">
275
+ <button onclick="switchTab('image')" class="flex-1 py-4 text-sm font-medium text-gray-400 hover:text-white transition-colors relative group active-tab" id="tab-btn-image">
276
+ <span class="flex items-center justify-center gap-2">
277
+ <i class="fas fa-image"></i> Analyse Image
278
+ </span>
279
+ <div class="absolute bottom-0 left-0 w-full h-0.5 bg-blue-500 transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></div>
280
  </button>
281
+ <button onclick="switchTab('video')" class="flex-1 py-4 text-sm font-medium text-gray-400 hover:text-white transition-colors relative group" id="tab-btn-video">
282
+ <span class="flex items-center justify-center gap-2">
283
+ <i class="fas fa-video"></i> Analyse Vidéo
284
+ </span>
285
+ <div class="absolute bottom-0 left-0 w-full h-0.5 bg-purple-500 transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></div>
286
  </button>
287
+ <button onclick="switchTab('camera')" class="flex-1 py-4 text-sm font-medium text-gray-400 hover:text-white transition-colors relative group" id="tab-btn-camera">
288
+ <span class="flex items-center justify-center gap-2">
289
+ <i class="fas fa-camera"></i> Live Caméra
290
+ </span>
291
+ <div class="absolute bottom-0 left-0 w-full h-0.5 bg-red-500 transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left"></div>
292
  </button>
293
  </div>
294
+
295
+ <!-- Tab Contents -->
296
+ <div class="p-8">
297
+
298
+ <!-- IMAGE TAB -->
299
+ <div id="content-image" class="tab-content transition-opacity duration-300">
300
+ <form id="image-form" class="space-y-6">
301
  {% csrf_token %}
302
+
303
+ <!-- Model Selector -->
304
+ <div class="bg-white/5 rounded-xl p-4 border border-white/10">
305
+ <label class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3 block">Modèle de Détection</label>
306
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
307
+ <label class="cursor-pointer relative">
308
+ <input type="radio" name="model_type" value="fire" class="model-radio sr-only">
309
+ <div class="p-3 rounded-lg border border-white/10 bg-black/20 hover:bg-white/5 transition-all flex items-center justify-center gap-2 text-sm font-medium text-gray-300">
310
+ <i class="fas fa-fire text-red-500 transition-transform"></i> Incendie
311
+ </div>
312
+ </label>
313
+ <label class="cursor-pointer relative">
314
+ <input type="radio" name="model_type" value="intrusion" class="model-radio sr-only">
315
+ <div class="p-3 rounded-lg border border-white/10 bg-black/20 hover:bg-white/5 transition-all flex items-center justify-center gap-2 text-sm font-medium text-gray-300">
316
+ <i class="fas fa-user-shield text-orange-500 transition-transform"></i> Intrusion
317
+ </div>
318
+ </label>
319
+ <label class="cursor-pointer relative">
320
+ <input type="radio" name="model_type" value="both" checked class="model-radio sr-only">
321
+ <div class="p-3 rounded-lg border border-white/10 bg-black/20 hover:bg-white/5 transition-all flex items-center justify-center gap-2 text-sm font-medium text-gray-300">
322
+ <i class="fas fa-layer-group text-blue-500 transition-transform"></i> Tout Détecter
323
+ </div>
324
+ </label>
325
  </div>
326
  </div>
327
+
328
+ <!-- Upload Area -->
329
+ <div class="upload-zone rounded-2xl p-10 text-center cursor-pointer relative group" id="image-dropzone">
330
+ <input type="file" name="image" id="image-input" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
331
+ <div class="transition-transform group-hover:scale-105 duration-300">
332
+ <div class="w-16 h-16 rounded-full bg-blue-500/10 flex items-center justify-center mx-auto mb-4 text-blue-400">
333
+ <i class="fas fa-cloud-upload-alt text-2xl"></i>
334
+ </div>
335
+ <h3 class="text-lg font-semibold text-white mb-2">Glissez votre image ici</h3>
336
+ <p class="text-sm text-gray-400">JPG, PNG supportés</p>
337
+ </div>
338
+ <!-- Preview -->
339
+ <div id="image-preview-container" class="hidden mt-6 relative rounded-lg overflow-hidden border border-white/20 shadow-lg max-w-sm mx-auto">
340
+ <img id="image-preview" src="" class="w-full h-auto object-cover">
341
+ <button type="button" id="clear-image" class="absolute top-2 right-2 w-8 h-8 bg-black/50 hover:bg-red-500 rounded-full text-white flex items-center justify-center transition-colors z-20">
342
+ <i class="fas fa-times"></i>
343
+ </button>
344
+ </div>
345
  </div>
346
+
347
+ <button type="submit" class="w-full py-4 rounded-xl bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-500 hover:to-blue-400 text-white font-bold shadow-lg shadow-blue-900/20 transition-all transform hover:scale-[1.01] flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
348
+ <span class="btn-text">Lancer l'Analyse</span>
349
+ <div class="loader w-5 h-5 border-2 border-white/30 rounded-full hidden"></div>
350
+ </button>
351
  </form>
352
  </div>
353
+
354
+ <!-- VIDEO TAB -->
355
+ <div id="content-video" class="tab-content hidden transition-opacity duration-300">
356
+ <form id="video-form" class="space-y-6">
357
  {% csrf_token %}
358
+
359
+ <!-- Model Selector (Video) -->
360
+ <div class="bg-white/5 rounded-xl p-4 border border-white/10">
361
+ <label class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3 block">Modèle de Détection</label>
362
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
363
+ <label class="cursor-pointer relative">
364
+ <input type="radio" name="model_type" value="fire" class="model-radio sr-only">
365
+ <div class="p-3 rounded-lg border border-white/10 bg-black/20 hover:bg-white/5 transition-all flex items-center justify-center gap-2 text-sm font-medium text-gray-300">
366
+ <i class="fas fa-fire text-red-500 transition-transform"></i> Incendie
367
+ </div>
368
+ </label>
369
+ <label class="cursor-pointer relative">
370
+ <input type="radio" name="model_type" value="intrusion" class="model-radio sr-only">
371
+ <div class="p-3 rounded-lg border border-white/10 bg-black/20 hover:bg-white/5 transition-all flex items-center justify-center gap-2 text-sm font-medium text-gray-300">
372
+ <i class="fas fa-user-shield text-orange-500 transition-transform"></i> Intrusion
373
+ </div>
374
+ </label>
375
+ <label class="cursor-pointer relative">
376
+ <input type="radio" name="model_type" value="both" checked class="model-radio sr-only">
377
+ <div class="p-3 rounded-lg border border-white/10 bg-black/20 hover:bg-white/5 transition-all flex items-center justify-center gap-2 text-sm font-medium text-gray-300">
378
+ <i class="fas fa-layer-group text-blue-500 transition-transform"></i> Tout Détecter
379
+ </div>
380
+ </label>
381
  </div>
382
  </div>
383
+
384
+ <div class="upload-zone rounded-2xl p-10 text-center cursor-pointer relative group" id="video-dropzone">
385
+ <input type="file" name="video" id="video-input" accept="video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
386
+ <div class="transition-transform group-hover:scale-105 duration-300">
387
+ <div class="w-16 h-16 rounded-full bg-purple-500/10 flex items-center justify-center mx-auto mb-4 text-purple-400">
388
+ <i class="fas fa-film text-2xl"></i>
389
+ </div>
390
+ <h3 class="text-lg font-semibold text-white mb-2">Glissez votre vidéo ici</h3>
391
+ <p class="text-sm text-gray-400">MP4, AVI, MOV</p>
392
+ </div>
393
+ <div id="video-preview-container" class="hidden mt-6 relative rounded-lg overflow-hidden border border-white/20 shadow-lg max-w-sm mx-auto">
394
+ <video id="video-preview" controls class="w-full h-auto"></video>
395
+ <button type="button" id="clear-video" class="absolute top-2 right-2 w-8 h-8 bg-black/50 hover:bg-red-500 rounded-full text-white flex items-center justify-center transition-colors z-20">
396
+ <i class="fas fa-times"></i>
397
+ </button>
398
+ </div>
399
  </div>
400
+
401
+ <button type="submit" class="w-full py-4 rounded-xl bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 text-white font-bold shadow-lg shadow-purple-900/20 transition-all transform hover:scale-[1.01] flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
402
+ <span class="btn-text">Analyser la Vidéo</span>
403
+ <div class="loader w-5 h-5 border-2 border-white/30 rounded-full hidden"></div>
404
+ </button>
405
  </form>
406
  </div>
407
+
408
+ <!-- CAMERA TAB -->
409
+ <div id="content-camera" class="tab-content hidden transition-opacity duration-300 text-center">
410
+ <div class="relative rounded-2xl overflow-hidden bg-black aspect-video mb-6 border border-white/10 shadow-2xl">
411
+ <video id="camera-video" autoplay playsinline class="w-full h-full object-cover"></video>
412
+ <canvas id="camera-canvas" class="hidden"></canvas>
413
+
414
+ <div id="camera-overlay" class="absolute inset-0 flex items-center justify-center bg-black/50">
415
+ <p class="text-gray-400"><i class="fas fa-camera-slash mr-2"></i> Caméra inactive</p>
 
 
 
 
416
  </div>
417
  </div>
418
+
419
+ <div class="flex justify-center gap-4">
420
+ <button id="start-camera" class="px-6 py-3 rounded-xl bg-green-600 hover:bg-green-500 text-white font-semibold shadow-lg shadow-green-900/20 transition-all flex items-center gap-2">
421
+ <i class="fas fa-power-off"></i> Activer
422
  </button>
423
+ <button id="capture-frame" class="px-6 py-3 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-lg shadow-blue-900/20 transition-all flex items-center gap-2 hidden">
424
+ <i class="fas fa-camera"></i> Capturer & Analyser
425
+ </button>
426
+ <button id="stop-camera" class="px-6 py-3 rounded-xl bg-red-600 hover:bg-red-500 text-white font-semibold shadow-lg shadow-red-900/20 transition-all flex items-center gap-2 hidden">
427
+ <i class="fas fa-stop"></i> Arrêter
428
  </button>
429
  </div>
430
  </div>
431
+
432
  </div>
433
  </div>
434
+
435
  <!-- Results Section -->
436
+ <div id="results-section" class="mt-12 hidden animate-fade-in-up">
437
+ <div class="glass-panel p-8 rounded-3xl border border-green-500/30 shadow-[0_0_50px_-12px_rgba(16,185,129,0.2)]">
438
+ <h3 class="text-2xl font-bold mb-6 flex items-center gap-3">
439
+ <i class="fas fa-check-circle text-green-500"></i> Résultat de l'analyse
440
+ </h3>
441
+
442
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
443
+ <div class="rounded-xl overflow-hidden border border-white/10 shadow-lg bg-black/40">
444
+ <img id="result-image" src="" class="w-full h-auto object-contain" alt="Résultat">
445
  </div>
446
+
447
+ <div>
448
+ <h4 class="text-lg font-semibold mb-4 text-gray-300">Détails des détections</h4>
449
+ <div id="detection-list" class="space-y-3">
450
+ <!-- Detections injected here -->
 
451
  </div>
452
+
453
+ <div class="mt-8 p-4 rounded-xl bg-blue-500/10 border border-blue-500/20">
454
+ <p class="text-sm text-blue-300">
455
+ <i class="fas fa-info-circle mr-2"></i>
456
+ L'analyse a été effectuée avec succès par nos modèles YOLOv8.
457
+ </p>
458
  </div>
459
  </div>
460
  </div>
461
  </div>
462
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
463
  </div>
464
  </section>
465
 
466
  <!-- Creator Section -->
467
+ <section id="about" class="py-20 px-4 bg-black/20">
468
+ <div class="max-w-4xl mx-auto">
469
+ <div class="glass-card p-8 md:p-12 rounded-3xl flex flex-col md:flex-row items-center gap-10">
470
+ <div class="w-40 h-40 rounded-full p-1 bg-gradient-brand shadow-xl flex-shrink-0 flex items-center justify-center bg-black">
471
+ <span class="text-4xl font-bold text-white">BB</span>
 
 
 
472
  </div>
473
+ <div class="text-center md:text-left">
474
+ <h2 class="text-3xl font-bold mb-2">L'Expertise BlackBenAI</h2>
475
+ <p class="text-blue-400 font-medium mb-4">Solutions IA Sur Mesure pour Entreprises</p>
476
+ <p class="text-gray-400 leading-relaxed mb-6">
477
+ FireWatch AI est une vitrine technologique créée par l'équipe de <strong class="text-white">BlackBenAI</strong>. Nous sommes spécialisés dans l'intégration de systèmes de vision par ordinateur pour sécuriser vos infrastructures critiques. De la conception au déploiement sur site, nous adaptons nos modèles à vos besoins uniques.
478
+ </p>
479
+ <div class="flex justify-center md:justify-start gap-4">
480
+ <a href="#" class="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors text-gray-400 hover:text-white"><i class="fas fa-globe"></i></a>
481
+ <a href="#" class="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors text-gray-400 hover:text-white"><i class="fab fa-linkedin"></i></a>
482
+ <a href="#" class="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors text-gray-400 hover:text-white"><i class="fas fa-envelope"></i></a>
 
 
 
 
 
483
  </div>
484
  </div>
485
  </div>
 
487
  </section>
488
 
489
  <!-- Contact Section -->
490
+ <section id="contact" class="py-20 px-4">
491
+ <div class="max-w-xl mx-auto">
492
+ <div class="text-center mb-10">
493
+ <h2 class="text-3xl font-bold mb-2">Contactez-nous</h2>
494
+ <p class="text-gray-400">Vous souhaitez intégrer cette technologie ? Discutons de votre projet.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  </div>
496
+
497
+ <form id="contact-form" class="glass-panel p-8 rounded-3xl space-y-5">
498
+ {% csrf_token %}
499
+
500
+ <!-- Alert Box -->
501
+ <div id="contact-alert" class="hidden p-4 rounded-xl text-sm font-medium transition-all duration-300">
502
+ <i class="fas fa-info-circle mr-2"></i> <span id="contact-message"></span>
503
+ </div>
504
+
505
+ <div>
506
+ <label class="block text-sm font-medium text-gray-400 mb-2">Votre Nom</label>
507
+ <input type="text" name="name" required class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all placeholder-gray-600" placeholder="John Doe">
508
+ </div>
509
+ <div>
510
+ <label class="block text-sm font-medium text-gray-400 mb-2">Email</label>
511
+ <input type="email" name="email" required class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all placeholder-gray-600" placeholder="[email protected]">
512
+ </div>
513
+ <div>
514
+ <label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
515
+ <textarea name="message" rows="4" required class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all placeholder-gray-600" placeholder="Votre message..."></textarea>
516
+ </div>
517
+
518
+ <button type="submit" class="w-full py-3 rounded-xl bg-white text-black font-bold hover:bg-gray-200 transition-colors flex items-center justify-center gap-2">
519
+ <span class="btn-text">Envoyer</span>
520
+ <i class="fas fa-paper-plane text-sm"></i>
521
+ </button>
522
+ </form>
523
  </div>
524
  </section>
525
 
526
  <!-- Footer -->
527
+ <footer class="py-8 border-t border-white/5 bg-black/20">
528
+ <div class="max-w-7xl mx-auto px-4 flex flex-col md:flex-row justify-between items-center gap-4">
529
+ <p class="text-gray-500 text-sm">&copy; 2025 FireWatch AI. Une innovation signée BlackBenAI.</p>
530
+ <div class="flex gap-6 text-sm text-gray-500">
531
+ <a href="{% url 'detection:privacy' %}" class="hover:text-white transition-colors">Confidentialité</a>
532
+ <a href="{% url 'detection:terms' %}" class="hover:text-white transition-colors">Conditions</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  </div>
534
  </div>
535
  </footer>
536
 
537
+ <!-- JavaScript Logic -->
538
  <script>
539
+ // --- UTILS ---
 
 
540
  function getCSRFToken() {
541
  return document.querySelector('[name=csrfmiddlewaretoken]').value;
542
  }
543
 
 
544
  function showAlert(type, message) {
545
+ // Generic alert handler (can be improved)
546
+ alert(message); // Fallback for now
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  }
548
 
549
+ // --- TABS ---
550
  function switchTab(tabName) {
551
+ // Hide all contents
552
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
553
+ // Show target
554
+ document.getElementById(`content-${tabName}`).classList.remove('hidden');
555
 
556
+ // Update buttons styling
557
+ document.querySelectorAll('[id^="tab-btn-"]').forEach(btn => {
558
+ const underline = btn.querySelector('div');
559
+ const icon = btn.querySelector('i');
560
+
561
+ if(btn.id === `tab-btn-${tabName}`) {
562
+ btn.classList.add('text-white');
563
+ btn.classList.remove('text-gray-400');
564
+ underline.classList.remove('scale-x-0');
565
+ underline.classList.add('scale-x-100');
566
+ } else {
567
+ btn.classList.remove('text-white');
568
+ btn.classList.add('text-gray-400');
569
+ underline.classList.add('scale-x-0');
570
+ underline.classList.remove('scale-x-100');
571
+ }
572
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  }
574
 
575
+ // --- PREVIEWS ---
576
+ // Image Preview
577
+ document.getElementById('image-input').addEventListener('change', function(e) {
 
 
 
 
 
578
  const file = e.target.files[0];
579
+ if(file) {
580
  const reader = new FileReader();
581
  reader.onload = function(e) {
582
+ document.getElementById('image-preview').src = e.target.result;
583
+ document.getElementById('image-preview-container').classList.remove('hidden');
584
+ }
585
  reader.readAsDataURL(file);
586
  }
587
  });
588
+ document.getElementById('clear-image').addEventListener('click', function() {
589
+ document.getElementById('image-input').value = '';
590
+ document.getElementById('image-preview-container').classList.add('hidden');
591
+ });
592
 
593
+ // Video Preview
594
+ document.getElementById('video-input').addEventListener('change', function(e) {
595
  const file = e.target.files[0];
596
+ if(file) {
597
  const url = URL.createObjectURL(file);
598
+ document.getElementById('video-preview').src = url;
599
+ document.getElementById('video-preview-container').classList.remove('hidden');
600
  }
601
  });
602
+ document.getElementById('clear-video').addEventListener('click', function() {
603
+ document.getElementById('video-input').value = '';
604
+ document.getElementById('video-preview-container').classList.add('hidden');
605
+ });
606
+
607
+ // --- RESULTS DISPLAY ---
608
+ function displayResults(data) {
609
+ const resultsSection = document.getElementById('results-section');
610
+ const resultImage = document.getElementById('result-image');
611
+ const list = document.getElementById('detection-list');
612
+
613
+ // Show Image
614
+ if(data.result_image_url) {
615
+ resultImage.src = data.result_image_url;
616
+ }
617
+
618
+ // List Detections
619
+ list.innerHTML = '';
620
+ if(data.detections && data.detections.length > 0) {
621
+ data.detections.forEach(det => {
622
+ let colorClass = 'bg-gray-500';
623
+ let icon = 'fa-question';
624
+
625
+ if(det.class_name === 'fire') { colorClass = 'bg-red-500'; icon = 'fa-fire'; }
626
+ else if(det.class_name === 'smoke') { colorClass = 'bg-gray-400'; icon = 'fa-smog'; }
627
+ else if(det.class_name === 'person' || det.class_name === 'intrusion') { colorClass = 'bg-orange-500'; icon = 'fa-user'; }
628
+
629
+ const item = document.createElement('div');
630
+ item.className = 'flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5';
631
+ item.innerHTML = `
632
+ <div class="flex items-center gap-3">
633
+ <div class="w-8 h-8 rounded-full ${colorClass}/20 flex items-center justify-center text-${colorClass.replace('bg-', '')}">
634
+ <i class="fas ${icon}"></i>
635
+ </div>
636
+ <span class="font-medium text-gray-200 capitalize">${det.class_name}</span>
637
+ </div>
638
+ <span class="text-sm font-bold text-blue-400">${(det.confidence * 100).toFixed(0)}%</span>
639
+ `;
640
+ list.appendChild(item);
641
+ });
642
+ } else {
643
+ list.innerHTML = '<div class="text-center text-gray-500 py-4">Aucune détection trouvée</div>';
644
+ }
645
+
646
+ resultsSection.classList.remove('hidden');
647
+ resultsSection.scrollIntoView({behavior: 'smooth'});
648
+ }
649
+
650
+ // --- FORM SUBMISSIONS ---
651
+
652
+ // Helper for loading state
653
+ function setLoading(form, isLoading) {
654
+ const btn = form.querySelector('button[type="submit"]');
655
+ const text = btn.querySelector('.btn-text');
656
+ const loader = btn.querySelector('.loader');
657
+
658
+ if(isLoading) {
659
+ btn.disabled = true;
660
+ text.classList.add('hidden');
661
+ loader.classList.remove('hidden');
662
+ } else {
663
+ btn.disabled = false;
664
+ text.classList.remove('hidden');
665
+ loader.classList.add('hidden');
666
+ }
667
+ }
668
 
669
+ // Image Form
670
  document.getElementById('image-form').addEventListener('submit', function(e) {
671
  e.preventDefault();
 
672
  const formData = new FormData(this);
673
+ const modelType = this.querySelector('input[name="model_type"]:checked').value;
674
+ formData.set('model_type', modelType);
675
 
676
+ setLoading(this, true);
677
 
678
  fetch("{% url 'detection:analyze_image' %}", {
679
  method: 'POST',
680
  body: formData,
681
+ headers: {'X-CSRFToken': getCSRFToken()}
 
 
682
  })
683
+ .then(r => r.json())
684
  .then(data => {
685
+ setLoading(this, false);
686
+ if(data.success) displayResults(data);
687
+ else alert('Erreur: ' + (data.error || 'Inconnue'));
 
 
 
 
 
688
  })
689
+ .catch(err => {
690
+ setLoading(this, false);
691
+ console.error(err);
692
+ alert('Erreur réseau');
693
  });
694
  });
695
 
696
+ // Video Form
697
  document.getElementById('video-form').addEventListener('submit', function(e) {
698
  e.preventDefault();
 
699
  const formData = new FormData(this);
700
+ const modelType = this.querySelector('input[name="model_type"]:checked').value;
701
+ formData.set('model_type', modelType);
702
 
703
+ setLoading(this, true);
704
 
705
  fetch("{% url 'detection:analyze_video' %}", {
706
  method: 'POST',
707
  body: formData,
708
+ headers: {'X-CSRFToken': getCSRFToken()}
 
 
709
  })
710
+ .then(r => r.json())
711
  .then(data => {
712
+ setLoading(this, false);
713
+ if(data.success) displayResults(data);
714
+ else alert('Erreur: ' + (data.error || 'Inconnue'));
 
 
 
 
 
715
  })
716
+ .catch(err => {
717
+ setLoading(this, false);
718
+ console.error(err);
719
+ alert('Erreur réseau');
720
  });
721
  });
722
 
723
+ // Contact Form (FIX CSRF)
724
  document.getElementById('contact-form').addEventListener('submit', function(e) {
725
  e.preventDefault();
 
726
  const formData = new FormData(this);
727
+ const btn = this.querySelector('button[type="submit"]');
728
+ const originalText = btn.innerHTML;
729
+ const alertBox = document.getElementById('contact-alert');
730
+ const msgSpan = document.getElementById('contact-message');
731
+
732
+ btn.disabled = true;
733
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
734
 
735
  fetch("{% url 'detection:contact' %}", {
736
  method: 'POST',
737
  body: formData,
738
+ headers: {'X-CSRFToken': getCSRFToken()}
 
 
739
  })
740
+ .then(r => r.json())
741
  .then(data => {
742
+ btn.disabled = false;
743
+ btn.innerHTML = originalText;
744
+
745
+ alertBox.classList.remove('hidden', 'bg-green-500/20', 'text-green-400', 'bg-red-500/20', 'text-red-400');
746
+
747
+ if(data.success) {
748
+ alertBox.classList.add('bg-green-500/20', 'text-green-400', 'alert-enter');
749
+ msgSpan.textContent = data.message;
750
  this.reset();
751
  } else {
752
+ alertBox.classList.add('bg-red-500/20', 'text-red-400', 'alert-enter');
753
+ msgSpan.textContent = data.error || 'Erreur inconnue';
754
  }
755
+ alertBox.classList.remove('hidden');
756
+
757
+ setTimeout(() => alertBox.classList.add('hidden'), 5000);
758
  })
759
+ .catch(err => {
760
+ btn.disabled = false;
761
+ btn.innerHTML = originalText;
762
+ alert('Erreur réseau');
763
  });
764
  });
765
 
766
+ // Camera Logic
767
+ let stream = null;
768
+ const video = document.getElementById('camera-video');
769
+ const canvas = document.getElementById('camera-canvas');
770
+
771
+ document.getElementById('start-camera').addEventListener('click', async () => {
772
+ try {
773
+ stream = await navigator.mediaDevices.getUserMedia({video: true});
774
+ video.srcObject = stream;
775
+ document.getElementById('camera-overlay').classList.add('hidden');
776
+ document.getElementById('start-camera').classList.add('hidden');
777
+ document.getElementById('capture-frame').classList.remove('hidden');
778
+ document.getElementById('stop-camera').classList.remove('hidden');
779
+ } catch(e) {
780
+ alert('Erreur caméra: ' + e.message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
  });
783
 
784
+ document.getElementById('stop-camera').addEventListener('click', () => {
785
+ if(stream) stream.getTracks().forEach(t => t.stop());
786
+ video.srcObject = null;
787
+ document.getElementById('camera-overlay').classList.remove('hidden');
788
+ document.getElementById('start-camera').classList.remove('hidden');
 
 
 
789
  document.getElementById('capture-frame').classList.add('hidden');
790
+ document.getElementById('stop-camera').classList.add('hidden');
791
  });
792
 
793
+ document.getElementById('capture-frame').addEventListener('click', () => {
794
+ if(video.videoWidth) {
795
+ canvas.width = video.videoWidth;
796
+ canvas.height = video.videoHeight;
797
+ canvas.getContext('2d').drawImage(video, 0, 0);
 
 
 
798
 
799
+ canvas.toBlob(blob => {
800
+ const fd = new FormData();
801
+ fd.append('image', blob, 'capture.jpg');
802
+ // Camera uses image endpoint, default model 'both'
803
+ fd.append('model_type', 'both');
804
 
805
  fetch("{% url 'detection:analyze_image' %}", {
806
  method: 'POST',
807
+ body: fd,
808
+ headers: {'X-CSRFToken': getCSRFToken()}
 
 
809
  })
810
+ .then(r => r.json())
811
  .then(data => {
812
+ if(data.success) displayResults(data);
813
+ else alert('Erreur: ' + data.error);
 
 
 
 
 
 
 
 
814
  });
815
+ }, 'image/jpeg');
816
  }
817
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  </script>
 
 
 
819
  </body>
820
  </html>
 
detection/templates/detection/privacy.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% load static %}
2
+ <!DOCTYPE html>
3
+ <html lang="fr">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Politique de Confidentialité - FireWatch AI</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ fontFamily: { sans: ['"Plus Jakarta Sans"', 'sans-serif'], display: ['"Outfit"', 'sans-serif'] },
15
+ colors: { brand: { dark: '#0B0F19' } }
16
+ }
17
+ }
18
+ }
19
+ </script>
20
+ <style>
21
+ body { background-color: #0B0F19; color: #F3F4F6; }
22
+ .glass-panel { background: rgba(17, 24, 39, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.08); }
23
+ </style>
24
+ </head>
25
+ <body class="antialiased p-6 md:p-12">
26
+ <div class="max-w-4xl mx-auto glass-panel rounded-3xl p-8 md:p-12 shadow-2xl">
27
+ <a href="{% url 'detection:index' %}" class="inline-flex items-center text-blue-400 hover:text-blue-300 mb-8 transition-colors">
28
+ &larr; Retour à l'accueil
29
+ </a>
30
+
31
+ <h1 class="text-3xl md:text-4xl font-display font-bold mb-8">Politique de Confidentialité</h1>
32
+
33
+ <div class="space-y-6 text-gray-300 leading-relaxed">
34
+ <p>Dernière mise à jour : 27 Décembre 2025</p>
35
+
36
+ <h2 class="text-xl font-bold text-white mt-8">1. Collecte des Données</h2>
37
+ <p>FireWatch AI, développé par BlackBenAI, collecte uniquement les données nécessaires au fonctionnement de la démonstration : images et vidéos téléchargées pour analyse. Ces fichiers sont traités temporairement et ne sont pas conservés à long terme sans votre accord explicite dans le cadre d'un contrat commercial.</p>
38
+
39
+ <h2 class="text-xl font-bold text-white mt-8">2. Utilisation des Données</h2>
40
+ <p>Les données soumises sont utilisées exclusivement pour :</p>
41
+ <ul class="list-disc pl-5 space-y-2">
42
+ <li>Effectuer les détections d'incendie et d'intrusion demandées.</li>
43
+ <li>Améliorer nos modèles de détection (uniquement si consenti).</li>
44
+ <li>Vous contacter si vous avez rempli le formulaire de contact.</li>
45
+ </ul>
46
+
47
+ <h2 class="text-xl font-bold text-white mt-8">3. Sécurité</h2>
48
+ <p>Nous mettons en œuvre des mesures de sécurité avancées pour protéger vos données contre tout accès non autorisé. Nos serveurs sont sécurisés et l'accès est strictement limité à l'équipe technique de BlackBenAI.</p>
49
+
50
+ <h2 class="text-xl font-bold text-white mt-8">4. Contact</h2>
51
+ <p>Pour toute question concernant vos données, contactez-nous via le formulaire de la page d'accueil ou à [email protected].</p>
52
+ </div>
53
+
54
+ <div class="mt-12 pt-8 border-t border-white/10 text-center text-sm text-gray-500">
55
+ &copy; 2025 FireWatch AI - BlackBenAI. Tous droits réservés.
56
+ </div>
57
+ </div>
58
+ </body>
59
+ </html>
detection/templates/detection/terms.html ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% load static %}
2
+ <!DOCTYPE html>
3
+ <html lang="fr">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Conditions d'Utilisation - FireWatch AI</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ fontFamily: { sans: ['"Plus Jakarta Sans"', 'sans-serif'], display: ['"Outfit"', 'sans-serif'] },
15
+ colors: { brand: { dark: '#0B0F19' } }
16
+ }
17
+ }
18
+ }
19
+ </script>
20
+ <style>
21
+ body { background-color: #0B0F19; color: #F3F4F6; }
22
+ .glass-panel { background: rgba(17, 24, 39, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.08); }
23
+ </style>
24
+ </head>
25
+ <body class="antialiased p-6 md:p-12">
26
+ <div class="max-w-4xl mx-auto glass-panel rounded-3xl p-8 md:p-12 shadow-2xl">
27
+ <a href="{% url 'detection:index' %}" class="inline-flex items-center text-blue-400 hover:text-blue-300 mb-8 transition-colors">
28
+ &larr; Retour à l'accueil
29
+ </a>
30
+
31
+ <h1 class="text-3xl md:text-4xl font-display font-bold mb-8">Conditions d'Utilisation</h1>
32
+
33
+ <div class="space-y-6 text-gray-300 leading-relaxed">
34
+ <p>Dernière mise à jour : 27 Décembre 2025</p>
35
+
36
+ <h2 class="text-xl font-bold text-white mt-8">1. Acceptation des Conditions</h2>
37
+ <p>En accédant à FireWatch AI, vous acceptez d'être lié par ces conditions d'utilisation. Si vous n'acceptez pas ces conditions, veuillez ne pas utiliser nos services.</p>
38
+
39
+ <h2 class="text-xl font-bold text-white mt-8">2. Usage de la Démonstration</h2>
40
+ <p>Ce site est une démonstration technique fournie par BlackBenAI. Il ne doit pas être utilisé comme unique système de sécurité critique sans validation et contrat de service dédié. Les résultats de détection sont fournis à titre indicatif.</p>
41
+
42
+ <h2 class="text-xl font-bold text-white mt-8">3. Propriété Intellectuelle</h2>
43
+ <p>Tous les contenus, modèles IA, codes sources et designs présents sur ce site sont la propriété exclusive de BlackBenAI, sauf indication contraire.</p>
44
+
45
+ <h2 class="text-xl font-bold text-white mt-8">4. Limitation de Responsabilité</h2>
46
+ <p>BlackBenAI ne saurait être tenu responsable des dommages directs ou indirects résultant de l'utilisation de cette démonstration, y compris les erreurs de détection (faux positifs/négatifs).</p>
47
+
48
+ <h2 class="text-xl font-bold text-white mt-8">5. Modifications</h2>
49
+ <p>Nous nous réservons le droit de modifier ces conditions à tout moment. Les modifications prennent effet dès leur publication sur cette page.</p>
50
+ </div>
51
+
52
+ <div class="mt-12 pt-8 border-t border-white/10 text-center text-sm text-gray-500">
53
+ &copy; 2025 FireWatch AI - BlackBenAI. Tous droits réservés.
54
+ </div>
55
+ </div>
56
+ </body>
57
+ </html>
detection/urls.py CHANGED
@@ -29,7 +29,12 @@ urlpatterns = [
29
  # Téléchargement des résultats
30
  path('download/results/<uuid:session_id>/', views.download_results, name='download_results'),
31
 
 
32
  # Statut des modèles IA
33
  path('api/models/status/', views.models_status_api, name='models_status'),
 
 
 
 
34
  ]
35
 
 
29
  # Téléchargement des résultats
30
  path('download/results/<uuid:session_id>/', views.download_results, name='download_results'),
31
 
32
+ # Statut des modèles IA
33
  # Statut des modèles IA
34
  path('api/models/status/', views.models_status_api, name='models_status'),
35
+
36
+ # Pages légales
37
+ path('privacy/', views.privacy_view, name='privacy'),
38
+ path('terms/', views.terms_view, name='terms'),
39
  ]
40
 
detection/views.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  Vues pour l'application Detection
3
  Créé par Marino ATOHOUN - FireWatch AI Project
 
4
  """
5
  import os
6
  import json
@@ -23,209 +24,318 @@ import logging
23
  # Par Marino ATOHOUN: Configuration du logging
24
  logger = logging.getLogger(__name__)
25
 
26
- # Par Marino ATOHOUN: Variables globales pour les modèles YOLOv8
27
  # Ces variables seront initialisées au démarrage du serveur
28
  fire_model = None
29
  intrusion_model = None
 
30
 
31
- # Par Marino ATOHOUN: Fonction pour charger les modèles YOLOv8
 
32
  def load_yolo_models():
33
  """
34
  Charge les modèles YOLOv8 pour la détection d'incendie et d'intrusion
35
  À appeler au démarrage du serveur Django
36
  """
37
- global fire_model, intrusion_model
38
 
39
  try:
40
- # Par Marino ATOHOUN: Importez YOLO ici quand vous avez ultralytics installé
41
- # from ultralytics import YOLO
42
 
43
  # Vérifier si les fichiers de modèles existent
44
  fire_model_path = settings.FIRE_MODEL_PATH
45
  intrusion_model_path = settings.INTRUSION_MODEL_PATH
46
 
47
- # Par Marino ATOHOUN: Décommentez ces lignes quand vous avez vos modèles
48
- # if os.path.exists(fire_model_path):
49
- # fire_model = YOLO(fire_model_path)
50
- # logger.info("Modèle d'incendie chargé avec succès")
51
- #
52
- # # Mettre à jour le statut dans la base de données
53
- # AIModelStatus.objects.update_or_create(
54
- # model_type='fire',
55
- # defaults={
56
- # 'model_path': str(fire_model_path),
57
- # 'is_loaded': True,
58
- # 'last_loaded': timezone.now(),
59
- # 'model_version': '1.0'
60
- # }
61
- # )
62
- # else:
63
- # logger.warning(f"Modèle d'incendie non trouvé: {fire_model_path}")
64
-
65
- # if os.path.exists(intrusion_model_path):
66
- # intrusion_model = YOLO(intrusion_model_path)
67
- # logger.info("Modèle d'intrusion chargé avec succès")
68
- #
69
- # # Mettre à jour le statut dans la base de données
70
- # AIModelStatus.objects.update_or_create(
71
- # model_type='intrusion',
72
- # defaults={
73
- # 'model_path': str(intrusion_model_path),
74
- # 'is_loaded': True,
75
- # 'last_loaded': timezone.now(),
76
- # 'model_version': '1.0'
77
- # }
78
- # )
79
- # else:
80
- # logger.warning(f"Modèle d'intrusion non trouvé: {intrusion_model_path}")
81
-
82
- # Par Marino ATOHOUN: Pour l'instant, on simule le chargement
83
- logger.info("Simulation du chargement des modèles - Remplacez par le vrai code")
84
-
85
- # Créer les entrées de statut pour la simulation
86
- AIModelStatus.objects.update_or_create(
87
- model_type='fire',
88
- defaults={
89
- 'model_path': str(fire_model_path),
90
- 'is_loaded': False, # Sera True quand vous aurez les vrais modèles
91
- 'last_loaded': None,
92
- 'model_version': '1.0'
93
- }
94
- )
95
-
96
- AIModelStatus.objects.update_or_create(
97
- model_type='intrusion',
98
- defaults={
99
- 'model_path': str(intrusion_model_path),
100
- 'is_loaded': False, # Sera True quand vous aurez les vrais modèles
101
- 'last_loaded': None,
102
- 'model_version': '1.0'
103
- }
104
- )
105
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  except Exception as e:
107
- logger.error(f"Erreur lors du chargement des modèles: {e}")
108
 
109
 
110
- # Par Marino ATOHOUN: Fonction pour effectuer la détection sur une image
111
- def detect_objects_in_image(image_path, session):
 
 
 
 
 
 
 
 
 
 
 
 
112
  """
113
- Effectue la détection d'objets sur une image
 
114
  Retourne une liste de détections
115
  """
116
  detections = []
117
 
118
  try:
119
- # Par Marino ATOHOUN: Ici vous intégrerez votre code de détection YOLOv8
120
- # Exemple de ce que vous devrez faire:
121
-
122
- # # Charger l'image
123
- # image = cv2.imread(image_path)
124
- #
125
- # # Détection avec le modèle d'incendie
126
- # if fire_model:
127
- # fire_results = fire_model(image)
128
- # for result in fire_results:
129
- # boxes = result.boxes
130
- # if boxes is not None:
131
- # for box in boxes:
132
- # x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
133
- # confidence = box.conf[0].cpu().numpy()
134
- # class_id = int(box.cls[0].cpu().numpy())
135
- #
136
- # # Mapper les classes selon votre modèle
137
- # class_name = 'fire' if class_id == 0 else 'smoke'
138
- #
139
- # detection = Detection.objects.create(
140
- # session=session,
141
- # class_name=class_name,
142
- # confidence=float(confidence),
143
- # bbox_x=float(x1),
144
- # bbox_y=float(y1),
145
- # bbox_width=float(x2 - x1),
146
- # bbox_height=float(y2 - y1)
147
- # )
148
- # detections.append(detection)
149
- #
150
- # # Détection avec le modèle d'intrusion
151
- # if intrusion_model:
152
- # intrusion_results = intrusion_model(image)
153
- # # Traitement similaire...
154
-
155
- # Par Marino ATOHOUN: Pour l'instant, on simule des détections
156
- # Remplacez cette section par votre vraie logique de détection
157
-
158
- # Simulation de détections aléatoires pour la démonstration
159
- import random
160
-
161
- simulation_detections = [
162
- {'class_name': 'fire', 'confidence': 0.92, 'bbox': [100, 100, 150, 150]},
163
- {'class_name': 'person', 'confidence': 0.87, 'bbox': [200, 150, 100, 200]},
164
- {'class_name': 'smoke', 'confidence': 0.78, 'bbox': [50, 50, 80, 100]},
165
- ]
166
-
167
- for sim_det in simulation_detections:
168
- if random.random() > 0.3: # 70% de chance d'apparaître
169
- detection = Detection.objects.create(
170
- session=session,
171
- class_name=sim_det['class_name'],
172
- confidence=sim_det['confidence'],
173
- bbox_x=sim_det['bbox'][0],
174
- bbox_y=sim_det['bbox'][1],
175
- bbox_width=sim_det['bbox'][2],
176
- bbox_height=sim_det['bbox'][3]
177
- )
178
- detections.append(detection)
179
-
180
- logger.info(f"Détections simulées créées: {len(detections)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
  except Exception as e:
183
- logger.error(f"Erreur lors de la détection: {e}")
 
 
184
 
185
  return detections
186
 
187
 
188
- # Par Marino ATOHOUN: Fonction pour dessiner les boîtes de détection
189
  def draw_detections_on_image(image_path, detections, output_path):
190
  """
191
  Dessine les boîtes de détection sur l'image et sauvegarde le résultat
192
  """
193
  try:
194
- # Par Marino ATOHOUN: Ici vous dessinerez les vraies boîtes de détection
195
- # Exemple de ce que vous devrez faire:
196
-
197
- # image = cv2.imread(image_path)
198
- #
199
- # for detection in detections:
200
- # x1 = int(detection.bbox_x)
201
- # y1 = int(detection.bbox_y)
202
- # x2 = int(detection.bbox_x + detection.bbox_width)
203
- # y2 = int(detection.bbox_y + detection.bbox_height)
204
- #
205
- # # Couleur selon le type de détection
206
- # if detection.class_name in ['fire', 'smoke']:
207
- # color = (0, 0, 255) # Rouge
208
- # else:
209
- # color = (255, 0, 0) # Bleu
210
- #
211
- # # Dessiner la boîte
212
- # cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
213
- #
214
- # # Ajouter le label
215
- # label = f"{detection.class_name}: {detection.confidence:.2f}"
216
- # cv2.putText(image, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
217
- #
218
- # # Sauvegarder l'image avec les détections
219
- # cv2.imwrite(output_path, image)
220
-
221
- # Par Marino ATOHOUN: Pour l'instant, on copie juste l'image originale
222
- # Remplacez par votre vraie logique de dessin
223
- import shutil
224
- shutil.copy2(image_path, output_path)
225
- logger.info(f"Image de résultat créée (simulation): {output_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
  except Exception as e:
228
- logger.error(f"Erreur lors du dessin des détections: {e}")
 
 
 
229
 
230
 
231
  def index_view(request):
@@ -264,7 +374,7 @@ def contact_view(request):
264
 
265
  return JsonResponse({
266
  'success': True,
267
- 'message': 'Votre message a été envoyé avec succès! Nous vous répondrons bientôt.'
268
  })
269
 
270
  except Exception as e:
@@ -278,8 +388,8 @@ def contact_view(request):
278
  @require_POST
279
  def analyze_image_view(request):
280
  """
281
- Gère l'upload et l'analyse d'une image
282
- Par Marino ATOHOUN
283
  """
284
  try:
285
  if 'image' not in request.FILES:
@@ -291,11 +401,11 @@ def analyze_image_view(request):
291
  image_file = request.FILES['image']
292
 
293
  # Vérifier le type de fichier
294
- allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp']
295
  if image_file.content_type not in allowed_types:
296
  return JsonResponse({
297
  'success': False,
298
- 'error': 'Type de fichier non supporté. Utilisez JPG, PNG ou BMP.'
299
  }, status=400)
300
 
301
  # Créer une session de détection
@@ -317,8 +427,12 @@ def analyze_image_view(request):
317
  # Mesurer le temps de traitement
318
  start_time = time.time()
319
 
320
- # Par Marino ATOHOUN: Effectuer la détection
321
- detections = detect_objects_in_image(full_image_path, session)
 
 
 
 
322
 
323
  # Créer l'image de résultat avec les détections
324
  result_filename = f'results/result_{session.session_id}.jpg'
@@ -345,18 +459,24 @@ def analyze_image_view(request):
345
 
346
  result_image_url = request.build_absolute_uri(settings.MEDIA_URL + result_filename)
347
 
348
- logger.info(f"Analyse d'image terminée: Session {session.session_id}, {len(detections)} détections")
349
 
350
  return JsonResponse({
351
  'success': True,
352
  'session_id': str(session.session_id),
353
  'detections': detections_data,
354
  'result_image_url': result_image_url,
355
- 'processing_time': session.processing_time
 
 
 
 
356
  })
357
 
358
  except Exception as e:
359
- logger.error(f"Erreur lors de l'analyse d'image: {e}")
 
 
360
  return JsonResponse({
361
  'success': False,
362
  'error': 'Une erreur est survenue lors de l\'analyse de l\'image.'
@@ -366,8 +486,8 @@ def analyze_image_view(request):
366
  @require_POST
367
  def analyze_video_view(request):
368
  """
369
- Gère l'upload et l'analyse d'une vidéo
370
- Par Marino ATOHOUN
371
  """
372
  try:
373
  if 'video' not in request.FILES:
@@ -379,11 +499,11 @@ def analyze_video_view(request):
379
  video_file = request.FILES['video']
380
 
381
  # Vérifier le type de fichier
382
- allowed_types = ['video/mp4', 'video/avi', 'video/mov', 'video/mkv']
383
  if video_file.content_type not in allowed_types:
384
  return JsonResponse({
385
  'success': False,
386
- 'error': 'Type de fichier non supporté. Utilisez MP4, AVI, MOV ou MKV.'
387
  }, status=400)
388
 
389
  # Créer une session de détection
@@ -399,70 +519,109 @@ def analyze_video_view(request):
399
  session.original_file = video_path
400
  session.save()
401
 
402
- # Par Marino ATOHOUN: Ici vous intégrerez votre logique d'analyse vidéo
403
- # Exemple de ce que vous devrez faire:
404
-
405
- # full_video_path = os.path.join(settings.MEDIA_ROOT, video_path)
406
- # cap = cv2.VideoCapture(full_video_path)
407
- #
408
- # frame_count = 0
409
- # detections = []
410
- #
411
- # while True:
412
- # ret, frame = cap.read()
413
- # if not ret:
414
- # break
415
- #
416
- # # Analyser chaque frame (ou une frame sur N pour optimiser)
417
- # if frame_count % 30 == 0: # Analyser une frame toutes les 30
418
- # # Sauvegarder temporairement la frame
419
- # temp_frame_path = f'/tmp/frame_{frame_count}.jpg'
420
- # cv2.imwrite(temp_frame_path, frame)
421
- #
422
- # # Analyser la frame
423
- # frame_detections = detect_objects_in_image(temp_frame_path, session)
424
- #
425
- # # Ajouter le numéro de frame et timestamp
426
- # for detection in frame_detections:
427
- # detection.frame_number = frame_count
428
- # detection.timestamp = frame_count / cap.get(cv2.CAP_PROP_FPS)
429
- # detection.save()
430
- #
431
- # detections.extend(frame_detections)
432
- #
433
- # frame_count += 1
434
- #
435
- # cap.release()
436
-
437
- # Par Marino ATOHOUN: Pour l'instant, simulation de l'analyse vidéo
438
  start_time = time.time()
439
 
440
- # Simuler quelques détections sur différentes frames
441
- import random
442
- simulation_detections = []
443
-
444
- for frame_num in [0, 30, 60, 90]:
445
- if random.random() > 0.5:
446
- detection = Detection.objects.create(
447
- session=session,
448
- class_name=random.choice(['fire', 'person', 'smoke']),
449
- confidence=random.uniform(0.7, 0.95),
450
- bbox_x=random.uniform(50, 200),
451
- bbox_y=random.uniform(50, 200),
452
- bbox_width=random.uniform(80, 150),
453
- bbox_height=random.uniform(80, 150),
454
- frame_number=frame_num,
455
- timestamp=frame_num / 30.0 # Supposer 30 FPS
456
- )
457
- simulation_detections.append(detection)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  session.processing_time = time.time() - start_time
460
  session.is_processed = True
461
  session.save()
462
 
463
  # Préparer la réponse
464
  detections_data = []
465
- for detection in simulation_detections:
466
  detections_data.append({
467
  'class_name': detection.class_name,
468
  'label': detection.get_class_name_display(),
@@ -472,18 +631,28 @@ def analyze_video_view(request):
472
  'timestamp': detection.timestamp
473
  })
474
 
475
- logger.info(f"Analyse vidéo terminée: Session {session.session_id}, {len(simulation_detections)} détections")
 
 
476
 
477
  return JsonResponse({
478
  'success': True,
479
  'session_id': str(session.session_id),
480
  'detections': detections_data,
 
481
  'processing_time': session.processing_time,
482
- 'message': 'Analyse vidéo terminée. Fonctionnalité complète disponible avec vos modèles YOLOv8.'
 
 
 
 
 
483
  })
484
 
485
  except Exception as e:
486
- logger.error(f"Erreur lors de l'analyse vidéo: {e}")
 
 
487
  return JsonResponse({
488
  'success': False,
489
  'error': 'Une erreur est survenue lors de l\'analyse de la vidéo.'
@@ -583,7 +752,7 @@ def download_results(request, session_id):
583
  def models_status_api(request):
584
  """
585
  API pour obtenir le statut des modèles IA
586
- Par Marino ATOHOUN
587
  """
588
  try:
589
  models_status = AIModelStatus.objects.all()
@@ -601,7 +770,12 @@ def models_status_api(request):
601
 
602
  return JsonResponse({
603
  'success': True,
604
- 'models': status_data
 
 
 
 
 
605
  })
606
 
607
  except Exception as e:
@@ -612,7 +786,15 @@ def models_status_api(request):
612
  }, status=500)
613
 
614
 
615
- # Par Marino ATOHOUN: Initialiser les modèles au démarrage
 
 
 
 
 
 
 
 
616
  # Cette fonction sera appelée quand Django démarre
617
  def initialize_models():
618
  """
@@ -621,10 +803,9 @@ def initialize_models():
621
  try:
622
  load_yolo_models()
623
  except Exception as e:
624
- logger.error(f"Erreur lors de l'initialisation des modèles: {e}")
625
 
626
 
627
- # Par Marino ATOHOUN: Appeler l'initialisation
628
- # Décommentez cette ligne quand vous aurez vos modèles
629
  initialize_models()
630
 
 
1
  """
2
  Vues pour l'application Detection
3
  Créé par Marino ATOHOUN - FireWatch AI Project
4
+ Modifié par BlackBenAI Team - Intégration réelle des modèles YOLOv8
5
  """
6
  import os
7
  import json
 
24
  # Par Marino ATOHOUN: Configuration du logging
25
  logger = logging.getLogger(__name__)
26
 
27
+ # Par BlackBenAI: Variables globales pour les modèles YOLOv8
28
  # Ces variables seront initialisées au démarrage du serveur
29
  fire_model = None
30
  intrusion_model = None
31
+ models_loaded = False
32
 
33
+
34
+ # Par BlackBenAI: Fonction pour charger les modèles YOLOv8
35
  def load_yolo_models():
36
  """
37
  Charge les modèles YOLOv8 pour la détection d'incendie et d'intrusion
38
  À appeler au démarrage du serveur Django
39
  """
40
+ global fire_model, intrusion_model, models_loaded
41
 
42
  try:
43
+ # Par BlackBenAI: Import de YOLO depuis ultralytics
44
+ from ultralytics import YOLO
45
 
46
  # Vérifier si les fichiers de modèles existent
47
  fire_model_path = settings.FIRE_MODEL_PATH
48
  intrusion_model_path = settings.INTRUSION_MODEL_PATH
49
 
50
+ # Charger le modèle d'incendie
51
+ if os.path.exists(fire_model_path):
52
+ fire_model = YOLO(str(fire_model_path))
53
+
54
+ # PATCH BlackBenAI: Empêcher le fusing automatique qui cause l'erreur 'Conv' object has no attribute 'bn'
55
+ # Le modèle semble déjà fusionné ou incompatible avec cette version d'Ultralytics pour le fusing
56
+ try:
57
+ if hasattr(fire_model, 'model') and fire_model.model:
58
+ fire_model.model.fuse = lambda *args, **kwargs: fire_model.model
59
+ logger.info("🔧 Patch 'fuse' appliqué au modèle d'incendie")
60
+ except Exception as e:
61
+ logger.warning(f"⚠️ Impossible d'appliquer le patch 'fuse' au modèle d'incendie: {e}")
62
+
63
+ logger.info(f"✅ Modèle d'incendie chargé avec succès: {fire_model_path}")
64
+
65
+ # Mettre à jour le statut dans la base de données
66
+ AIModelStatus.objects.update_or_create(
67
+ model_type='fire',
68
+ defaults={
69
+ 'model_path': str(fire_model_path),
70
+ 'is_loaded': True,
71
+ 'last_loaded': timezone.now(),
72
+ 'model_version': '1.0'
73
+ }
74
+ )
75
+ else:
76
+ logger.warning(f"⚠️ Modèle d'incendie non trouvé: {fire_model_path}")
77
+ AIModelStatus.objects.update_or_create(
78
+ model_type='fire',
79
+ defaults={
80
+ 'model_path': str(fire_model_path),
81
+ 'is_loaded': False,
82
+ 'last_loaded': None,
83
+ 'model_version': '1.0'
84
+ }
85
+ )
86
+
87
+ # Charger le modèle d'intrusion
88
+ if os.path.exists(intrusion_model_path):
89
+ intrusion_model = YOLO(str(intrusion_model_path))
90
+
91
+ # PATCH BlackBenAI: Empêcher le fusing automatique
92
+ try:
93
+ if hasattr(intrusion_model, 'model') and intrusion_model.model:
94
+ intrusion_model.model.fuse = lambda *args, **kwargs: intrusion_model.model
95
+ logger.info("🔧 Patch 'fuse' appliqué au modèle d'intrusion")
96
+ except Exception as e:
97
+ logger.warning(f"⚠️ Impossible d'appliquer le patch 'fuse' au modèle d'intrusion: {e}")
98
+
99
+ logger.info(f"✅ Modèle d'intrusion chargé avec succès: {intrusion_model_path}")
100
+
101
+ # Mettre à jour le statut dans la base de données
102
+ AIModelStatus.objects.update_or_create(
103
+ model_type='intrusion',
104
+ defaults={
105
+ 'model_path': str(intrusion_model_path),
106
+ 'is_loaded': True,
107
+ 'last_loaded': timezone.now(),
108
+ 'model_version': '1.0'
109
+ }
110
+ )
111
+ else:
112
+ logger.warning(f"⚠️ Modèle d'intrusion non trouvé: {intrusion_model_path}")
113
+ AIModelStatus.objects.update_or_create(
114
+ model_type='intrusion',
115
+ defaults={
116
+ 'model_path': str(intrusion_model_path),
117
+ 'is_loaded': False,
118
+ 'last_loaded': None,
119
+ 'model_version': '1.0'
120
+ }
121
+ )
122
+
123
+ models_loaded = (fire_model is not None) or (intrusion_model is not None)
124
+
125
+ if models_loaded:
126
+ logger.info("🚀 Modèles YOLOv8 initialisés avec succès!")
127
+ else:
128
+ logger.warning("⚠️ Aucun modèle YOLOv8 n'a pu être chargé")
129
+
130
+ except ImportError as e:
131
+ logger.error(f"❌ ultralytics n'est pas installé: {e}")
132
+ logger.info("💡 Installez avec: pip install ultralytics")
133
  except Exception as e:
134
+ logger.error(f"Erreur lors du chargement des modèles: {e}")
135
 
136
 
137
+ # Par BlackBenAI: Mapping des classes pour les modèles
138
+ # Ajustez ces mappings selon les classes de vos modèles
139
+ FIRE_CLASS_NAMES = {
140
+ 0: 'fire',
141
+ 1: 'smoke',
142
+ }
143
+
144
+ INTRUSION_CLASS_NAMES = {
145
+ 0: 'person',
146
+ }
147
+
148
+
149
+ # Par BlackBenAI: Fonction pour effectuer la détection sur une image
150
+ def detect_objects_in_image(image_path, session, model_type='both'):
151
  """
152
+ Effectue la détection d'objets sur une image avec les modèles YOLOv8
153
+ model_type: 'fire', 'intrusion', ou 'both'
154
  Retourne une liste de détections
155
  """
156
  detections = []
157
 
158
  try:
159
+ # Charger l'image avec OpenCV
160
+ image = cv2.imread(image_path)
161
+
162
+ if image is None:
163
+ logger.error(f"❌ Impossible de charger l'image: {image_path}")
164
+ return detections
165
+
166
+ # Détection avec le modèle d'incendie
167
+ if fire_model is not None and model_type in ['fire', 'both']:
168
+ logger.info("🔥 Exécution de la détection d'incendie...")
169
+ try:
170
+ fire_results = fire_model(image, verbose=False)
171
+
172
+ for result in fire_results:
173
+ boxes = result.boxes
174
+ if boxes is not None and len(boxes) > 0:
175
+ for box in boxes:
176
+ # Extraire les coordonnées de la boîte
177
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
178
+ confidence = float(box.conf[0].cpu().numpy())
179
+ class_id = int(box.cls[0].cpu().numpy())
180
+
181
+ # Obtenir le nom de la classe
182
+ class_name = FIRE_CLASS_NAMES.get(class_id, 'fire')
183
+
184
+ # Filtrer par confiance minimale
185
+ if confidence >= 0.5:
186
+ detection = Detection.objects.create(
187
+ session=session,
188
+ class_name=class_name,
189
+ confidence=confidence,
190
+ bbox_x=float(x1),
191
+ bbox_y=float(y1),
192
+ bbox_width=float(x2 - x1),
193
+ bbox_height=float(y2 - y1)
194
+ )
195
+ detections.append(detection)
196
+ logger.info(f" → Détecté: {class_name} ({confidence:.2%})")
197
+ except Exception as e:
198
+ logger.error(f"❌ Erreur lors de la détection incendie: {e}")
199
+ if "has no attribute 'bn'" in str(e):
200
+ logger.error("⚠️ Problème de compatibilité 'fusing' détecté. Vérifiez la version d'Ultralytics.")
201
+
202
+ # Détection avec le modèle d'intrusion
203
+ if intrusion_model is not None and model_type in ['intrusion', 'both']:
204
+ logger.info("👤 Exécution de la détection d'intrusion...")
205
+ try:
206
+ intrusion_results = intrusion_model(image, verbose=False)
207
+
208
+ for result in intrusion_results:
209
+ boxes = result.boxes
210
+ if boxes is not None and len(boxes) > 0:
211
+ for box in boxes:
212
+ # Extraire les coordonnées de la boîte
213
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
214
+ confidence = float(box.conf[0].cpu().numpy())
215
+ class_id = int(box.cls[0].cpu().numpy())
216
+
217
+ # Obtenir le nom de la classe
218
+ class_name = INTRUSION_CLASS_NAMES.get(class_id, 'person')
219
+
220
+ # Filtrer par confiance minimale
221
+ if confidence >= 0.5:
222
+ detection = Detection.objects.create(
223
+ session=session,
224
+ class_name=class_name,
225
+ confidence=confidence,
226
+ bbox_x=float(x1),
227
+ bbox_y=float(y1),
228
+ bbox_width=float(x2 - x1),
229
+ bbox_height=float(y2 - y1)
230
+ )
231
+ detections.append(detection)
232
+ logger.info(f" → Détecté: {class_name} ({confidence:.2%})")
233
+ except Exception as e:
234
+ logger.error(f"❌ Erreur lors de la détection intrusion: {e}")
235
+ if "has no attribute 'bn'" in str(e):
236
+ logger.error("⚠️ Problème de compatibilité 'fusing' détecté. Vérifiez la version d'Ultralytics.")
237
+
238
+ # Fallback si aucun modèle n'est chargé
239
+ if fire_model is None and intrusion_model is None:
240
+ logger.warning("⚠️ Aucun modèle chargé - Mode simulation activé")
241
+ # Mode simulation pour la démo
242
+ import random
243
+ simulation_detections = [
244
+ {'class_name': 'fire', 'confidence': 0.92, 'bbox': [100, 100, 150, 150]},
245
+ {'class_name': 'person', 'confidence': 0.87, 'bbox': [200, 150, 100, 200]},
246
+ ]
247
+
248
+ for sim_det in simulation_detections:
249
+ if random.random() > 0.5:
250
+ detection = Detection.objects.create(
251
+ session=session,
252
+ class_name=sim_det['class_name'],
253
+ confidence=sim_det['confidence'],
254
+ bbox_x=sim_det['bbox'][0],
255
+ bbox_y=sim_det['bbox'][1],
256
+ bbox_width=sim_det['bbox'][2],
257
+ bbox_height=sim_det['bbox'][3]
258
+ )
259
+ detections.append(detection)
260
+
261
+ logger.info(f"✅ Détections terminées: {len(detections)} objets trouvés")
262
 
263
  except Exception as e:
264
+ logger.error(f"Erreur lors de la détection: {e}")
265
+ import traceback
266
+ logger.error(traceback.format_exc())
267
 
268
  return detections
269
 
270
 
271
+ # Par BlackBenAI: Fonction pour dessiner les boîtes de détection
272
  def draw_detections_on_image(image_path, detections, output_path):
273
  """
274
  Dessine les boîtes de détection sur l'image et sauvegarde le résultat
275
  """
276
  try:
277
+ # Charger l'image
278
+ image = cv2.imread(image_path)
279
+
280
+ if image is None:
281
+ logger.error(f"❌ Impossible de charger l'image pour le dessin: {image_path}")
282
+ return
283
+
284
+ for detection in detections:
285
+ x1 = int(detection.bbox_x)
286
+ y1 = int(detection.bbox_y)
287
+ x2 = int(detection.bbox_x + detection.bbox_width)
288
+ y2 = int(detection.bbox_y + detection.bbox_height)
289
+
290
+ # Couleur selon le type de détection
291
+ if detection.class_name in ['fire']:
292
+ color = (0, 0, 255) # Rouge pour le feu
293
+ label_bg = (0, 0, 180)
294
+ elif detection.class_name in ['smoke']:
295
+ color = (128, 128, 128) # Gris pour la fumée
296
+ label_bg = (100, 100, 100)
297
+ else:
298
+ color = (255, 165, 0) # Orange pour les personnes/intrusions
299
+ label_bg = (200, 130, 0)
300
+
301
+ # Dessiner la boîte avec épaisseur
302
+ cv2.rectangle(image, (x1, y1), (x2, y2), color, 3)
303
+
304
+ # Préparer le label
305
+ label = f"{detection.class_name.upper()}: {detection.confidence:.0%}"
306
+ font = cv2.FONT_HERSHEY_SIMPLEX
307
+ font_scale = 0.6
308
+ thickness = 2
309
+
310
+ # Calculer la taille du texte
311
+ (text_width, text_height), baseline = cv2.getTextSize(label, font, font_scale, thickness)
312
+
313
+ # Dessiner le fond du label
314
+ cv2.rectangle(image,
315
+ (x1, y1 - text_height - 10),
316
+ (x1 + text_width + 10, y1),
317
+ label_bg, -1)
318
+
319
+ # Dessiner le texte
320
+ cv2.putText(image, label, (x1 + 5, y1 - 5),
321
+ font, font_scale, (255, 255, 255), thickness)
322
+
323
+ # Ajouter un watermark BlackBenAI
324
+ h, w = image.shape[:2]
325
+ watermark = "FireWatch AI by BlackBenAI"
326
+ font = cv2.FONT_HERSHEY_SIMPLEX
327
+ cv2.putText(image, watermark, (10, h - 15),
328
+ font, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
329
+
330
+ # Sauvegarder l'image avec les détections
331
+ cv2.imwrite(output_path, image)
332
+ logger.info(f"✅ Image de résultat sauvegardée: {output_path}")
333
 
334
  except Exception as e:
335
+ logger.error(f"Erreur lors du dessin des détections: {e}")
336
+ # En cas d'erreur, copier l'image originale
337
+ import shutil
338
+ shutil.copy2(image_path, output_path)
339
 
340
 
341
  def index_view(request):
 
374
 
375
  return JsonResponse({
376
  'success': True,
377
+ 'message': 'Votre message a été envoyé avec succès! L\'équipe BlackBenAI vous répondra bientôt.'
378
  })
379
 
380
  except Exception as e:
 
388
  @require_POST
389
  def analyze_image_view(request):
390
  """
391
+ Gère l'upload et l'analyse d'une image avec les modèles YOLOv8
392
+ Par Marino ATOHOUN & BlackBenAI Team
393
  """
394
  try:
395
  if 'image' not in request.FILES:
 
401
  image_file = request.FILES['image']
402
 
403
  # Vérifier le type de fichier
404
+ allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/bmp', 'image/webp']
405
  if image_file.content_type not in allowed_types:
406
  return JsonResponse({
407
  'success': False,
408
+ 'error': 'Type de fichier non supporté. Utilisez JPG, PNG, BMP ou WebP.'
409
  }, status=400)
410
 
411
  # Créer une session de détection
 
427
  # Mesurer le temps de traitement
428
  start_time = time.time()
429
 
430
+ # Récupérer le type de modèle choisi (par défaut 'both')
431
+ model_type = request.POST.get('model_type', 'both')
432
+ logger.info(f"🔍 Analyse demandée avec modèle: {model_type}")
433
+
434
+ # Par BlackBenAI: Effectuer la détection réelle avec YOLOv8
435
+ detections = detect_objects_in_image(full_image_path, session, model_type)
436
 
437
  # Créer l'image de résultat avec les détections
438
  result_filename = f'results/result_{session.session_id}.jpg'
 
459
 
460
  result_image_url = request.build_absolute_uri(settings.MEDIA_URL + result_filename)
461
 
462
+ logger.info(f"Analyse d'image terminée: Session {session.session_id}, {len(detections)} détections en {session.processing_time:.2f}s")
463
 
464
  return JsonResponse({
465
  'success': True,
466
  'session_id': str(session.session_id),
467
  'detections': detections_data,
468
  'result_image_url': result_image_url,
469
+ 'processing_time': session.processing_time,
470
+ 'models_used': {
471
+ 'fire': fire_model is not None,
472
+ 'intrusion': intrusion_model is not None
473
+ }
474
  })
475
 
476
  except Exception as e:
477
+ logger.error(f"Erreur lors de l'analyse d'image: {e}")
478
+ import traceback
479
+ logger.error(traceback.format_exc())
480
  return JsonResponse({
481
  'success': False,
482
  'error': 'Une erreur est survenue lors de l\'analyse de l\'image.'
 
486
  @require_POST
487
  def analyze_video_view(request):
488
  """
489
+ Gère l'upload et l'analyse d'une vidéo avec les modèles YOLOv8
490
+ Par Marino ATOHOUN & BlackBenAI Team
491
  """
492
  try:
493
  if 'video' not in request.FILES:
 
499
  video_file = request.FILES['video']
500
 
501
  # Vérifier le type de fichier
502
+ allowed_types = ['video/mp4', 'video/avi', 'video/mov', 'video/mkv', 'video/webm']
503
  if video_file.content_type not in allowed_types:
504
  return JsonResponse({
505
  'success': False,
506
+ 'error': 'Type de fichier non supporté. Utilisez MP4, AVI, MOV, MKV ou WebM.'
507
  }, status=400)
508
 
509
  # Créer une session de détection
 
519
  session.original_file = video_path
520
  session.save()
521
 
522
+ # Par BlackBenAI: Analyse réelle de la vidéo
523
+ full_video_path = os.path.join(settings.MEDIA_ROOT, video_path)
524
+
525
+ # Récupérer le type de modèle choisi (par défaut 'both')
526
+ model_type = request.POST.get('model_type', 'both')
527
+ logger.info(f"🔍 Analyse vidéo demandée avec modèle: {model_type}")
528
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  start_time = time.time()
530
 
531
+ cap = cv2.VideoCapture(full_video_path)
532
+
533
+ if not cap.isOpened():
534
+ return JsonResponse({
535
+ 'success': False,
536
+ 'error': 'Impossible d\'ouvrir la vidéo.'
537
+ }, status=400)
538
+
539
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30
540
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
541
+
542
+ frame_count = 0
543
+ all_detections = []
544
+ frames_analyzed = 0
545
+
546
+ # Analyser une frame toutes les N frames pour optimiser
547
+ analyze_every_n = max(1, int(fps)) # Analyser ~1 frame par seconde
548
+
549
+ # Créer un dossier temporaire pour les frames
550
+ temp_dir = os.path.join(settings.MEDIA_ROOT, 'temp', str(session.session_id))
551
+ os.makedirs(temp_dir, exist_ok=True)
552
+
553
+ best_frame = None
554
+ best_frame_detections = []
555
+
556
+ logger.info(f"📹 Analyse vidéo: {total_frames} frames, {fps:.1f} FPS")
557
+
558
+ while True:
559
+ ret, frame = cap.read()
560
+ if not ret:
561
+ break
562
+
563
+ # Analyser une frame sur N
564
+ if frame_count % analyze_every_n == 0:
565
+ # Sauvegarder la frame temporairement
566
+ temp_frame_path = os.path.join(temp_dir, f'frame_{frame_count}.jpg')
567
+ cv2.imwrite(temp_frame_path, frame)
568
+
569
+ # Créer une mini-session pour cette frame
570
+ frame_detections = detect_objects_in_image(temp_frame_path, session, model_type)
571
+
572
+ # Ajouter le numéro de frame et timestamp
573
+ for detection in frame_detections:
574
+ detection.frame_number = frame_count
575
+ detection.timestamp = frame_count / fps
576
+ detection.save()
577
+
578
+ all_detections.extend(frame_detections)
579
+ frames_analyzed += 1
580
+
581
+ # Garder la frame avec le plus de détections
582
+ if len(frame_detections) > len(best_frame_detections):
583
+ best_frame = frame.copy()
584
+ best_frame_detections = frame_detections
585
+
586
+ # Supprimer la frame temporaire
587
+ os.remove(temp_frame_path)
588
+
589
+ frame_count += 1
590
+
591
+ cap.release()
592
+
593
+ # Nettoyer le dossier temporaire
594
+ try:
595
+ os.rmdir(temp_dir)
596
+ except:
597
+ pass
598
 
599
+ # Créer une image de résultat avec la meilleure frame
600
+ result_filename = f'results/result_{session.session_id}.jpg'
601
+ result_path = os.path.join(settings.MEDIA_ROOT, result_filename)
602
+ os.makedirs(os.path.dirname(result_path), exist_ok=True)
603
+
604
+ if best_frame is not None:
605
+ temp_best_path = os.path.join(settings.MEDIA_ROOT, 'temp', f'best_{session.session_id}.jpg')
606
+ os.makedirs(os.path.dirname(temp_best_path), exist_ok=True)
607
+ cv2.imwrite(temp_best_path, best_frame)
608
+ draw_detections_on_image(temp_best_path, best_frame_detections, result_path)
609
+ os.remove(temp_best_path)
610
+ else:
611
+ # Si pas de frame, créer une image placeholder
612
+ placeholder = np.zeros((480, 640, 3), dtype=np.uint8)
613
+ cv2.putText(placeholder, "Aucune detection", (50, 240),
614
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
615
+ cv2.imwrite(result_path, placeholder)
616
+
617
+ session.result_file = result_filename
618
  session.processing_time = time.time() - start_time
619
  session.is_processed = True
620
  session.save()
621
 
622
  # Préparer la réponse
623
  detections_data = []
624
+ for detection in all_detections:
625
  detections_data.append({
626
  'class_name': detection.class_name,
627
  'label': detection.get_class_name_display(),
 
631
  'timestamp': detection.timestamp
632
  })
633
 
634
+ result_image_url = request.build_absolute_uri(settings.MEDIA_URL + result_filename)
635
+
636
+ logger.info(f"✅ Analyse vidéo terminée: Session {session.session_id}, {len(all_detections)} détections sur {frames_analyzed} frames en {session.processing_time:.2f}s")
637
 
638
  return JsonResponse({
639
  'success': True,
640
  'session_id': str(session.session_id),
641
  'detections': detections_data,
642
+ 'result_image_url': result_image_url,
643
  'processing_time': session.processing_time,
644
+ 'frames_analyzed': frames_analyzed,
645
+ 'total_frames': total_frames,
646
+ 'models_used': {
647
+ 'fire': fire_model is not None,
648
+ 'intrusion': intrusion_model is not None
649
+ }
650
  })
651
 
652
  except Exception as e:
653
+ logger.error(f"Erreur lors de l'analyse vidéo: {e}")
654
+ import traceback
655
+ logger.error(traceback.format_exc())
656
  return JsonResponse({
657
  'success': False,
658
  'error': 'Une erreur est survenue lors de l\'analyse de la vidéo.'
 
752
  def models_status_api(request):
753
  """
754
  API pour obtenir le statut des modèles IA
755
+ Par Marino ATOHOUN & BlackBenAI Team
756
  """
757
  try:
758
  models_status = AIModelStatus.objects.all()
 
770
 
771
  return JsonResponse({
772
  'success': True,
773
+ 'models': status_data,
774
+ 'runtime_status': {
775
+ 'fire_model_loaded': fire_model is not None,
776
+ 'intrusion_model_loaded': intrusion_model is not None,
777
+ 'models_loaded': models_loaded
778
+ }
779
  })
780
 
781
  except Exception as e:
 
786
  }, status=500)
787
 
788
 
789
+ # Par BlackBenAI: Initialiser les modèles au démarrage
790
+ def privacy_view(request):
791
+ """Vue pour la politique de confidentialité"""
792
+ return render(request, 'detection/privacy.html')
793
+
794
+ def terms_view(request):
795
+ """Vue pour les conditions d'utilisation"""
796
+ return render(request, 'detection/terms.html')
797
+
798
  # Cette fonction sera appelée quand Django démarre
799
  def initialize_models():
800
  """
 
803
  try:
804
  load_yolo_models()
805
  except Exception as e:
806
+ logger.error(f"Erreur lors de l'initialisation des modèles: {e}")
807
 
808
 
809
+ # Par BlackBenAI: Appeler l'initialisation au chargement du module
 
810
  initialize_models()
811