Public Access
192 lines
5.9 KiB
C#
192 lines
5.9 KiB
C#
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<float[]> 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<float[]> DecodeCompatible(
|
|
SpeakerIdentity identity,
|
|
string modelId,
|
|
ILogger logger)
|
|
{
|
|
return DecodeCompatibleEntries(identity, modelId, logger)
|
|
.Select(entry => entry.Vector)
|
|
.ToList();
|
|
}
|
|
|
|
public static IReadOnlyList<DecodedSpeakerVoiceVector> DecodeCompatibleEntries(
|
|
SpeakerIdentity identity,
|
|
string modelId,
|
|
ILogger logger)
|
|
{
|
|
var vectors = new List<DecodedSpeakerVoiceVector>();
|
|
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));
|
|
}
|
|
}
|