Celianv2 commited on
Commit
f76cdd0
·
verified ·
1 Parent(s): 0fa3d0e

revert to fix audio js

Browse files
.gitattributes CHANGED
@@ -3,3 +3,4 @@ Jalon3.pdf filter=lfs diff=lfs merge=lfs -text
3
  tableaux.pdf filter=lfs diff=lfs merge=lfs -text
4
  jericho/static/images/logoReachyMini.png filter=lfs diff=lfs merge=lfs -text
5
  FonctionsDesCodes.pdf filter=lfs diff=lfs merge=lfs -text
 
 
3
  tableaux.pdf filter=lfs diff=lfs merge=lfs -text
4
  jericho/static/images/logoReachyMini.png filter=lfs diff=lfs merge=lfs -text
5
  FonctionsDesCodes.pdf filter=lfs diff=lfs merge=lfs -text
6
+ Rapport%20robot%20emotion.pdf filter=lfs diff=lfs merge=lfs -text
Rapport%20robot%20emotion.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6201240eab4b538e4a9d2c755a713e40289a89c0d1a3c6e03fa0401021a7d51f
3
+ size 380333
gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Rapport[[:space:]]robot[[:space:]]emotion.pdf filter=lfs diff=lfs merge=lfs -text
2
+ Jalon3.pdf filter=lfs diff=lfs merge=lfs -text
3
+ tableaux.pdf filter=lfs diff=lfs merge=lfs -text
4
+ jericho/static/images/logoReachyMini.png filter=lfs diff=lfs merge=lfs -text
5
+ FonctionsDesCodes.pdf filter=lfs diff=lfs merge=lfs -text
gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.egg-info/
3
+ build/
4
+ projects/
5
+ *.mp4
6
+ *.png
7
+ *.task
jericho/emotion.py CHANGED
@@ -55,13 +55,12 @@ class EMAFilter: # classe pour lisser les mouvements afin d eviter les micromouv
55
 
56
 
57
 
58
-
59
- def _pts(lms, indices, w, h): # convertit point en px car mediapipe utilise les pourcentage donc c plus facile pour
60
  return np.array([[lms[i].x * w, lms[i].y * h] for i in indices])
61
 
62
- def ear(lms, indices, w, h): # calcule ouverture des yeaux| lms landmarks contient coordonnées du visages et indices ctient les points de la zone concernes
63
  p = _pts(lms, indices, w, h) # coordonnée de l'oeil
64
- A = np.linalg.norm(p[1]-p[5]) # distance verti oeil gauche
65
  B = np.linalg.norm(p[2]-p[4]) # " " " droit
66
  C = np.linalg.norm(p[0]-p[3]) # largeur oeil
67
  return (A+B) / (2.0 * C + 1e-6) #formul calculant ear
@@ -129,7 +128,7 @@ def emotions_records(records): # fonction analyse la video pour trouver les émo
129
  return float(np.mean([r[key] for r in valid_recs]))
130
 
131
  def get_std(key, func=None): # calcul ecart type signal
132
- if func: return max(float(np.std([func(r) for r in valid_recs])), 1e-6) # pour pas prendre si la personne ne bouge pas
133
  return max(float(np.std([r[key] for r in valid_recs])), 1e-6)
134
 
135
 
@@ -196,7 +195,7 @@ def emotions_records(records): # fonction analyse la video pour trouver les émo
196
  else:
197
  emotion = "NEUTRE"
198
 
199
- #changement d'emotion que 1 x par sec max
200
  ts = rec.get("timestamp_s", 0.0)
201
  if emotion != current_emotion and (ts - emotion_change_time) >= EMOTION_MIN_DURATION:
202
  current_emotion = emotion
@@ -233,6 +232,8 @@ def annotate_frame(frame, lms, sig, w, h): # fonction pour annoter la video
233
  f"Yaw:{sig['yaw']:+.1f} Pitch:{sig['pitch']:+.1f} Roll:{sig['roll']:+.1f}", # orientation tete
