From 833fc708dc901bdf20ed0cbe4f956b4aaff613dd Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Jun 2026 23:42:12 +0200 Subject: [PATCH] Relaxed constraints on play queue auto filling + cleaned audio similarity constraints --- .../AudioSimilarityEngine.impl.hpp | 80 +++------ .../InterpolationFitConstraint.hpp | 69 ++++++++ .../MaxDistanceConstraint.hpp | 70 ++++++++ .../SmoothTransitionConstraint.hpp | 68 +++++++ .../impl/clusters/ClustersEngine.cpp | 1 + .../InterpolationFitConstraint.hpp | 34 ---- .../MaxDistanceConstraint.hpp | 43 ----- .../SmoothTransitionConstraint.hpp | 34 ---- .../TrackCandidateContext.hpp | 3 +- .../recommendation/test/ConstraintsTest.cpp | 167 ++++++++++++++---- 10 files changed, 369 insertions(+), 200 deletions(-) create mode 100644 src/libs/services/recommendation/impl/audio-similarity/InterpolationFitConstraint.hpp create mode 100644 src/libs/services/recommendation/impl/audio-similarity/MaxDistanceConstraint.hpp create mode 100644 src/libs/services/recommendation/impl/audio-similarity/SmoothTransitionConstraint.hpp delete mode 100644 src/libs/services/recommendation/impl/track-selection-constraints/InterpolationFitConstraint.hpp delete mode 100644 src/libs/services/recommendation/impl/track-selection-constraints/MaxDistanceConstraint.hpp delete mode 100644 src/libs/services/recommendation/impl/track-selection-constraints/SmoothTransitionConstraint.hpp diff --git a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp index 63998b6d..bba489df 100644 --- a/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp +++ b/src/libs/services/recommendation/impl/audio-similarity/AudioSimilarityEngine.impl.hpp @@ -49,13 +49,13 @@ #include "math/PrincipalComponents.hpp" #include "math/StatsAccumulator.hpp" +#include "InterpolationFitConstraint.hpp" +#include "MaxDistanceConstraint.hpp" #include "NearDuplicateEmbeddingConstraint.hpp" +#include "SmoothTransitionConstraint.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/SameReleaseConstraint.hpp" -#include "track-selection-constraints/SmoothTransitionConstraint.hpp" #include "Types.hpp" @@ -121,17 +121,17 @@ namespace lms::recommendation _similarityEvaluator = {}; _similarityEvaluator.addHardConstraint(std::make_unique()); _similarityEvaluator.addHardConstraint(std::make_unique>(_trackVectors, nearDuplicateThreshold)); - _similarityEvaluator.addHardConstraint(std::make_unique(_trackDistanceThreshold)); - _similarityEvaluator.addSoftConstraint(std::make_unique(), interpolationFitWeight); - _similarityEvaluator.addSoftConstraint(std::make_unique(), smoothTransitionWeight); + _similarityEvaluator.addHardConstraint(std::make_unique>(_trackVectors, _trackDistanceThreshold)); + _similarityEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), interpolationFitWeight); + _similarityEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), smoothTransitionWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameArtistWeight); _pathEvaluator = {}; _pathEvaluator.addHardConstraint(std::make_unique()); _pathEvaluator.addHardConstraint(std::make_unique>(_trackVectors, nearDuplicateThreshold)); - _pathEvaluator.addSoftConstraint(std::make_unique(), interpolationFitWeight); - _pathEvaluator.addSoftConstraint(std::make_unique(), smoothTransitionWeight); + _pathEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), interpolationFitWeight); + _pathEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), smoothTransitionWeight); _pathEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); _pathEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameArtistWeight); } @@ -210,8 +210,6 @@ namespace lms::recommendation rankedTracks.resize(candidateCount); // 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, // same artist) treat them as already taken, preventing the first results from being // from the same release/artist as the inputs. @@ -219,8 +217,6 @@ namespace lms::recommendation selectedTracks.reserve(selectedTracks.size() + maxCount); res.reserve(maxCount); - const ReducedVector* previousVector{}; - while (res.size() < maxCount && !rankedTracks.empty()) { std::optional bestIdx; @@ -228,15 +224,12 @@ namespace lms::recommendation for (std::size_t i{}; i < rankedTracks.size(); ++i) { - const auto& [candidateId, distanceToQuery]{ rankedTracks[i] }; - const ReducedVector* candidateVector{ _trackVectors.at(candidateId) }; - const float distanceToPrevious{ previousVector ? math::NormalizedCosineDistance{ *previousVector }(*candidateVector) : 0.F }; + const db::TrackId candidateId{ rankedTracks[i].first }; const TrackCandidateContext context{ .candidateTrackId = candidateId, .selectedTracks = selectedTracks, - .distanceToQuery = distanceToQuery, - .distanceToPrevious = distanceToPrevious, + .seedTrackIds = tracksId, }; if (_similarityEvaluator.rejects(context)) @@ -256,7 +249,6 @@ namespace lms::recommendation const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] }; res.push_back({ .id = selectedId, .distance = distanceToQuery }); selectedTracks.push_back(selectedId); - previousVector = _trackVectors.at(selectedId); rankedTracks.erase(std::begin(rankedTracks) + static_cast(*bestIdx)); } @@ -284,26 +276,28 @@ namespace lms::recommendation path.reserve(maxCount); path.push_back(startTrackId); - const ReducedVector* previousVector{ itStart->second }; - static constexpr std::size_t DefaultNeighborCount{ 16 }; - static constexpr std::size_t BroadNeighborCount{ 64 }; - std::size_t neighborCount{ DefaultNeighborCount }; + static constexpr std::size_t NeighborCount{ 32 }; const std::size_t interiorCount{ (maxCount > 2) ? (maxCount - 2) : 0 }; - auto evaluateCandidates = [&](const TrackResults& neighborList) -> std::optional { + for (std::size_t i{}; i < interiorCount; ++i) + { + const float t{ static_cast(i + 1) / static_cast(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 stepSeedTrackIds{ stepSeedTrackId }; + std::optional best; float bestScore{ std::numeric_limits::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{ - .candidateTrackId = candidateId, + .candidateTrackId = candidateTrackId, .selectedTracks = path, - .distanceToQuery = candidateDistance, - .distanceToPrevious = transitionDistance, + .seedTrackIds = stepSeedTrackIds, }; if (_pathEvaluator.rejects(context)) @@ -313,34 +307,14 @@ namespace lms::recommendation if (score < bestScore) { bestScore = score; - best = candidateId; + best = candidateTrackId; } } - return best; - }; - - for (std::size_t i{}; i < interiorCount; ++i) - { - const float t{ static_cast(i + 1) / static_cast(interiorCount + 1) }; - auto queryPoint{ startVector + direction * t }; - queryPoint.normalizeL2(); - - const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) }; - std::optional bestCandidate{ evaluateCandidates(neighbors) }; - - if (!bestCandidate && neighborCount < BroadNeighborCount) - { - neighborCount = BroadNeighborCount; - const auto broaderNeighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) }; - bestCandidate = evaluateCandidates(broaderNeighbors); - } - - if (!bestCandidate) + if (!best) continue; - path.push_back(*bestCandidate); - previousVector = _trackVectors.at(*bestCandidate); + path.push_back(*best); } if (maxCount > 1) diff --git a/src/libs/services/recommendation/impl/audio-similarity/InterpolationFitConstraint.hpp b/src/libs/services/recommendation/impl/audio-similarity/InterpolationFitConstraint.hpp new file mode 100644 index 00000000..0f96edbb --- /dev/null +++ b/src/libs/services/recommendation/impl/audio-similarity/InterpolationFitConstraint.hpp @@ -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 . + */ + +#pragma once + +#include +#include + +#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 + class InterpolationFitConstraint : public ITrackCandidateSoftConstraint + { + public: + using ReducedVector = math::Vector; + using TrackVectorMap = std::unordered_map; + + 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::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::max()) ? 0.F : best; + } + + private: + const TrackVectorMap& _trackVectors; + }; +} // namespace lms::recommendation diff --git a/src/libs/services/recommendation/impl/audio-similarity/MaxDistanceConstraint.hpp b/src/libs/services/recommendation/impl/audio-similarity/MaxDistanceConstraint.hpp new file mode 100644 index 00000000..8f5a6c20 --- /dev/null +++ b/src/libs/services/recommendation/impl/audio-similarity/MaxDistanceConstraint.hpp @@ -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 . + */ + +#pragma once + +#include + +#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 + class MaxDistanceConstraint : public ITrackCandidateHardConstraint + { + public: + using ReducedVector = math::Vector; + using TrackVectorMap = std::unordered_map; + + 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 diff --git a/src/libs/services/recommendation/impl/audio-similarity/SmoothTransitionConstraint.hpp b/src/libs/services/recommendation/impl/audio-similarity/SmoothTransitionConstraint.hpp new file mode 100644 index 00000000..409a7333 --- /dev/null +++ b/src/libs/services/recommendation/impl/audio-similarity/SmoothTransitionConstraint.hpp @@ -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 . + */ + +#pragma once + +#include + +#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 + class SmoothTransitionConstraint : public ITrackCandidateSoftConstraint + { + public: + using ReducedVector = math::Vector; + using TrackVectorMap = std::unordered_map; + + 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 diff --git a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp index 9e9b88a3..1efee5a5 100644 --- a/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp +++ b/src/libs/services/recommendation/impl/clusters/ClustersEngine.cpp @@ -278,6 +278,7 @@ namespace lms::recommendation const TrackCandidateContext context{ .candidateTrackId = candidates[i], .selectedTracks = selectedTracks, + .seedTrackIds = {}, }; if (_trackEvaluator.rejects(context)) diff --git a/src/libs/services/recommendation/impl/track-selection-constraints/InterpolationFitConstraint.hpp b/src/libs/services/recommendation/impl/track-selection-constraints/InterpolationFitConstraint.hpp deleted file mode 100644 index 06a5a492..00000000 --- a/src/libs/services/recommendation/impl/track-selection-constraints/InterpolationFitConstraint.hpp +++ /dev/null @@ -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 . - */ - -#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 diff --git a/src/libs/services/recommendation/impl/track-selection-constraints/MaxDistanceConstraint.hpp b/src/libs/services/recommendation/impl/track-selection-constraints/MaxDistanceConstraint.hpp deleted file mode 100644 index 443b05c1..00000000 --- a/src/libs/services/recommendation/impl/track-selection-constraints/MaxDistanceConstraint.hpp +++ /dev/null @@ -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 . - */ - -#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 diff --git a/src/libs/services/recommendation/impl/track-selection-constraints/SmoothTransitionConstraint.hpp b/src/libs/services/recommendation/impl/track-selection-constraints/SmoothTransitionConstraint.hpp deleted file mode 100644 index e911ad86..00000000 --- a/src/libs/services/recommendation/impl/track-selection-constraints/SmoothTransitionConstraint.hpp +++ /dev/null @@ -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 . - */ - -#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 diff --git a/src/libs/services/recommendation/impl/track-selection-constraints/TrackCandidateContext.hpp b/src/libs/services/recommendation/impl/track-selection-constraints/TrackCandidateContext.hpp index 625299fc..e690398e 100644 --- a/src/libs/services/recommendation/impl/track-selection-constraints/TrackCandidateContext.hpp +++ b/src/libs/services/recommendation/impl/track-selection-constraints/TrackCandidateContext.hpp @@ -29,7 +29,6 @@ namespace lms::recommendation { db::TrackId candidateTrackId; std::span selectedTracks; - float distanceToQuery{}; - float distanceToPrevious{}; + std::span seedTrackIds; }; } // namespace lms::recommendation diff --git a/src/libs/services/recommendation/test/ConstraintsTest.cpp b/src/libs/services/recommendation/test/ConstraintsTest.cpp index 5fe8dd29..4d9cc9b9 100644 --- a/src/libs/services/recommendation/test/ConstraintsTest.cpp +++ b/src/libs/services/recommendation/test/ConstraintsTest.cpp @@ -24,7 +24,10 @@ #include "database/objects/TrackId.hpp" #include "math/Vector.hpp" +#include "audio-similarity/InterpolationFitConstraint.hpp" +#include "audio-similarity/MaxDistanceConstraint.hpp" #include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp" +#include "audio-similarity/SmoothTransitionConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp" @@ -41,7 +44,6 @@ namespace const db::TrackId T2{ 2 }; const db::TrackId T3{ 3 }; const db::TrackId T4{ 4 }; - const db::TrackId T5{ 5 }; const db::ArtistId A1{ 10 }; const db::ArtistId A2{ 20 }; @@ -53,20 +55,20 @@ namespace TEST(DuplicateTrackConstraint, acceptsNewCandidate) { const std::vector selected{ T1, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T3, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); } TEST(DuplicateTrackConstraint, rejectsAlreadySelected) { const std::vector selected{ T1, T2, T3 }; - const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_TRUE(DuplicateTrackConstraint{}.rejects(ctx)); } TEST(DuplicateTrackConstraint, acceptsWhenSelectionEmpty) { - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; EXPECT_FALSE(DuplicateTrackConstraint{}.rejects(ctx)); } @@ -78,7 +80,7 @@ TEST(SameArtistConstraint, zeroScoreWhenNoSharedArtist) }; const SameArtistConstraint constraint{ meta }; const std::vector 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); } @@ -90,7 +92,7 @@ TEST(SameArtistConstraint, fullScoreWhenMostRecentMatchesArtist) }; const SameArtistConstraint constraint{ meta }; const std::vector 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); } @@ -105,7 +107,7 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist) const SameArtistConstraint constraint{ meta }; const std::vector 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); } @@ -113,23 +115,15 @@ TEST(SameArtistConstraint, halfScoreWhenSecondMostRecentMatchesArtist) TEST(SameArtistConstraint, trackOutsideWindowIsIgnored) { 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 } } }, { T2, { .releaseId = {}, .artistIds = { A2 } } }, { T3, { .releaseId = {}, .artistIds = { A1 } } }, }; - const SameArtistConstraint constraint2{ meta2, /*window=*/1 }; + const SameArtistConstraint constraint{ meta, /*window=*/1 }; const std::vector selected{ T3, T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; - EXPECT_FLOAT_EQ(constraint2.computeScore(ctx), 0.F); + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); } TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap) @@ -137,7 +131,7 @@ TEST(SameArtistConstraint, zeroScoreWhenCandidateNotInMap) const TrackMetadataMap meta{}; const SameArtistConstraint constraint{ meta }; const std::vector 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); } @@ -149,7 +143,7 @@ TEST(SameReleaseConstraint, zeroScoreWhenNoSharedRelease) }; const SameReleaseConstraint constraint{ meta }; const std::vector 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); } @@ -161,7 +155,7 @@ TEST(SameReleaseConstraint, fullScoreWhenMostRecentMatchesRelease) }; const SameReleaseConstraint constraint{ meta }; const std::vector 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); } @@ -173,7 +167,7 @@ TEST(SameReleaseConstraint, zeroScoreWhenCandidateHasNoRelease) }; const SameReleaseConstraint constraint{ meta }; const std::vector 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); } @@ -183,14 +177,14 @@ TEST(TrackCandidateEvaluator, hardConstraintRejects) evaluator.addHardConstraint(std::make_unique()); const std::vector selected{ T1 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_TRUE(evaluator.rejects(ctx)); } TEST(TrackCandidateEvaluator, noHardConstraintDoesNotReject) { TrackCandidateEvaluator evaluator; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; EXPECT_FALSE(evaluator.rejects(ctx)); } @@ -204,7 +198,7 @@ TEST(TrackCandidateEvaluator, softConstraintScoreIsWeighted) evaluator.addSoftConstraint(std::make_unique(meta), 2.F); const std::vector 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); } @@ -220,7 +214,7 @@ TEST(TrackCandidateEvaluator, multipleSoftConstraintsAreAccumulated) evaluator.addSoftConstraint(std::make_unique(meta), 1.F); const std::vector 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); } @@ -236,7 +230,7 @@ TEST(TrackCandidateEvaluator, hardConstraintPassesEvenWithSoftConstraints) evaluator.addSoftConstraint(std::make_unique(meta), 1.F); const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_FALSE(evaluator.rejects(ctx)); EXPECT_FLOAT_EQ(evaluator.score(ctx), 1.F); } @@ -256,7 +250,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenCandidateNotInMap) const TestVectorMap trackVectors{ { T1, &v_x } }; const TestConstraint constraint{ trackVectors, 0.1F }; const std::vector selected{ T1 }; - const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T2, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_FALSE(constraint.rejects(ctx)); } @@ -264,7 +258,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectionEmpty) { const TestVectorMap trackVectors{ { T1, &v_x } }; const TestConstraint constraint{ trackVectors, 0.1F }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {} }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} }; EXPECT_FALSE(constraint.rejects(ctx)); } @@ -273,7 +267,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenSelectedTrackNotInMap) const TestVectorMap trackVectors{ { T1, &v_x } }; const TestConstraint constraint{ trackVectors, 0.1F }; const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_FALSE(constraint.rejects(ctx)); } @@ -282,7 +276,7 @@ TEST(NearDuplicateEmbeddingConstraint, rejectsWhenDistanceBelowThreshold) const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_x } }; const TestConstraint constraint{ trackVectors, 0.1F }; const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; EXPECT_TRUE(constraint.rejects(ctx)); } @@ -291,7 +285,7 @@ TEST(NearDuplicateEmbeddingConstraint, acceptsWhenDistanceAboveThreshold) const TestVectorMap trackVectors{ { T1, &v_x }, { T2, &v_y } }; const TestConstraint constraint{ trackVectors, 0.1F }; const std::vector selected{ T2 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; 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 TestConstraint constraint{ trackVectors, 0.1F }; const std::vector selected{ T3, T4 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; 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 TestConstraint constraint{ trackVectors, 0.1F }; const std::vector selected{ T2, T3 }; - const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; 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 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 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 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 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 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 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 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 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 selected{ T3, T2 }; + const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; + EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); +}