VeuReu commited on
Commit
8d19899
·
verified ·
1 Parent(s): da15379

Upload 2 files

Browse files
Files changed (2) hide show
  1. api.py +129 -39
  2. face_classifier.py +22 -5
api.py CHANGED
@@ -12,6 +12,7 @@ from datetime import datetime
12
  from typing import Dict
13
  from enum import Enum
14
  import os
 
15
 
16
  from video_processing import process_video_pipeline
17
  from audio_tools import process_audio_for_video, extract_audio_ffmpeg, embed_voice_segments
@@ -80,12 +81,14 @@ def describe_image_with_svision(image_path: str, is_face: bool = True) -> tuple[
80
  if is_face:
81
  context = {
82
  "task": "describe_person",
83
- "instructions": "Descriu la persona en la imatge. Inclou: edat aproximada (jove/adult), gènere, característiques físiques notables (ulleres, barba, bigoti, etc.), expressió i vestimenta."
 
84
  }
85
  else:
86
  context = {
87
  "task": "describe_scene",
88
- "instructions": "Descriu l'escena en la imatge. Inclou: tipus de localització (interior/exterior), elements principals, ambient, il·luminació."
 
89
  }
90
 
91
  # Llamar a svision
@@ -574,17 +577,28 @@ def process_video_job(job_id: str):
574
  best_face = face_detections_sorted[0]
575
  best_face_path = faces_root / best_face['file']
576
 
577
- print(f"[{job_id}] [VALIDATION] Cluster {char_id}: validant millor cara (score={best_face['score']:.0f}px²)")
 
578
 
579
  validation = validate_and_classify_face(str(best_face_path))
580
 
581
  if not validation:
582
- print(f"[{job_id}] [VALIDATION] ✗ Cluster {char_id}: error en validació, eliminant")
583
  continue
584
 
 
 
 
 
 
 
 
 
 
 
585
  # PASO 3: Verificar si és una cara vàlida
586
  if not validation['is_valid_face'] or validation['face_confidence'] < FACE_CONFIDENCE_THRESHOLD:
587
- print(f"[{job_id}] [VALIDATION] ✗ Cluster {char_id}: score baix ({validation['face_confidence']:.2f}), eliminant tot el clúster")
588
  continue
589
 
590
  # PASO 4: És una cara vàlida! Crear carpeta
@@ -624,6 +638,11 @@ def process_video_job(job_id: str):
624
  gender = validation['gender']
625
  character_name = get_random_catalan_name_by_gender(gender, char_id)
626
 
 
 
 
 
 
627
  character_data = {
628
  "id": char_id,
629
  "name": character_name,
@@ -641,9 +660,13 @@ def process_video_job(job_id: str):
641
 
642
  characters_validated.append(character_data)
643
 
644
- print(f"[{job_id}] [VALIDATION] ✓ Cluster {char_id}: cara vàlida! "
645
- f"Nom={character_name}, Gender={gender} (conf={validation['gender_confidence']:.2f}), "
646
- f"Mostrant {len(files)}/{total_faces} cares")
 
 
 
 
647
 
648
  # Estadístiques finals
649
  eliminated_count = original_cluster_count - len(characters_validated)
@@ -1022,58 +1045,93 @@ async def detect_scenes(
1022
  continue
1023
  clusters.setdefault(int(lbl), []).append(i)
1024
 
1025
- # VALIDACIÓ: Mesurar robustesa dels clusters i fusionar si són massa similars
1026
  # Calcular centroides (histograma promig de cada cluster)
1027
  centroids = {}
1028
  for lbl, idxs in clusters.items():
1029
  cluster_histograms = X[idxs]
1030
  centroids[lbl] = np.mean(cluster_histograms, axis=0)
1031
 
1032
- # Comparar distàncies entre clusters
1033
- # Si dos clusters tenen una distància euclidiana < threshold, són massa similars
1034
- SIMILARITY_THRESHOLD = 0.15 # Ajustable: més baix = més estricte
1035
 
1036
- # Calcular matriu de distàncies entre centroides
 
 
 
 
1037
  cluster_labels = sorted(centroids.keys())
1038
- distances = {}
 
1039
  for i, lbl1 in enumerate(cluster_labels):
1040
  for lbl2 in cluster_labels[i+1:]:
 
1041
  dist = np.linalg.norm(centroids[lbl1] - centroids[lbl2])
1042
- distances[(lbl1, lbl2)] = dist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
 
1044
- # Trobar parelles de clusters massa similars i fusionar-los
1045
- merged = {} # mapatge de label_old -> label_new
1046
- for lbl in cluster_labels:
1047
- merged[lbl] = lbl
1048
 
1049
- # Fusionar clusters similars (greedy approach)
1050
- for (lbl1, lbl2), dist in sorted(distances.items(), key=lambda x: x[1]):
1051
- if dist < SIMILARITY_THRESHOLD:
1052
- # Fusionar lbl2 amb lbl1
1053
- current_lbl1 = merged.get(lbl1, lbl1)
1054
- current_lbl2 = merged.get(lbl2, lbl2)
1055
- if current_lbl1 != current_lbl2:
1056
- # Assignar lbl2 al grup de lbl1
1057
- for k, v in merged.items():
1058
- if v == current_lbl2:
1059
- merged[k] = current_lbl1
1060
- print(f"[SCENE VALIDATION] Fusionant clusters {lbl2} i {lbl1} (distància={dist:.3f})")
1061
 
1062
  # Aplicar fusió als clusters
1063
  new_clusters = {}
1064
  for lbl, idxs in clusters.items():
1065
- new_lbl = merged[lbl]
1066
- if new_lbl not in new_clusters:
1067
- new_clusters[new_lbl] = []
1068
- new_clusters[new_lbl].extend(idxs)
 
 
 
 
 
1069
 
1070
- clusters = new_clusters
1071
  final_clusters = len(clusters)
1072
  eliminated = initial_clusters - final_clusters
1073
 
1074
- if eliminated > 0:
1075
- print(f"[SCENE VALIDATION] Reduït de {initial_clusters} a {final_clusters} clusters "
1076
- f"(eliminats {eliminated} clusters massa similars)")
 
 
 
 
1077
 
1078
  # Escriure imatges representatives per a cada clúster
1079
  base = TEMP_ROOT / video_name / "scenes"
@@ -1110,6 +1168,38 @@ async def detect_scenes(
1110
  scene_description, scene_name = describe_image_with_svision(str(rep_full_path), is_face=False)
1111
  if not scene_name:
1112
  scene_name = f"Escena {lbl+1}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1113
  except Exception as e:
1114
  print(f"Error describiendo {scene_id}: {e}")
1115
 
 
12
  from typing import Dict
13
  from enum import Enum
14
  import os
15
+ import yaml
16
 
17
  from video_processing import process_video_pipeline
18
  from audio_tools import process_audio_for_video, extract_audio_ffmpeg, embed_voice_segments
 
81
  if is_face:
82
  context = {
83
  "task": "describe_person",
84
+ "instructions": "Descriu la persona en la imatge. Inclou: edat aproximada (jove/adult), gènere, característiques físiques notables (ulleres, barba, bigoti, etc.), expressió i vestimenta.",
85
+ "max_tokens": 256
86
  }
87
  else:
88
  context = {
89
  "task": "describe_scene",
90
+ "instructions": "Descriu aquesta escena breument en 2-3 frases: tipus de localització i elements principals.",
91
+ "max_tokens": 128
92
  }
93
 
94
  # Llamar a svision
 
577
  best_face = face_detections_sorted[0]
578
  best_face_path = faces_root / best_face['file']
579
 
580
+ print(f"[{job_id}] [VALIDATION] Cluster {char_id}: validant millor cara (bbox_area={best_face['score']:.0f}px²)")
581
+ print(f"[{job_id}] [VALIDATION] Cluster {char_id}: millor cara path={best_face_path}")
582
 
583
  validation = validate_and_classify_face(str(best_face_path))
584
 
585
  if not validation:
586
+ print(f"[{job_id}] [VALIDATION] ✗ Cluster {char_id}: error en validació DeepFace, eliminant cluster")
587
  continue
588
 
589
+ # Mostrar resultados detallados de DeepFace
590
+ print(f"[{job_id}] [DEEPFACE RESULT] Cluster {char_id}:")
591
+ print(f"[{job_id}] - is_valid_face: {validation['is_valid_face']}")
592
+ print(f"[{job_id}] - face_confidence: {validation['face_confidence']:.3f}")
593
+ print(f"[{job_id}] - man_prob: {validation['man_prob']:.3f}")
594
+ print(f"[{job_id}] - woman_prob: {validation['woman_prob']:.3f}")
595
+ print(f"[{job_id}] - gender_diff: {abs(validation['man_prob'] - validation['woman_prob']):.3f}")
596
+ print(f"[{job_id}] - gender_assigned: {validation['gender']}")
597
+ print(f"[{job_id}] - gender_confidence: {validation['gender_confidence']:.3f}")
598
+
599
  # PASO 3: Verificar si és una cara vàlida
600
  if not validation['is_valid_face'] or validation['face_confidence'] < FACE_CONFIDENCE_THRESHOLD:
601
+ print(f"[{job_id}] [VALIDATION] ✗ Cluster {char_id}: NO ES UNA CARA VÁLIDA (face_confidence={validation['face_confidence']:.3f} < threshold={FACE_CONFIDENCE_THRESHOLD}), eliminant tot el clúster")
602
  continue
603
 
604
  # PASO 4: És una cara vàlida! Crear carpeta
 
638
  gender = validation['gender']
639
  character_name = get_random_catalan_name_by_gender(gender, char_id)
640
 
641
+ print(f"[{job_id}] [NAME GENERATION] Cluster {char_id}:")
642
+ print(f"[{job_id}] - Gender detectado: {gender}")
643
+ print(f"[{job_id}] - Nombre asignado: {character_name}")
644
+ print(f"[{job_id}] - Seed usado: {char_id}")
645
+
646
  character_data = {
647
  "id": char_id,
648
  "name": character_name,
 
660
 
661
  characters_validated.append(character_data)
662
 
663
+ print(f"[{job_id}] [VALIDATION] ✓ Cluster {char_id}: CARA VÁLIDA!")
664
+ print(f"[{job_id}] Nombre: {character_name}")
665
+ print(f"[{job_id}] Género: {gender} (man={validation['man_prob']:.3f}, woman={validation['woman_prob']:.3f})")
666
+ print(f"[{job_id}] Confianza género: {validation['gender_confidence']:.3f}")
667
+ print(f"[{job_id}] Confianza cara: {validation['face_confidence']:.3f}")
668
+ print(f"[{job_id}] Caras mostradas: {len(files)}/{total_faces}")
669
+ print(f"[{job_id}] Imagen representativa: {best_face_path.name}")
670
 
671
  # Estadístiques finals
672
  eliminated_count = original_cluster_count - len(characters_validated)
 
1045
  continue
1046
  clusters.setdefault(int(lbl), []).append(i)
1047
 
1048
+ # VALIDACIÓ MILLORADA: Fusionar clusters molt similars de forma més agressiva
1049
  # Calcular centroides (histograma promig de cada cluster)
1050
  centroids = {}
1051
  for lbl, idxs in clusters.items():
1052
  cluster_histograms = X[idxs]
1053
  centroids[lbl] = np.mean(cluster_histograms, axis=0)
1054
 
1055
+ print(f"[SCENE VALIDATION] Validant similaritat entre {len(centroids)} clusters...")
 
 
1056
 
1057
+ # Thresholds més agressius per fusionar escenes similars
1058
+ SIMILARITY_THRESHOLD = 0.25 # Aumentado de 0.15 a 0.25 (fusiona más)
1059
+ CORRELATION_THRESHOLD = 0.85 # Correlación mínima para considerar similares
1060
+
1061
+ # Calcular matriu de distàncies i correlacions entre centroides
1062
  cluster_labels = sorted(centroids.keys())
1063
+ similarities = {}
1064
+
1065
  for i, lbl1 in enumerate(cluster_labels):
1066
  for lbl2 in cluster_labels[i+1:]:
1067
+ # Distancia euclidiana (normalizada)
1068
  dist = np.linalg.norm(centroids[lbl1] - centroids[lbl2])
1069
+
1070
+ # Correlación de Pearson entre histogramas
1071
+ corr = np.corrcoef(centroids[lbl1], centroids[lbl2])[0, 1]
1072
+
1073
+ # Son similares si:
1074
+ # - Distancia baja (< threshold) O
1075
+ # - Correlación alta (> threshold)
1076
+ are_similar = (dist < SIMILARITY_THRESHOLD) or (corr > CORRELATION_THRESHOLD)
1077
+
1078
+ similarities[(lbl1, lbl2)] = {
1079
+ 'distance': dist,
1080
+ 'correlation': corr,
1081
+ 'similar': are_similar
1082
+ }
1083
+
1084
+ if are_similar:
1085
+ print(f"[SCENE VALIDATION] Clusters {lbl1} i {lbl2} són similars: "
1086
+ f"dist={dist:.3f} (threshold={SIMILARITY_THRESHOLD}), "
1087
+ f"corr={corr:.3f} (threshold={CORRELATION_THRESHOLD})")
1088
+
1089
+ # Union-Find para fusionar clusters transitivamente
1090
+ # Si A~B y B~C, entonces A~B~C (todos en el mismo grupo)
1091
+ parent = {lbl: lbl for lbl in cluster_labels}
1092
 
1093
+ def find(x):
1094
+ if parent[x] != x:
1095
+ parent[x] = find(parent[x]) # Path compression
1096
+ return parent[x]
1097
 
1098
+ def union(x, y):
1099
+ root_x = find(x)
1100
+ root_y = find(y)
1101
+ if root_x != root_y:
1102
+ parent[root_y] = root_x
1103
+
1104
+ # Fusionar todos los clusters similares
1105
+ fusion_count = 0
1106
+ for (lbl1, lbl2), sim in similarities.items():
1107
+ if sim['similar']:
1108
+ union(lbl1, lbl2)
1109
+ fusion_count += 1
1110
 
1111
  # Aplicar fusió als clusters
1112
  new_clusters = {}
1113
  for lbl, idxs in clusters.items():
1114
+ root = find(lbl)
1115
+ if root not in new_clusters:
1116
+ new_clusters[root] = []
1117
+ new_clusters[root].extend(idxs)
1118
+
1119
+ # Reordenar labels para que sean consecutivos
1120
+ final_clusters_dict = {}
1121
+ for i, (root, idxs) in enumerate(sorted(new_clusters.items())):
1122
+ final_clusters_dict[i] = idxs
1123
 
1124
+ clusters = final_clusters_dict
1125
  final_clusters = len(clusters)
1126
  eliminated = initial_clusters - final_clusters
1127
 
1128
+ print(f"[SCENE VALIDATION] ===== RESULTADO =====")
1129
+ print(f"[SCENE VALIDATION] Clusters inicials: {initial_clusters}")
1130
+ print(f"[SCENE VALIDATION] Fusions realitzades: {fusion_count}")
1131
+ print(f"[SCENE VALIDATION] Clusters finals: {final_clusters}")
1132
+ print(f"[SCENE VALIDATION] Clusters eliminats (fusionats): {eliminated}")
1133
+ print(f"[SCENE VALIDATION] Reducció: {(eliminated/initial_clusters*100):.1f}%")
1134
+ print(f"[SCENE VALIDATION] =======================")
1135
 
1136
  # Escriure imatges representatives per a cada clúster
1137
  base = TEMP_ROOT / video_name / "scenes"
 
1168
  scene_description, scene_name = describe_image_with_svision(str(rep_full_path), is_face=False)
1169
  if not scene_name:
1170
  scene_name = f"Escena {lbl+1}"
1171
+
1172
+ # Si tenemos descripción, generar nombre corto con schat
1173
+ if scene_description:
1174
+ print(f"Llamando a schat para generar nombre corto de {scene_id}...")
1175
+ try:
1176
+ # Usar LLMRouter para llamar a schat
1177
+ config_path = os.getenv("CONFIG_YAML", "config.yaml")
1178
+ if os.path.exists(config_path):
1179
+ with open(config_path, 'r', encoding='utf-8') as f:
1180
+ cfg = yaml.safe_load(f) or {}
1181
+ router = LLMRouter(cfg)
1182
+
1183
+ prompt = f"Basant-te en aquesta descripció d'una escena, genera un nom curt de menys de 3 paraules que la resumeixi:\n\n{scene_description}\n\nNom de l'escena:"
1184
+
1185
+ short_name = router.instruct(
1186
+ prompt=prompt,
1187
+ system="Ets un assistent que genera noms curts i descriptius per a escenes. Respon NOMÉS amb el nom, sense explicacions.",
1188
+ model="salamandra-instruct"
1189
+ ).strip()
1190
+
1191
+ # Limpiar posibles comillas o puntuación extra
1192
+ short_name = short_name.strip('"\'.,!?').strip()
1193
+
1194
+ if short_name and len(short_name) > 0:
1195
+ scene_name = short_name
1196
+ print(f"[schat] Nom generat: {scene_name}")
1197
+ else:
1198
+ print(f"[schat] No s'ha generat nom, usant fallback")
1199
+ except Exception as e_schat:
1200
+ print(f"Error generando nombre con schat: {e_schat}")
1201
+ # Mantener el nombre de svision si schat falla
1202
+
1203
  except Exception as e:
1204
  print(f"Error describiendo {scene_id}: {e}")
1205
 
face_classifier.py CHANGED
@@ -48,6 +48,7 @@ def validate_and_classify_face(image_path: str) -> Optional[Dict[str, Any]]:
48
 
49
  # DeepFace pot retornar llista si detecta múltiples cares
50
  if isinstance(result, list):
 
51
  result = result[0] if result else None
52
 
53
  if not result:
@@ -61,39 +62,55 @@ def validate_and_classify_face(image_path: str) -> Optional[Dict[str, Any]]:
61
  'woman_prob': 0.0
62
  }
63
 
 
 
 
64
  # Extreure informació de gènere
65
  gender_info = result.get('gender', {})
 
66
 
67
  if isinstance(gender_info, dict):
68
  # DeepFace retorna percentatges, convertir a 0-1
69
  man_prob = gender_info.get('Man', 0) / 100.0
70
  woman_prob = gender_info.get('Woman', 0) / 100.0
 
71
  else:
72
  # Fallback si el format és diferent
 
73
  man_prob = 0.5
74
  woman_prob = 0.5
75
 
76
  # Determinar gènere basat en les probabilitats
77
  gender_diff = abs(man_prob - woman_prob)
78
 
 
 
79
  # Si la diferència és petita (< threshold), considerar neutre
80
  if gender_diff < GENDER_NEUTRAL_THRESHOLD:
81
  gender = 'Neutral'
82
  gender_confidence = 0.5
 
83
  else:
84
  gender = 'Man' if man_prob > woman_prob else 'Woman'
85
  gender_confidence = max(man_prob, woman_prob)
 
86
 
87
  # Confiança de detecció de cara
88
- # DeepFace no proporciona score directament, però si va retornar resultat
89
- # assumim que és cara vàlida amb confiança alta
90
- face_confidence = result.get('face_confidence', 0.9) # Default alt si detecta
91
 
92
  # Si DeepFace va retornar resultat, assumir que és cara vàlida
93
  is_valid_face = True
94
 
95
- logger.info(f"[DeepFace] Resultat: gender={gender}, confidence={gender_confidence:.2f}, "
96
- f"man={man_prob:.2f}, woman={woman_prob:.2f}")
 
 
 
 
 
 
97
 
98
  return {
99
  'is_valid_face': is_valid_face,
 
48
 
49
  # DeepFace pot retornar llista si detecta múltiples cares
50
  if isinstance(result, list):
51
+ logger.info(f"[DeepFace] Resultado es lista con {len(result)} elementos")
52
  result = result[0] if result else None
53
 
54
  if not result:
 
62
  'woman_prob': 0.0
63
  }
64
 
65
+ # LOG: Ver estructura completa del resultado
66
+ logger.info(f"[DeepFace] Resultado completo de analyze: {result}")
67
+
68
  # Extreure informació de gènere
69
  gender_info = result.get('gender', {})
70
+ logger.info(f"[DeepFace] gender_info type: {type(gender_info)}, value: {gender_info}")
71
 
72
  if isinstance(gender_info, dict):
73
  # DeepFace retorna percentatges, convertir a 0-1
74
  man_prob = gender_info.get('Man', 0) / 100.0
75
  woman_prob = gender_info.get('Woman', 0) / 100.0
76
+ logger.info(f"[DeepFace] Extraído de dict - Man: {man_prob:.3f}, Woman: {woman_prob:.3f}")
77
  else:
78
  # Fallback si el format és diferent
79
+ logger.warning(f"[DeepFace] gender_info NO es dict, usando fallback 0.5/0.5")
80
  man_prob = 0.5
81
  woman_prob = 0.5
82
 
83
  # Determinar gènere basat en les probabilitats
84
  gender_diff = abs(man_prob - woman_prob)
85
 
86
+ logger.info(f"[DeepFace] Diferencia Man-Woman: {gender_diff:.3f} (threshold neutral={GENDER_NEUTRAL_THRESHOLD})")
87
+
88
  # Si la diferència és petita (< threshold), considerar neutre
89
  if gender_diff < GENDER_NEUTRAL_THRESHOLD:
90
  gender = 'Neutral'
91
  gender_confidence = 0.5
92
+ logger.info(f"[DeepFace] → Asignado NEUTRAL (diferencia {gender_diff:.3f} < {GENDER_NEUTRAL_THRESHOLD})")
93
  else:
94
  gender = 'Man' if man_prob > woman_prob else 'Woman'
95
  gender_confidence = max(man_prob, woman_prob)
96
+ logger.info(f"[DeepFace] → Asignado {gender.upper()} (man_prob={man_prob:.3f}, woman_prob={woman_prob:.3f})")
97
 
98
  # Confiança de detecció de cara
99
+ # DeepFace no proporciona score directamente en analyze(), pero si retornó resultado
100
+ # asumimos que es cara válida con confianza alta
101
+ face_confidence = result.get('face_confidence', 0.9) # Default alto si detecta
102
 
103
  # Si DeepFace va retornar resultat, assumir que és cara vàlida
104
  is_valid_face = True
105
 
106
+ logger.info(f"[DeepFace] ===== RESUMEN FINAL =====")
107
+ logger.info(f"[DeepFace] is_valid_face: {is_valid_face}")
108
+ logger.info(f"[DeepFace] face_confidence: {face_confidence:.3f}")
109
+ logger.info(f"[DeepFace] gender: {gender}")
110
+ logger.info(f"[DeepFace] gender_confidence: {gender_confidence:.3f}")
111
+ logger.info(f"[DeepFace] man_prob: {man_prob:.3f}")
112
+ logger.info(f"[DeepFace] woman_prob: {woman_prob:.3f}")
113
+ logger.info(f"[DeepFace] ==========================")
114
 
115
  return {
116
  'is_valid_face': is_valid_face,