234
  f"EAR L:{sig['ear_left']:.3f} R:{sig['ear_right']:.3f}", # ouverture des yeux
235
  f"MAR:{sig['mar']:.3f} Smile:{sig['smile']:.3f}", # ouverture de la bouche et le sourire
 
 
236
  f"T:{sig['timestamp_s']:.2f}s", # Affiche le temps en secondes
237
  ]
238
  for i, line in enumerate(lines): # boucle pour ecrire les lignes au dessus
@@ -285,7 +286,8 @@ def _face_frame_runs(records):
285
 
286
  def merge_filtered_audio(original_video, silent_video, output_path, face_runs, fps):
287
 
288
-
 
289
 
290
  if not _ffmpeg_available():
291
  return False
@@ -293,9 +295,10 @@ def merge_filtered_audio(original_video, silent_video, output_path, face_runs, f
293
  if not face_runs:
294
  return False
295
 
296
- # Trop de micro-coupures vidéo synchronisée robot/visuel sans audio.
 
297
  if len(face_runs) > 300:
298
- print(" son pas synchronisé à la video car traitement trop long.")
299
  return False
300
 
301
  # aselect garde uniquement les morceaux où un visage existait, puis asetpts compacte la timeline audio.
@@ -345,9 +348,9 @@ def process_video(video_path, output_dir):
345
  base_options=base_opts, # Passe les options de base
346
  running_mode=mp_vision.RunningMode.VIDEO,
347
  num_faces=1, #un visage
348
- min_face_detection_confidence=0.5, # toléerance de détection du visage à 50%
349
- min_face_presence_confidence=0.5, # tolerance de maintien du visage à 50%
350
- min_tracking_confidence=0.5, # tolerance de suivi de mouvement à 50%
351
  output_face_blendshapes=False,
352
  output_facial_transformation_matrixes=True, # matrice pour suivi de l'orientation
353
  )
@@ -493,7 +496,7 @@ def process_video(video_path, output_dir):
493
  "timeline": "compact_face_only",
494
  "frames": compact_records,
495
  },
496
- f, ensure_ascii=False, indent=2
497
  )
498
  print(f"\n fait, {skipped_count} frames sans visage supprimées. Fichiers dans : {os.path.abspath(output_dir)}/")
499
 
 
55
 
56
 
57
 
58
+ def _pts(lms, indices, w, h): # convertit point en px
 
59
  return np.array([[lms[i].x * w, lms[i].y * h] for i in indices])
60
 
61
+ def ear(lms, indices, w, h): # calcule ouverture des yeaux
62
  p = _pts(lms, indices, w, h) # coordonnée de l'oeil
63
+ A = np.linalg.norm(p[1]-p[5]) # distance vert oeil gauche
64
  B = np.linalg.norm(p[2]-p[4]) # " " " droit
65
  C = np.linalg.norm(p[0]-p[3]) # largeur oeil
66
  return (A+B) / (2.0 * C + 1e-6) #formul calculant ear
 
128
  return float(np.mean([r[key] for r in valid_recs]))
129
 
130
  def get_std(key, func=None): # calcul ecart type signal
131
+ if func: return max(float(np.std([func(r) for r in valid_recs])), 1e-6)
132
  return max(float(np.std([r[key] for r in valid_recs])), 1e-6)
133
 
134
 
 
195
  else:
196
  emotion = "NEUTRE"
197
 
198
+ # cooldown : on ne change d'emotion que si au moins 1 seconde s'est ecoulee depuis le dernier changement
199
  ts = rec.get("timestamp_s", 0.0)
200
  if emotion != current_emotion and (ts - emotion_change_time) >= EMOTION_MIN_DURATION:
201
  current_emotion = emotion
 
232
  f"Yaw:{sig['yaw']:+.1f} Pitch:{sig['pitch']:+.1f} Roll:{sig['roll']:+.1f}", # orientation tete
233
  f"EAR L:{sig['ear_left']:.3f} R:{sig['ear_right']:.3f}", # ouverture des yeux
