Relaxed constraints on play queue auto filling + cleaned audio similarity constraints
This commit is contained in:
+27
-53
@@ -49,13 +49,13 @@
|
|||||||
#include "math/PrincipalComponents.hpp"
|
#include "math/PrincipalComponents.hpp"
|
||||||
#include "math/StatsAccumulator.hpp"
|
#include "math/StatsAccumulator.hpp"
|
||||||
|
|
||||||
|
#include "InterpolationFitConstraint.hpp"
|
||||||
|
#include "MaxDistanceConstraint.hpp"
|
||||||
#include "NearDuplicateEmbeddingConstraint.hpp"
|
#include "NearDuplicateEmbeddingConstraint.hpp"
|
||||||
|
#include "SmoothTransitionConstraint.hpp"
|
||||||
#include "track-selection-constraints/DuplicateTrackConstraint.hpp"
|
#include "track-selection-constraints/DuplicateTrackConstraint.hpp"
|
||||||
#include "track-selection-constraints/InterpolationFitConstraint.hpp"
|
|
||||||
#include "track-selection-constraints/MaxDistanceConstraint.hpp"
|
|
||||||
#include "track-selection-constraints/SameArtistConstraint.hpp"
|
#include "track-selection-constraints/SameArtistConstraint.hpp"
|
||||||
#include "track-selection-constraints/SameReleaseConstraint.hpp"
|
#include "track-selection-constraints/SameReleaseConstraint.hpp"
|
||||||
#include "track-selection-constraints/SmoothTransitionConstraint.hpp"
|
|
||||||
|
|
||||||
#include "Types.hpp"
|
#include "Types.hpp"
|
||||||
|
|
||||||
@@ -121,17 +121,17 @@ namespace lms::recommendation
|
|||||||
_similarityEvaluator = {};
|
_similarityEvaluator = {};
|
||||||
_similarityEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
_similarityEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
||||||
_similarityEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold));
|
_similarityEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold));
|
||||||
_similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint>(_trackDistanceThreshold));
|
_similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint<ReducedDimCount>>(_trackVectors, _trackDistanceThreshold));
|
||||||
_similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint>(), interpolationFitWeight);
|
_similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
|
||||||
_similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint>(), smoothTransitionWeight);
|
_similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
|
||||||
_similarityEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
|
_similarityEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
|
||||||
_similarityEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
|
_similarityEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
|
||||||
|
|
||||||
_pathEvaluator = {};
|
_pathEvaluator = {};
|
||||||
_pathEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
_pathEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
||||||
_pathEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold));
|
_pathEvaluator.addHardConstraint(std::make_unique<NearDuplicateEmbeddingConstraint<ReducedDimCount>>(_trackVectors, nearDuplicateThreshold));
|
||||||
_pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint>(), interpolationFitWeight);
|
_pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
|
||||||
_pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint>(), smoothTransitionWeight);
|
_pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
|
||||||
_pathEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
|
_pathEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
|
||||||
_pathEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
|
_pathEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
|
||||||
}
|
}
|
||||||
@@ -210,8 +210,6 @@ namespace lms::recommendation
|
|||||||
rankedTracks.resize(candidateCount);
|
rankedTracks.resize(candidateCount);
|
||||||
|
|
||||||
// Greedy selection: at each step pick the candidate with the lowest penalized score.
|
// Greedy selection: at each step pick the candidate with the lowest penalized score.
|
||||||
// distanceToPrevious is the cosine distance to the last selected track, so that
|
|
||||||
// SmoothTransitionConstraint penalises large acoustic jumps between consecutive results.
|
|
||||||
// Pre-seed selectedTracks with the input tracks so that soft constraints (same release,
|
// Pre-seed selectedTracks with the input tracks so that soft constraints (same release,
|
||||||
// same artist) treat them as already taken, preventing the first results from being
|
// same artist) treat them as already taken, preventing the first results from being
|
||||||
// from the same release/artist as the inputs.
|
// from the same release/artist as the inputs.
|
||||||
@@ -219,8 +217,6 @@ namespace lms::recommendation
|
|||||||
selectedTracks.reserve(selectedTracks.size() + maxCount);
|
selectedTracks.reserve(selectedTracks.size() + maxCount);
|
||||||
res.reserve(maxCount);
|
res.reserve(maxCount);
|
||||||
|
|
||||||
const ReducedVector* previousVector{};
|
|
||||||
|
|
||||||
while (res.size() < maxCount && !rankedTracks.empty())
|
while (res.size() < maxCount && !rankedTracks.empty())
|
||||||
{
|
{
|
||||||
std::optional<std::size_t> bestIdx;
|
std::optional<std::size_t> bestIdx;
|
||||||
@@ -228,15 +224,12 @@ namespace lms::recommendation
|
|||||||
|
|
||||||
for (std::size_t i{}; i < rankedTracks.size(); ++i)
|
for (std::size_t i{}; i < rankedTracks.size(); ++i)
|
||||||
{
|
{
|
||||||
const auto& [candidateId, distanceToQuery]{ rankedTracks[i] };
|
const db::TrackId candidateId{ rankedTracks[i].first };
|
||||||
const ReducedVector* candidateVector{ _trackVectors.at(candidateId) };
|
|
||||||
const float distanceToPrevious{ previousVector ? math::NormalizedCosineDistance{ *previousVector }(*candidateVector) : 0.F };
|
|
||||||
|
|
||||||
const TrackCandidateContext context{
|
const TrackCandidateContext context{
|
||||||
.candidateTrackId = candidateId,
|
.candidateTrackId = candidateId,
|
||||||
.selectedTracks = selectedTracks,
|
.selectedTracks = selectedTracks,
|
||||||
.distanceToQuery = distanceToQuery,
|
.seedTrackIds = tracksId,
|
||||||
.distanceToPrevious = distanceToPrevious,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_similarityEvaluator.rejects(context))
|
if (_similarityEvaluator.rejects(context))
|
||||||
@@ -256,7 +249,6 @@ namespace lms::recommendation
|
|||||||
const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] };
|
const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] };
|
||||||
res.push_back({ .id = selectedId, .distance = distanceToQuery });
|
res.push_back({ .id = selectedId, .distance = distanceToQuery });
|
||||||
selectedTracks.push_back(selectedId);
|
selectedTracks.push_back(selectedId);
|
||||||
previousVector = _trackVectors.at(selectedId);
|
|
||||||
rankedTracks.erase(std::begin(rankedTracks) + static_cast<std::ptrdiff_t>(*bestIdx));
|
rankedTracks.erase(std::begin(rankedTracks) + static_cast<std::ptrdiff_t>(*bestIdx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,26 +276,28 @@ namespace lms::recommendation
|
|||||||
path.reserve(maxCount);
|
path.reserve(maxCount);
|
||||||
path.push_back(startTrackId);
|
path.push_back(startTrackId);
|
||||||
|
|
||||||
const ReducedVector* previousVector{ itStart->second };
|
static constexpr std::size_t NeighborCount{ 32 };
|
||||||
static constexpr std::size_t DefaultNeighborCount{ 16 };
|
|
||||||
static constexpr std::size_t BroadNeighborCount{ 64 };
|
|
||||||
std::size_t neighborCount{ DefaultNeighborCount };
|
|
||||||
const std::size_t interiorCount{ (maxCount > 2) ? (maxCount - 2) : 0 };
|
const std::size_t interiorCount{ (maxCount > 2) ? (maxCount - 2) : 0 };
|
||||||
|
|
||||||
auto evaluateCandidates = [&](const TrackResults& neighborList) -> std::optional<db::TrackId> {
|
for (std::size_t i{}; i < interiorCount; ++i)
|
||||||
|
{
|
||||||
|
const float t{ static_cast<float>(i + 1) / static_cast<float>(interiorCount + 1) };
|
||||||
|
auto queryPoint{ startVector + direction * t };
|
||||||
|
queryPoint.normalizeL2();
|
||||||
|
|
||||||
|
const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, NeighborCount, endTrackId) };
|
||||||
|
const db::TrackId stepSeedTrackId{ neighbors.empty() ? startTrackId : neighbors[0].id };
|
||||||
|
const std::array<db::TrackId, 1> stepSeedTrackIds{ stepSeedTrackId };
|
||||||
|
|
||||||
std::optional<db::TrackId> best;
|
std::optional<db::TrackId> best;
|
||||||
float bestScore{ std::numeric_limits<float>::max() };
|
float bestScore{ std::numeric_limits<float>::max() };
|
||||||
|
|
||||||
for (const auto& [candidateId, candidateDistance] : neighborList)
|
for (const auto& [candidateTrackId, candidateDistance] : neighbors)
|
||||||
{
|
{
|
||||||
const auto* candidateVector{ _trackVectors.at(candidateId) };
|
|
||||||
const float transitionDistance{ math::NormalizedCosineDistance{ *previousVector }(*candidateVector) };
|
|
||||||
|
|
||||||
const TrackCandidateContext context{
|
const TrackCandidateContext context{
|
||||||
.candidateTrackId = candidateId,
|
.candidateTrackId = candidateTrackId,
|
||||||
.selectedTracks = path,
|
.selectedTracks = path,
|
||||||
.distanceToQuery = candidateDistance,
|
.seedTrackIds = stepSeedTrackIds,
|
||||||
.distanceToPrevious = transitionDistance,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_pathEvaluator.rejects(context))
|
if (_pathEvaluator.rejects(context))
|
||||||
@@ -313,34 +307,14 @@ namespace lms::recommendation
|
|||||||
if (score < bestScore)
|
if (score < bestScore)
|
||||||
{
|
{
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
best = candidateId;
|
best = candidateTrackId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return best;
|
if (!best)
|
||||||
};
|
|
||||||
|
|
||||||
for (std::size_t i{}; i < interiorCount; ++i)
|
|
||||||
{
|
|
||||||
const float t{ static_cast<float>(i + 1) / static_cast<float>(interiorCount + 1) };
|
|
||||||
auto queryPoint{ startVector + direction * t };
|
|
||||||
queryPoint.normalizeL2();
|
|
||||||
|
|
||||||
const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) };
|
|
||||||
std::optional<db::TrackId> bestCandidate{ evaluateCandidates(neighbors) };
|
|
||||||
|
|
||||||
if (!bestCandidate && neighborCount < BroadNeighborCount)
|
|
||||||
{
|
|
||||||
neighborCount = BroadNeighborCount;
|
|
||||||
const auto broaderNeighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) };
|
|
||||||
bestCandidate = evaluateCandidates(broaderNeighbors);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bestCandidate)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
path.push_back(*bestCandidate);
|
path.push_back(*best);
|
||||||
previousVector = _trackVectors.at(*bestCandidate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxCount > 1)
|
if (maxCount > 1)
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "database/objects/TrackId.hpp"
|
||||||
|
#include "math/NormalizedCosineDistance.hpp"
|
||||||
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
#include "Types.hpp"
|
||||||
|
#include "track-selection-constraints/ITrackCandidateSoftConstraint.hpp"
|
||||||
|
|
||||||
|
namespace lms::recommendation
|
||||||
|
{
|
||||||
|
template<std::size_t ReducedDimCount>
|
||||||
|
class InterpolationFitConstraint : public ITrackCandidateSoftConstraint
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ReducedVector = math::Vector<ReducedDimCount, FloatType>;
|
||||||
|
using TrackVectorMap = std::unordered_map<db::TrackId, const ReducedVector*>;
|
||||||
|
|
||||||
|
explicit InterpolationFitConstraint(const TrackVectorMap& trackVectors)
|
||||||
|
: _trackVectors{ trackVectors }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~InterpolationFitConstraint() override = default;
|
||||||
|
InterpolationFitConstraint(const InterpolationFitConstraint&) = delete;
|
||||||
|
InterpolationFitConstraint& operator=(const InterpolationFitConstraint&) = delete;
|
||||||
|
|
||||||
|
float computeScore(const TrackCandidateContext& context) const override
|
||||||
|
{
|
||||||
|
const auto itCand{ _trackVectors.find(context.candidateTrackId) };
|
||||||
|
if (itCand == _trackVectors.cend())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
float best{ std::numeric_limits<float>::max() };
|
||||||
|
for (const db::TrackId seedId : context.seedTrackIds)
|
||||||
|
{
|
||||||
|
const auto it{ _trackVectors.find(seedId) };
|
||||||
|
if (it != _trackVectors.cend())
|
||||||
|
best = std::min(best, math::computeNormalizedCosineDistance(*it->second, *itCand->second));
|
||||||
|
}
|
||||||
|
return (best == std::numeric_limits<float>::max()) ? 0.F : best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TrackVectorMap& _trackVectors;
|
||||||
|
};
|
||||||
|
} // namespace lms::recommendation
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "database/objects/TrackId.hpp"
|
||||||
|
#include "math/NormalizedCosineDistance.hpp"
|
||||||
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
#include "Types.hpp"
|
||||||
|
#include "track-selection-constraints/ITrackCandidateHardConstraint.hpp"
|
||||||
|
|
||||||
|
namespace lms::recommendation
|
||||||
|
{
|
||||||
|
// Rejects any candidate whose distance to every seed track exceeds the threshold
|
||||||
|
template<std::size_t ReducedDimCount>
|
||||||
|
class MaxDistanceConstraint : public ITrackCandidateHardConstraint
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ReducedVector = math::Vector<ReducedDimCount, FloatType>;
|
||||||
|
using TrackVectorMap = std::unordered_map<db::TrackId, const ReducedVector*>;
|
||||||
|
|
||||||
|
MaxDistanceConstraint(const TrackVectorMap& trackVectors, float threshold)
|
||||||
|
: _trackVectors{ trackVectors }
|
||||||
|
, _threshold{ threshold }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~MaxDistanceConstraint() override = default;
|
||||||
|
MaxDistanceConstraint(const MaxDistanceConstraint&) = delete;
|
||||||
|
MaxDistanceConstraint& operator=(const MaxDistanceConstraint&) = delete;
|
||||||
|
|
||||||
|
bool rejects(const TrackCandidateContext& context) const override
|
||||||
|
{
|
||||||
|
const auto itCand{ _trackVectors.find(context.candidateTrackId) };
|
||||||
|
if (itCand == _trackVectors.cend())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const db::TrackId seedId : context.seedTrackIds)
|
||||||
|
{
|
||||||
|
const auto it{ _trackVectors.find(seedId) };
|
||||||
|
if (it != _trackVectors.cend() && math::computeNormalizedCosineDistance(*it->second, *itCand->second) <= _threshold)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !context.seedTrackIds.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TrackVectorMap& _trackVectors;
|
||||||
|
float _threshold;
|
||||||
|
};
|
||||||
|
} // namespace lms::recommendation
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "database/objects/TrackId.hpp"
|
||||||
|
#include "math/NormalizedCosineDistance.hpp"
|
||||||
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
#include "Types.hpp"
|
||||||
|
#include "track-selection-constraints/ITrackCandidateSoftConstraint.hpp"
|
||||||
|
|
||||||
|
namespace lms::recommendation
|
||||||
|
{
|
||||||
|
template<std::size_t ReducedDimCount>
|
||||||
|
class SmoothTransitionConstraint : public ITrackCandidateSoftConstraint
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ReducedVector = math::Vector<ReducedDimCount, FloatType>;
|
||||||
|
using TrackVectorMap = std::unordered_map<db::TrackId, const ReducedVector*>;
|
||||||
|
|
||||||
|
explicit SmoothTransitionConstraint(const TrackVectorMap& trackVectors)
|
||||||
|
: _trackVectors{ trackVectors }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~SmoothTransitionConstraint() override = default;
|
||||||
|
SmoothTransitionConstraint(const SmoothTransitionConstraint&) = delete;
|
||||||
|
SmoothTransitionConstraint& operator=(const SmoothTransitionConstraint&) = delete;
|
||||||
|
|
||||||
|
float computeScore(const TrackCandidateContext& context) const override
|
||||||
|
{
|
||||||
|
if (context.selectedTracks.empty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto itCand{ _trackVectors.find(context.candidateTrackId) };
|
||||||
|
if (itCand == _trackVectors.cend())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto it{ _trackVectors.find(context.selectedTracks.back()) };
|
||||||
|
if (it == _trackVectors.cend())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return math::computeNormalizedCosineDistance(*it->second, *itCand->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const TrackVectorMap& _trackVectors;
|
||||||
|
};
|
||||||
|
} // namespace lms::recommendation
|
||||||
@@ -278,6 +278,7 @@ namespace lms::recommendation
|
|||||||
const TrackCandidateContext context{
|
const TrackCandidateContext context{
|
||||||
.candidateTrackId = candidates[i],
|
.candidateTrackId = candidates[i],
|
||||||
.selectedTracks = selectedTracks,
|
.selectedTracks = selectedTracks,
|
||||||
|
.seedTrackIds = {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_trackEvaluator.rejects(context))
|
if (_trackEvaluator.rejects(context))
|
||||||
|
|||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ITrackCandidateSoftConstraint.hpp"
|
|
||||||
|
|
||||||
namespace lms::recommendation
|
|
||||||
{
|
|
||||||
class InterpolationFitConstraint : public ITrackCandidateSoftConstraint
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
float computeScore(const TrackCandidateContext& context) const override
|
|
||||||
{
|
|
||||||
return context.distanceToQuery;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace lms::recommendation
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ITrackCandidateHardConstraint.hpp"
|
|
||||||
|
|
||||||
namespace lms::recommendation
|
|
||||||
{
|
|
||||||
// Rejects any candidate whose distance to the query exceeds a given threshold
|
|
||||||
class MaxDistanceConstraint : public ITrackCandidateHardConstraint
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit MaxDistanceConstraint(float threshold)
|
|
||||||
: _threshold{ threshold }
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rejects(const TrackCandidateContext& context) const override
|
|
||||||
{
|
|
||||||
return context.distanceToQuery > _threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
float _threshold;
|
|
||||||
};
|
|
||||||
} // namespace lms::recommendation
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2026 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ITrackCandidateSoftConstraint.hpp"
|
|
||||||
|
|
||||||
namespace lms::recommendation
|
|
||||||
{
|
|
||||||
class SmoothTransitionConstraint : public ITrackCandidateSoftConstraint
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
float computeScore(const TrackCandidateContext& context) const override
|
|
||||||
{
|
|
||||||
return context.distanceToPrevious;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} // namespace lms::recommendation
|
|
||||||
+1
-2
@@ -29,7 +29,6 @@ namespace lms::recommendation
|
|||||||
{
|
{
|
||||||
db::TrackId candidateTrackId;
|
db::TrackId candidateTrackId;
|
||||||
std::span<const db::TrackId> selectedTracks;
|
std::span<const db::TrackId> selectedTracks;
|
||||||
float distanceToQuery{};
|
std::span<const db::TrackId> seedTrackIds;
|
||||||
float distanceToPrevious{};
|
|
||||||
};
|
};
|
||||||
} // namespace lms::recommendation
|
} // namespace lms::recommendation
|
||||||
|
|||||||
@@ -24,7 +24,10 @@
|
|||||||
#include "database/objects/TrackId.hpp"
|
#include "database/objects/TrackId.hpp"
|
||||||
#include "math/Vector.hpp"
|
#include "math/Vector.hpp"
|
||||||
|
|
||||||
|
#include "audio-similarity/InterpolationFitConstraint.hpp"
|
||||||
|
#include "audio-similarity/MaxDistanceConstraint.hpp"
|
||||||
#include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp"
|
#include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp"
|
||||||
|
#include "audio-similarity/SmoothTransitionConstraint.hpp"
|
||||||
#include "track-selection-constraints/DuplicateTrackConstraint.hpp"
|
#include "track-selection-constraints/DuplicateTrackConstraint.hpp"
|
||||||
#include "track-selection-constraints/SameArtistConstraint.hpp"
|
#include "track-selection-constraints/SameArtistConstraint.hpp"
|
||||||
#include "track-selection-constraints/SameReleaseConstraint.hpp"
|
#include "track-selection-constraints/SameReleaseConstraint.hpp"
|
||||||
@@ -41,7 +44,6 @@ namespace
|
|||||||
const db::TrackId T2{ 2 };
|
const db::TrackId T2{ 2 };
|
||||||
const db::TrackId T3{ 3 };
|
const db::TrackId T3{ 3 };
|
||||||
const db::TrackId T4{ 4 };
|
const db::TrackId T4{ 4 };
|
||||||
const db::TrackId T5{ 5 };
|
|
||||||
|
|
||||||
const db::ArtistId A1{ 10 };
|
const db::ArtistId A1{ 10 };
|
||||||
const db::ArtistId A2{ 20 };
|
const db::ArtistId A2{ 20 };
|
||||||
@@ -53,20 +55,20 @@ namespace
|
|||||||
TEST(DuplicateTrackConstraint, acceptsNewCandidate)
|
TEST(DuplicateTrackConstraint, acceptsNewCandidate)
|
||||||
{
|
{
|
||||||
const std::vector<db::TrackId> selected{ T1, T2 };
|
const std::vector<db::TrackId> selected{ T1, T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
|
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DuplicateTrackConstraint, rejectsAlreadySelected)
|
TEST(DuplicateTrackConstraint, rejectsAlreadySelected)
|
||||||
{
|
{
|
||||||
const std::vector<db::TrackId> selected{ T1, T2, T3 };
|
const std::vector<db::TrackId> selected{ T1, T2, T3 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx));
|
EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty)
|
TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty)
|
||||||
{
|
{
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
|
EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist)
|
|||||||
};
|
};
|
||||||
const SameArtistConstraint constraint{ meta };
|
const SameArtistConstraint constraint{ meta };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist)
|
|||||||
};
|
};
|
||||||
const SameArtistConstraint constraint{ meta };
|
const SameArtistConstraint constraint{ meta };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
|
||||||
}
|
}
|
||||||
@@ -105,7 +107,7 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist)
|
|||||||
const SameArtistConstraint constraint{ meta };
|
const SameArtistConstraint constraint{ meta };
|
||||||
|
|
||||||
const std::vector<db::TrackId> selected{ T3, T2 };
|
const std::vector<db::TrackId> selected{ T3, T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
|
||||||
}
|
}
|
||||||
@@ -113,23 +115,15 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist)
|
|||||||
TEST(SameArtistConstraint, trackOutsideWindowIsIgnored)
|
TEST(SameArtistConstraint, trackOutsideWindowIsIgnored)
|
||||||
{
|
{
|
||||||
TrackMetadataMap meta{
|
TrackMetadataMap meta{
|
||||||
{ T1, { .releaseId = {}, .artistIds = { A1 } } },
|
|
||||||
{ T2, { .releaseId = {}, .artistIds = { A2 } } },
|
|
||||||
{ T3, { .releaseId = {}, .artistIds = { A2 } } },
|
|
||||||
{ T4, { .releaseId = {}, .artistIds = { A2 } } },
|
|
||||||
{ T5, { .releaseId = {}, .artistIds = { A1 } } }, // outside window=4
|
|
||||||
};
|
|
||||||
const SameArtistConstraint constraint{ meta, /*window=*/4 };
|
|
||||||
TrackMetadataMap meta2{
|
|
||||||
{ T1, { .releaseId = {}, .artistIds = { A1 } } },
|
{ T1, { .releaseId = {}, .artistIds = { A1 } } },
|
||||||
{ T2, { .releaseId = {}, .artistIds = { A2 } } },
|
{ T2, { .releaseId = {}, .artistIds = { A2 } } },
|
||||||
{ T3, { .releaseId = {}, .artistIds = { A1 } } },
|
{ T3, { .releaseId = {}, .artistIds = { A1 } } },
|
||||||
};
|
};
|
||||||
const SameArtistConstraint constraint2{ meta2, /*window=*/1 };
|
const SameArtistConstraint constraint{ meta, /*window=*/1 };
|
||||||
|
|
||||||
const std::vector<db::TrackId> selected{ T3, T2 };
|
const std::vector<db::TrackId> selected{ T3, T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FLOAT_EQ(constraint2.computeScore(ctx), 0.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap)
|
TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap)
|
||||||
@@ -137,7 +131,7 @@ TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap)
|
|||||||
const TrackMetadataMap meta{};
|
const TrackMetadataMap meta{};
|
||||||
const SameArtistConstraint constraint{ meta };
|
const SameArtistConstraint constraint{ meta };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +143,7 @@ TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease)
|
|||||||
};
|
};
|
||||||
const SameReleaseConstraint constraint{ meta };
|
const SameReleaseConstraint constraint{ meta };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +155,7 @@ TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease)
|
|||||||
};
|
};
|
||||||
const SameReleaseConstraint constraint{ meta };
|
const SameReleaseConstraint constraint{ meta };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 1.F);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +167,7 @@ TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease)
|
|||||||
};
|
};
|
||||||
const SameReleaseConstraint constraint{ meta };
|
const SameReleaseConstraint constraint{ meta };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,14 +177,14 @@ TEST(TrackCandidateEvaluator, hardConstraintRejects)
|
|||||||
evaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
evaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
|
||||||
|
|
||||||
const std::vector<db::TrackId> selected{ T1 };
|
const std::vector<db::TrackId> selected{ T1 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_TRUE(evaluator.rejects(ctx));
|
EXPECT_TRUE(evaluator.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject)
|
TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject)
|
||||||
{
|
{
|
||||||
TrackCandidateEvaluator evaluator;
|
TrackCandidateEvaluator evaluator;
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(evaluator.rejects(ctx));
|
EXPECT_FALSE(evaluator.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +198,7 @@ TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted)
|
|||||||
evaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(meta), 2.F);
|
evaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(meta), 2.F);
|
||||||
|
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
|
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
|
||||||
}
|
}
|
||||||
@@ -220,7 +214,7 @@ TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated)
|
|||||||
evaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(meta), 1.F);
|
evaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(meta), 1.F);
|
||||||
|
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
|
||||||
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
|
EXPECT_FLOAT_EQ(evaluator.score(ctx), 2.F);
|
||||||
}
|
}
|
||||||
@@ -236,7 +230,7 @@ TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints)
|
|||||||
evaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(meta), 1.F);
|
evaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(meta), 1.F);
|
||||||
|
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(evaluator.rejects(ctx));
|
EXPECT_FALSE(evaluator.rejects(ctx));
|
||||||
EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F);
|
EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F);
|
||||||
}
|
}
|
||||||
@@ -256,7 +250,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap)
|
|||||||
const TestVectorMap trackVectors{ { T1, &v_x } };
|
const TestVectorMap trackVectors{ { T1, &v_x } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const std::vector<db::TrackId> selected{ T1 };
|
const std::vector<db::TrackId> selected{ T1 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(constraint.rejects(ctx));
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +258,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty)
|
|||||||
{
|
{
|
||||||
const TestVectorMap trackVectors{ { T1, &v_x } };
|
const TestVectorMap trackVectors{ { T1, &v_x } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(constraint.rejects(ctx));
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +267,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap)
|
|||||||
const TestVectorMap trackVectors{ { T1, &v_x } };
|
const TestVectorMap trackVectors{ { T1, &v_x } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(constraint.rejects(ctx));
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +276,7 @@ TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold)
|
|||||||
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_TRUE(constraint.rejects(ctx));
|
EXPECT_TRUE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +285,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold)
|
|||||||
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const std::vector<db::TrackId> selected{ T2 };
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(constraint.rejects(ctx));
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +294,7 @@ TEST(NearDuplicateEmbeddingConstraint, rejectsWhenOneOfManySelectedIsNearDuplica
|
|||||||
const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } };
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T3, &v_y }, { T4, &v_x } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const std::vector<db::TrackId> selected{ T3, T4 };
|
const std::vector<db::TrackId> selected{ T3, T4 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_TRUE(constraint.rejects(ctx));
|
EXPECT_TRUE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +303,111 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenAllSelectedAreFarEnough)
|
|||||||
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } };
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_y } };
|
||||||
const TestConstraint constraint{ trackVectors, 0.1F };
|
const TestConstraint constraint{ trackVectors, 0.1F };
|
||||||
const std::vector<db::TrackId> selected{ T2, T3 };
|
const std::vector<db::TrackId> selected{ T2, T3 };
|
||||||
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected };
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
EXPECT_FALSE(constraint.rejects(ctx));
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(InterpolationFitConstraint, zeroScoreWhenNoSeeds)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x } };
|
||||||
|
const InterpolationFitConstraint<2> constraint{ trackVectors };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InterpolationFitConstraint, zeroScoreWhenCandidateMatchesSeed)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
|
||||||
|
const InterpolationFitConstraint<2> constraint{ trackVectors };
|
||||||
|
const std::vector<db::TrackId> seeds{ T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InterpolationFitConstraint, halfScoreWhenCandidateOrthogonalToSeed)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
|
||||||
|
const InterpolationFitConstraint<2> constraint{ trackVectors };
|
||||||
|
const std::vector<db::TrackId> seeds{ T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InterpolationFitConstraint, usesClosestSeed)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } };
|
||||||
|
const InterpolationFitConstraint<2> constraint{ trackVectors };
|
||||||
|
const std::vector<db::TrackId> seeds{ T3, T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MaxDistanceConstraint, acceptsWhenNoSeeds)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x } };
|
||||||
|
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
|
||||||
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MaxDistanceConstraint, acceptsWhenWithinThresholdOfOneSeed)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
|
||||||
|
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
|
||||||
|
const std::vector<db::TrackId> seeds{ T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
|
||||||
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MaxDistanceConstraint, rejectsWhenBeyondThresholdFromAllSeeds)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
|
||||||
|
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.1F };
|
||||||
|
const std::vector<db::TrackId> seeds{ T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
|
||||||
|
EXPECT_TRUE(constraint.rejects(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MaxDistanceConstraint, acceptsWhenAnyOfManySeedsIsCloseEnough)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y }, { T3, &v_x } };
|
||||||
|
const MaxDistanceConstraint<2> constraint{ trackVectors, 0.5F };
|
||||||
|
const std::vector<db::TrackId> seeds{ T2, T3 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = seeds };
|
||||||
|
EXPECT_FALSE(constraint.rejects(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SmoothTransitionConstraint, zeroScoreWhenNoSelectedTracks)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x } };
|
||||||
|
const SmoothTransitionConstraint<2> constraint{ trackVectors };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SmoothTransitionConstraint, zeroScoreWhenPreviousMatchesCandidate)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } };
|
||||||
|
const SmoothTransitionConstraint<2> constraint{ trackVectors };
|
||||||
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SmoothTransitionConstraint, halfScoreWhenPreviousOrthogonalToCandidate)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } };
|
||||||
|
const SmoothTransitionConstraint<2> constraint{ trackVectors };
|
||||||
|
const std::vector<db::TrackId> selected{ T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack)
|
||||||
|
{
|
||||||
|
const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x }, { T3, &v_y } };
|
||||||
|
const SmoothTransitionConstraint<2> constraint{ trackVectors };
|
||||||
|
const std::vector<db::TrackId> selected{ T3, T2 };
|
||||||
|
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
|
||||||
|
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user