export video/image
This commit is contained in:
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-05-14 17:29+0200\n"
|
"POT-Creation-Date: 2026-05-15 15:08+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -80,7 +80,7 @@ msgstr "Prerequisite"
|
|||||||
#: home/templates/docs/doc_database.html:363
|
#: home/templates/docs/doc_database.html:363
|
||||||
#: home/templates/docs/doc_database.html:366
|
#: home/templates/docs/doc_database.html:366
|
||||||
#: home/templates/docs/doc_database.html:367
|
#: home/templates/docs/doc_database.html:367
|
||||||
#: scanner/templates/scanner/base.html:132 scanner/views.py:131
|
#: scanner/templates/scanner/base.html:132 scanner/views.py:133
|
||||||
msgid "Calibration"
|
msgid "Calibration"
|
||||||
msgstr "Calibration"
|
msgstr "Calibration"
|
||||||
|
|
||||||
@@ -409,8 +409,7 @@ msgid "Tester le circuit"
|
|||||||
msgstr "Test circuit"
|
msgstr "Test circuit"
|
||||||
|
|
||||||
#: home/templates/docs/doc_calibration.html:420
|
#: home/templates/docs/doc_calibration.html:420
|
||||||
msgid ""
|
msgid "Vérifier les déplacements en parcourant automatiquement tous les puits."
|
||||||
"Vérifier les déplacements en parcourant automatiquement tous les puits."
|
|
||||||
msgstr "Check movements by automatically browsing all wells."
|
msgstr "Check movements by automatically browsing all wells."
|
||||||
|
|
||||||
#: home/templates/docs/doc_calibration.html:431
|
#: home/templates/docs/doc_calibration.html:431
|
||||||
@@ -803,8 +802,8 @@ msgid ""
|
|||||||
"Sessions, expériences, configurations et import CSV — interface "
|
"Sessions, expériences, configurations et import CSV — interface "
|
||||||
"Administration Django"
|
"Administration Django"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Sessions, experiments, configurations and import CSV — Django Administration"
|
"Sessions, experiments, configurations and import CSV — Django Administration "
|
||||||
" interface"
|
"interface"
|
||||||
|
|
||||||
#: home/templates/docs/doc_experiment.html:32
|
#: home/templates/docs/doc_experiment.html:32
|
||||||
#: home/templates/docs/doc_experiment.html:86
|
#: home/templates/docs/doc_experiment.html:86
|
||||||
@@ -847,8 +846,7 @@ msgstr "CSV import"
|
|||||||
|
|
||||||
#: home/templates/docs/doc_experiment.html:55
|
#: home/templates/docs/doc_experiment.html:55
|
||||||
msgid ""
|
msgid ""
|
||||||
"C'est l'interface Administration Django qui est chargée de créer les "
|
"C'est l'interface Administration Django qui est chargée de créer les données."
|
||||||
"données."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"The Django Administration interface is responsible for creating the data."
|
"The Django Administration interface is responsible for creating the data."
|
||||||
|
|
||||||
@@ -947,8 +945,8 @@ msgstr "Date of export"
|
|||||||
|
|
||||||
#: home/templates/docs/doc_experiment.html:169
|
#: home/templates/docs/doc_experiment.html:169
|
||||||
msgid ""
|
msgid ""
|
||||||
"Crée une tâche différée pour exporter automatiquement les images et vidéos à"
|
"Crée une tâche différée pour exporter automatiquement les images et vidéos à "
|
||||||
" la date choisie."
|
"la date choisie."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Creates a delayed task to automatically export images and videos to the "
|
"Creates a delayed task to automatically export images and videos to the "
|
||||||
"selected date."
|
"selected date."
|
||||||
@@ -996,8 +994,7 @@ msgid "Par défaut, tous les puits sont actifs."
|
|||||||
msgstr "By default, all wells are active."
|
msgstr "By default, all wells are active."
|
||||||
|
|
||||||
#: home/templates/docs/doc_experiment.html:255
|
#: home/templates/docs/doc_experiment.html:255
|
||||||
msgid ""
|
msgid "Il est possible d'ignorer le balayage d'un ou plusieurs puits au choix."
|
||||||
"Il est possible d'ignorer le balayage d'un ou plusieurs puits au choix."
|
|
||||||
msgstr "It is possible to ignore the scanning of one or more wells of choice."
|
msgstr "It is possible to ignore the scanning of one or more wells of choice."
|
||||||
|
|
||||||
#: home/templates/docs/doc_experiment.html:265
|
#: home/templates/docs/doc_experiment.html:265
|
||||||
@@ -1630,7 +1627,7 @@ msgstr "Back home"
|
|||||||
msgid "Ensemble d'expériences"
|
msgid "Ensemble d'expériences"
|
||||||
msgstr "Set of experiments"
|
msgstr "Set of experiments"
|
||||||
|
|
||||||
#: planarian/templates/planarian/base.html:30
|
#: planarian/templates/planarian/base.html:27
|
||||||
msgid "Exporter les metrics de l'expérience"
|
msgid "Exporter les metrics de l'expérience"
|
||||||
msgstr "Export the metrics of the experience"
|
msgstr "Export the metrics of the experience"
|
||||||
|
|
||||||
@@ -2450,6 +2447,14 @@ msgstr "Scanning started..."
|
|||||||
msgid "Erreur lors du démarrage du balayage..."
|
msgid "Erreur lors du démarrage du balayage..."
|
||||||
msgstr "Error starting scanning..."
|
msgstr "Error starting scanning..."
|
||||||
|
|
||||||
|
#: scanner/tasks.py:21
|
||||||
|
msgid "Balayage démarré."
|
||||||
|
msgstr "Scanning started..."
|
||||||
|
|
||||||
|
#: scanner/tasks.py:27
|
||||||
|
msgid "Replay démarré."
|
||||||
|
msgstr "Replay started..."
|
||||||
|
|
||||||
#: scanner/templates/scanner/base.html:5
|
#: scanner/templates/scanner/base.html:5
|
||||||
msgid "Barre de côté"
|
msgid "Barre de côté"
|
||||||
msgstr "Side bar"
|
msgstr "Side bar"
|
||||||
@@ -2548,7 +2553,7 @@ msgstr "Creation of experience sessions"
|
|||||||
msgid "Gestions des expériences"
|
msgid "Gestions des expériences"
|
||||||
msgstr "Experience management"
|
msgstr "Experience management"
|
||||||
|
|
||||||
#: scanner/templates/scanner/base.html:137 scanner/views.py:121
|
#: scanner/templates/scanner/base.html:137 scanner/views.py:123
|
||||||
msgid "Balayage multi-puits"
|
msgid "Balayage multi-puits"
|
||||||
msgstr "Multi-well sweeping"
|
msgstr "Multi-well sweeping"
|
||||||
|
|
||||||
@@ -2560,7 +2565,7 @@ msgstr "Results"
|
|||||||
msgid "Export CSV des expériences"
|
msgid "Export CSV des expériences"
|
||||||
msgstr "CSV export of experiments"
|
msgstr "CSV export of experiments"
|
||||||
|
|
||||||
#: scanner/templates/scanner/base.html:146 scanner/views.py:185
|
#: scanner/templates/scanner/base.html:146 scanner/views.py:187
|
||||||
msgid "Gestionnaire d'images"
|
msgid "Gestionnaire d'images"
|
||||||
msgstr "Image Manager"
|
msgstr "Image Manager"
|
||||||
|
|
||||||
@@ -2719,22 +2724,34 @@ msgid "Tester le cirduit"
|
|||||||
msgstr "Test for circumference"
|
msgstr "Test for circumference"
|
||||||
|
|
||||||
#: scanner/templates/scanner/calibration.html:207
|
#: scanner/templates/scanner/calibration.html:207
|
||||||
#: scanner/templates/scanner/scanning.html:58
|
#: scanner/templates/scanner/scanning.html:59
|
||||||
msgid "ARRET"
|
msgid "ARRET"
|
||||||
msgstr "JUDGMENT"
|
msgstr "JUDGMENT"
|
||||||
|
|
||||||
#: scanner/templates/scanner/images.html:30
|
#: scanner/templates/scanner/images.html:27
|
||||||
msgid "Exporter l'ensemble des images"
|
msgid "Exporter l'ensemble des images de la session"
|
||||||
msgstr "Export All Images"
|
msgstr "Export All Images for that session"
|
||||||
|
|
||||||
#: scanner/templates/scanner/images.html:48
|
#: scanner/templates/scanner/images.html:45
|
||||||
msgid "Télécharger"
|
msgid "Télécharger"
|
||||||
msgstr "Download"
|
msgstr "Download"
|
||||||
|
|
||||||
#: scanner/templates/scanner/replay.html:32
|
#: scanner/templates/scanner/images.html:65
|
||||||
|
msgid "Confirmer l'export des images."
|
||||||
|
msgstr "Export must be confirmed."
|
||||||
|
|
||||||
|
#: scanner/templates/scanner/images.html:66
|
||||||
|
msgid "ATTENTION le processus peut être long!"
|
||||||
|
msgstr "BEWARE The procedure can be lengthy."
|
||||||
|
|
||||||
|
#: scanner/templates/scanner/replay.html:29
|
||||||
msgid "Exporter l'ensemble des vidéos"
|
msgid "Exporter l'ensemble des vidéos"
|
||||||
msgstr "Export all videos"
|
msgstr "Export all videos"
|
||||||
|
|
||||||
|
#: scanner/templates/scanner/replay.html:78
|
||||||
|
msgid "Confirmer l'export des vidéos. ATTENTION le processus peut être long!."
|
||||||
|
msgstr "Confirm videos exportation. BEWARE the procedure can be lengthy."
|
||||||
|
|
||||||
#: scanner/templates/scanner/scanning.html:14
|
#: scanner/templates/scanner/scanning.html:14
|
||||||
msgid "Choix de la session"
|
msgid "Choix de la session"
|
||||||
msgstr "Selection of session"
|
msgstr "Selection of session"
|
||||||
@@ -2756,26 +2773,26 @@ msgstr "scanning"
|
|||||||
msgid "Simuler"
|
msgid "Simuler"
|
||||||
msgstr "Simulate"
|
msgstr "Simulate"
|
||||||
|
|
||||||
#: scanner/views.py:109
|
#: scanner/views.py:111
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr "Documentation"
|
msgstr "Documentation"
|
||||||
|
|
||||||
#: scanner/views.py:230
|
#: scanner/views.py:222 scanner/views.py:273
|
||||||
|
msgid "Erreur d'exportation"
|
||||||
|
msgstr "Export error"
|
||||||
|
|
||||||
|
#: scanner/views.py:246
|
||||||
msgid "Gestionnaire de vidéos"
|
msgid "Gestionnaire de vidéos"
|
||||||
msgstr "Video Manager"
|
msgstr "Video Manager"
|
||||||
|
|
||||||
#: scanner/views.py:252
|
#: scanner/views.py:268
|
||||||
msgid "Images téléchargées"
|
msgid "Images téléchargées"
|
||||||
msgstr "Downloaded images"
|
msgstr "Downloaded images"
|
||||||
|
|
||||||
#: scanner/views.py:255
|
#: scanner/views.py:271
|
||||||
msgid "Vidéos téléchargées"
|
msgid "Vidéos téléchargées"
|
||||||
msgstr "Videos downloaded"
|
msgstr "Videos downloaded"
|
||||||
|
|
||||||
#: scanner/views.py:257
|
|
||||||
msgid "Erreur d'exportation"
|
|
||||||
msgstr "Export error"
|
|
||||||
|
|
||||||
#: templates/admin/base_site.html:4
|
#: templates/admin/base_site.html:4
|
||||||
msgid "Django site admin"
|
msgid "Django site admin"
|
||||||
msgstr "Django website admin"
|
msgstr "Django website admin"
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ from .models import ExperimentConfig
|
|||||||
class ExperimentConfigAdmin(admin.ModelAdmin):
|
class ExperimentConfigAdmin(admin.ModelAdmin):
|
||||||
"""Admin Django pour les configurations d'expérience."""
|
"""Admin Django pour les configurations d'expérience."""
|
||||||
readonly_fields = ('experiment', 'px_per_mm', 'fps', 'well_radius_mm',)
|
readonly_fields = ('experiment', 'px_per_mm', 'fps', 'well_radius_mm',)
|
||||||
list_display = ("experiment_key", "well", "active", "px_per_mm", "fps",
|
list_display = ("experiment_key", "well", "active", "planarian_count", "px_per_mm", "fps",
|
||||||
"thresh_immobile", "thresh_mobile",
|
"thresh_immobile", "thresh_mobile",
|
||||||
"photo_mode", "chemo_strength", "created_at", )
|
"photo_mode", "chemo_strength", "created_at", )
|
||||||
|
|
||||||
list_filter = ("experiment_key", "photo_mode", "tube_axis")
|
list_filter = ("experiment_key__session_experiments__session", "experiment_key", "photo_mode", "thigmotaxis_wall_dist_mm", "planarian_count")
|
||||||
search_fields = ("experiment_key", "well_name", "description")
|
search_fields = ("experiment_key", "well_name", "description")
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="w3-margin-left w3-margin-bottom">
|
<div class="w3-margin-left w3-margin-bottom">
|
||||||
{% for ss in experiments %}
|
{% include "scanner/experiment-inc.html" %}
|
||||||
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
|
|
||||||
<label>{{ ss }}</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% block export_csv %}
|
{% block export_csv %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import posix_ipc
|
|||||||
import mmap
|
import mmap
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from django.http import JsonResponse, HttpResponse
|
#from django.http import JsonResponse, HttpResponse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from reduct.time import unix_timestamp_to_iso
|
from reduct.time import unix_timestamp_to_iso
|
||||||
|
|
||||||
@@ -38,12 +38,7 @@ async def remove_video_by_uuid(uuid, start_ts=None, end_ts=None, when=None):
|
|||||||
|
|
||||||
|
|
||||||
async def remove_video(uuid, start_ts, end_ts, when=None):
|
async def remove_video(uuid, start_ts, end_ts, when=None):
|
||||||
try:
|
|
||||||
await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
|
await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
|
||||||
return JsonResponse({'state': 'ok'}, status=200)
|
|
||||||
except Exception as e:
|
|
||||||
return JsonResponse({'error': str(e)}, status=500)
|
|
||||||
|
|
||||||
|
|
||||||
async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
|
async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
|
||||||
try:
|
try:
|
||||||
@@ -79,7 +74,8 @@ async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc
|
|||||||
total += 1
|
total += 1
|
||||||
|
|
||||||
if not frame_sizes:
|
if not frame_sizes:
|
||||||
return JsonResponse({'error': 'Aucune frame trouvée'}, status=404)
|
raise Exception("No frame found!")
|
||||||
|
#return JsonResponse({'error': 'Aucune frame trouvée'}, status=404)
|
||||||
|
|
||||||
video_path = os.path.join(settings.MEDIA_ROOT, f"output.{opencv_video_type}")
|
video_path = os.path.join(settings.MEDIA_ROOT, f"output.{opencv_video_type}")
|
||||||
fourcc = cv2.VideoWriter_fourcc(* opencv_fourcc_format)
|
fourcc = cv2.VideoWriter_fourcc(* opencv_fourcc_format)
|
||||||
@@ -97,36 +93,16 @@ async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc
|
|||||||
video.write(frame)
|
video.write(frame)
|
||||||
current_offset += size
|
current_offset += size
|
||||||
|
|
||||||
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
|
progress_bar(i + 1, total, prefix=f'Progress {uuid}:', suffix='Ended', length=30)
|
||||||
i+=1
|
i+=1
|
||||||
|
|
||||||
video.release()
|
video.release()
|
||||||
|
|
||||||
# Nettoie la mémoire partagée
|
# Nettoie la mémoire partagée
|
||||||
shm.unlink()
|
shm.unlink()
|
||||||
|
|
||||||
# Vérifier que le fichier existe
|
return { "status": 404, "success": True, "video_path": video_path}
|
||||||
if not os.path.exists(video_path):
|
|
||||||
logger.error(f"Fichier non créé: {video_path}")
|
|
||||||
return JsonResponse({'error': 'Erreur création vidéo'}, status=500)
|
|
||||||
|
|
||||||
# Lit le fichier vidéo généré
|
|
||||||
with open(video_path, 'rb') as f:
|
|
||||||
video_bytes = f.read()
|
|
||||||
|
|
||||||
# Retourne la vidéo en réponse
|
|
||||||
response = HttpResponse(video_bytes, content_type='video/mp4')
|
|
||||||
response['Content-Disposition'] = f'attachment; filename="{video_path}"'
|
|
||||||
response['Content-Length'] = os.path.getsize(video_path)
|
|
||||||
|
|
||||||
|
|
||||||
# Supprime le fichier temporaire
|
|
||||||
os.remove(video_path)
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"shm_download_video: {e}")
|
return {"status": 500, "success": False, "video_path": video_path, "error": str(e)}
|
||||||
return JsonResponse({'error': str(e)}, status=500)
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
##
|
##
|
||||||
@@ -247,7 +223,7 @@ async def export_images_zip(
|
|||||||
total += 1
|
total += 1
|
||||||
|
|
||||||
if not frame_sizes:
|
if not frame_sizes:
|
||||||
return {"status": "error", "message": "Aucune frame trouvée"}
|
return {"status": "error", "message": "No frame found!..."}
|
||||||
|
|
||||||
# --- Génération du ZIP ---
|
# --- Génération du ZIP ---
|
||||||
max_zip_bytes = max_zip_size_mb * 1024 * 1024 if max_zip_size_mb > 0 else 0
|
max_zip_bytes = max_zip_size_mb * 1024 * 1024 if max_zip_size_mb > 0 else 0
|
||||||
@@ -297,7 +273,7 @@ async def export_images_zip(
|
|||||||
zf.writestr(f"{uuid}_{ts_iso}.jpg", buf.tobytes())
|
zf.writestr(f"{uuid}_{ts_iso}.jpg", buf.tobytes())
|
||||||
written += 1
|
written += 1
|
||||||
|
|
||||||
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
|
progress_bar(i + 1, total, prefix=f'Progress {uuid}:', suffix='Ended', length=30)
|
||||||
i+=1
|
i+=1
|
||||||
current_offset += size
|
current_offset += size
|
||||||
|
|
||||||
@@ -402,7 +378,7 @@ async def export_video_mp4(
|
|||||||
total +=1
|
total +=1
|
||||||
|
|
||||||
if not frame_sizes:
|
if not frame_sizes:
|
||||||
return {"status": "error", "message": "Aucune frame trouvée"}
|
return {"status": "error", "message": "No frame found!..."}
|
||||||
|
|
||||||
|
|
||||||
# --- Génération du MP4 ---
|
# --- Génération du MP4 ---
|
||||||
@@ -458,14 +434,14 @@ async def export_video_mp4(
|
|||||||
written += 1
|
written += 1
|
||||||
current_offset += size
|
current_offset += size
|
||||||
|
|
||||||
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
|
progress_bar(i + 1, total, prefix=f'Progress {uuid}:', suffix='Ended', length=30)
|
||||||
i+=1
|
i+=1
|
||||||
|
|
||||||
if video:
|
if video:
|
||||||
video.release()
|
video.release()
|
||||||
|
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
return {"status": "error", "message": f"Fichier {opencv_video_type} non créé"}
|
return {"status": "error", "message": f"File {opencv_video_type} not created!..."}
|
||||||
|
|
||||||
## Copie vers les destinations (local + Samba)
|
## Copie vers les destinations (local + Samba)
|
||||||
filename = os.path.basename(video_path)
|
filename = os.path.basename(video_path)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ TUBE_AXIS_TYPE = [
|
|||||||
|
|
||||||
class Configuration(models.Model):
|
class Configuration(models.Model):
|
||||||
name = models.CharField(_("Nom de la Configuration"), help_text=_("Nom de la configuration"), max_length=100, null=True, blank=False, default=_("Configuration par défaut"))
|
name = models.CharField(_("Nom de la Configuration"), help_text=_("Nom de la configuration"), max_length=100, null=True, blank=False, default=_("Configuration par défaut"))
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
# Dashboard configuration
|
# Dashboard configuration
|
||||||
sidebar_width = models.CharField(_("Barre latérale"), help_text=_("Largeur barre latérale (css)"), max_length=32, null=True, blank=False, default="350px")
|
sidebar_width = models.CharField(_("Barre latérale"), help_text=_("Largeur barre latérale (css)"), max_length=32, null=True, blank=False, default="350px")
|
||||||
default_grid_columns = models.PositiveSmallIntegerField(_("Colonnes de la grille par défaut"), help_text=_("Nombre de colonnes de la grille par défaut"), blank=False, default=3)
|
default_grid_columns = models.PositiveSmallIntegerField(_("Colonnes de la grille par défaut"), help_text=_("Nombre de colonnes de la grille par défaut"), blank=False, default=3)
|
||||||
@@ -94,7 +94,7 @@ class Configuration(models.Model):
|
|||||||
return f'{self.name}'
|
return f'{self.name}'
|
||||||
|
|
||||||
class Well(models.Model):
|
class Well(models.Model):
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
name = models.CharField(_("Nom"), help_text=_("Nom du puit: Ai..Di"), unique=True, max_length=4, null=True, blank=True)
|
name = models.CharField(_("Nom"), help_text=_("Nom du puit: Ai..Di"), unique=True, max_length=4, null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -110,7 +110,7 @@ class Well(models.Model):
|
|||||||
class MultiWell(models.Model):
|
class MultiWell(models.Model):
|
||||||
# Identification
|
# Identification
|
||||||
label = models.CharField(_("Label"), help_text=_("Label du multi-puit"), max_length=100, null=True, blank=True)
|
label = models.CharField(_("Label"), help_text=_("Label du multi-puit"), max_length=100, null=True, blank=True)
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
position = models.CharField(_("Position"), help_text=_('Position du multi-puits sur la table'), unique=True, max_length=8, choices=MULTIWELL_POSITION, null=True, blank=False)
|
position = models.CharField(_("Position"), help_text=_('Position du multi-puits sur la table'), unique=True, max_length=8, choices=MULTIWELL_POSITION, null=True, blank=False)
|
||||||
default = models.BooleanField(_("Par défaut"), help_text=_('Multi-puit par défaut'), default=False)
|
default = models.BooleanField(_("Par défaut"), help_text=_('Multi-puit par défaut'), default=False)
|
||||||
# Configuration
|
# Configuration
|
||||||
@@ -175,7 +175,7 @@ class MultiWell(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class WellPosition(models.Model):
|
class WellPosition(models.Model):
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True)
|
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ class Experiment(models.Model):
|
|||||||
comment = models.TextField(_("Commentaires"), help_text=_("Descriptions de l'expérience"), null=True, blank=True)
|
comment = models.TextField(_("Commentaires"), help_text=_("Descriptions de l'expérience"), null=True, blank=True)
|
||||||
identifier = models.CharField(_("Identifiant d'expérience"), unique=True, max_length=100, null=True, blank=False )
|
identifier = models.CharField(_("Identifiant d'expérience"), unique=True, max_length=100, null=True, blank=False )
|
||||||
|
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée de la prise de vue en secondes'), blank=False, default=120)
|
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée de la prise de vue en secondes'), blank=False, default=120)
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ class Experiment(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.identifier = f'{self.id}-{self.multiwell.position}_{self.created.isoformat()[:19]}'
|
self.identifier = f'{self.multiwell.position}_{self.created.isoformat()[:19]}'
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ class Experiment(models.Model):
|
|||||||
verbose_name_plural = _("Expériences")
|
verbose_name_plural = _("Expériences")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.identifier}'
|
return f'{self.identifier} [ {self.title} ]'
|
||||||
|
|
||||||
|
|
||||||
class Session(models.Model):
|
class Session(models.Model):
|
||||||
@@ -276,7 +276,7 @@ class Session(models.Model):
|
|||||||
ERROR = "error", _("Erreur")
|
ERROR = "error", _("Erreur")
|
||||||
|
|
||||||
name = models.CharField(_("Nom de la session"), help_text=_("Session d'expérience. 4 Multi-puits maximum"), max_length=100, null=True, blank=False)
|
name = models.CharField(_("Nom de la session"), help_text=_("Session d'expérience. 4 Multi-puits maximum"), max_length=100, null=True, blank=False)
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
active = models.BooleanField(_("Active"), default=True)
|
active = models.BooleanField(_("Active"), default=True)
|
||||||
expected_export = models.DateTimeField(_("Date d'exportation"), help_text=_("Date d'exportation prévue"), null=True, blank=True)
|
expected_export = models.DateTimeField(_("Date d'exportation"), help_text=_("Date d'exportation prévue"), null=True, blank=True)
|
||||||
expected_scanning = models.DateTimeField(_("Date du balayage"), help_text=_("Date du balayage prévue"), null=True, blank=True)
|
expected_scanning = models.DateTimeField(_("Date du balayage"), help_text=_("Date du balayage prévue"), null=True, blank=True)
|
||||||
@@ -313,7 +313,7 @@ class Session(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
state = _("Terminée") if not self.active else _("Active")
|
state = _("Terminée") if not self.active else _("Active")
|
||||||
return f'{self.id}: {self.name} {state}'
|
return f'[ {self.id} ] {self.name} ({state})'
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Session)
|
@receiver(post_save, sender=Session)
|
||||||
@@ -376,9 +376,9 @@ def delete_periodic_task(sender, instance, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class SessionExperiment(models.Model):
|
class SessionExperiment(models.Model):
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.SET_NULL, null=True, blank=True)
|
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.CASCADE, null=True, blank=True)
|
||||||
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.SET_NULL, null=True, blank=True, related_name="session_experiments")
|
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="session_experiments")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def experiment_by_session(cls, session_id, active=True):
|
def experiment_by_session(cls, session_id, active=True):
|
||||||
@@ -407,8 +407,8 @@ class SessionExperiment(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ExperimentWell(models.Model):
|
class ExperimentWell(models.Model):
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
|
||||||
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.SET_NULL, null=True, blank=True, related_name="experimentwell")
|
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="experimentwell")
|
||||||
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True, related_name="wellexperiment")
|
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True, related_name="wellexperiment")
|
||||||
active = models.BooleanField(_("Active"), default=True)
|
active = models.BooleanField(_("Active"), default=True)
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class MultiWellManager:
|
|||||||
multiwell = experiment.multiwell
|
multiwell = experiment.multiwell
|
||||||
|
|
||||||
if self.process.use_tracking:
|
if self.process.use_tracking:
|
||||||
cfg = ExperimentConfig.objects.filter(experiment_id=experiment.id, well_id=well.id).first()
|
cfg = ExperimentConfig.objects.filter(experiment_key_id=experiment.id, well=well.name).first()
|
||||||
if not cfg:
|
if not cfg:
|
||||||
raise Exception(f"Configuration d'expérience introuvable pour {experiment} / {well}")
|
raise Exception(f"Configuration d'expérience introuvable pour {experiment} / {well}")
|
||||||
# reset PlanarianTracker => on_well_change
|
# reset PlanarianTracker => on_well_change
|
||||||
@@ -166,7 +166,9 @@ class MultiWellManager:
|
|||||||
self.process.data.record = True
|
self.process.data.record = True
|
||||||
self.process._send(current=well_position.order)
|
self.process._send(current=well_position.order)
|
||||||
|
|
||||||
logger.info(f"Starting capture for {uuid} ordre: {well_position.order}")
|
msg = f"Starting capture for {uuid} ordre: {well_position.order}"
|
||||||
|
logger.info(msg)
|
||||||
|
self.process._send(well_state=msg)
|
||||||
|
|
||||||
start = time.monotonic()
|
start = time.monotonic()
|
||||||
while not self.stop_playing.is_set():
|
while not self.stop_playing.is_set():
|
||||||
@@ -249,7 +251,9 @@ class MultiWellManager:
|
|||||||
started = timezone.now()
|
started = timezone.now()
|
||||||
for obs in experiments:
|
for obs in experiments:
|
||||||
|
|
||||||
logger.warning(f"Starting scan for {obs} (well {pos}/{len(experiments)})")
|
msg = f"Starting scan for {obs} (well {pos}/{len(experiments)})"
|
||||||
|
logger.warning(msg)
|
||||||
|
self.process._send(well_state=msg)
|
||||||
|
|
||||||
if self.stop_playing.is_set():
|
if self.stop_playing.is_set():
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class ScannerManager {
|
|||||||
this.scan_state = options.scan_state;
|
this.scan_state = options.scan_state;
|
||||||
this.sim_bt = options.simulate || null;
|
this.sim_bt = options.simulate || null;
|
||||||
this.well_btn = options.well_btn;
|
this.well_btn = options.well_btn;
|
||||||
|
this.well_state = options.well_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
init_controls() {
|
init_controls() {
|
||||||
@@ -56,6 +57,7 @@ class ScannerManager {
|
|||||||
}
|
}
|
||||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||||
if (payload.scan_state) { this.scan_state.textContent=payload.scan_state;}
|
if (payload.scan_state) { this.scan_state.textContent=payload.scan_state;}
|
||||||
|
if (payload.well_state) { this.well_state.textContent=payload.well_state;}
|
||||||
|
|
||||||
if (payload.buttons) {
|
if (payload.buttons) {
|
||||||
this.well_btn.innerHTML = payload.buttons;
|
this.well_btn.innerHTML = payload.buttons;
|
||||||
@@ -67,8 +69,6 @@ class ScannerManager {
|
|||||||
btn.classList.remove('w3-green');
|
btn.classList.remove('w3-green');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch(e) { console.log(e); }
|
} catch(e) { console.log(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import requests
|
|||||||
from celery import shared_task, group, chord, chain
|
from celery import shared_task, group, chord, chain
|
||||||
from celery.utils.log import get_task_logger
|
from celery.utils.log import get_task_logger
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .process import ScannerProcess, ReplayProcess, redisDB
|
from .process import ScannerProcess, ReplayProcess, redisDB
|
||||||
from .export_tasks import shm_download_video, export_images_zip, export_video_mp4
|
from .export_tasks import shm_download_video, export_images_zip, export_video_mp4
|
||||||
from .constants import ScannerConstants
|
from .constants import ScannerConstants
|
||||||
@@ -18,20 +18,34 @@ logger = get_task_logger(__name__)
|
|||||||
def scanner_start():
|
def scanner_start():
|
||||||
scanner = ScannerProcess()
|
scanner = ScannerProcess()
|
||||||
scanner.start()
|
scanner.start()
|
||||||
return f"Scanner démarré."
|
return str(_("Scanner démarré."))
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def replay_start():
|
def replay_start():
|
||||||
replay = ReplayProcess(latency=5.0)
|
replay = ReplayProcess(latency=5.0)
|
||||||
replay.start()
|
replay.start()
|
||||||
return f"Replay démarré."
|
return str(_("Replay démarré."))
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def download_video(uuid, start_ts, end_ts, frame_rate=10, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
|
@shared_task(bind=True)
|
||||||
|
def download_video(self, uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
|
||||||
|
"""
|
||||||
|
Exécuter directement la fonction async DANS la tâche Celery.
|
||||||
|
Sans passer par une wrapper.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return asyncio.run(shm_download_video(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type))
|
logger.info(f"Task {self.request.id}: Processing {uuid} from {start_ts} to {end_ts}")
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
result = loop.run_until_complete(
|
||||||
|
shm_download_video(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type) )
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"download_video: {e}")
|
logger.error(f"shm_download_video_task error: {e}", exc_info=True)
|
||||||
|
return { 'status': 'error', 'error': str(e), 'uuid': uuid}
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def export_images(
|
def export_images(
|
||||||
@@ -47,6 +61,7 @@ def export_images(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"export_images: {e}")
|
logger.error(f"export_images: {e}")
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def export_all_images(session_id=None):
|
def export_all_images(session_id=None):
|
||||||
try:
|
try:
|
||||||
@@ -74,11 +89,9 @@ def export_all_images(session_id=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"export_images: {e}")
|
logger.error(f"export_images: {e}")
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def export_videos(uuid: str,
|
def export_videos(uuid: str, start_ts: float | None = None, end_ts: float | None = None, frame_rate: int = 5,
|
||||||
start_ts: float | None = None,
|
|
||||||
end_ts: float | None = None,
|
|
||||||
frame_rate: int = 5,
|
|
||||||
opencv_fourcc_format='mp4v',
|
opencv_fourcc_format='mp4v',
|
||||||
opencv_video_type='mp4',
|
opencv_video_type='mp4',
|
||||||
max_video_size_mb: int = 0,
|
max_video_size_mb: int = 0,
|
||||||
@@ -88,6 +101,7 @@ def export_videos(uuid: str,
|
|||||||
return asyncio.run(export_video_mp4(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type, max_video_size_mb, max_width, max_height))
|
return asyncio.run(export_video_mp4(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type, max_video_size_mb, max_width, max_height))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"export_videos: {e}")
|
logger.error(f"export_videos: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% for ss in experiments %}
|
||||||
|
<div>
|
||||||
|
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
|
||||||
|
<label>{{ ss }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
@@ -16,18 +16,15 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="w3-margin-left w3-margin-bottom">
|
<div class="w3-margin-left w3-margin-bottom">
|
||||||
{% for ss in experiments %}
|
{% include "scanner/experiment-inc.html" %}
|
||||||
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
|
|
||||||
<label>{{ ss.multiwell }}</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if current_session.id and current_experiment %}
|
{% if current_session.id and current_experiment %}
|
||||||
{% multiwell_cards current_session.id current_experiment %}
|
{% multiwell_cards current_session.id current_experiment %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if current_session.id %}
|
{% if current_session.id %}
|
||||||
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_images()">
|
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_images({{ current_session.id }})">
|
||||||
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images" %}<br>
|
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images de la session" %}<br>
|
||||||
<span class="w3-margin-left">{{ export_destination }}
|
<span class="w3-margin-left">{{ export_destination }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -65,7 +62,8 @@
|
|||||||
const duration = parseInt("{{ duration }}");
|
const duration = parseInt("{{ duration }}");
|
||||||
const uuid_btn = sId("btn-" + "{{ uuid }}");
|
const uuid_btn = sId("btn-" + "{{ uuid }}");
|
||||||
const image_endpoint = "{% url 'scanner:export_api' %}";
|
const image_endpoint = "{% url 'scanner:export_api' %}";
|
||||||
const sid = parseInt("{{ cursid }}");
|
const export_msg = "{% trans "Confirmer l'export des images." %}";
|
||||||
|
const export_msg_long = "{% trans "ATTENTION le processus peut être long!" %}";
|
||||||
|
|
||||||
function setGridColumns(col) { container.style.setProperty("--grid-columns", col); updateActiveButton(col); }
|
function setGridColumns(col) { container.style.setProperty("--grid-columns", col); updateActiveButton(col); }
|
||||||
function handleMultiwell(b) { b.classList.add('w3-green'); }
|
function handleMultiwell(b) { b.classList.add('w3-green'); }
|
||||||
@@ -78,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function image_save(name, img) {
|
function image_save(name, img) {
|
||||||
const ok = confirm(`Télécharger le fichier ?\n\n${name}`);
|
const ok = confirm(export_msg);
|
||||||
if (!ok) return false;
|
if (!ok) return false;
|
||||||
fetch(img)
|
fetch(img)
|
||||||
.then(res => res.blob())
|
.then(res => res.blob())
|
||||||
@@ -94,7 +92,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function download_all_images() {
|
function download_all_images(sid) {
|
||||||
|
const ok = confirm(export_msg +' '+ export_msg_long);
|
||||||
|
if (!ok) return false;
|
||||||
fetch(image_endpoint, {
|
fetch(image_endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Erreur:', error);
|
console.error('Erreur:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
setGridColumns(columns);
|
setGridColumns(columns);
|
||||||
if (uuid_btn)
|
if (uuid_btn)
|
||||||
|
|||||||
@@ -17,10 +17,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="w3-margin-left w3-margin-bottom">
|
<div class="w3-margin-left w3-margin-bottom">
|
||||||
{% for ss in experiments %}
|
{% include "scanner/experiment-inc.html" %}
|
||||||
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
|
|
||||||
<label>{{ ss.multiwell }}</label>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_session.id and current_experiment %}
|
{% if current_session.id and current_experiment %}
|
||||||
@@ -28,7 +25,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% if current_session.id %}
|
{% if current_session.id %}
|
||||||
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_videos()">
|
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_videos({{ current_session.id }})">
|
||||||
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des vidéos" %}<br>
|
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des vidéos" %}<br>
|
||||||
<span class="w3-margin-left">{{ export_destination }}</span>
|
<span class="w3-margin-left">{{ export_destination }}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -78,9 +75,12 @@
|
|||||||
const ws_route = "{{ ws_route }}";
|
const ws_route = "{{ ws_route }}";
|
||||||
const uuid_btn = sId("btn-" + "{{ uuid }}");
|
const uuid_btn = sId("btn-" + "{{ uuid }}");
|
||||||
const image_endpoint = "{% url 'scanner:export_api' %}";
|
const image_endpoint = "{% url 'scanner:export_api' %}";
|
||||||
const sid = parseInt("{{ cursid }}");
|
const export_msg = "{% trans "Confirmer l'export des vidéos. ATTENTION le processus peut être long!" %}";
|
||||||
|
|
||||||
function download_all_videos() {
|
|
||||||
|
function download_all_videos(sid) {
|
||||||
|
const ok = confirm(export_msg);
|
||||||
|
if (!ok) return false;
|
||||||
fetch(image_endpoint, {
|
fetch(image_endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -51,7 +51,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="w3-col">
|
<div class="w3-col">
|
||||||
<div id="_well_btn" class="w3-margin-top"></div>
|
<div id="_well_state" class="w3-margin-top w3-small w3-text-amber w3-justify"></div>
|
||||||
|
<div id="_well_btn"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col">
|
<div class="w3-col">
|
||||||
<button id="_halt" class="w3-button w3-red w3-round-large w3-margin-top w3-block">
|
<button id="_halt" class="w3-button w3-red w3-round-large w3-margin-top w3-block">
|
||||||
@@ -63,8 +64,8 @@
|
|||||||
<div class="w3-margin-top w3-padding-small w3-small">
|
<div class="w3-margin-top w3-padding-small w3-small">
|
||||||
<span id="_ts"></span>
|
<span id="_ts"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-margin-top w3-small w3-center w3-justify">
|
<div class="w3-margin-top w3-small">
|
||||||
<span id="_scan_state" class="w3-text-amber w3-padding"></span>
|
<span id="_scan_state" class="w3-text-amber w3-padding w3-justify"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +101,9 @@
|
|||||||
frame_count : sId("_count"),
|
frame_count : sId("_count"),
|
||||||
scan_state : sId("_scan_state"),
|
scan_state : sId("_scan_state"),
|
||||||
simulate : sId("_simulate"),
|
simulate : sId("_simulate"),
|
||||||
well_btn : sId("_well_btn")
|
well_btn : sId("_well_btn"),
|
||||||
|
well_state : sId("_well_state")
|
||||||
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/scanner/js/scanning.js"></script>
|
<script src="/static/scanner/js/scanning.js"></script>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
#
|
#
|
||||||
|
import os
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
import base64, json
|
import base64, json
|
||||||
from django.shortcuts import render #, redirect
|
from django.shortcuts import render #, redirect
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse, FileResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_GET, require_POST
|
from django.views.decorators.http import require_GET, require_POST
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from reduct.time import unix_timestamp_to_iso
|
from reduct.time import unix_timestamp_to_iso
|
||||||
from modules.system_stats import get_cached_stats, start_background_updater
|
from modules.system_stats import get_cached_stats, start_background_updater
|
||||||
from modules import reductstore
|
from modules import reductstore
|
||||||
@@ -198,12 +200,27 @@ def images_view(request):
|
|||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def download_api(request):
|
def download_api(request):
|
||||||
data = json.loads(request.body.decode() or "{}")
|
data = json.loads(request.body.decode() or "{}")
|
||||||
|
try:
|
||||||
action = data.get("action")
|
action = data.get("action")
|
||||||
if action=='download':
|
if action=='download':
|
||||||
uuid, dt_start, dt_stop, frame_rate = data.get("uuid"), data.get("dt_start"), data.get("dt_stop"), data.get("fps")
|
uuid, dt_start, dt_stop, frame_rate = data.get("uuid"), data.get("dt_start"), data.get("dt_stop"), data.get("fps")
|
||||||
return download_video.delay(uuid, dt_start, dt_stop, frame_rate=frame_rate) # @UndefinedVariable
|
task = download_video.delay(uuid, dt_start, dt_stop, frame_rate=frame_rate) # @UndefinedVariable
|
||||||
else:
|
result = task.get(timeout=300)
|
||||||
return JsonResponse({"state": False})
|
|
||||||
|
if result.get('success'):
|
||||||
|
video_path = result.get('video_path')
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
raise Exception(f"File not created: {video_path}")
|
||||||
|
|
||||||
|
response = FileResponse(open(video_path, 'rb'), content_type='video/mp4')
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{video_path}"'
|
||||||
|
response['Content-Length'] = os.path.getsize(video_path)
|
||||||
|
os.remove(video_path)
|
||||||
|
return response
|
||||||
|
return JsonResponse(result)
|
||||||
|
except Exception as e:
|
||||||
|
print("download_api error", e)
|
||||||
|
return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), })
|
||||||
|
|
||||||
|
|
||||||
def get_video(uuid):
|
def get_video(uuid):
|
||||||
@@ -246,14 +263,13 @@ def export_api(request):
|
|||||||
data = json.loads(request.body.decode() or "{}")
|
data = json.loads(request.body.decode() or "{}")
|
||||||
session_id = data.get("sid")
|
session_id = data.get("sid")
|
||||||
action = data.get("action")
|
action = data.get("action")
|
||||||
|
try:
|
||||||
if action == 'export_images':
|
if action == 'export_images':
|
||||||
export_all_images(session_id)
|
export_all_images(session_id)
|
||||||
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
|
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
|
||||||
elif action == 'export_videos':
|
elif action == 'export_videos':
|
||||||
export_all_videos(session_id)
|
export_all_videos(session_id)
|
||||||
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
|
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
|
||||||
else:
|
except:
|
||||||
return JsonResponse({"success": False, "msg": str(_("Erreur d'exportation"))})
|
return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), })
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user