234
  f"MAR:{sig['mar']:.3f} Smile:{sig['smile']:.3f}", # ouverture de la bouche et le sourire
235
+ # f"BrowRaise L:{sig['brow_raise_left']:.3f} R:{sig['brow_raise_right']:.3f}", # hauteur des sourcils
236
+ # f"BrowFurrow:{sig['brow_furrow']:.3f}", # froncement
237
  f"T:{sig['timestamp_s']:.2f}s", # Affiche le temps en secondes
238
  ]
239
  for i, line in enumerate(lines): # boucle pour ecrire les lignes au dessus
 
286
 
287
  def merge_filtered_audio(original_video, silent_video, output_path, face_runs, fps):
288
 
289
+ #Recolle l'audio en supprimant les mêmes intervalles que les frames sans visage.
290
+ #Si l'audio n'est pas disponible, la vidéo muette reste utilisée.
291
 
292
  if not _ffmpeg_available():
293
  return False
 
295
  if not face_runs:
296
  return False
297
 
298
+ # Trop de micro-coupures peut créer une commande ffmpeg très longue.
299
+ # Dans ce cas, on garde la vidéo synchronisée robot/visuel sans audio.
300
  if len(face_runs) > 300:
301
+ print(" Trop de segments audio à concaténer : export vidéo muette synchronisée.")
302
  return False
303
 
304
  # aselect garde uniquement les morceaux où un visage existait, puis asetpts compacte la timeline audio.
 
348
  base_options=base_opts, # Passe les options de base
349
  running_mode=mp_vision.RunningMode.VIDEO,
350
  num_faces=1, #un visage
351
+ min_face_detection_confidence=0.5, # Tolérance de détection du visage à 50%
352
+ min_face_presence_confidence=0.5, # Tolérance de maintien du visage à 50%
353
+ min_tracking_confidence=0.5, # Tolérance de suivi de mouvement à 50%
354
  output_face_blendshapes=False,
355
  output_facial_transformation_matrixes=True, # matrice pour suivi de l'orientation
356
  )
 
496
  "timeline": "compact_face_only",
497
  "frames": compact_records,
498
  },
499
+ f, ensure_ascii=False, indent=2 # Avec une jolie indentation (2 espaces) pour que ça soit lisible
500
  )
501
  print(f"\n fait, {skipped_count} frames sans visage supprimées. Fichiers dans : {os.path.abspath(output_dir)}/")
502
 
jericho/main.py CHANGED
@@ -38,7 +38,7 @@ def moving_average(values: list[float], window: int = 7) -> np.ndarray: # fct li
38
  return smoothed[: len(arr)]
39
 
40
  class SignalMapper: # classe map signaux visage vers robot
41
- # limites angles robot
42
  HEAD_YAW_MAX = np.deg2rad(5)
43
  HEAD_PITCH_MAX = np.deg2rad(15)
44
  HEAD_ROLL_MAX = np.deg2rad(15)
@@ -50,7 +50,7 @@ class SignalMapper: # classe map signaux visage vers robot
50
  ANTENNA_MAX_col = np.deg2rad(110)
51
  COMBINED_YAW_LIMIT = np.deg2rad(60)
52
 
53
- # multiplicateurs mouvements
54
  YAW_AMP = 8.0
55
  PITCH_AMP = 5.0
56
  ROLL_AMP = 3.0
@@ -148,7 +148,7 @@ class SignalMapper: # classe map signaux visage vers robot
148
 
149
  w_h = self.SMOOTH_WINDOW_HEAD
150
 
151
- # applique fct lissage tete + position
152
  s_yaw = moving_average(raw_yaw, w_h)
153
  s_pitch = moving_average(raw_pitch, w_h)
154
  s_roll = moving_average(raw_roll, w_h)
 
38
  return smoothed[: len(arr)]
39
 
40
  class SignalMapper: # classe map signaux visage vers robot
41
+ # constantes limites angles robot
42
  HEAD_YAW_MAX = np.deg2rad(5)
43
  HEAD_PITCH_MAX = np.deg2rad(15)
