Upload 2 files
Browse files- api.py +129 -39
- 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
|
|
|
|
| 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 (
|
|
|
|
| 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
|
| 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}:
|
| 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}:
|
| 645 |
-
|
| 646 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
cluster_labels = sorted(centroids.keys())
|
| 1038 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
|
| 1062 |
# Aplicar fusió als clusters
|
| 1063 |
new_clusters = {}
|
| 1064 |
for lbl, idxs in clusters.items():
|
| 1065 |
-
|
| 1066 |
-
if
|
| 1067 |
-
new_clusters[
|
| 1068 |
-
new_clusters[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1069 |
|
| 1070 |
-
clusters =
|
| 1071 |
final_clusters = len(clusters)
|
| 1072 |
eliminated = initial_clusters - final_clusters
|
| 1073 |
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 89 |
-
#
|
| 90 |
-
face_confidence = result.get('face_confidence', 0.9) # Default
|
| 91 |
|
| 92 |
# Si DeepFace va retornar resultat, assumir que és cara vàlida
|
| 93 |
is_valid_face = True
|
| 94 |
|
| 95 |
-
logger.info(f"[DeepFace]
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|