Spaces:
Running
Running
File size: 7,558 Bytes
e4f2040 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | import os
from concurrent.futures import ThreadPoolExecutor, as_completed
import numpy as np
from models.face_recognition import (
AdaFaceRecognition,
extract_faces,
_get_face_analyzer,
_dist_to_confidence,
_face_to_base64,
)
from utils.vtt_parser import parse_sprite_vtt
HIT_CONF_THRESH = 0.15
CAST_SCORE_THRESH = 0.9
CAST_HIT_THRESH = 2
RECLAIM_SIM_THRESH = 0.35
GAP_TOLERANCE = 2
TOP_K_CANDIDATES = 10
CONFIDENCE_FLOOR = 0.05
# Core-aware parallelism. On Hugging Face Spaces (low vCPU), this auto-scales down.
MAX_WORKERS = max(1, os.cpu_count() or 1)
# Shared buffalo_sc 640x640 analyzer — ONNX sessions are thread-safe.
_sprite_ana = None
def _get_sprite_analyzer():
global _sprite_ana
if _sprite_ana is None:
_sprite_ana = _get_face_analyzer(det_size=(640, 640))
return _sprite_ana
def _detect_one(frame_args):
"""Detect faces in a single frame. Called by ThreadPoolExecutor."""
idx, frame, mc, ana = frame_args
results = []
for d in extract_faces(frame, min_confidence=mc, analyzer=ana):
results.append((d['face'], idx))
return results
def _detect_faces(frames, min_confidence):
"""Per-frame face detection with buffalo_sc at 640x640.
Single-core: sequential per-frame.
Multi-core: parallel per-frame via ThreadPoolExecutor.
"""
n_frames = len(frames)
if n_frames == 0:
return []
ana = _get_sprite_analyzer()
if MAX_WORKERS <= 1:
results = []
for idx, frame in enumerate(frames):
for d in extract_faces(frame, min_confidence=min_confidence, analyzer=ana):
results.append((d['face'], idx))
return results
face_crops = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
args = [(idx, f, min_confidence, ana) for idx, f in enumerate(frames)]
for fut in as_completed(pool.submit(_detect_one, a) for a in args):
face_crops.extend(fut.result())
face_crops.sort(key=lambda x: x[1])
return face_crops
def identify_sprite_performers(sprite_image, vtt_text, data_manager,
detection_threshold=0.5):
yield (0.0, "Parsing VTT cue sheet")
cues = parse_sprite_vtt(vtt_text)
if not cues:
raise ValueError("No cues found in VTT file")
yield (0.05, "Extracting sprite frames")
sprite = np.array(sprite_image)
frames = [sprite[c['y']:c['y'] + c['h'], c['x']:c['x'] + c['w']] for c in cues]
yield (0.1, f"Detecting faces across {len(frames)} frames\u2026")
face_crops = _detect_faces(frames, min_confidence=detection_threshold)
yield (0.6, f"Found faces in {len(set(fc[1] for fc in face_crops))} frames")
if not face_crops:
raise ValueError("Face detection found no faces in any sprite frame.")
yield (0.6, f"Generating embeddings for {len(face_crops)} faces\u2026")
face_embs = AdaFaceRecognition().get_adaface_embeddings_batch(
np.stack([fc[0] for fc in face_crops], axis=0)
)
yield (0.7, f"Matching {len(face_embs)} faces against performer index\u2026")
performer_data = {}
face_results = []
for emb in face_embs:
ids, dists = data_manager.query_adaface_index(emb, TOP_K_CANDIDATES)
confident = []
all_hits = []
for pid, dist in zip(ids, dists):
conf = _dist_to_confidence(dist)
if conf < CONFIDENCE_FLOOR:
break
info = data_manager.get_performer_info(pid, conf, dist)
if info and info.get('name'):
p = performer_data.setdefault(pid, {"score": 0.0, "hits": 0, "conf_hits": 0})
p["score"] += conf
p["hits"] += 1
all_hits.append((pid, conf))
if conf >= HIT_CONF_THRESH:
p["conf_hits"] += 1
confident.append(pid)
face_results.append((confident, emb, all_hits))
yield (0.85, "Clustering results by performer")
cast = [pid for pid, p in performer_data.items()
if p["score"] >= CAST_SCORE_THRESH and p["conf_hits"] >= CAST_HIT_THRESH]
if not cast and face_crops:
raise ValueError(
f"Faces detected in {len(set(fc[1] for fc in face_crops))} frames, but "
"no performer could be identified with confidence. "
"Individual frame matches were too weak to cluster into a reliable result."
)
centroids = {}
for pid in cast:
embs = [face_results[i][1] for i in range(len(face_results)) if pid in face_results[i][0]]
if embs:
centroids[pid] = np.mean(embs, axis=0)
cue_sets = {pid: set() for pid in cast}
face_indices = {pid: [] for pid in cast}
for i, (confident, _, _) in enumerate(face_results):
for pid in confident:
if pid not in cue_sets:
continue
cue_sets[pid].add(face_crops[i][1])
face_indices[pid].append(i)
for i, (confident, emb, _) in enumerate(face_results):
if not confident and centroids:
best = max(((pid, float(np.dot(emb, c))) for pid, c in centroids.items()), key=lambda x: x[1], default=(None, 0))
if best[0] is not None and best[1] >= RECLAIM_SIM_THRESH:
cue_sets[best[0]].add(face_crops[i][1])
face_indices[best[0]].append(i)
yield (0.95, "Building response")
response = []
for pid in cast:
info = data_manager.get_performer_info(pid, 0, 0)
if not info:
continue
centroid = centroids[pid]
best_idx = max(face_indices[pid], key=lambda i: float(np.dot(face_results[i][1], centroid))) if face_indices[pid] else None
thumb = _face_to_base64(face_crops[best_idx][0]) if best_idx is not None else info.get('image', '')
shot_groups = _group_adjacent_indices(cue_sets[pid])
shot_confs = []
for group in shot_groups:
max_in_group = 0.0
for idx in face_indices[pid]:
if face_crops[idx][1] not in group:
continue
for hit_pid, conf in face_results[idx][2]:
if hit_pid == pid and conf > max_in_group:
max_in_group = conf
shot_confs.append(max_in_group)
response.append({
"id": pid,
"name": info['name'],
"confidence": int((1.0 - np.prod([1.0 - c for c in shot_confs])) * 100),
"thumbnail": thumb,
"time_ranges": _merge_time_ranges(cue_sets[pid], cues),
"hit_count": performer_data[pid]["hits"],
"shot_count": len(shot_confs),
"performer_url": info.get('performer_url', ''),
"source": info.get('source', ''),
"gender": info.get('gender'),
})
response.sort(key=lambda x: x['hit_count'], reverse=True)
yield {"performers": response}
def _group_adjacent_indices(indices, gap_tolerance=GAP_TOLERANCE):
if not indices:
return []
sorted_idx = sorted(set(indices))
groups = [[sorted_idx[0]]]
for i in range(1, len(sorted_idx)):
if sorted_idx[i] - sorted_idx[i - 1] <= gap_tolerance + 1:
groups[-1].append(sorted_idx[i])
else:
groups.append([sorted_idx[i]])
return groups
def _merge_time_ranges(cue_indices, cues, gap_tolerance=GAP_TOLERANCE):
groups = _group_adjacent_indices(cue_indices, gap_tolerance)
return [{"start": cues[g[0]]["start"], "end": cues[g[-1]]["end"]} for g in groups]
|