44
  HEAD_ROLL_MAX = np.deg2rad(15)
 
50
  ANTENNA_MAX_col = np.deg2rad(110)
51
  COMBINED_YAW_LIMIT = np.deg2rad(60)
52
 
53
+ # multiplicateurs amplifs mouvements
54
  YAW_AMP = 8.0
55
  PITCH_AMP = 5.0
56
  ROLL_AMP = 3.0
 
148
 
149
  w_h = self.SMOOTH_WINDOW_HEAD
150
 
151
+ # applique fct lissage (tete + position uniquement, emotions deja calculees par emotion.py)
152
  s_yaw = moving_average(raw_yaw, w_h)
153
  s_pitch = moving_average(raw_pitch, w_h)
154
  s_roll = moving_average(raw_roll, w_h)
jericho/static/main.js CHANGED
@@ -9,7 +9,6 @@ const videoPlayer = document.getElementById('video-player');
9
  videoPlayer.muted = true;
10
  videoPlayer.defaultMuted = true;
11
 
12
- // Sécurise le texte pour éviter les bugs d'affichage si le nom de la vidéo contient des caractères bizarres
13
  function escapeHtml(value) {
14
  return String(value)
15
  .replaceAll('&', '&')
@@ -19,7 +18,6 @@ function escapeHtml(value) {
19
  .replaceAll("'", ''');
20
  }
21
 
22
- // Envoie l'état actuel de la vidéo (temps précis, si elle est en lecture ou pause) au serveur Python
23
  async function sendPlayback(action, options = {}) {
24
  const projectName = options.projectName || currentLoadedProject;
25
  if (!projectName) return;
@@ -49,7 +47,6 @@ async function sendPlayback(action, options = {}) {
49
  }
50
  }
51
 
52
- // Lance le chronomètre qui synchronise la vidéo avec le robot (s'active 2 fois par seconde)
53
  function startSyncLoop() {
54
  stopSyncLoop();
55
  syncTimer = setInterval(() => {
@@ -59,7 +56,6 @@ function startSyncLoop() {
59
  }, SYNC_INTERVAL_MS);
60
  }
61
 
62
- // Arrête le chronomètre de synchronisation
63
  function stopSyncLoop() {
64
  if (syncTimer) {
65
  clearInterval(syncTimer);
@@ -67,7 +63,6 @@ function stopSyncLoop() {
67
  }
68
  }
69
 
70
- // Initialisation au chargement de la page web : lance la bibliothèque et prépare les boutons d'émotions
71
  document.addEventListener('DOMContentLoaded', () => {
72
  loadLibrary();
73
  setInterval(loadLibrary, 3000);
@@ -97,7 +92,6 @@ document.addEventListener('DOMContentLoaded', () => {
97
  });
98
  });
99
 
100
- // Gère l'envoi d'un fichier vidéo local depuis l'ordinateur vers le serveur
101
  document.getElementById('import-btn').onclick = function() {
102
  const name = document.getElementById('video-name').value.trim();
103
  const file = document.getElementById('video-file').files[0];
@@ -116,7 +110,6 @@ document.getElementById('import-btn').onclick = function() {
116
  const reader = new FileReader();
117
  reader.readAsDataURL(file);
118
 
119
- // S'exécute quand la vidéo est transformée en texte (Base64) et prête à partir
120
  reader.onload = async function () {
121
  const base64data = reader.result;
122
 
@@ -147,7 +140,6 @@ document.getElementById('import-btn').onclick = function() {
147
  };
148
  };
149
 
150
- // Gère l'envoi d'un lien YouTube au serveur pour qu'il le télécharge
151
  document.getElementById('yt-import-btn').onclick = async function() {
152
  const name = document.getElementById('yt-name').value.trim();
153
  const url = document.getElementById('yt-url').value.trim();
@@ -189,7 +181,6 @@ document.getElementById('yt-import-btn').onclick = async function() {
189
  }
190
  };
191
 
192
- // Demande au serveur de supprimer définitivement un projet
193
  window.deleteProject = async function(event, name) {
194
  event.stopPropagation();
195
 
@@ -221,7 +212,6 @@ window.deleteProject = async function(event, name) {
221
  }
222
  };
223
 
224
- // Interroge le serveur pour récupérer la liste des vidéos et met à jour l'affichage
225
  async function loadLibrary() {
226
  try {
227
  const response = await fetch('/api/library');
@@ -286,7 +276,6 @@ async function loadLibrary() {
286
  }
287
  }
288
 
289
- // Charge la vidéo cliquée dans le lecteur principal et réinitialise l'affichage
290
  window.loadVideoPlayer = async function(name, url) {
291
  if (currentLoadedProject && currentLoadedProject !== name) {
292
  await sendPlayback('stop', { projectName: currentLoadedProject, playing: false });
@@ -322,7 +311,6 @@ window.loadVideoPlayer = async function(name, url) {
322
  }
323
  };
324
 
325
- // Prévient le serveur quand l'utilisateur clique sur Lecture
326
  videoPlayer.addEventListener('play', () => {
327
  if (!currentLoadedProject) return;
328
  robotStatus.textContent = 'Lecture synchronisée — audio robot';
@@ -330,7 +318,6 @@ videoPlayer.addEventListener('play', () => {
330
  startSyncLoop();
331
  });
332
 
333
- // Prévient le serveur quand l'utilisateur met la vidéo en pause
334
  videoPlayer.addEventListener('pause', () => {
335
  if (!currentLoadedProject || videoPlayer.ended) return;
336
  robotStatus.textContent = 'En pause';
@@ -338,31 +325,26 @@ videoPlayer.addEventListener('pause', () => {
338
  stopSyncLoop();
339
  });
340
 
341
- // Prévient le serveur quand on clique sur la barre d'avancement pour faire un saut
342
  videoPlayer.addEventListener('seeking', () => {
343
  if (!currentLoadedProject) return;
344
  sendPlayback('seek', { playing: !videoPlayer.paused && !videoPlayer.ended });
345
  });
346
 
347
- // Relance le processus juste après que le saut dans la vidéo soit terminé
348
  videoPlayer.addEventListener('seeked', () => {
349
  if (!currentLoadedProject) return;
350
  sendPlayback('seek', { playing: !videoPlayer.paused && !videoPlayer.ended });
351
  });
352
 
353
- // Mouvement naturel de la vidéo (déjà géré par startSyncLoop, donc pas besoin de surcharger)
354
  videoPlayer.addEventListener('timeupdate', () => {
355
  // synchro geree par startSyncLoop(), ici pas de double envoi
356
  });
357
 
358
- // Prévient le serveur que la vidéo est complètement terminée
359
  videoPlayer.addEventListener('ended', () => {
360
  robotStatus.textContent = 'Terminé';
361
  sendPlayback('stop', { playing: false });
362
  stopSyncLoop();
363
  });
364
 
365
- // Ajoute une nouvelle ligne avec une émotion et un temps dans le créateur de séquence
366
  function createEmotionRow() {
367
  const row = document.createElement('div');
368
  row.className = 'emotion-row';
@@ -408,7 +390,6 @@ function createEmotionRow() {
408
  document.getElementById('sequence-list').appendChild(row);
409
  }
410
 
411
- // Configure le bouton "+" pour ajouter des lignes au créateur de séquence
412
  document.addEventListener('DOMContentLoaded', () => {
413
  const addEmotionBtn = document.getElementById('add-emotion-btn');
414
  if(addEmotionBtn) {
@@ -427,7 +408,6 @@ document.addEventListener('DOMContentLoaded', () => {
427
  }
428
  });
429
 
430
- // Rassemble les émotions choisies et envoie l'ordre au serveur de créer la séquence
431
  document.getElementById('save-seq-btn').onclick = async function() {
432
  const name = document.getElementById('seq-name').value.trim();
433
  const msg = document.getElementById('seq-msg');
 
9
  videoPlayer.muted = true;
10
  videoPlayer.defaultMuted = true;
11
 
 
12
  function escapeHtml(value) {
13
  return String(value)
14
  .replaceAll('&', '&')
 
18
  .replaceAll("'", ''');
19
  }
20
 
 
21
  async function sendPlayback(action, options = {}) {
22
  const projectName = options.projectName || currentLoadedProject;
23
  if (!projectName) return;
 
47
  }
48
  }
