Files
linkedstorm/app/cron/cluster_unknowns.py
T
2026-05-28 10:49:24 -05:00

65 lines
2.2 KiB
Python

import sqlite3
import numpy as np
from app.shared.config import Settings
from app.pipeline.recognition import unpack_embedding, cosine_similarity
def cluster_unknown_speakers(settings: Settings, db: sqlite3.Connection) -> None:
"""
Groups unknown/ambiguous utterances from the last 7 days by voice similarity.
Assigns speaker_id from an existing 'Unknown #N' speaker or creates a new one.
Threshold: 0.70 cosine similarity.
"""
CLUSTER_THRESHOLD = 0.70
rows = db.execute("""
SELECT id, embedding FROM utterances
WHERE match_status IN ('unknown', 'ambiguous')
AND embedding IS NOT NULL
AND date(start_time) >= date('now', '-7 days')
ORDER BY start_time ASC
""").fetchall()
if not rows:
return
clusters: list = []
cluster_centroids: list = []
for row in rows:
emb = unpack_embedding(row["embedding"])
best_cluster = None
best_sim = 0.0
for i, centroid in enumerate(cluster_centroids):
sim = cosine_similarity(emb, centroid)
if sim > best_sim and sim >= CLUSTER_THRESHOLD:
best_sim = sim
best_cluster = i
if best_cluster is not None:
clusters[best_cluster].append(row["id"])
n = len(clusters[best_cluster])
cluster_centroids[best_cluster] = (
(cluster_centroids[best_cluster] * (n - 1) + emb) / n
)
else:
clusters.append([row["id"]])
cluster_centroids.append(emb)
for i, cluster in enumerate(clusters):
if len(cluster) < 2:
continue
name = f"Unknown #{i + 1}"
existing = db.execute("SELECT id FROM speakers WHERE name=?", (name,)).fetchone()
if existing:
speaker_id = existing["id"]
else:
cur = db.execute("INSERT INTO speakers (name) VALUES (?)", (name,))
db.commit()
speaker_id = cur.lastrowid
for utt_id in cluster:
db.execute(
"UPDATE utterances SET speaker_id=?, match_status='ambiguous' WHERE id=?",
(speaker_id, utt_id),
)
db.commit()