feat: OCR independent chunks + TMDb seed with file_uuid

- Rule 1 now creates OCR-only chunks instead of merging into ASRX
- generate_seed_embeddings.py supports --file-uuid parameter
- get_seeds() filters by file_uuid
- identity_matcher.py uses file_uuid for seed matching
- Push QDRANT_API_KEY to Python subprocesses
- Face clustering uses frame+bbox matching instead of face_id
- Portal uses JWT authentication
- FilesView filter logic fixed
This commit is contained in:
Accusys
2026-07-06 08:56:56 +08:00
parent cb604b74ec
commit 799ede5a0e
10 changed files with 147 additions and 38 deletions
+48 -7
View File
@@ -104,9 +104,14 @@ def main():
print(f"[FACE_CLUSTER] Loading embeddings from Qdrant for {UUID}...")
try:
import requests
qdrant_url = "http://localhost:6333"
qdrant_url = os.environ.get("QDRANT_URL", "http://localhost:6333")
qdrant_api_key = os.environ.get("QDRANT_API_KEY", "")
collection = "_faces"
headers = {}
if qdrant_api_key:
headers["api-key"] = qdrant_api_key
# Query all embeddings for this file_uuid
response = requests.post(
f"{qdrant_url}/collections/{collection}/points/scroll",
@@ -118,7 +123,8 @@ def main():
},
"limit": 10000,
"with_vector": True
}
},
headers=headers
)
if response.status_code == 200:
@@ -140,22 +146,57 @@ def main():
print(f"[FACE_CLUSTER] Failed to load embeddings from Qdrant: {e}")
embedding_map = {}
# Use embeddings from Qdrant or face.json
# Use embeddings from Qdrant - match by frame + bbox
embeddings = []
face_refs = []
print(f"🔍 Collecting face embeddings for {UUID}...")
# Build a lookup: (frame, bbox_center) -> embedding
# Use frame number and approximate bbox center for matching
qdrant_by_frame = {}
for point in points:
payload = point.get("payload", {})
frame = payload.get("frame")
bbox = payload.get("bbox", {})
vector = point.get("vector")
if frame is not None and vector:
# Use frame + bbox center as key
cx = bbox.get("x", 0) + bbox.get("width", 0) // 2
cy = bbox.get("y", 0) + bbox.get("height", 0) // 2
key = (frame, cx, cy)
if key not in qdrant_by_frame:
qdrant_by_frame[key] = vector
print(f"[FACE_CLUSTER] Built Qdrant lookup with {len(qdrant_by_frame)} entries")
for frame_idx, frame_obj in enumerate(frames_list):
frame_num = frame_obj.get("frame", frame_idx)
faces = frame_obj.get("faces", [])
if not faces:
continue
for face_idx, face in enumerate(faces):
face_id = face.get("face_id")
if face_id and face_id in embedding_map:
embeddings.append(embedding_map[face_id])
face_refs.append({"frame_idx": frame_idx, "face_idx": face_idx, "face_id": face_id})
x = face.get("x", 0)
y = face.get("y", 0)
w = face.get("width", 0)
h = face.get("height", 0)
cx = x + w // 2
cy = y + h // 2
# Try exact match first
key = (frame_num, cx, cy)
if key in qdrant_by_frame:
embeddings.append(qdrant_by_frame[key])
face_refs.append({"frame_idx": frame_idx, "face_idx": face_idx})
continue
# Try approximate match (within 50 pixels)
for (qf, qx, qy), vec in qdrant_by_frame.items():
if qf == frame_num and abs(qx - cx) < 50 and abs(qy - cy) < 50:
embeddings.append(vec)
face_refs.append({"frame_idx": frame_idx, "face_idx": face_idx})
break
if not embeddings:
print("❌ No embeddings found in Qdrant.")
+33 -16
View File
@@ -46,11 +46,12 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
FACENET_PATH = os.path.join(SCRIPT_DIR, "..", "models", "facenet512.mlpackage")
def get_tmdb_identities(limit: int = None) -> List[Dict]:
def get_tmdb_identities(limit: int = None, file_uuid: str = None) -> List[Dict]:
"""Query PG for TMDb identities with profile photos
Args:
limit: Max identities to process
file_uuid: Filter by file_uuid via file_identities table
Returns:
List of {id, uuid, name, tmdb_id, tmdb_profile}
@@ -63,20 +64,34 @@ def get_tmdb_identities(limit: int = None) -> List[Dict]:
if SCHEMA == "public":
table = "identities"
file_table = "file_identities"
else:
table = f"{SCHEMA}.identities"
file_table = f"{SCHEMA}.file_identities"
query = f"""
SELECT id, uuid, name, tmdb_id, tmdb_profile
FROM {table}
WHERE source = 'tmdb' AND tmdb_profile IS NOT NULL
ORDER BY id
"""
if file_uuid:
query = f"""
SELECT DISTINCT i.id, i.uuid, i.name, i.tmdb_id, i.tmdb_profile
FROM {table} i
JOIN {file_table} fi ON fi.identity_id = i.id
WHERE i.source = 'tmdb' AND i.tmdb_profile IS NOT NULL
AND fi.file_uuid = %s
ORDER BY i.id
"""
if limit:
query += f" LIMIT {limit}"
cur.execute(query, (file_uuid,))
else:
query = f"""
SELECT id, uuid, name, tmdb_id, tmdb_profile
FROM {table}
WHERE source = 'tmdb' AND tmdb_profile IS NOT NULL
ORDER BY id
"""
if limit:
query += f" LIMIT {limit}"
cur.execute(query)
if limit:
query += f" LIMIT {limit}"
cur.execute(query)
rows = cur.fetchall()
cur.close()
conn.close()
@@ -180,7 +195,7 @@ def extract_face_embedding(image_path: str) -> Optional[List[float]]:
return None
def generate_seed_embeddings(limit: int = None, dry_run: bool = False) -> Dict:
def generate_seed_embeddings(limit: int = None, dry_run: bool = False, file_uuid: str = None) -> Dict:
"""Generate embeddings for all TMDb identities
Args:
@@ -198,14 +213,14 @@ def generate_seed_embeddings(limit: int = None, dry_run: bool = False) -> Dict:
"errors": [],
}
identities = get_tmdb_identities(limit)
identities = get_tmdb_identities(limit, file_uuid)
result["total"] = len(identities)
if not identities:
print("[SEED] No TMDb identities with profile photos")
print(f"[SEED] No TMDb identities with profile photos{' for ' + file_uuid if file_uuid else ''}")
return result
print(f"[SEED] Found {len(identities)} TMDb identities")
print(f"[SEED] Found {len(identities)} TMDb identities{' for ' + file_uuid if file_uuid else ''}")
if not dry_run:
ensure_seeds_collection()
@@ -259,6 +274,7 @@ def generate_seed_embeddings(limit: int = None, dry_run: bool = False) -> Dict:
name=name,
embedding=embedding,
source="tmdb",
file_uuid=file_uuid,
tmdb_id=tmdb_id,
)
result["success"] += 1
@@ -280,12 +296,13 @@ def main():
parser.add_argument("--dry-run", action="store_true", help="Don't push to Qdrant")
parser.add_argument("--tmdb-api-key", help="TMDb API key (optional, for rate limiting)")
parser.add_argument("--output", help="Output JSON file path")
parser.add_argument("--file-uuid", help="File UUID to generate seeds for")
args = parser.parse_args()
if args.tmdb_api_key:
TMDB_API_KEY = args.tmdb_api_key
result = generate_seed_embeddings(args.limit, args.dry_run)
result = generate_seed_embeddings(args.limit, args.dry_run, args.file_uuid)
output_json = json.dumps(result, indent=2, ensure_ascii=False)
+2 -2
View File
@@ -79,10 +79,10 @@ def match_faces_round_1(file_uuid: str) -> dict:
{trace_id: {identity_id, identity_uuid, name, score, suggested_by: 'tmdb'}}
"""
traces = get_trace_representatives(file_uuid)
seeds = get_seeds(source="tmdb")
seeds = get_seeds(source="tmdb", file_uuid=file_uuid)
if not seeds:
print("[MATCH] No TMDb seeds available")
print(f"[MATCH] No TMDb seeds available for {file_uuid}")
return {}
suggestions = {}
+9 -6
View File
@@ -493,11 +493,12 @@ def push_seed_embedding(
raise RuntimeError(f"Qdrant seed push failed: HTTP {e.code} - {error_body}")
def get_seeds(source: str = None) -> list:
def get_seeds(source: str = None, file_uuid: str = None) -> list:
"""Get all seed points
Args:
source: Filter by source ('tmdb', 'manual', 'propagation'), or None for all
file_uuid: Filter by file_uuid, or None for all
Returns:
List of seed points with payload and vector
@@ -514,12 +515,14 @@ def get_seeds(source: str = None) -> list:
"with_vector": True,
}
filters = []
if source:
body["filter"] = {
"must": [
{"key": "source", "match": {"value": source}}
]
}
filters.append({"key": "source", "match": {"value": source}})
if file_uuid:
filters.append({"key": "file_uuid", "match": {"value": file_uuid}})
if filters:
body["filter"] = {"must": filters}
if offset:
body["offset"] = offset