49
 
 
50
  function startSyncLoop() {
51
  stopSyncLoop();
52
  syncTimer = setInterval(() => {
 
56
  }, SYNC_INTERVAL_MS);
57
  }
58
 
 
59
  function stopSyncLoop() {
60
  if (syncTimer) {
61
  clearInterval(syncTimer);
 
63
  }
64
  }
65
 
 
66
  document.addEventListener('DOMContentLoaded', () => {
67
  loadLibrary();
68
  setInterval(loadLibrary, 3000);
 
92
  });
93
  });
94
 
 
95
  document.getElementById('import-btn').onclick = function() {
96
  const name = document.getElementById('video-name').value.trim();
97
  const file = document.getElementById('video-file').files[0];
 
110
  const reader = new FileReader();
111
  reader.readAsDataURL(file);
112
 
 
113
  reader.onload = async function () {
114
  const base64data = reader.result;
115
 
 
140
  };
141
  };
142
 
 
143
  document.getElementById('yt-import-btn').onclick = async function() {
144
  const name = document.getElementById('yt-name').value.trim();
145
  const url = document.getElementById('yt-url').value.trim();
 
181
  }
182
  };
183
 
 
184
  window.deleteProject = async function(event, name) {
185
  event.stopPropagation();
186
 
 
212
  }
