using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; namespace MeetingAssistant.Speakers; internal sealed record DecodedSpeakerVoiceVector( SpeakerVoiceVector Stored, float[] Vector); internal static class ResemblyzerVectorContract { public const int Dimensions = 256; } internal static class SpeakerVoiceVectors { public static float[] Decode(SpeakerVoiceVector stored) { if (stored.Dimensions != ResemblyzerVectorContract.Dimensions || stored.VectorBytes.Length != stored.Dimensions * sizeof(float)) { throw new InvalidDataException( $"Stored voice vector {stored.Id} has invalid dimension or byte length metadata."); } var vector = new float[stored.Dimensions]; for (var index = 0; index < vector.Length; index++) { vector[index] = BinaryPrimitives.ReadSingleLittleEndian( stored.VectorBytes.AsSpan(index * sizeof(float), sizeof(float))); if (!float.IsFinite(vector[index])) { throw new InvalidDataException($"Stored voice vector {stored.Id} contains a non-finite value."); } } return vector; } public static int AddDistinct( SpeakerIdentity identity, IEnumerable vectors, string modelId, int maximumCount, DateTimeOffset createdAt) { var limit = Math.Max(1, maximumCount); var fingerprints = identity.VoiceVectors .Select(vector => vector.Fingerprint) .ToHashSet(StringComparer.Ordinal); var added = 0; foreach (var vector in vectors) { if (identity.VoiceVectors.Count >= limit) { break; } var bytes = Encode(vector); var fingerprint = Fingerprint(modelId, bytes); if (!fingerprints.Add(fingerprint)) { continue; } identity.VoiceVectors.Add(new SpeakerVoiceVector { ModelId = modelId, Dimensions = vector.Length, VectorBytes = bytes, Fingerprint = fingerprint, CreatedAt = createdAt }); added++; } return added; } public static IReadOnlyList DecodeCompatible( SpeakerIdentity identity, string modelId, ILogger logger) { return DecodeCompatibleEntries(identity, modelId, logger) .Select(entry => entry.Vector) .ToList(); } public static IReadOnlyList DecodeCompatibleEntries( SpeakerIdentity identity, string modelId, ILogger logger) { var vectors = new List(); foreach (var stored in identity.VoiceVectors .Where(vector => string.Equals(vector.ModelId, modelId, StringComparison.Ordinal)) .OrderBy(vector => vector.CreatedAt) .ThenBy(vector => vector.Id)) { try { vectors.Add(new DecodedSpeakerVoiceVector(stored, Decode(stored))); } catch (InvalidDataException exception) { logger.LogWarning( exception, "Ignoring invalid stored voice vector {VectorId} for identity {IdentityId}", stored.Id, identity.Id); } } return vectors; } public static double Cosine(float[] first, float[] second) { double dot = 0; for (var index = 0; index < first.Length; index++) { dot += first[index] * second[index]; } return dot; } public static float[] Normalize(float[]? vector, string description) { if (vector is null || vector.Length != ResemblyzerVectorContract.Dimensions) { throw new InvalidDataException( $"{description} has {vector?.Length ?? 0} dimensions; expected {ResemblyzerVectorContract.Dimensions}."); } double magnitudeSquared = 0; foreach (var value in vector) { if (!float.IsFinite(value)) { throw new InvalidDataException($"{description} contains a non-finite value."); } magnitudeSquared += value * value; } var magnitude = Math.Sqrt(magnitudeSquared); if (magnitude <= double.Epsilon) { throw new InvalidDataException($"{description} has zero magnitude."); } return vector.Select(value => (float)(value / magnitude)).ToArray(); } private static byte[] Encode(float[] vector) { if (vector.Length != ResemblyzerVectorContract.Dimensions) { throw new InvalidDataException( $"Voice vector has {vector.Length} dimensions; expected {ResemblyzerVectorContract.Dimensions}."); } var bytes = new byte[vector.Length * sizeof(float)]; for (var index = 0; index < vector.Length; index++) { if (!float.IsFinite(vector[index])) { throw new InvalidDataException("Voice vector contains a non-finite value."); } BinaryPrimitives.WriteSingleLittleEndian( bytes.AsSpan(index * sizeof(float), sizeof(float)), vector[index]); } return bytes; } private static string Fingerprint(string modelId, byte[] vectorBytes) { var modelBytes = Encoding.UTF8.GetBytes(modelId); var input = new byte[modelBytes.Length + 1 + vectorBytes.Length]; modelBytes.CopyTo(input, 0); input[modelBytes.Length] = 0; vectorBytes.CopyTo(input, modelBytes.Length + 1); return Convert.ToHexString(SHA256.HashData(input)); } }