export video/image

This commit is contained in:
2026-05-15 16:14:26 +02:00
parent 4fb7fa8fd3
commit 7760d7ae7c
13 changed files with 183 additions and 149 deletions
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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:366
#: 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"
msgstr "Calibration"
@@ -409,8 +409,7 @@ msgid "Tester le circuit"
msgstr "Test circuit"
#: home/templates/docs/doc_calibration.html:420
msgid ""
"Vérifier les déplacements en parcourant automatiquement tous les puits."
msgid "Vérifier les déplacements en parcourant automatiquement tous les puits."
msgstr "Check movements by automatically browsing all wells."
#: home/templates/docs/doc_calibration.html:431
@@ -803,8 +802,8 @@ msgid ""
"Sessions, expériences, configurations et import CSV — interface "
"Administration Django"
msgstr ""
"Sessions, experiments, configurations and import CSV — Django Administration"
" interface"
"Sessions, experiments, configurations and import CSV — Django Administration "
"interface"
#: home/templates/docs/doc_experiment.html:32
#: home/templates/docs/doc_experiment.html:86
@@ -847,8 +846,7 @@ msgstr "CSV import"
#: home/templates/docs/doc_experiment.html:55
msgid ""
"C'est l'interface Administration Django qui est chargée de créer les "
"données."
"C'est l'interface Administration Django qui est chargée de créer les données."
msgstr ""
"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
msgid ""
"Crée une tâche différée pour exporter automatiquement les images et vidéos à"
" la date choisie."
"Crée une tâche différée pour exporter automatiquement les images et vidéos à "
"la date choisie."
msgstr ""
"Creates a delayed task to automatically export images and videos to the "
"selected date."
@@ -996,8 +994,7 @@ msgid "Par défaut, tous les puits sont actifs."
msgstr "By default, all wells are active."
#: home/templates/docs/doc_experiment.html:255
msgid ""
"Il est possible d'ignorer le balayage d'un ou plusieurs puits au choix."
msgid "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."
#: home/templates/docs/doc_experiment.html:265
@@ -1630,7 +1627,7 @@ msgstr "Back home"
msgid "Ensemble d'expériences"
msgstr "Set of experiments"
#: planarian/templates/planarian/base.html:30
#: planarian/templates/planarian/base.html:27
msgid "Exporter les metrics de l'expérience"
msgstr "Export the metrics of the experience"
@@ -2450,6 +2447,14 @@ msgstr "Scanning started..."
msgid "Erreur lors du démarrage du balayage..."
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
msgid "Barre de côté"
msgstr "Side bar"
@@ -2548,7 +2553,7 @@ msgstr "Creation of experience sessions"
msgid "Gestions des expériences"
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"
msgstr "Multi-well sweeping"
@@ -2560,7 +2565,7 @@ msgstr "Results"
msgid "Export CSV des expériences"
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"
msgstr "Image Manager"
@@ -2719,22 +2724,34 @@ msgid "Tester le cirduit"
msgstr "Test for circumference"
#: scanner/templates/scanner/calibration.html:207
#: scanner/templates/scanner/scanning.html:58
#: scanner/templates/scanner/scanning.html:59
msgid "ARRET"
msgstr "JUDGMENT"
#: scanner/templates/scanner/images.html:30
msgid "Exporter l'ensemble des images"
msgstr "Export All Images"
#: scanner/templates/scanner/images.html:27
msgid "Exporter l'ensemble des images de la session"
msgstr "Export All Images for that session"
#: scanner/templates/scanner/images.html:48
#: scanner/templates/scanner/images.html:45
msgid "Télécharger"
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"
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
msgid "Choix de la session"
msgstr "Selection of session"
@@ -2756,26 +2773,26 @@ msgstr "scanning"
msgid "Simuler"
msgstr "Simulate"
#: scanner/views.py:109
#: scanner/views.py:111
msgid "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"
msgstr "Video Manager"
#: scanner/views.py:252
#: scanner/views.py:268
msgid "Images téléchargées"
msgstr "Downloaded images"
#: scanner/views.py:255
#: scanner/views.py:271
msgid "Vidéos téléchargées"
msgstr "Videos downloaded"
#: scanner/views.py:257
msgid "Erreur d'exportation"
msgstr "Export error"
#: templates/admin/base_site.html:4
msgid "Django site admin"
msgstr "Django website admin"
+2 -2
View File
@@ -9,11 +9,11 @@ from .models import ExperimentConfig
class ExperimentConfigAdmin(admin.ModelAdmin):
"""Admin Django pour les configurations d'expérience."""
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",
"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")
ordering = ("-created_at",)
@@ -17,10 +17,7 @@
{% endfor %}
</select>
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<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 %}
{% include "scanner/experiment-inc.html" %}
</div>
</form>
{% block export_csv %}
+12 -36
View File
@@ -7,7 +7,7 @@ import posix_ipc
import mmap
import cv2
import numpy as np
from django.http import JsonResponse, HttpResponse
#from django.http import JsonResponse, HttpResponse
from django.conf import settings
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):
try:
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)
await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
try:
@@ -79,7 +74,8 @@ async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc
total += 1
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}")
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)
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
video.release()
# Nettoie la mémoire partagée
shm.unlink()
# Vérifier que le fichier existe
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
return { "status": 404, "success": True, "video_path": video_path}
except Exception as e:
logger.error(f"shm_download_video: {e}")
return JsonResponse({'error': str(e)}, status=500)
return {"status": 500, "success": False, "video_path": video_path, "error": str(e)}
# ─────────────────────────────────────────────
##
@@ -247,7 +223,7 @@ async def export_images_zip(
total += 1
if not frame_sizes:
return {"status": "error", "message": "Aucune frame trouvée"}
return {"status": "error", "message": "No frame found!..."}
# --- Génération du ZIP ---
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())
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
current_offset += size
@@ -402,7 +378,7 @@ async def export_video_mp4(
total +=1
if not frame_sizes:
return {"status": "error", "message": "Aucune frame trouvée"}
return {"status": "error", "message": "No frame found!..."}
# --- Génération du MP4 ---
@@ -458,14 +434,14 @@ async def export_video_mp4(
written += 1
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
if video:
video.release()
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)
filename = os.path.basename(video_path)
+14 -14
View File
@@ -42,7 +42,7 @@ TUBE_AXIS_TYPE = [
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"))
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
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)
@@ -94,7 +94,7 @@ class Configuration(models.Model):
return f'{self.name}'
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)
class Meta:
@@ -110,7 +110,7 @@ class Well(models.Model):
class MultiWell(models.Model):
# Identification
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)
default = models.BooleanField(_("Par défaut"), help_text=_('Multi-puit par défaut'), default=False)
# Configuration
@@ -175,7 +175,7 @@ class MultiWell(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)
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)
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)
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):
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)
@@ -264,7 +264,7 @@ class Experiment(models.Model):
verbose_name_plural = _("Expériences")
def __str__(self):
return f'{self.identifier}'
return f'{self.identifier} [ {self.title} ]'
class Session(models.Model):
@@ -276,7 +276,7 @@ class Session(models.Model):
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)
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)
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)
@@ -313,7 +313,7 @@ class Session(models.Model):
def __str__(self):
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)
@@ -376,9 +376,9 @@ def delete_periodic_task(sender, instance, **kwargs):
class SessionExperiment(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.SET_NULL, 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")
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.CASCADE, null=True, blank=True)
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="session_experiments")
@classmethod
def experiment_by_session(cls, session_id, active=True):
@@ -407,8 +407,8 @@ class SessionExperiment(models.Model):
class ExperimentWell(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, 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")
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.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")
active = models.BooleanField(_("Active"), default=True)
+7 -3
View File
@@ -151,7 +151,7 @@ class MultiWellManager:
multiwell = experiment.multiwell
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:
raise Exception(f"Configuration d'expérience introuvable pour {experiment} / {well}")
# reset PlanarianTracker => on_well_change
@@ -166,7 +166,9 @@ class MultiWellManager:
self.process.data.record = True
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()
while not self.stop_playing.is_set():
@@ -249,7 +251,9 @@ class MultiWellManager:
started = timezone.now()
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():
break
@@ -25,6 +25,7 @@ class ScannerManager {
this.scan_state = options.scan_state;
this.sim_bt = options.simulate || null;
this.well_btn = options.well_btn;
this.well_state = options.well_state;
}
init_controls() {
@@ -56,6 +57,7 @@ class ScannerManager {
}
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
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) {
this.well_btn.innerHTML = payload.buttons;
@@ -67,8 +69,6 @@ class ScannerManager {
btn.classList.remove('w3-green');
});
}
} catch(e) { console.log(e); }
}
+25 -11
View File
@@ -5,7 +5,7 @@ import requests
from celery import shared_task, group, chord, chain
from celery.utils.log import get_task_logger
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .process import ScannerProcess, ReplayProcess, redisDB
from .export_tasks import shm_download_video, export_images_zip, export_video_mp4
from .constants import ScannerConstants
@@ -18,20 +18,34 @@ logger = get_task_logger(__name__)
def scanner_start():
scanner = ScannerProcess()
scanner.start()
return f"Scanner démarré."
return str(_("Scanner démarré."))
@shared_task
def replay_start():
replay = ReplayProcess(latency=5.0)
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:
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:
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
def export_images(
@@ -47,6 +61,7 @@ def export_images(
except Exception as e:
logger.error(f"export_images: {e}")
@shared_task
def export_all_images(session_id=None):
try:
@@ -74,11 +89,9 @@ def export_all_images(session_id=None):
except Exception as e:
logger.error(f"export_images: {e}")
@shared_task
def export_videos(uuid: str,
start_ts: float | None = None,
end_ts: float | None = None,
frame_rate: int = 5,
def export_videos(uuid: str, start_ts: float | None = None, end_ts: float | None = None, frame_rate: int = 5,
opencv_fourcc_format='mp4v',
opencv_video_type='mp4',
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))
except Exception as e:
logger.error(f"export_videos: {e}")
return {"success": False, "error": str(e)}
@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 %}
</select>
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<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 %}
{% include "scanner/experiment-inc.html" %}
</div>
{% if current_session.id and current_experiment %}
{% multiwell_cards current_session.id current_experiment %}
{% endif %}
</form>
{% if current_session.id %}
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_images()">
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images" %}<br>
<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 de la session" %}<br>
<span class="w3-margin-left">{{ export_destination }}
</a>
{% endif %}
@@ -65,7 +62,8 @@
const duration = parseInt("{{ duration }}");
const uuid_btn = sId("btn-" + "{{ uuid }}");
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 handleMultiwell(b) { b.classList.add('w3-green'); }
@@ -78,7 +76,7 @@
}
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;
fetch(img)
.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, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -110,6 +110,7 @@
.catch(error => {
console.error('Erreur:', error);
});
}
setGridColumns(columns);
if (uuid_btn)
@@ -17,10 +17,7 @@
{% endfor %}
</select>
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<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 %}
{% include "scanner/experiment-inc.html" %}
</div>
{% if current_session.id and current_experiment %}
@@ -28,7 +25,7 @@
{% endif %}
</form>
{% 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>
<span class="w3-margin-left">{{ export_destination }}</span>
</a>
@@ -78,9 +75,12 @@
const ws_route = "{{ ws_route }}";
const uuid_btn = sId("btn-" + "{{ uuid }}");
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, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -51,7 +51,8 @@
</div>
{% endif %}
<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 class="w3-col">
<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">
<span id="_ts"></span>
</div>
<div class="w3-margin-top w3-small w3-center w3-justify">
<span id="_scan_state" class="w3-text-amber w3-padding"></span>
<div class="w3-margin-top w3-small">
<span id="_scan_state" class="w3-text-amber w3-padding w3-justify"></span>
</div>
</div>
</div>
@@ -100,7 +101,9 @@
frame_count : sId("_count"),
scan_state : sId("_scan_state"),
simulate : sId("_simulate"),
well_btn : sId("_well_btn")
well_btn : sId("_well_btn"),
well_state : sId("_well_state")
};
</script>
<script src="/static/scanner/js/scanning.js"></script>
+33 -17
View File
@@ -1,13 +1,15 @@
#
import os
from asgiref.sync import async_to_sync
import base64, json
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.views.decorators.http import require_GET, require_POST
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required, user_passes_test
from django.conf import settings
from reduct.time import unix_timestamp_to_iso
from modules.system_stats import get_cached_stats, start_background_updater
from modules import reductstore
@@ -198,12 +200,27 @@ def images_view(request):
@csrf_exempt
def download_api(request):
data = json.loads(request.body.decode() or "{}")
action = data.get("action")
if action=='download':
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
else:
return JsonResponse({"state": False})
try:
action = data.get("action")
if action=='download':
uuid, dt_start, dt_stop, frame_rate = data.get("uuid"), data.get("dt_start"), data.get("dt_stop"), data.get("fps")
task = download_video.delay(uuid, dt_start, dt_stop, frame_rate=frame_rate) # @UndefinedVariable
result = task.get(timeout=300)
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):
@@ -246,14 +263,13 @@ def export_api(request):
data = json.loads(request.body.decode() or "{}")
session_id = data.get("sid")
action = data.get("action")
if action == 'export_images':
export_all_images(session_id)
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
elif action == 'export_videos':
export_all_videos(session_id)
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
else:
return JsonResponse({"success": False, "msg": str(_("Erreur d'exportation"))})
try:
if action == 'export_images':
export_all_images(session_id)
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
elif action == 'export_videos':
export_all_videos(session_id)
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
except:
return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), })