Add application file
Browse files- .dockerignore +29 -0
- .gitignore +1 -1
- Dockerfile +21 -20
- detection/admin.py +138 -31
- detection/templates/detection/index.html +635 -533
- detection/templates/detection/privacy.html +59 -0
- detection/templates/detection/terms.html +57 -0
- detection/urls.py +5 -0
- detection/views.py +424 -243
.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 |
-
|
| 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 |
-
#
|
| 3 |
|
| 4 |
FROM python:3.11-slim
|
| 5 |
|
| 6 |
# Métadonnées
|
| 7 |
-
LABEL maintainer="
|
| 8 |
LABEL description="FireWatch AI - Système de détection d'incendie et d'intrusion"
|
| 9 |
-
LABEL version="1.
|
| 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
|
| 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 |
-
#
|
| 43 |
-
|
| 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 |
-
#
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
# Commande de santé
|
| 60 |
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
| 61 |
-
CMD curl -f http://localhost:
|
| 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:
|
| 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 = ('
|
| 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 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
fieldsets = (
|
| 61 |
('Informations de session', {
|
| 62 |
'fields': ('session_id', 'detection_type', 'created_at')
|
| 63 |
}),
|
| 64 |
-
('
|
| 65 |
-
'fields': ('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}),
|
| 67 |
-
('
|
| 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 = ('
|
| 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 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
def bbox_preview(self, obj):
|
| 88 |
-
"""Aperçu de la bounding box"""
|
| 89 |
-
return f"
|
| 90 |
-
bbox_preview.short_description = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
('
|
| 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 = "
|
| 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
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 20 |
body {
|
| 21 |
-
|
| 22 |
-
|
|
|
|
| 23 |
}
|
| 24 |
-
|
| 25 |
-
.
|
| 26 |
-
background:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 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 |
-
.
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
}
|
| 46 |
-
|
| 47 |
-
.
|
| 48 |
-
|
| 49 |
-
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
-
.
|
| 53 |
-
|
| 54 |
}
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
color: #
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
</style>
|
| 83 |
</head>
|
| 84 |
-
<body class="
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
</div>
|
| 93 |
-
<
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
</
|
| 99 |
-
<
|
| 100 |
-
<i class="fas fa-bars"></i>
|
| 101 |
-
</button>
|
| 102 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
</div>
|
| 104 |
-
</
|
| 105 |
|
| 106 |
<!-- Hero Section -->
|
| 107 |
-
<section class="
|
| 108 |
-
<div class="
|
| 109 |
-
<div class="
|
| 110 |
-
<
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
</div>
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</div>
|
| 124 |
</div>
|
| 125 |
</section>
|
| 126 |
|
| 127 |
-
<!--
|
| 128 |
-
<section id="
|
| 129 |
-
<div class="
|
| 130 |
-
|
| 131 |
-
<div class="
|
| 132 |
-
<div class="
|
| 133 |
-
<
|
| 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-
|
| 140 |
-
<
|
| 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 |
-
<
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
</div>
|
| 154 |
</div>
|
| 155 |
</section>
|
| 156 |
|
| 157 |
-
<!-- Demo Section -->
|
| 158 |
-
<section id="demo" class="py-
|
| 159 |
-
<div class="
|
| 160 |
-
<
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
</button>
|
| 168 |
-
<button onclick="switchTab('video')" class="
|
| 169 |
-
<
|
|
|
|
|
|
|
|
|
|
| 170 |
</button>
|
| 171 |
-
<button onclick="switchTab('camera')" class="
|
| 172 |
-
<
|
|
|
|
|
|
|
|
|
|
| 173 |
</button>
|
| 174 |
</div>
|
| 175 |
-
|
| 176 |
-
<!-- Tab
|
| 177 |
-
<div class="p-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
| 181 |
{% csrf_token %}
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
<
|
| 186 |
-
<
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
</div>
|
| 193 |
</div>
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
<
|
| 199 |
-
<
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
</form>
|
| 207 |
</div>
|
| 208 |
-
|
| 209 |
-
<!--
|
| 210 |
-
<div id="video
|
| 211 |
-
<form id="video-form"
|
| 212 |
{% csrf_token %}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
<
|
| 217 |
-
<
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
</div>
|
| 227 |
</div>
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
</form>
|
| 241 |
</div>
|
| 242 |
-
|
| 243 |
-
<!--
|
| 244 |
-
<div id="camera
|
| 245 |
-
<div class="
|
| 246 |
-
<
|
| 247 |
-
<
|
| 248 |
-
|
| 249 |
-
<
|
| 250 |
-
<i class="fas fa-
|
| 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 |
-
|
| 258 |
-
|
| 259 |
-
|
|
|
|
| 260 |
</button>
|
| 261 |
-
<button id="capture-frame" class="bg-
|
| 262 |
-
<i class="fas fa-camera
|
|
|
|
|
|
|
|
|
|
| 263 |
</button>
|
| 264 |
</div>
|
| 265 |
</div>
|
|
|
|
| 266 |
</div>
|
| 267 |
</div>
|
| 268 |
-
|
| 269 |
<!-- Results Section -->
|
| 270 |
-
<div id="results" class="
|
| 271 |
-
<div class="p-
|
| 272 |
-
<h3 class="text-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
| 278 |
</div>
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
</ul>
|
| 285 |
</div>
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
| 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="
|
| 312 |
-
<div class="
|
| 313 |
-
<
|
| 314 |
-
|
| 315 |
-
|
| 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:
|
| 321 |
-
<
|
| 322 |
-
<p class="text-
|
| 323 |
-
<p class="
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
<a href="#" class="text-gray-
|
| 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-
|
| 343 |
-
<div class="
|
| 344 |
-
<
|
| 345 |
-
|
| 346 |
-
<
|
| 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="
|
| 373 |
-
<div class="
|
| 374 |
-
<
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 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>© 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 |
-
//
|
| 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 |
-
|
| 414 |
-
|
| 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 |
-
//
|
| 438 |
function switchTab(tabName) {
|
| 439 |
-
// Hide all
|
| 440 |
-
document.querySelectorAll('.tab-content').forEach(
|
| 441 |
-
|
| 442 |
-
});
|
| 443 |
|
| 444 |
-
//
|
| 445 |
-
document.querySelectorAll('
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 464 |
-
|
| 465 |
-
|
| 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
|
| 473 |
const reader = new FileReader();
|
| 474 |
reader.onload = function(e) {
|
| 475 |
-
document.getElementById('preview
|
| 476 |
-
document.getElementById('image-preview').classList.remove('hidden');
|
| 477 |
-
}
|
| 478 |
reader.readAsDataURL(file);
|
| 479 |
}
|
| 480 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
|
| 482 |
-
|
|
|
|
| 483 |
const file = e.target.files[0];
|
| 484 |
-
if
|
| 485 |
const url = URL.createObjectURL(file);
|
| 486 |
-
document.getElementById('preview
|
| 487 |
-
document.getElementById('video-preview').classList.remove('hidden');
|
| 488 |
}
|
| 489 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
-
//
|
| 492 |
document.getElementById('image-form').addEventListener('submit', function(e) {
|
| 493 |
e.preventDefault();
|
| 494 |
-
|
| 495 |
const formData = new FormData(this);
|
| 496 |
-
const
|
|
|
|
| 497 |
|
| 498 |
-
|
| 499 |
|
| 500 |
fetch("{% url 'detection:analyze_image' %}", {
|
| 501 |
method: 'POST',
|
| 502 |
body: formData,
|
| 503 |
-
headers: {
|
| 504 |
-
'X-CSRFToken': getCSRFToken()
|
| 505 |
-
}
|
| 506 |
})
|
| 507 |
-
.then(
|
| 508 |
.then(data => {
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 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(
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
});
|
| 523 |
});
|
| 524 |
|
| 525 |
-
//
|
| 526 |
document.getElementById('video-form').addEventListener('submit', function(e) {
|
| 527 |
e.preventDefault();
|
| 528 |
-
|
| 529 |
const formData = new FormData(this);
|
| 530 |
-
const
|
|
|
|
| 531 |
|
| 532 |
-
|
| 533 |
|
| 534 |
fetch("{% url 'detection:analyze_video' %}", {
|
| 535 |
method: 'POST',
|
| 536 |
body: formData,
|
| 537 |
-
headers: {
|
| 538 |
-
'X-CSRFToken': getCSRFToken()
|
| 539 |
-
}
|
| 540 |
})
|
| 541 |
-
.then(
|
| 542 |
.then(data => {
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 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(
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
});
|
| 557 |
});
|
| 558 |
|
| 559 |
-
//
|
| 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(
|
| 573 |
.then(data => {
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
this.reset();
|
| 577 |
} else {
|
| 578 |
-
|
|
|
|
| 579 |
}
|
|
|
|
|
|
|
|
|
|
| 580 |
})
|
| 581 |
-
.catch(
|
| 582 |
-
|
| 583 |
-
|
|
|
|
| 584 |
});
|
| 585 |
});
|
| 586 |
|
| 587 |
-
//
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 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',
|
| 651 |
-
if (
|
| 652 |
-
|
| 653 |
-
|
| 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('
|
| 660 |
});
|
| 661 |
|
| 662 |
-
document.getElementById('capture-frame').addEventListener('click',
|
| 663 |
-
if
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
const ctx = cameraCanvas.getContext('2d');
|
| 669 |
-
ctx.drawImage(cameraVideo, 0, 0);
|
| 670 |
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
|
|
|
| 675 |
|
| 676 |
fetch("{% url 'detection:analyze_image' %}", {
|
| 677 |
method: 'POST',
|
| 678 |
-
body:
|
| 679 |
-
headers: {
|
| 680 |
-
'X-CSRFToken': getCSRFToken()
|
| 681 |
-
}
|
| 682 |
})
|
| 683 |
-
.then(
|
| 684 |
.then(data => {
|
| 685 |
-
if
|
| 686 |
-
|
| 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'
|
| 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">© 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 |
+
← 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 |
+
© 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 |
+
← 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 |
+
© 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
|
| 27 |
# Ces variables seront initialisées au démarrage du serveur
|
| 28 |
fire_model = None
|
| 29 |
intrusion_model = None
|
|
|
|
| 30 |
|
| 31 |
-
|
|
|
|
| 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
|
| 41 |
-
|
| 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 |
-
#
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
'
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
except Exception as e:
|
| 107 |
-
logger.error(f"Erreur lors du chargement des modèles: {e}")
|
| 108 |
|
| 109 |
|
| 110 |
-
# Par
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
#
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
#
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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!
|
| 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
|
| 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 |
-
#
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
#
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 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 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
|