213
  };
214
 
 
215
  async function loadLibrary() {
216
  try {
217
  const response = await fetch('/api/library');
 
276
  }
277
  }
278
 
 
279
  window.loadVideoPlayer = async function(name, url) {
280
  if (currentLoadedProject && currentLoadedProject !== name) {
281
  await sendPlayback('stop', { projectName: currentLoadedProject, playing: false });
 
311
  }
312
  };
313
 
 
314
  videoPlayer.addEventListener('play', () => {
315
  if (!currentLoadedProject) return;
316
  robotStatus.textContent = 'Lecture synchronisée — audio robot';
 
318
  startSyncLoop();
319
  });
320
 
 
321
  videoPlayer.addEventListener('pause', () => {
322
  if (!currentLoadedProject || videoPlayer.ended) return;
323
  robotStatus.textContent = 'En pause';
 
325
  stopSyncLoop();
326
  });
327
 
 
328
  videoPlayer.addEventListener('seeking', () => {
329
  if (!currentLoadedProject) return;
330
  sendPlayback('seek', { playing: !videoPlayer.paused && !videoPlayer.ended });
331
  });
332
 
 
333
  videoPlayer.addEventListener('seeked', () => {
334
  if (!currentLoadedProject) return;
335
  sendPlayback('seek', { playing: !videoPlayer.paused && !videoPlayer.ended });
336
  });
337
 
 
338
  videoPlayer.addEventListener('timeupdate', () => {
339
  // synchro geree par startSyncLoop(), ici pas de double envoi
340
  });
341
 
 
342
  videoPlayer.addEventListener('ended', () => {
343
  robotStatus.textContent = 'Terminé';
344
  sendPlayback('stop', { playing: false });
345
  stopSyncLoop();
346
  });
347
 
 
348
  function createEmotionRow() {
349
  const row = document.createElement('div');
350
  row.className = 'emotion-row';
 
390
  document.getElementById('sequence-list').appendChild(row);
391
  }
392
 
 
393
  document.addEventListener('DOMContentLoaded', () => {
394
  const addEmotionBtn = document.getElementById('add-emotion-btn');
395
  if(addEmotionBtn) {
 
408
  }
409
  });
410
 
 
411
  document.getElementById('save-seq-btn').onclick = async function() {
412
  const name = document.getElementById('seq-name').value.trim();
413
  const msg = document.getElementById('seq-msg');