diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/asr/SpeakerClusterer.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/asr/SpeakerClusterer.kt index 26066a1..927a1fb 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/asr/SpeakerClusterer.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/asr/SpeakerClusterer.kt @@ -159,13 +159,21 @@ class SpeakerClusterer( } val assignment = if (best != null && bestSim >= matchThreshold) { - val n = minOf(best.count, maxSamplesPerCentroid) - val merged = FloatArray(emb.size) { i -> (best.centroid[i] * n + emb[i]) / (n + 1) } - l2normalize(merged) - best.centroid = merged - best.count += 1 - repo.speakers.updateEmbedding(best.id, merged.toLeBytes(), best.count) - best.id to bestSim + if (autoIndexOf(best.name) == null) { + // 人工命名 anchor:只归属、**冻结质心不漂移**。否则 anchor 吸入相邻 + // 说话人的段后质心会朝其漂移、再吸走更多 → 雪球式吞并(实测秦子琛 + // 把宜博从 ~125 段啃到只剩 56)。冻结后 anchor 始终是用户标定的方向。 + best.id to bestSim + } else { + // 自动簇:滑动均值精修质心(自适应、不被早期样本钉死)。 + val n = minOf(best.count, maxSamplesPerCentroid) + val merged = FloatArray(emb.size) { i -> (best.centroid[i] * n + emb[i]) / (n + 1) } + l2normalize(merged) + best.centroid = merged + best.count += 1 + repo.speakers.updateEmbedding(best.id, merged.toLeBytes(), best.count) + best.id to bestSim + } } else if (seg.durationMs >= minNewClusterMs) { val norm = emb.copyOf().also { l2normalize(it) } seq += 1