export video/image
This commit is contained in:
@@ -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,15 +38,10 @@ 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:
|
||||
try:
|
||||
record_manager = CameraRecordManager(cameraDB)
|
||||
total_size = await record_manager.size(uuid, start_ts, end_ts)
|
||||
|
||||
@@ -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)
|
||||
i+=1
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -159,14 +159,16 @@ class MultiWellManager:
|
||||
|
||||
## create uuid for this capture
|
||||
uuid = f'{self.process.data.session}-{multiwell.position}-{well.name}'
|
||||
|
||||
|
||||
## start recording
|
||||
self.process.data.uuid = uuid
|
||||
if not simulate:
|
||||
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,7 +57,8 @@ 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;
|
||||
const span = this.crop.querySelector("span.crop"); span.style.color = '#f0f';
|
||||
@@ -66,9 +68,7 @@ class ScannerManager {
|
||||
if (btn.value==payload.current) { btn.classList.add('w3-green'); return; }
|
||||
btn.classList.remove('w3-green');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -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,21 +18,35 @@ 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'):
|
||||
try:
|
||||
return asyncio.run(shm_download_video(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type))
|
||||
|
||||
@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:
|
||||
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(
|
||||
uuid: str,
|
||||
@@ -46,7 +60,8 @@ def export_images(
|
||||
return asyncio.run(export_images_zip(uuid, start_ts, end_ts, jpeg_quality, max_zip_size_mb, max_image_width, max_image_height))
|
||||
except Exception as e:
|
||||
logger.error(f"export_images: {e}")
|
||||
|
||||
|
||||
|
||||
@shared_task
|
||||
def export_all_images(session_id=None):
|
||||
try:
|
||||
@@ -72,13 +87,11 @@ def export_all_images(session_id=None):
|
||||
|
||||
return job_zip
|
||||
except Exception as e:
|
||||
logger.error(f"export_images: {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' },
|
||||
@@ -109,7 +109,8 @@
|
||||
})
|
||||
.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>
|
||||
|
||||
@@ -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,13 +200,28 @@ 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):
|
||||
oldest, latest = async_to_sync(reductstore.old_last_dates)(cameraDB, entry_name=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")), })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user