Add anonymous speaker tracking (online diarization)

Unrecognized speakers now get stable IDs like "unknown_a7f3" instead
of None. Uses online clustering of Resemblyzer embeddings:
- Matches against tracked anonymous speakers (cosine > 0.70)
- Updates running average embedding on re-identification
- Creates new ID from SHA-256 hash of quantized embedding
- Expires after 1 hour of silence, max 10 tracked simultaneously

New API: POST /speakers/promote?anon_id=unknown_a7f3&name=Alex
Promotes an anonymous speaker to enrolled using their averaged embedding.

Flow: unknown person speaks → "unknown_a7f3" → you ask "who's that?" →
promote to "Bob" → now recognized by name going forward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex
2026-04-12 21:58:30 -05:00
parent 5c72491ee9
commit 05034acd27
2 changed files with 114 additions and 7 deletions

View File

@@ -868,6 +868,19 @@ async def list_speakers():
return {"speakers": speaker_recognizer.list_speakers()}
@app.post("/speakers/promote")
async def promote_speaker(anon_id: str, name: str):
"""Promote an anonymous speaker (unknown_XXXX) to an enrolled speaker.
Uses their accumulated embedding average — no new audio needed."""
if speaker_recognizer is None:
raise HTTPException(status_code=503, detail="Speaker recognition not available")
if not anon_id.startswith("unknown_"):
raise HTTPException(status_code=400, detail="anon_id must start with 'unknown_'")
if speaker_recognizer.promote_anonymous(anon_id, name):
return {"promoted": anon_id, "name": name, "speakers": speaker_recognizer.list_speakers()}
raise HTTPException(status_code=404, detail=f"Anonymous speaker '{anon_id}' not found")
@app.delete("/speakers/{name}")
async def delete_speaker(name: str):
"""Remove a speaker."""