Replaced the near similar filter constraint by a duplicate recording mbid one

This commit is contained in:
emeric
2026-06-13 23:28:07 +02:00
parent af018a9166
commit 845c437364
6 changed files with 505 additions and 369 deletions
@@ -50,10 +50,10 @@
#include "InterpolationFitConstraint.hpp" #include "InterpolationFitConstraint.hpp"
#include "MaxDistanceConstraint.hpp" #include "MaxDistanceConstraint.hpp"
#include "NearDuplicateEmbeddingConstraint.hpp"
#include "SmoothTransitionConstraint.hpp" #include "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/SameRecordingMBIDConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "Types.hpp" #include "Types.hpp"
@@ -121,11 +121,9 @@ namespace lms::recommendation
constexpr float smoothTransitionWeight{ 0.2F }; constexpr float smoothTransitionWeight{ 0.2F };
constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameReleaseWeight{ 0.5F };
constexpr float sameArtistWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F };
constexpr float nearDuplicateThreshold{ 0.01F };
_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<SameRecordingMBIDConstraint>(_trackMetadata));
_similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint<ReducedDimCount>>(_trackVectors, _trackDistanceThreshold)); _similarityEvaluator.addHardConstraint(std::make_unique<MaxDistanceConstraint<ReducedDimCount>>(_trackVectors, _trackDistanceThreshold));
_similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight); _similarityEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
_similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight); _similarityEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), smoothTransitionWeight);
@@ -134,7 +132,7 @@ namespace lms::recommendation
_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<SameRecordingMBIDConstraint>(_trackMetadata));
_pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight); _pathEvaluator.addSoftConstraint(std::make_unique<InterpolationFitConstraint<ReducedDimCount>>(_trackVectors), interpolationFitWeight);
_pathEvaluator.addSoftConstraint(std::make_unique<SmoothTransitionConstraint<ReducedDimCount>>(_trackVectors), 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);
@@ -595,28 +593,29 @@ namespace lms::recommendation
_trackVectors.try_emplace(trackId, &reducedVector); _trackVectors.try_emplace(trackId, &reducedVector);
}); });
db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) {
std::vector<std::reference_wrapper<const ReducedVector>> releaseTrackFeatures; const auto itVec{ _trackVectors.find(track->getId()) };
if (itVec == _trackVectors.cend())
return;
db::Track::FindParameters params; auto& meta{ _trackMetadata[track->getId()] };
params.setRelease(release->getId()); const db::ReleaseId releaseId{ track->getReleaseId() };
meta.releaseId = releaseId;
meta.recordingMBID = track->getRecordingMBID();
const auto trackIds{ db::Track::findIds(session, params) }; if (releaseId.isValid())
for (const db::TrackId trackId : trackIds.results) _releaseVectors[releaseId].emplace_back(*itVec->second);
{
const auto itFeatures{ _trackVectors.find(trackId) };
if (itFeatures != std::cend(_trackVectors))
{
assert(itFeatures->second);
releaseTrackFeatures.emplace_back(*itFeatures->second);
_trackMetadata[trackId].releaseId = release->getId();
}
}
if (!releaseTrackFeatures.empty())
_releaseVectors.try_emplace(release->getId(), std::move(releaseTrackFeatures));
}); });
math::MedoidCalculator<ReducedVector> calc;
for (const auto& [id, vecs] : _releaseVectors)
{
calc.clear();
for (const auto& v : vecs)
calc.add(v.get());
_releaseMedoids.try_emplace(id, calc.finalize());
}
db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) {
const auto mbid{ artist->getMBID() }; const auto mbid{ artist->getMBID() };
// skip "Various Artists" to avoid false artist matches // skip "Various Artists" to avoid false artist matches
@@ -658,15 +657,6 @@ namespace lms::recommendation
_artistVectors.try_emplace(artist->getId(), std::move(artistTrackVectors)); _artistVectors.try_emplace(artist->getId(), std::move(artistTrackVectors));
}); });
math::MedoidCalculator<ReducedVector> calc;
for (const auto& [id, vecs] : _releaseVectors)
{
calc.clear();
for (const auto& v : vecs)
calc.add(v.get());
_releaseMedoids.try_emplace(id, calc.finalize());
}
for (const auto& [id, vecs] : _artistVectors) for (const auto& [id, vecs] : _artistVectors)
{ {
calc.clear(); calc.clear();
@@ -23,7 +23,6 @@
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <random>
#include <unordered_set> #include <unordered_set>
#include "database/IDb.hpp" #include "database/IDb.hpp"
@@ -39,6 +38,7 @@
#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/SameRecordingMBIDConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "track-selection-constraints/TrackCandidateContext.hpp" #include "track-selection-constraints/TrackCandidateContext.hpp"
@@ -103,6 +103,7 @@ namespace lms::recommendation
constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameReleaseWeight{ 0.5F };
constexpr float sameArtistWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F };
_trackEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>()); _trackEvaluator.addHardConstraint(std::make_unique<DuplicateTrackConstraint>());
_trackEvaluator.addHardConstraint(std::make_unique<SameRecordingMBIDConstraint>(_trackMetadata));
_trackEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight); _trackEvaluator.addSoftConstraint(std::make_unique<SameReleaseConstraint>(_trackMetadata), sameReleaseWeight);
_trackEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight); _trackEvaluator.addSoftConstraint(std::make_unique<SameArtistConstraint>(_trackMetadata), sameArtistWeight);
} }
@@ -122,8 +123,8 @@ namespace lms::recommendation
db::Session& session{ _db.getTLSSession() }; db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
buildTrackMetadata(session);
buildTrackClusters(session); buildTrackClusters(session);
buildTrackMetadata(session);
buildReleaseClusters(); buildReleaseClusters();
buildArtistClusters(); buildArtistClusters();
@@ -134,11 +135,17 @@ namespace lms::recommendation
{ {
LOG(DEBUG, "building track metadata..."); LOG(DEBUG, "building track metadata...");
db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { // Ensure cluster tracks with no release/artist have an entry
db::Track::FindParameters params; for (const auto& [trackId, _] : _trackClusters)
params.setRelease(release->getId()); _trackMetadata.try_emplace(trackId);
for (const db::TrackId trackId : db::Track::findIds(session, params).results)
_trackMetadata[trackId].releaseId = release->getId(); db::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) {
const auto it{ _trackMetadata.find(track->getId()) };
if (it != _trackMetadata.cend())
{
it->second.releaseId = track->getReleaseId();
it->second.recordingMBID = track->getRecordingMBID();
}
}); });
db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) {
@@ -54,8 +54,8 @@ namespace lms::recommendation
ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override; ArtistResults findSimilarArtists(db::ArtistId artistId, core::EnumSet<db::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
TrackResults greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const; TrackResults greedySelect(std::vector<db::TrackId> candidates, std::vector<db::TrackId> selectedTracks, std::size_t maxCount) const;
void buildTrackMetadata(db::Session& session);
void buildTrackClusters(db::Session& session); void buildTrackClusters(db::Session& session);
void buildTrackMetadata(db::Session& session);
void buildReleaseClusters(); void buildReleaseClusters();
void buildArtistClusters(); void buildArtistClusters();
@@ -0,0 +1,53 @@
/*
* 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"
#include "TrackMetadata.hpp"
namespace lms::recommendation
{
class SameRecordingMBIDConstraint : public ITrackCandidateHardConstraint
{
public:
SameRecordingMBIDConstraint(const TrackMetadataMap& trackMetadata)
: _trackMetadata{ trackMetadata }
{
}
bool rejects(const TrackCandidateContext& context) const override
{
const auto it{ _trackMetadata.find(context.candidateTrackId) };
if (it == _trackMetadata.cend() || !it->second.recordingMBID)
return false;
for (const db::TrackId selectedId : context.selectedTracks)
{
const auto itSel{ _trackMetadata.find(selectedId) };
if (itSel != _trackMetadata.cend() && itSel->second.recordingMBID == it->second.recordingMBID)
return true;
}
return false;
}
private:
const TrackMetadataMap& _trackMetadata;
};
} // namespace lms::recommendation
@@ -19,9 +19,11 @@
#pragma once #pragma once
#include <optional>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "core/UUID.hpp"
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
@@ -31,8 +33,8 @@ namespace lms::recommendation
struct TrackMetadata struct TrackMetadata
{ {
db::ReleaseId releaseId; // invalid if track has no release db::ReleaseId releaseId; // invalid if track has no release
std::vector<db::ArtistId> artistIds; // sorted; album artists only std::vector<db::ArtistId> artistIds; // sorted, album artists only
// Future: db::MediaLibraryId mediaLibraryId; std::optional<core::UUID> recordingMBID; // absent if track has no recording MBID
}; };
using TrackMetadataMap = std::unordered_map<db::TrackId, TrackMetadata>; using TrackMetadataMap = std::unordered_map<db::TrackId, TrackMetadata>;
@@ -24,20 +24,22 @@
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
#include "math/Vector.hpp" #include "math/Vector.hpp"
#include "core/UUID.hpp"
#include "audio-similarity/InterpolationFitConstraint.hpp" #include "audio-similarity/InterpolationFitConstraint.hpp"
#include "audio-similarity/MaxDistanceConstraint.hpp" #include "audio-similarity/MaxDistanceConstraint.hpp"
#include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp" #include "audio-similarity/NearDuplicateEmbeddingConstraint.hpp"
#include "audio-similarity/SmoothTransitionConstraint.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/SameRecordingMBIDConstraint.hpp"
#include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp"
#include "track-selection-constraints/TrackCandidateContext.hpp" #include "track-selection-constraints/TrackCandidateContext.hpp"
#include "track-selection-constraints/TrackCandidateEvaluator.hpp" #include "track-selection-constraints/TrackCandidateEvaluator.hpp"
#include "track-selection-constraints/TrackMetadata.hpp" #include "track-selection-constraints/TrackMetadata.hpp"
using namespace lms; namespace lms::recommendation::tests
using namespace lms::recommendation; {
namespace namespace
{ {
const db::TrackId T1{ 1 }; const db::TrackId T1{ 1 };
@@ -411,3 +413,85 @@ TEST(SmoothTransitionConstraint, usesMostRecentlySelectedTrack)
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} }; const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F); EXPECT_FLOAT_EQ(constraint.computeScore(ctx), 0.F);
} }
namespace
{
const core::UUID MBID1{ *core::UUID::fromString("3d2508f7-3a7e-4f25-895c-fca079dd71ce") };
const core::UUID MBID2{ *core::UUID::fromString("8e44e4ea-9bce-47c2-a48c-8aa338242fb2") };
} // namespace
TEST(SameRecordingMBIDConstraint, acceptsWhenCandidateNotInMap)
{
const TrackMetadataMap meta{ { T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } };
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenCandidateHasNoMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = std::nullopt } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenSelectionEmpty)
{
const TrackMetadataMap meta{ { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } };
const SameRecordingMBIDConstraint constraint{ meta };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = {}, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenNoneOfSelectedHasSameMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID2 } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, rejectsWhenOneOfSelectedHasSameMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID2 } },
{ T3, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2, T3 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_TRUE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenSelectedTrackNotInMap)
{
const TrackMetadataMap meta{ { T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } } };
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
TEST(SameRecordingMBIDConstraint, acceptsWhenSelectedTrackHasNoMBID)
{
const TrackMetadataMap meta{
{ T1, { .releaseId = {}, .artistIds = {}, .recordingMBID = MBID1 } },
{ T2, { .releaseId = {}, .artistIds = {}, .recordingMBID = std::nullopt } },
};
const SameRecordingMBIDConstraint constraint{ meta };
const std::vector<db::TrackId> selected{ T2 };
const TrackCandidateContext ctx{ .candidateTrackId = T1, .selectedTracks = selected, .seedTrackIds = {} };
EXPECT_FALSE(constraint.rejects(ctx));
}
} // namespace lms::recommendation::tests