diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/recommendation/CMakeLists.txt index 26e29757..b7a52f12 100644 --- a/src/libs/recommendation/CMakeLists.txt +++ b/src/libs/recommendation/CMakeLists.txt @@ -1,8 +1,10 @@ add_library(lmsrecommendation SHARED impl/clusters/ClustersClassifier.cpp + impl/features/FeaturesClassifierCache.cpp + impl/features/FeaturesClassifier.cpp + impl/features/FeaturesDefs.cpp impl/Engine.cpp - impl/ClassifierCreator.cpp ) target_include_directories(lmsrecommendation INTERFACE diff --git a/src/libs/recommendation/impl/ClassifierCreator.cpp b/src/libs/recommendation/impl/ClassifierCreator.cpp deleted file mode 100644 index 90dd651b..00000000 --- a/src/libs/recommendation/impl/ClassifierCreator.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2020 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 . - */ - -#include "recommendation/FeaturesClassifierCreator.hpp" -#include "recommendation/IClassifier.hpp" - -namespace Recommendation -{ - - std::unique_ptr createFeaturesClassifier() - { - return {}; - } - - -} - diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index 2a4aa772..4b4a6b67 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -24,6 +24,7 @@ #include "database/ScanSettings.hpp" #include "database/TrackList.hpp" +#include "utils/Exception.hpp" #include "utils/Logger.hpp" namespace Recommendation { @@ -56,6 +57,8 @@ Engine::stop() assert(_running); _running = false; + cancelPendingClassifiers(); + _ioService.stop(); } @@ -73,32 +76,18 @@ Engine::requestReload() std::vector Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) { - const std::unordered_set trackIds {[&]() -> std::unordered_set - { - auto transaction {session.createSharedTransaction()}; - - Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)}; - if (trackList) - { - const std::vector orderedTrackIds {trackList->getTrackIds()}; - return std::unordered_set {std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)}; - } - - return {}; - }()}; - - if (trackIds.empty()) - return {}; - std::shared_lock lock {_classifiersMutex}; + std::vector res; + for (const auto& [priority, classifier] : _classifiers) { - if (std::any_of(std::cbegin(trackIds), std::cend(trackIds), [&](Database::IdType trackId) { return classifier->isTrackClassified(trackId); } )) - return classifier->getSimilarTracksFromTrackList(session, trackListId, maxCount); + res = classifier->getSimilarTracksFromTrackList(session, trackListId, maxCount); + if (!res.empty()) + break; } - return {}; + return res; } std::vector @@ -106,13 +95,16 @@ Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set< { std::shared_lock lock {_classifiersMutex}; + std::vector res; + for (const auto& [priority, classifier] : _classifiers) { - if (std::any_of(std::cbegin(trackIds), std::cend(trackIds), [&](Database::IdType trackId) { return classifier->isTrackClassified(trackId); } )) - return classifier->getSimilarTracks(dbSession, trackIds, maxCount); + res = classifier->getSimilarTracks(dbSession, trackIds, maxCount); + if (!res.empty()) + break; } - return {}; + return res; } std::vector @@ -120,13 +112,16 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas { std::shared_lock lock {_classifiersMutex}; + std::vector res; + for (const auto& [priority, classifier] : _classifiers) { - if (classifier->isReleaseClassified(releaseId)) - return classifier->getSimilarReleases(dbSession, releaseId, maxCount); + res = classifier->getSimilarReleases(dbSession, releaseId, maxCount); + if (!res.empty()) + break; } - return {}; + return res; } std::vector @@ -134,13 +129,16 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI { std::shared_lock lock {_classifiersMutex}; + std::vector res; + for (const auto& [priority, classifier] : _classifiers) { - if (classifier->isArtistClassified(artistId)) - return classifier->getSimilarArtists(dbSession, artistId, maxCount); + res = classifier->getSimilarArtists(dbSession, artistId, maxCount); + if (!res.empty()) + return res; } - return {}; + return res; } void @@ -159,14 +157,35 @@ Engine::reload() std::map> newClassifiers; + // TODO RAII this + auto addClassifier = [&](ClassifierPriority prio, std::unique_ptr classifier) + { + try + { + addPendingClassifier(*classifier.get()); + bool res {classifier->init(_dbSession)}; + removePendingClassifier(*classifier.get()); + + if (res) + newClassifiers.emplace(prio, std::move(classifier)); + + return res; + } + catch (LmsException& e) + { + removePendingClassifier(*classifier.get()); + throw; + } + }; + switch (engineType) { case ScanSettings::RecommendationEngineType::Features: -// newClassifiers.emplace_back(0, createFeaturesClassifier()); // higher priority -// [[fallthrough]]; + addClassifier(0, createFeaturesClassifier()); // higher priority + [[fallthrough]]; case ScanSettings::RecommendationEngineType::Clusters: - newClassifiers.emplace(1, createClustersClassifier(_dbSession)); // lower priority + addClassifier(1, createClustersClassifier()); // lower priority break; } @@ -180,19 +199,29 @@ Engine::reload() _sigReloaded.emit(); } - void -Engine::clearClassifiers() +Engine::cancelPendingClassifiers() { + std::unique_lock lock {_classifiersMutex}; + + for (IClassifier* classifier : _pendingClassifiers) + classifier->requestCancelInit(); } void -Engine::addClassifier(std::unique_ptr classifier, unsigned priority) +Engine::addPendingClassifier(IClassifier& classifier) { - std::unique_lock lock {_classifiersMutex}; + std::unique_lock lock {_classifiersMutex}; - _classifiers.emplace(priority, std::move(classifier)); + _pendingClassifiers.insert(&classifier); } +void +Engine::removePendingClassifier(IClassifier& classifier) +{ + std::unique_lock lock {_classifiersMutex}; + + _pendingClassifiers.erase(&classifier); +} } // ns Similarity diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp index 9fde430e..17208b77 100644 --- a/src/libs/recommendation/impl/Engine.hpp +++ b/src/libs/recommendation/impl/Engine.hpp @@ -52,8 +52,9 @@ namespace Recommendation void reload(); - void clearClassifiers(); - void addClassifier(std::unique_ptr classifier, unsigned priority); + void cancelPendingClassifiers(); + void addPendingClassifier(IClassifier& classifier); + void removePendingClassifier(IClassifier& classifier); bool _running {}; Wt::WIOService _ioService; @@ -62,6 +63,7 @@ namespace Recommendation std::shared_mutex _classifiersMutex; std::map> _classifiers; + std::unordered_set _pendingClassifiers; }; } // ns Recommendation diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp index f0aee487..b433d833 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp @@ -28,56 +28,11 @@ namespace Recommendation { -std::unique_ptr createClustersClassifier(Database::Session& session) +std::unique_ptr createClustersClassifier() { - return std::make_unique(session); + return std::make_unique(); } -ClusterClassifier::ClusterClassifier(Database::Session& session) -{ - classify(session); -} - -void -ClusterClassifier::classify(Database::Session& session) -{ - auto transaction {session.createSharedTransaction()}; - - { - std::vector trackIds {Database::Track::getAllIdsWithClusters(session)}; - _classifiedTracks = std::unordered_set(std::cbegin(trackIds), std::cend(trackIds)); - } - - { - std::vector releaseIds {Database::Release::getAllIdsWithClusters(session)}; - _classifiedReleases = std::unordered_set(std::cbegin(releaseIds), std::cend(releaseIds)); - } - - { - std::vector artistIds {Database::Artist::getAllIdsWithClusters(session)}; - _classifiedArtists = std::unordered_set(std::cbegin(artistIds), std::cend(artistIds)); - } -} - -bool -ClusterClassifier::isTrackClassified(Database::IdType trackId) const -{ - return _classifiedTracks.find(trackId) != std::cend(_classifiedTracks); -} - -bool -ClusterClassifier::isReleaseClassified(Database::IdType releaseId) const -{ - return _classifiedReleases.find(releaseId) != std::cend(_classifiedReleases); -} - -bool -ClusterClassifier::isArtistClassified(Database::IdType artistId) const -{ - return _classifiedArtists.find(artistId) != std::cend(_classifiedArtists); -} - - std::vector ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount) const { diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp index 2f44205e..469a6ee8 100644 --- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp +++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp @@ -28,7 +28,7 @@ namespace Recommendation class ClusterClassifier : public IClassifier { public: - ClusterClassifier(Database::Session& session); + ClusterClassifier() = default; ClusterClassifier(const ClusterClassifier&) = delete; ClusterClassifier(ClusterClassifier&&) = delete; ClusterClassifier& operator=(const ClusterClassifier&) = delete; @@ -36,20 +36,16 @@ namespace Recommendation private: - bool isTrackClassified(Database::IdType trackId) const override; - bool isReleaseClassified(Database::IdType releaseId) const override; - bool isArtistClassified(Database::IdType artistId) const override; + std::string_view getName() const { return "Clusters"; } + + bool init(Database::Session&) override {return true;} + void requestCancelInit() override {} std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override; std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override; - void classify(Database::Session& session); - - std::unordered_set _classifiedArtists; - std::unordered_set _classifiedReleases; - std::unordered_set _classifiedTracks; }; } // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp new file mode 100644 index 00000000..f4f4ad90 --- /dev/null +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2018 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 . + */ + +#include "FeaturesClassifier.hpp" + +#include + +#include "database/Artist.hpp" +#include "database/Release.hpp" +#include "database/Session.hpp" +#include "database/Track.hpp" +#include "database/TrackFeatures.hpp" +#include "database/TrackList.hpp" +#include "som/DataNormalizer.hpp" +#include "utils/Logger.hpp" +#include "utils/Random.hpp" + + +namespace Recommendation { + +std::unique_ptr createFeaturesClassifier() +{ + return std::make_unique(); +} + +const FeatureSettingsMap& +FeaturesClassifier::getDefaultTrainFeatureSettings() +{ + static const FeatureSettingsMap defaultTrainFeatureSettings + { + { "lowlevel.spectral_energyband_high.mean", {1}}, + { "lowlevel.spectral_rolloff.median", {1}}, + { "lowlevel.spectral_contrast_valleys.var", {1}}, + { "lowlevel.erbbands.mean", {1}}, + { "lowlevel.gfcc.mean", {1}}, + }; + + return defaultTrainFeatureSettings; +} + +static +std::optional +getTrackFeatureValues(FeaturesClassifier::FeaturesFetchFunc func, Database::IdType trackId, const std::unordered_set& featureNames) +{ + return func(trackId, featureNames); +} + +static +std::optional +getTrackFeatureValuesFromDb(Database::Session& session, Database::IdType trackId, const std::unordered_set& featureNames) +{ + auto func = [&](Database::IdType trackId, const std::unordered_set& featureNames) + { + std::optional res; + + auto transaction {session.createSharedTransaction()}; + + Database::Track::pointer track {Database::Track::getById(session, trackId)}; + if (!track) + return res; + + res = track->getTrackFeatures()->getFeatureValuesMap(featureNames); + if (res->empty()) + res.reset(); + + return res; + }; + + return getTrackFeatureValues(func, trackId, featureNames); +} + +static +std::optional +convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions) +{ + std::size_t i {}; + std::optional res {SOM::InputVector {nbDimensions}}; + for (const auto& [featureName, values] : featureValuesMap) + { + if (values.size() != getFeatureDef(featureName).nbDimensions) + { + LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size(); + res.reset(); + break; + } + + for (double val : values) + (*res)[i++] = val; + } + + return res; +} + +static +SOM::InputVector +getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions) +{ + SOM::InputVector weights {nbDimensions}; + std::size_t index {}; + for (const auto& [featureName, featureSettings] : featureSettingsMap) + { + const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions}; + + for (std::size_t i {}; i < featureNbDimensions; ++i) + weights[index++] = (1. / featureNbDimensions * featureSettings.weight); + } + + assert(index == nbDimensions); + + return weights; +} + +bool +FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSettings& trainSettings) +{ + LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier..."; + + std::unordered_set featureNames; + std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)), + [](const auto& itFeatureSetting) { return itFeatureSetting.first; }); + + const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0}, + [](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })}; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions; + + std::vector trackIds; + { + auto transaction {session.createSharedTransaction()}; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features..."; + trackIds = Database::Track::getAllIdsWithFeatures(session); + LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)"; + } + + std::vector samples; + std::vector samplesTrackIds; + + samples.reserve(trackIds.size()); + samplesTrackIds.reserve(trackIds.size()); + + LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features..."; + for (Database::IdType trackId : trackIds) + { + if (_initCancelled) + return false; + + std::optional featureValuesMap; + + if (_featuresFetchFunc) + featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames); + else + featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames); + + if (!featureValuesMap) + continue; + + std::optional inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)}; + if (!inputVector) + continue; + + samples.emplace_back(std::move(*inputVector)); + samplesTrackIds.emplace_back(trackId); + } + LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE"; + + if (samples.empty()) + { + LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!"; + return false; + } + + LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data..."; + SOM::DataNormalizer dataNormalizer {nbDimensions}; + + dataNormalizer.computeNormalizationFactors(samples); + for (auto& sample : samples) + dataNormalizer.normalizeData(sample); + + const SOM::Coordinate size {static_cast(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))}; + LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network"; + + SOM::Network network {size, size, nbDimensions}; + + SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)}; + network.setDataWeights(weights); + + auto progressIndicator{[](const auto& iter) + { + LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount; + }}; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Training network..."; + network.train(samples, trainSettings.iterationCount, progressIndicator); + LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE"; + + if (_initCancelled) + return false; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks..."; + ObjectPositions trackPositions; + for (std::size_t i {}; i < samples.size(); ++i) + { + if (_initCancelled) + return false; + + const SOM::Position position {network.getClosestRefVectorPosition(samples[i])}; + + trackPositions[samplesTrackIds[i]].insert(position); + } + + LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE"; + + return init(session, std::move(network), std::move(trackPositions)); +} + +bool +FeaturesClassifier::initFromCache(Database::Session& session, const FeaturesClassifierCache& cache) +{ + LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache..."; + + return init(session, std::move(cache._network), cache._trackPositions); +} + +std::vector +FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) const +{ + const std::unordered_set trackIds {[&]() -> std::unordered_set + { + auto transaction {session.createSharedTransaction()}; + + const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)}; + if (trackList) + { + const std::vector orderedTrackIds {trackList->getTrackIds()}; + return std::unordered_set {std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)}; + } + + return {}; + }()}; + + return getSimilarTracks(session, trackIds, maxCount); +} + +std::vector +FeaturesClassifier::getSimilarTracks(Database::Session&, const std::unordered_set& tracksIds, std::size_t maxCount) const +{ + return getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount); +} + +std::vector +FeaturesClassifier::getSimilarReleases(Database::Session&, Database::IdType releaseId, std::size_t maxCount) const +{ + return getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount); +} + +std::vector +FeaturesClassifier::getSimilarArtists(Database::Session&, Database::IdType artistId, std::size_t maxCount) const +{ + return getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount); +} + +FeaturesClassifierCache +FeaturesClassifier::toCache() const +{ + return FeaturesClassifierCache {*_network, _trackPositions}; +} + +bool +FeaturesClassifier::init(Database::Session& session) +{ + std::optional cache {FeaturesClassifierCache::read()}; + if (cache) + return initFromCache(session, *cache); + + TrainSettings trainSettings; + trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings(); + + bool res {initFromTraining(session, trainSettings)}; + if (res) + toCache().write(); + + return res; +} + +void +FeaturesClassifier::requestCancelInit() +{ + LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation"; + _initCancelled = true; +} + +bool +FeaturesClassifier::init(Database::Session& session, + SOM::Network network, + const ObjectPositions& tracksPosition) +{ + _networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian(); + LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian; + + const SOM::Coordinate width {network.getWidth()}; + const SOM::Coordinate height {network.getHeight()}; + + _artistsMap = MatrixOfObjects {width, height}; + _releasesMap = MatrixOfObjects {width, height}; + _tracksMap = MatrixOfObjects {width, height}; + + LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps..."; + + for (auto itTrackCoord : tracksPosition) + { + if (_initCancelled) + return false; + + auto transaction {session.createSharedTransaction()}; + + Database::IdType trackId {itTrackCoord.first}; + const std::unordered_set& positionSet {itTrackCoord.second}; + + const Database::Track::pointer track {Database::Track::getById(session, trackId)}; + if (!track) + continue; + + for (const SOM::Position& position : positionSet) + { + _tracksMap[position].insert(trackId); + _trackPositions[trackId].insert(position); + + if (track->getRelease()) + { + _releasePositions[track->getRelease().id()].insert(position); + _releasesMap[position].insert(track->getRelease().id()); + } + for (const auto& artist : track->getArtists()) + { + _artistPositions[artist.id()].insert(position); + _artistsMap[position].insert(artist.id()); + } + } + } + + _network = std::make_unique(std::move(network)); + + LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully initialized!"; + + return true; +} + +std::unordered_set +FeaturesClassifier::getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions) +{ + std::unordered_set res; + + if (ids.empty()) + return res; + + for (auto id : ids) + { + auto it = objectPositions.find(id); + if (it == objectPositions.end()) + continue; + + for (const auto& position : it->second) + res.insert(position); + } + + return res; +} + +std::unordered_set +FeaturesClassifier::getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap) +{ + std::unordered_set res; + + for (const auto& position : positionSet) + { + for (auto id : objectsMap.get(position)) + res.insert(id); + } + + return res; +} + +std::vector +FeaturesClassifier::getSimilarObjects(const std::unordered_set& ids, + const MatrixOfObjects& objectsMap, + const ObjectPositions& objectPosition, + std::size_t maxCount) const +{ + std::vector res; + + std::unordered_set searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)}; + if (searchedRefVectorsPosition.empty()) + return res; + + while (1) + { + std::unordered_set closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectsMap)}; + + // Remove objects that are already in input or already reported + for (auto id : ids) + closestObjectIds.erase(id); + + { + std::vector objectIdsToAdd {std::cbegin(closestObjectIds), std::cend(closestObjectIds)}; + Random::shuffleContainer(objectIdsToAdd ); + std::copy(std::cbegin(objectIdsToAdd), std::cend(objectIdsToAdd), std::back_inserter(res)); + } + + if (res.size() > maxCount) + res.resize(maxCount); + + if (res.size() == maxCount) + break; + + // If there is not enough objects, try again with closest neighbour until there is too much distance + const std::optional closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)}; + if (!closestRefVectorPosition) + break; + + searchedRefVectorsPosition.insert(closestRefVectorPosition.value()); + } + + return res; +} + + + +} // ns Recommendation diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp new file mode 100644 index 00000000..d4856a96 --- /dev/null +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 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 + +#include "recommendation/IClassifier.hpp" +#include "som/DataNormalizer.hpp" +#include "som/Network.hpp" +#include "FeaturesClassifierCache.hpp" +#include "FeaturesDefs.hpp" + +namespace Database +{ + class Session; +} + +namespace Recommendation { + +using FeatureWeight = double; + +class FeaturesClassifier : public IClassifier +{ + public: + FeaturesClassifier() = default; + FeaturesClassifier(const FeaturesClassifier&) = delete; + FeaturesClassifier(FeaturesClassifier&&) = delete; + FeaturesClassifier& operator=(const FeaturesClassifier&) = delete; + FeaturesClassifier& operator=(FeaturesClassifier&&) = delete; + + using FeaturesFetchFunc = std::function>>(Database::IdType /*trackId*/, const std::unordered_set& /*features*/)>; + // Default is to retrieve the features from the database (may be slow). + // Use this only if you want to train different searchers with some cached data + static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; } + + static const FeatureSettingsMap& getDefaultTrainFeatureSettings(); + private: + + std::string_view getName() const { return "Features"; } + + bool init(Database::Session& session) override; + void requestCancelInit() override; + + std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override; + std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override; + std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const; + std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const; + + bool initFromCache(Database::Session& session, const FeaturesClassifierCache& cache); + + // Use training (may be very slow) + struct TrainSettings + { + std::size_t iterationCount {10}; + float sampleCountPerNeuron {4}; + FeatureSettingsMap featureSettingsMap; + }; + bool initFromTraining(Database::Session& session, const TrainSettings& trainSettings); + + using ObjectPositions = std::unordered_map>; + using MatrixOfObjects = SOM::Matrix>; + + bool init(Database::Session& session, + SOM::Network network, + const ObjectPositions& tracksPosition); + + FeaturesClassifierCache toCache() const; + + static std::unordered_set getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions); + static std::unordered_set getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap); + + std::vector getSimilarObjects(const std::unordered_set& ids, + const SOM::Matrix>& objectsMap, + const ObjectPositions& objectPosition, + std::size_t maxCount) const; + + bool _initCancelled {}; + std::unique_ptr _network; + double _networkRefVectorsDistanceMedian {}; + + MatrixOfObjects _artistsMap; + ObjectPositions _artistPositions; + + MatrixOfObjects _releasesMap; + ObjectPositions _releasePositions; + + MatrixOfObjects _tracksMap; + ObjectPositions _trackPositions; + + static inline FeaturesFetchFunc _featuresFetchFunc; +}; + +} // ns Recommendation diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.cpp b/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp similarity index 76% rename from src/libs/recommendation/impl/features/SimilarityFeaturesCache.cpp rename to src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp index 235ed331..fc80098d 100644 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.cpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp @@ -17,7 +17,7 @@ * along with LMS. If not, see . */ -#include "SimilarityFeaturesCache.hpp" +#include "FeaturesClassifierCache.hpp" #include #include @@ -26,7 +26,7 @@ #include "utils/Logger.hpp" #include "utils/Service.hpp" -namespace Similarity { +namespace Recommendation { static @@ -38,7 +38,7 @@ std::filesystem::path getCacheDirectory() static std::filesystem::path getCacheNetworkFilePath() { return getCacheDirectory() / "network"; -}; +} static std::filesystem::path getCacheTrackPositionsFilePath() { @@ -79,26 +79,25 @@ networkToCacheFile(const SOM::Network& network, std::filesystem::path path) boost::property_tree::write_xml(path.string(), root); - LMS_LOG(SIMILARITY, DEBUG) << "Created network cache"; + LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache"; return true; } catch (boost::property_tree::ptree_error& error) { - LMS_LOG(SIMILARITY, ERROR) << "Cannot create network cache: " << error.what(); + LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what(); return false; } } -static std::optional -createNetworkFromCacheFile(const std::filesystem::path& path) +FeaturesClassifierCache::createNetworkFromCacheFile(const std::filesystem::path& path) { if (!std::filesystem::exists(path)) return std::nullopt; try { - LMS_LOG(SIMILARITY, INFO) << "Reading network from cache..."; + LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache..."; boost::property_tree::ptree root; @@ -132,20 +131,19 @@ createNetworkFromCacheFile(const std::filesystem::path& path) res.setRefVector({x, y}, refVector); } - LMS_LOG(SIMILARITY, INFO) << "Successfully read network from cache"; + LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache"; return res; } catch (boost::property_tree::ptree_error& error) { - LMS_LOG(SIMILARITY, ERROR) << "Cannot read network cache: " << error.what(); + LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what(); return std::nullopt; } } -static bool -objectPositionToCacheFile(const std::map>& objectsPosition, std::filesystem::path path) +FeaturesClassifierCache::objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path) { try { @@ -174,24 +172,23 @@ objectPositionToCacheFile(const std::map>> -createObjectPositionsFromCacheFile(std::filesystem::path path) +std::optional +FeaturesClassifierCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path) { try { - LMS_LOG(SIMILARITY, INFO) << "Reading object position from cache..."; + LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache..."; boost::property_tree::ptree root; boost::property_tree::read_xml(path.string(), root); - std::map> res; + ObjectPositions res; for (const auto& object : root.get_child("objects")) { @@ -205,42 +202,40 @@ createObjectPositionsFromCacheFile(std::filesystem::path path) } } - LMS_LOG(SIMILARITY, INFO) << "Successfully read object position from cache"; + LMS_LOG(RECOMMENDATION, INFO) << "Successfully read object position from cache"; return res; } catch (boost::property_tree::ptree_error& error) { - LMS_LOG(SIMILARITY, ERROR) << "Cannot create object position from cache file: " << error.what(); + LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what(); return std::nullopt; } } void -FeaturesCache::invalidate() +FeaturesClassifierCache::invalidate() { std::filesystem::remove(getCacheNetworkFilePath()); std::filesystem::remove(getCacheTrackPositionsFilePath()); } -std::optional -FeaturesCache::read() +std::optional +FeaturesClassifierCache::read() { - std::optional res; - auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())}; if (!network) - return res; + return std::nullopt; auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())}; if (!trackPositions) - return res; + return std::nullopt; - return FeaturesCache{std::move(*network), std::move(*trackPositions)}; + return FeaturesClassifierCache {std::move(*network), std::move(*trackPositions)}; } void -FeaturesCache::write() +FeaturesClassifierCache::write() const { std::filesystem::create_directories(ServiceProvider::get()->getPath("working-dir") / "cache" / "features"); @@ -251,11 +246,10 @@ FeaturesCache::write() } } -FeaturesCache::FeaturesCache(SOM::Network network, ObjectPositions trackPositions) +FeaturesClassifierCache::FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions) : _network {std::move(network)}, _trackPositions {std::move(trackPositions)} { } - -} // namespace Similarity +} // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.hpp b/src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp similarity index 53% rename from src/libs/recommendation/impl/features/SimilarityFeaturesCache.hpp rename to src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp index a12334f3..25ddaeff 100644 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.hpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp @@ -19,33 +19,36 @@ #pragma once -#include -#include -#include +#include +#include +#include #include "database/Types.hpp" #include "som/Network.hpp" -namespace Similarity { +namespace Recommendation { -class FeaturesCache +class FeaturesClassifierCache { public: - static void invalidate(); - static std::optional read(); - void write(); + static std::optional read(); + void write() const; private: - using ObjectPositions = std::map>; + using ObjectPositions = std::unordered_map>; - FeaturesCache(SOM::Network network, ObjectPositions trackPositions); + FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions); - friend class FeaturesSearcher; + static std::optional createNetworkFromCacheFile(const std::filesystem::path& path); + static std::optional createObjectPositionsFromCacheFile(const std::filesystem::path& path); + static bool objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path); + + friend class FeaturesClassifier; SOM::Network _network; ObjectPositions _trackPositions; }; -} // namespace Similarity +} // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesDefs.cpp b/src/libs/recommendation/impl/features/FeaturesDefs.cpp similarity index 99% rename from src/libs/recommendation/impl/features/SimilarityFeaturesDefs.cpp rename to src/libs/recommendation/impl/features/FeaturesDefs.cpp index a0aecc4d..b3de7dcb 100644 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesDefs.cpp +++ b/src/libs/recommendation/impl/features/FeaturesDefs.cpp @@ -17,14 +17,14 @@ * along with LMS. If not, see . */ -#include "SimilarityFeaturesDefs.hpp" +#include "FeaturesDefs.hpp" #include #include #include "utils/Exception.hpp" -namespace Similarity { +namespace Recommendation { static const std::unordered_map featureDefinitions { @@ -398,5 +398,5 @@ getFeatureNames() return res; } -} // namespace Similarity +} // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesDefs.hpp b/src/libs/recommendation/impl/features/FeaturesDefs.hpp similarity index 95% rename from src/libs/recommendation/impl/features/SimilarityFeaturesDefs.hpp rename to src/libs/recommendation/impl/features/FeaturesDefs.hpp index 58eed273..d6e1d759 100644 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesDefs.hpp +++ b/src/libs/recommendation/impl/features/FeaturesDefs.hpp @@ -24,7 +24,7 @@ #include #include -namespace Similarity { +namespace Recommendation { using FeatureName = std::string; using FeatureNames = std::unordered_set; @@ -46,4 +46,4 @@ struct FeatureSettings }; using FeatureSettingsMap = std::unordered_map; -} // namespace Similarity +} // namespace Recommendation diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesScannerAddon.cpp b/src/libs/recommendation/impl/features/SimilarityFeaturesScannerAddon.cpp deleted file mode 100644 index 620cedc8..00000000 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesScannerAddon.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2019 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 . - */ - -#include "similarity/features/SimilarityFeaturesScannerAddon.hpp" - -#include "AcousticBrainzUtils.hpp" -#include "database/ScanSettings.hpp" -#include "database/Track.hpp" -#include "database/TrackFeatures.hpp" -#include "utils/Logger.hpp" - -#include "SimilarityFeaturesCache.hpp" - -namespace Similarity { - -static -bool -hasAtLeastOneTrackWithFeatures(Database::Session& session) -{ - auto transaction {session.createSharedTransaction()}; - return !Database::Track::getAllIdsWithFeatures(session, 1).empty(); -} - -struct TrackInfo -{ - Database::IdType id; - std::optional mbid; -}; - -static -std::vector -getTracksWithMBIDAndMissingFeatures(Database::Session& dbSession) -{ - std::vector res; - - auto transaction {dbSession.createSharedTransaction()}; - - auto tracks {Database::Track::getAllWithMBIDAndMissingFeatures(dbSession)}; - for (const Database::Track::pointer& track : tracks) - res.push_back({track.id(), track->getMBID()}); - - return res; -} - -FeaturesScannerAddon::FeaturesScannerAddon(Database::Db& db) -: _dbSession {db} -{ - std::optional cache {Similarity::FeaturesCache::read()}; - if (cache) - { - auto searcher {std::make_shared(_dbSession, *cache, [&]() { return _stopRequested; })}; - if (searcher->isValid()) - std::atomic_store(&_searcher, searcher); - } -} - -std::shared_ptr -FeaturesScannerAddon::getSearcher() -{ - return std::atomic_load(&_searcher); -} - -void -FeaturesScannerAddon::requestStop() -{ - _stopRequested = true; -} - -void -FeaturesScannerAddon::trackUpdated(Database::IdType trackId) -{ - auto uniqueTransaction {_dbSession.createUniqueTransaction()}; - - auto track {Database::Track::getById(_dbSession, trackId)}; - if (!track) - return; - - track.modify()->setFeatures({}); -} - -void -FeaturesScannerAddon::preScanComplete() -{ - { - auto transaction {_dbSession.createSharedTransaction()}; - - if (Database::ScanSettings::get(_dbSession)->getSimilarityEngineType() != Database::ScanSettings::SimilarityEngineType::Features) - { - LMS_LOG(DBUPDATER, INFO) << "Do not fetch features since the engine type does not make use of them"; - return; - } - } - - LMS_LOG(DBUPDATER, DEBUG) << "Getting tracks with missing Features..."; - const std::vector tracksInfo {getTracksWithMBIDAndMissingFeatures(_dbSession)}; - LMS_LOG(DBUPDATER, DEBUG) << "Getting tracks with missing Features DONE (found " << tracksInfo.size() << ")"; - - if (!tracksInfo.empty()) - Similarity::FeaturesCache::invalidate(); - - for (const TrackInfo& trackInfo : tracksInfo) - { - if (_stopRequested) - return; - - if (trackInfo.mbid) - fetchFeatures(trackInfo.id, *trackInfo.mbid); - } - - updateSearcher(); -} - -void -FeaturesScannerAddon::updateSearcher() -{ - LMS_LOG(SIMILARITY, INFO) << "Updating searcher..."; - - if (!hasAtLeastOneTrackWithFeatures(_dbSession)) - { - LMS_LOG(DBUPDATER, INFO) << "No track found with features!"; - std::atomic_store(&_searcher, std::shared_ptr{}); - return; - } - - Similarity::FeaturesSearcher::TrainSettings trainSettings; - trainSettings.featureSettingsMap = FeaturesSearcher::getDefaultTrainFeatureSettings(); - - auto searcher {std::make_shared(_dbSession, trainSettings, [&]() { return _stopRequested; })}; - if (searcher->isValid()) - { - std::atomic_store(&_searcher, searcher); - FeaturesCache cache{searcher->toCache()}; - cache.write(); - - LMS_LOG(DBUPDATER, INFO) << "New features similarity searcher instanciated"; - } - else - { - LMS_LOG(DBUPDATER, ERROR) << "Cannot set up a valid features similarity searcher!"; - std::atomic_store(&_searcher, std::shared_ptr{}); - } -} - -bool -FeaturesScannerAddon::fetchFeatures(Database::IdType trackId, const UUID& MBID) -{ - std::map features; - - LMS_LOG(DBUPDATER, DEBUG) << "Fetching low level features for track '" << MBID.getAsString() << "'"; - const std::string data {AcousticBrainz::extractLowLevelFeatures(MBID)}; - if (data.empty()) - { - LMS_LOG(DBUPDATER, ERROR) << "Track " << trackId << ", MBID = '" << MBID.getAsString() << "': cannot extract features using AcousticBrainz"; - return false; - } - - { - auto uniqueTransaction {_dbSession.createUniqueTransaction()}; - - Wt::Dbo::ptr track {Database::Track::getById(_dbSession, trackId)}; - if (!track) - return false; - - Database::TrackFeatures::create(_dbSession, track, data); - } - - return true; -} - -} // namespace Similarity - diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesSearcher.cpp b/src/libs/recommendation/impl/features/SimilarityFeaturesSearcher.cpp deleted file mode 100644 index a1ab5987..00000000 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesSearcher.cpp +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright (C) 2018 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 . - */ - -#include "SimilarityFeaturesSearcher.hpp" - -#include -#include - -#include "database/Artist.hpp" -#include "database/Release.hpp" -#include "database/Session.hpp" -#include "database/Track.hpp" -#include "database/TrackFeatures.hpp" -#include "som/DataNormalizer.hpp" -#include "utils/Logger.hpp" - - -namespace Similarity { - -const FeatureSettingsMap& -FeaturesSearcher::getDefaultTrainFeatureSettings() -{ - static FeatureSettingsMap defaultTrainFeatureSettings - { - { "lowlevel.spectral_energyband_high.mean", {1}}, - { "lowlevel.spectral_rolloff.median", {1}}, - { "lowlevel.spectral_contrast_valleys.var", {1}}, - { "lowlevel.erbbands.mean", {1}}, - { "lowlevel.gfcc.mean", {1}}, - }; - - return defaultTrainFeatureSettings; -} - -static -std::optional -getTrackFeatureValues(FeaturesSearcher::FeaturesFetchFunc func, Database::IdType trackId, const std::unordered_set& featureNames) -{ - return func(trackId, featureNames); -} - -static -std::optional -getTrackFeatureValuesFromDb(Database::Session& session, Database::IdType trackId, const std::unordered_set& featureNames) -{ - auto func = [&](Database::IdType trackId, const std::unordered_set& featureNames) - { - std::optional res; - - auto transaction {session.createSharedTransaction()}; - - Database::Track::pointer track {Database::Track::getById(session, trackId)}; - if (!track) - return res; - - res = track->getTrackFeatures()->getFeatureValuesMap(featureNames); - if (res->empty()) - res.reset(); - - return res; - }; - - return getTrackFeatureValues(func, trackId, featureNames); -} - -static -std::optional -convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions) -{ - std::size_t i {}; - std::optional res {SOM::InputVector {nbDimensions}}; - for (const auto& [featureName, values] : featureValuesMap) - { - if (values.size() != getFeatureDef(featureName).nbDimensions) - { - LMS_LOG(SIMILARITY, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size(); - res.reset(); - break; - } - - for (double val : values) - (*res)[i++] = val; - } - - return res; -} - -static -SOM::InputVector -getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions) -{ - SOM::InputVector weights {nbDimensions}; - std::size_t index {}; - for (const auto& [featureName, featureSettings] : featureSettingsMap) - { - const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions}; - - for (std::size_t i {}; i < featureNbDimensions; ++i) - weights[index++] = (1. / featureNbDimensions * featureSettings.weight); - } - - assert(index == nbDimensions); - - return weights; -} - -FeaturesSearcher::FeaturesSearcher(Database::Session& session, - const TrainSettings& trainSettings, - StopRequestedFunction stopRequested) -{ - LMS_LOG(SIMILARITY, INFO) << "Constructing features searcher..."; - - std::unordered_set featureNames; - std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)), - [](const auto& itFeatureSetting) { return itFeatureSetting.first; }); - - const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0}, - [](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })}; - - LMS_LOG(SIMILARITY, DEBUG) << "Features dimension = " << nbDimensions; - - std::vector trackIds; - { - auto transaction {session.createSharedTransaction()}; - - LMS_LOG(SIMILARITY, DEBUG) << "Getting Tracks with features..."; - trackIds = Database::Track::getAllIdsWithFeatures(session); - LMS_LOG(SIMILARITY, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)"; - } - - std::vector samples; - std::vector samplesTrackIds; - - samples.reserve(trackIds.size()); - samplesTrackIds.reserve(trackIds.size()); - - LMS_LOG(SIMILARITY, DEBUG) << "Extracting features..."; - for (Database::IdType trackId : trackIds) - { - if (stopRequested && stopRequested()) - return; - - std::optional featureValuesMap; - - if (_featuresFetchFunc) - featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames); - else - featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames); - - if (!featureValuesMap) - continue; - - std::optional inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)}; - if (!inputVector) - continue; - - samples.emplace_back(std::move(*inputVector)); - samplesTrackIds.emplace_back(trackId); - } - LMS_LOG(SIMILARITY, DEBUG) << "Extracting features DONE"; - - if (samples.empty()) - { - LMS_LOG(SIMILARITY, INFO) << "Nothing to classify!"; - return; - } - - LMS_LOG(SIMILARITY, DEBUG) << "Normalizing data..."; - SOM::DataNormalizer dataNormalizer {nbDimensions}; - - dataNormalizer.computeNormalizationFactors(samples); - for (auto& sample : samples) - dataNormalizer.normalizeData(sample); - - SOM::Coordinate size {static_cast(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))}; - LMS_LOG(SIMILARITY, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network"; - - SOM::Network network {size, size, nbDimensions}; - - SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)}; - network.setDataWeights(weights); - - auto progressIndicator{[](const auto& iter) - { - LMS_LOG(SIMILARITY, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount; - }}; - - LMS_LOG(SIMILARITY, DEBUG) << "Training network..."; - network.train(samples, trainSettings.iterationCount, progressIndicator, stopRequested); - LMS_LOG(SIMILARITY, DEBUG) << "Training network DONE"; - - if (stopRequested && stopRequested()) - return; - - LMS_LOG(SIMILARITY, DEBUG) << "Classifying tracks..."; - std::map> trackPositions; - for (std::size_t i {}; i < samples.size(); ++i) - { - if (stopRequested && stopRequested()) - return; - - const SOM::Position position {network.getClosestRefVectorPosition(samples[i])}; - - trackPositions[samplesTrackIds[i]].insert(position); - } - - LMS_LOG(SIMILARITY, DEBUG) << "Classifying tracks DONE"; - - init(session, std::move(network), std::move(trackPositions), stopRequested); - - LMS_LOG(SIMILARITY, INFO) << "Successfully constructed features searcher"; -} - -FeaturesSearcher::FeaturesSearcher(Database::Session& session, FeaturesCache cache, StopRequestedFunction stopRequested) -{ - LMS_LOG(SIMILARITY, INFO) << "Constructing features searcher from cache..."; - - init(session, std::move(cache._network), std::move(cache._trackPositions), stopRequested); - - LMS_LOG(SIMILARITY, INFO) << "Successfully constructed features searcher from cache"; -} - -bool -FeaturesSearcher::isValid() const -{ - return _network.get() != nullptr; -} - -bool -FeaturesSearcher::isTrackClassified(Database::IdType trackId) const -{ - return (_trackPositions.find(trackId) != _trackPositions.end()); -} - -bool -FeaturesSearcher::isReleaseClassified(Database::IdType releaseId) const -{ - return (_releasePositions.find(releaseId) != _releasePositions.end()); -} - -bool -FeaturesSearcher::isArtistClassified(Database::IdType artistId) const -{ - return (_artistPositions.find(artistId) != _artistPositions.end()); -} - -std::vector -FeaturesSearcher::getSimilarTracks(const std::set& tracksIds, std::size_t maxCount) const -{ - return getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount); -} - -std::vector -FeaturesSearcher::getSimilarReleases(Database::IdType releaseId, std::size_t maxCount) const -{ - return getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount); -} - -std::vector -FeaturesSearcher::getSimilarArtists(Database::IdType artistId, std::size_t maxCount) const -{ - return getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount); -} - -void -FeaturesSearcher::dump(Database::Session& session, std::ostream& os) const -{ - if (!isValid()) - { - os << "Invalid searcher" << std::endl; - return; - } - - os << "Number of tracks classified: " << _trackPositions.size() << std::endl; - os << "Network size: " << _network->getWidth() << " * " << _network->getHeight() << std::endl; - os << "Ref vectors median distance = " << _networkRefVectorsDistanceMedian << std::endl; - - auto transaction {session.createSharedTransaction()}; - - for (SOM::Coordinate y {}; y < _network->getHeight(); ++y) - { - for (SOM::Coordinate x {}; x < _network->getWidth(); ++x) - { - const auto& trackIds {_tracksMap[{x, y}]}; - - os << "{" << x << ", " << y << "}"; - - if (y > 0) - os << " - {" << x << ", " << y - 1 << "}: " << _network->getRefVectorsDistance({x, y}, {x, y - 1}); - if (x > 0) - os << " - {" << x - 1 << ", " << y << "}: " << _network->getRefVectorsDistance({x, y}, {x - 1, y}); - if (y != _network->getHeight() - 1) - os << " - {" << x << ", " << y + 1 << "}: " << _network->getRefVectorsDistance({x, y}, {x, y + 1}); - if (x != _network->getWidth() - 1) - os << " - {" << x + 1 << ", " << y << "}: " << _network->getRefVectorsDistance({x, y}, {x + 1, y}); - os << std::endl; - - for (Database::IdType trackId : trackIds) - { - auto track {Database::Track::getById(session, trackId)}; - if (!track) - continue; - - os << "\t"; - for (auto artist : track->getArtists()) - os << artist->getName() << " - "; - if (track->getRelease()) - os << track->getRelease()->getName() << " - "; - os << track->getName() << std::endl; - } - - } - os << std::endl; - } -} - -FeaturesCache -FeaturesSearcher::toCache() const -{ - return FeaturesCache{*_network, _trackPositions}; -} - -void -FeaturesSearcher::init(Database::Session& session, - SOM::Network network, - std::map> tracksPosition, - std::function stopRequested) -{ - _networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian(); - LMS_LOG(SIMILARITY, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian; - - SOM::Coordinate width {network.getWidth()}; - SOM::Coordinate height {network.getHeight()}; - - _artistsMap = SOM::Matrix>{width, height}; - _releasesMap = SOM::Matrix>{width, height}; - _tracksMap = SOM::Matrix>{width, height}; - - LMS_LOG(SIMILARITY, DEBUG) << "Constructing maps..."; - - for (auto itTrackCoord : tracksPosition) - { - if (stopRequested && stopRequested()) - return; - - auto transaction {session.createSharedTransaction()}; - - Database::IdType trackId {itTrackCoord.first}; - const std::set& positionSet {itTrackCoord.second}; - - const Database::Track::pointer track {Database::Track::getById(session, trackId)}; - if (!track) - continue; - - for (const SOM::Position& position : positionSet) - { - _tracksMap[position].insert(trackId); - _trackPositions[trackId].insert(position); - - if (track->getRelease()) - { - _releasePositions[track->getRelease().id()].insert(position); - _releasesMap[position].insert(track->getRelease().id()); - } - for (const auto& artist : track->getArtists()) - { - _artistPositions[artist.id()].insert(position); - _artistsMap[position].insert(artist.id()); - } - } - } - - _network = std::make_unique(std::move(network)); - - LMS_LOG(SIMILARITY, DEBUG) << "Constructing maps... DONE"; - -} - -static -std::set -getMatchingRefVectorsPosition(const std::set& ids, const std::map>& objectPosition) -{ - std::set res; - - if (ids.empty()) - return res; - - for (auto id : ids) - { - auto it = objectPosition.find(id); - if (it == objectPosition.end()) - continue; - - for (const auto& position : it->second) - res.insert(position); - } - - return res; -} - -static -std::set -getObjectsIds(const std::set& positionSet, const SOM::Matrix>& objectsMap ) -{ - std::set res; - - for (const auto& position : positionSet) - { - for (auto id : objectsMap.get(position)) - res.insert(id); - } - - return res; -} - -std::vector -FeaturesSearcher::getSimilarObjects(const std::set& ids, - const SOM::Matrix>& objectsMap, - const std::map>& objectPosition, - std::size_t maxCount) const -{ - std::vector res; - - if (!isValid()) - return res; - - auto now {std::chrono::system_clock::now()}; - std::mt19937 randGenerator{static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; - - std::set searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)}; - if (searchedRefVectorsPosition.empty()) - return res; - - while (1) - { - std::set closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectsMap)}; - - // Remove objects that are already in input or already reported - for (auto id : ids) - closestObjectIds.erase(id); - - for (auto id : res) - closestObjectIds.erase(id); - - { - std::vector objectIdsToAdd {closestObjectIds.begin(), closestObjectIds.end()}; - std::shuffle(objectIdsToAdd.begin(), objectIdsToAdd.end(), randGenerator); - std::copy(objectIdsToAdd.begin(), objectIdsToAdd.end(), std::back_inserter(res)); - } - - if (res.size() > maxCount) - res.resize(maxCount); - - if (res.size() == maxCount) - break; - - // If there is not enough objects, try again with closest neighbour until there is too much distance - std::optional closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)}; - if (!closestRefVectorPosition) - break; - - searchedRefVectorsPosition.insert(*closestRefVectorPosition); - } - - return res; -} - - - -} // ns Similarity diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesSearcher.hpp b/src/libs/recommendation/impl/features/SimilarityFeaturesSearcher.hpp deleted file mode 100644 index 625877c1..00000000 --- a/src/libs/recommendation/impl/features/SimilarityFeaturesSearcher.hpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2018 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 -#include - -#include "database/Types.hpp" -#include "som/DataNormalizer.hpp" -#include "som/Network.hpp" -#include "SimilarityFeaturesCache.hpp" -#include "SimilarityFeaturesDefs.hpp" - -namespace Database -{ - class Session; -} - -namespace Similarity { - -using FeatureWeight = double; - -class FeaturesSearcher -{ - public: - - using StopRequestedFunction = std::function; // return true if stop requested - - // Use cache - FeaturesSearcher(Database::Session& session, FeaturesCache cache, StopRequestedFunction stopRequested); - - // Use training (may be very slow) - struct TrainSettings - { - std::size_t iterationCount {10}; - float sampleCountPerNeuron {4}; - FeatureSettingsMap featureSettingsMap; - }; - FeaturesSearcher(Database::Session& session, const TrainSettings& trainSettings, StopRequestedFunction stopRequested = {}); - - static const FeatureSettingsMap& getDefaultTrainFeatureSettings(); - - bool isValid() const; - - bool isTrackClassified(Database::IdType trackId) const; - bool isReleaseClassified(Database::IdType releaseId) const; - bool isArtistClassified(Database::IdType artistId) const; - - std::vector getSimilarTracks(const std::set& tracksId, std::size_t maxCount) const; - std::vector getSimilarReleases(Database::IdType releaseId, std::size_t maxCount) const; - std::vector getSimilarArtists(Database::IdType artistId, std::size_t maxCount) const; - - void dump(Database::Session& session, std::ostream& os) const; - - FeaturesCache toCache() const; - - using FeaturesFetchFunc = std::function>>(Database::IdType /*trackId*/, const std::unordered_set& /*features*/)>; - // Default is to retrieve the features from the database (may be slow). - // Use this only if you want to train different searchers with the same data - static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; } - - private: - - using ObjectPositions = std::map>; - - void init(Database::Session& session, - SOM::Network network, - ObjectPositions tracksPosition, - StopRequestedFunction stopRequested); - - std::vector getSimilarObjects(const std::set& ids, - const SOM::Matrix>& objectsMap, - const ObjectPositions& objectPosition, - std::size_t maxCount) const; - - std::unique_ptr _network; - double _networkRefVectorsDistanceMedian {}; - - SOM::Matrix> _artistsMap; - ObjectPositions _artistPositions; - - SOM::Matrix> _releasesMap; - ObjectPositions _releasePositions; - - SOM::Matrix> _tracksMap; - ObjectPositions _trackPositions; - - static inline FeaturesFetchFunc _featuresFetchFunc; -}; - -} // ns Similarity diff --git a/src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp b/src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp index 3f4dfbe8..c9527ef8 100644 --- a/src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp +++ b/src/libs/recommendation/include/recommendation/ClustersClassifierCreator.hpp @@ -25,6 +25,6 @@ namespace Recommendation { class IClassifier; - std::unique_ptr createClustersClassifier(Database::Session& session); + std::unique_ptr createClustersClassifier(); } diff --git a/src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp b/src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp index 9a763020..28a079d7 100644 --- a/src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp +++ b/src/libs/recommendation/include/recommendation/FeaturesClassifierCreator.hpp @@ -20,11 +20,10 @@ #pragma once #include +#include "recommendation/IClassifier.hpp" namespace Recommendation { - class IClassifier; - std::unique_ptr createFeaturesClassifier(); } diff --git a/src/libs/recommendation/include/recommendation/IClassifier.hpp b/src/libs/recommendation/include/recommendation/IClassifier.hpp index 6619ccd2..e1c01f2f 100644 --- a/src/libs/recommendation/include/recommendation/IClassifier.hpp +++ b/src/libs/recommendation/include/recommendation/IClassifier.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -37,9 +38,10 @@ namespace Recommendation public: virtual ~IClassifier() = default; - virtual bool isTrackClassified(Database::IdType trackId) const = 0; - virtual bool isReleaseClassified(Database::IdType releaseId) const = 0; - virtual bool isArtistClassified(Database::IdType artistId) const = 0; + virtual std::string_view getName() const = 0; + + virtual bool init(Database::Session& session) = 0; + virtual void requestCancelInit() = 0; virtual std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const = 0; virtual std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const = 0; diff --git a/src/libs/scanner/CMakeLists.txt b/src/libs/scanner/CMakeLists.txt index 3d7a77f3..57fae63a 100644 --- a/src/libs/scanner/CMakeLists.txt +++ b/src/libs/scanner/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(lmsscanner SHARED + impl/AcousticBrainzUtils.cpp impl/MediaScanner.cpp impl/MediaScannerStats.cpp ) diff --git a/src/libs/recommendation/impl/features/AcousticBrainzUtils.cpp b/src/libs/scanner/impl/AcousticBrainzUtils.cpp similarity index 84% rename from src/libs/recommendation/impl/features/AcousticBrainzUtils.cpp rename to src/libs/scanner/impl/AcousticBrainzUtils.cpp index b33f5c4f..970e04da 100644 --- a/src/libs/recommendation/impl/features/AcousticBrainzUtils.cpp +++ b/src/libs/scanner/impl/AcousticBrainzUtils.cpp @@ -28,6 +28,7 @@ #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" +#include "utils/UUID.hpp" namespace AcousticBrainz @@ -50,7 +51,7 @@ getJsonData(const UUID& mbid) if (!client.get(url)) { - LMS_LOG(SIMILARITY, ERROR) << "Cannot perform a GET request to url '" << url << "'"; + LMS_LOG(DBUPDATER, ERROR) << "Cannot perform a GET request to url '" << url << "'"; return {}; } @@ -59,13 +60,13 @@ getJsonData(const UUID& mbid) { if (ec) { - LMS_LOG(SIMILARITY, ERROR) << "GET request to url '" << url << "' failed: " << ec.message(); + LMS_LOG(DBUPDATER, ERROR) << "GET request to url '" << url << "' failed: " << ec.message(); return; } if (msg.status() != 200) { - LMS_LOG(SIMILARITY, ERROR) << "GET request to url '" << url << "' failed: status = " << msg.status() << ", body = " << msg.body(); + LMS_LOG(DBUPDATER, ERROR) << "GET request to url '" << url << "' failed: status = " << msg.status() << ", body = " << msg.body(); return; } diff --git a/src/libs/recommendation/impl/features/AcousticBrainzUtils.hpp b/src/libs/scanner/impl/AcousticBrainzUtils.hpp similarity index 96% rename from src/libs/recommendation/impl/features/AcousticBrainzUtils.hpp rename to src/libs/scanner/impl/AcousticBrainzUtils.hpp index 70d8fdf6..cdd73d77 100644 --- a/src/libs/recommendation/impl/features/AcousticBrainzUtils.hpp +++ b/src/libs/scanner/impl/AcousticBrainzUtils.hpp @@ -21,7 +21,7 @@ #include -#include "utils/UUID.hpp" +class UUID; namespace AcousticBrainz { diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 84e78d92..236333d4 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -28,10 +28,13 @@ #include "database/Release.hpp" #include "database/ScanSettings.hpp" #include "database/Track.hpp" +#include "database/TrackFeatures.hpp" #include "metadata/TagLibParser.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" #include "utils/Path.hpp" +#include "utils/UUID.hpp" +#include "AcousticBrainzUtils.hpp" using namespace Database; @@ -420,7 +423,10 @@ MediaScanner::scan(boost::system::error_code err) if (_running) checkDuplicatedAudioFiles(stats); - LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), duplicates = " << stats.duplicates.size(); + // Now update all the track features if needed + fetchTrackFeatures(stats); + + LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size(); LMS_LOG(DBUPDATER, INFO) << "Optimizing db..."; _dbSession.optimize(); @@ -449,6 +455,76 @@ MediaScanner::scan(boost::system::error_code err) } } +bool +MediaScanner::fetchTrackFeatures(Database::IdType trackId, const UUID& MBID) +{ + std::map features; + + LMS_LOG(DBUPDATER, INFO) << "Fetching low level features for track '" << MBID.getAsString() << "'"; + const std::string data {AcousticBrainz::extractLowLevelFeatures(MBID)}; + if (data.empty()) + { + LMS_LOG(DBUPDATER, ERROR) << "Track " << trackId << ", MBID = '" << MBID.getAsString() << "': cannot extract features using AcousticBrainz"; + return false; + } + + { + auto uniqueTransaction {_dbSession.createUniqueTransaction()}; + + Wt::Dbo::ptr track {Database::Track::getById(_dbSession, trackId)}; + if (!track) + return false; + + Database::TrackFeatures::create(_dbSession, track, data); + } + + return true; +} + +void +MediaScanner::fetchTrackFeatures(ScanStats& stats) +{ + if (_recommendationEngineType != ScanSettings::RecommendationEngineType::Features) + return; + + LMS_LOG(DBUPDATER, INFO) << "Fetching missing track features..."; + + struct TrackInfo + { + Database::IdType id; + UUID mbid; + }; + + const auto tracksToFetch {[&]() + { + std::vector res; + + auto transaction {_dbSession.createSharedTransaction()}; + + auto tracks {Database::Track::getAllWithMBIDAndMissingFeatures(_dbSession)}; + for (const auto& track : tracks) + res.emplace_back(TrackInfo {track.id(), *track->getMBID()}); + + return res; + }()}; + + stats.featuresToFetch = tracksToFetch.size(); + + LMS_LOG(DBUPDATER, INFO) << "Found " << tracksToFetch.size() << " track(s) to fetch!"; + + for (const TrackInfo& trackToFetch : tracksToFetch) + { + if (!_running) + return; + + if (fetchTrackFeatures(trackToFetch.id, trackToFetch.mbid)) + stats.featuresFetched++; + + } + + LMS_LOG(DBUPDATER, INFO) << "Track features fetched!"; +} + void MediaScanner::refreshScanSettings() { @@ -464,6 +540,7 @@ MediaScanner::refreshScanSettings() _fileExtensions = scanSettings->getAudioFileExtensions(); _mediaDirectory = scanSettings->getMediaDirectory(); + _recommendationEngineType = scanSettings->getRecommendationEngineType(); auto clusterTypes = scanSettings->getClusterTypes(); std::set clusterTypeNames; @@ -648,6 +725,7 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S track.modify()->setYear(*trackInfo->originalYear); track.modify()->setMBID(trackInfo->musicBrainzRecordID); + track.modify()->setFeatures({}); // TODO: only if MBID changed? track.modify()->setHasCover(trackInfo->hasCover); track.modify()->setCopyright(trackInfo->copyright); track.modify()->setCopyrightURL(trackInfo->copyrightURL); diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index 54bb70ce..645538a0 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -35,6 +35,7 @@ #include "metadata/IParser.hpp" #include "scanner/IMediaScanner.hpp" +class UUID; namespace Scanner { @@ -72,6 +73,8 @@ class MediaScanner : public IMediaScanner void scan(boost::system::error_code ec); void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); + bool fetchTrackFeatures(Database::IdType trackId, const UUID& MBID); + void fetchTrackFeatures(ScanStats& stats); // Helpers void refreshScanSettings(); @@ -102,11 +105,12 @@ class MediaScanner : public IMediaScanner Wt::WDateTime _nextScheduledScan; // Current scan settings - std::size_t _scanVersion {}; - Wt::WTime _startTime; + std::size_t _scanVersion {}; + Wt::WTime _startTime; Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never}; std::set _fileExtensions; std::filesystem::path _mediaDirectory; + Database::ScanSettings::RecommendationEngineType _recommendationEngineType; }; // class MediaScanner diff --git a/src/libs/scanner/include/scanner/MediaScannerStats.hpp b/src/libs/scanner/include/scanner/MediaScannerStats.hpp index 6f4e514d..99c898b6 100644 --- a/src/libs/scanner/include/scanner/MediaScannerStats.hpp +++ b/src/libs/scanner/include/scanner/MediaScannerStats.hpp @@ -77,10 +77,13 @@ namespace Scanner { std::size_t skips {}; // no change since last scan std::size_t scans {}; // actually scanned filed - std::size_t additions {}; // Added in DB + std::size_t additions {}; // added in DB std::size_t deletions {}; // removed from DB std::size_t updates {}; // updated file in DB + std::size_t featuresFetched {}; // features fetched in DB + std::size_t featuresToFetch {}; // features to be fetched in DB + std::vector errors; std::vector duplicates; diff --git a/src/libs/som/CMakeLists.txt b/src/libs/som/CMakeLists.txt index 7cb1b45e..2e19b9d2 100644 --- a/src/libs/som/CMakeLists.txt +++ b/src/libs/som/CMakeLists.txt @@ -16,3 +16,5 @@ target_link_libraries(lmssom PUBLIC lmsutils ) +set_property(TARGET lmssom PROPERTY POSITION_INDEPENDENT_CODE ON) + diff --git a/src/libs/som/impl/Network.cpp b/src/libs/som/impl/Network.cpp index deea6f7b..75e60fce 100644 --- a/src/libs/som/impl/Network.cpp +++ b/src/libs/som/impl/Network.cpp @@ -201,9 +201,9 @@ Network::getClosestRefVectorPosition(const InputVector& data, InputVector::Dista } std::optional -Network::getClosestRefVectorPosition(const std::set& refVectorsPosition, InputVector::Distance maxDistance) const +Network::getClosestRefVectorPosition(const std::unordered_set& refVectorsPosition, InputVector::Distance maxDistance) const { - std::set neighboursPosition; + std::unordered_set neighboursPosition; for (const Position& refVectorPosition : refVectorsPosition) { if (refVectorPosition.y > 0) diff --git a/src/libs/som/include/som/InputVector.hpp b/src/libs/som/include/som/InputVector.hpp index 058e0fee..50737fec 100644 --- a/src/libs/som/include/som/InputVector.hpp +++ b/src/libs/som/include/som/InputVector.hpp @@ -31,7 +31,7 @@ namespace SOM class Exception : public LmsException { public: - Exception(const std::string& msg) : LmsException(msg) {} + using LmsException::LmsException; }; class InputVector diff --git a/src/libs/som/include/som/Matrix.hpp b/src/libs/som/include/som/Matrix.hpp index 32df1bff..2688d729 100644 --- a/src/libs/som/include/som/Matrix.hpp +++ b/src/libs/som/include/som/Matrix.hpp @@ -21,7 +21,7 @@ #include #include -#include +#include #include namespace SOM @@ -116,3 +116,20 @@ class Matrix }; } // ns SOM + +namespace std { + +template<> +class hash +{ + public: + size_t operator()(const SOM::Position& s) const + { + size_t h1 = std::hash()(s.x); + size_t h2 = std::hash()(s.y); + return h1 ^ (h2 << 1); + } +}; + +} // ns std + diff --git a/src/libs/som/include/som/Network.hpp b/src/libs/som/include/som/Network.hpp index cf04cf72..f3ba2df1 100644 --- a/src/libs/som/include/som/Network.hpp +++ b/src/libs/som/include/som/Network.hpp @@ -20,7 +20,7 @@ #pragma once #include -#include +#include #include #include #include @@ -70,7 +70,7 @@ class Network Position getClosestRefVectorPosition(const InputVector& data) const; std::optional getClosestRefVectorPosition(const InputVector& data, InputVector::Distance maxDistance) const; - std::optional getClosestRefVectorPosition(const std::set& refVectorsPosition, InputVector::Distance maxDistance) const; + std::optional getClosestRefVectorPosition(const std::unordered_set& refVectorsPosition, InputVector::Distance maxDistance) const; InputVector::Distance getRefVectorsDistance(const Position& position1, const Position& position2) const; diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 1b6e504e..54fe076c 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -148,7 +148,7 @@ int main(int argc, char* argv[]) { auto status = mediaScanner.getStatus(); - if (status.lastCompleteScanStats->nbChanges() > 0) + if (status.lastCompleteScanStats->nbChanges() > 0 || status.lastCompleteScanStats->featuresFetched > 0) { LMS_LOG(MAIN, INFO) << "Scanner changed some files, reloading the recommendation engine..."; recommendationEngine.requestReload(); diff --git a/src/test/som/SomTest.cpp b/src/test/som/SomTest.cpp index 6f4edb37..bfbf05a5 100644 --- a/src/test/som/SomTest.cpp +++ b/src/test/som/SomTest.cpp @@ -82,7 +82,7 @@ int main() assert((std::abs(distFunc({1, 0}, {1, 0.33}, weights) - distFunc({1, 0.66}, {1, 1.}, weights)) < EPSILON)); { - std::set positions; + std::unordered_set positions; for (const InputVector& data : trainData) positions.insert(network.getClosestRefVectorPosition(data)); assert(positions.size() == 4); diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index b5ea5aa4..954af8c0 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -149,7 +149,7 @@ int main(int argc, char *argv[]) engine->start(); - std::cout << "Wating for the recommendation engine to be loaded..." << std::endl; + std::cout << "Waiting for the recommendation engine to be loaded..." << std::endl; sem.wait(); std::cout << "Recommendation engine loaded!" << std::endl;