Make the audio similarity engine the default one for new installs, if available

This commit is contained in:
emeric
2026-07-12 15:38:29 +02:00
parent 714b8640da
commit 78c624bde2
12 changed files with 40 additions and 36 deletions
+1 -1
View File
@@ -42,8 +42,8 @@ _LMS_ provides several ways to help you find the music you like:
* ... * ...
__Note__: the recommendation engine supports two modes: __Note__: the recommendation engine supports two modes:
* **Audio similarity** (default): uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 to ~25k tracks/hour on an Intel Core i5-13500.
* **Tag-based**: uses metadata tags `genre`, `mood`, `grouping`, and `language`. * **Tag-based**: uses metadata tags `genre`, `mood`, `grouping`, and `language`.
* **Audio similarity**: uses MusicNN embeddings. Extraction throughput depends heavily on both the number of scanner threads and the speed of your storage. Typical reported speeds range from ~1k tracks/hour on a Raspberry Pi 4 (3 threads) to ~25k tracks/hour on an Intel Core i5-13500 (10 threads). To speed up extraction, consider tweaking `scanner-thread-count` in `lms.conf` (defaults to half the number of logical CPUs).
## About tags ## About tags
_LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md). _LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md).
+4 -6
View File
@@ -180,17 +180,15 @@ namespace lms::db
bool Session::migrateSchemaIfNeeded() bool Session::migrateSchemaIfNeeded()
{ {
const bool migrationPerformed{ Migration::doDbMigration(*this) }; return Migration::doDbMigration(*this);
}
// TODO: move this elsewhere void Session::createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType)
{ {
auto uniqueTransaction{ createWriteTransaction() }; auto uniqueTransaction{ createWriteTransaction() };
if (!ScanSettings::find(*this)) if (!ScanSettings::find(*this))
create<ScanSettings>(); create<ScanSettings>().modify()->setRecommendationEngineType(defaultRecommendationEngineType);
}
return migrationPerformed;
} }
void Session::createIndexesIfNeeded() void Session::createIndexesIfNeeded()
@@ -28,6 +28,7 @@
#include "database/Transaction.hpp" #include "database/Transaction.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Types.hpp"
namespace lms::db namespace lms::db
{ {
@@ -58,6 +59,7 @@ namespace lms::db
void prepareTablesIfNeeded(); // need to run only once at startup void prepareTablesIfNeeded(); // need to run only once at startup
bool migrateSchemaIfNeeded(); // returns true if migration was performed bool migrateSchemaIfNeeded(); // returns true if migration was performed
void createScanSettingsIfNeeded(RecommendationEngineType defaultRecommendationEngineType = RecommendationEngineType::Clusters);
void createIndexesIfNeeded(); void createIndexesIfNeeded();
void vacuumIfNeeded(); void vacuumIfNeeded();
void vacuum(); void vacuum();
@@ -29,6 +29,7 @@
#include "database/IdType.hpp" #include "database/IdType.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/objects/Types.hpp"
LMS_DECLARE_IDTYPE(ScanSettingsId) LMS_DECLARE_IDTYPE(ScanSettingsId)
@@ -49,14 +50,6 @@ namespace lms::db
Hourly, Hourly,
}; };
// Do not modify values (just add)
enum class RecommendationEngineType
{
Clusters = 0,
None = 2,
AudioSimilarity = 3,
};
ScanSettings() = default; ScanSettings() = default;
static pointer find(Session& session, std::string_view name = ""); static pointer find(Session& session, std::string_view name = "");
@@ -114,6 +114,13 @@ namespace lms::db
PubDateDesc, PubDateDesc,
}; };
enum class RecommendationEngineType
{
Clusters = 0,
None = 2,
AudioSimilarity = 3,
};
enum class ReleaseArtistLinkSortMethod enum class ReleaseArtistLinkSortMethod
{ {
None, None,
+1
View File
@@ -344,6 +344,7 @@ VALUES
// Now perform full migration // Now perform full migration
db.getTLSSession().migrateSchemaIfNeeded(); db.getTLSSession().migrateSchemaIfNeeded();
db.getTLSSession().createScanSettingsIfNeeded();
// Now perform some dummy finds to ensure all fields are correctly mapped // Now perform some dummy finds to ensure all fields are correctly mapped
{ {
@@ -32,35 +32,35 @@ namespace lms::recommendation
{ {
namespace namespace
{ {
db::ScanSettings::RecommendationEngineType getRecommendationEngineType(db::Session& session) db::RecommendationEngineType getRecommendationEngineType(db::Session& session)
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
return db::ScanSettings::find(session)->getRecommendationEngineType(); return db::ScanSettings::find(session)->getRecommendationEngineType();
} }
EngineType toEngineType(db::ScanSettings::RecommendationEngineType type) EngineType toEngineType(db::RecommendationEngineType type)
{ {
switch (type) switch (type)
{ {
case db::ScanSettings::RecommendationEngineType::None: case db::RecommendationEngineType::None:
return EngineType::None; return EngineType::None;
case db::ScanSettings::RecommendationEngineType::Clusters: case db::RecommendationEngineType::Clusters:
return EngineType::Clusters; return EngineType::Clusters;
case db::ScanSettings::RecommendationEngineType::AudioSimilarity: case db::RecommendationEngineType::AudioSimilarity:
return EngineType::AudioSimilarity; return EngineType::AudioSimilarity;
} }
return EngineType::None; return EngineType::None;
} }
std::unique_ptr<IEngine> createEngine(db::ScanSettings::RecommendationEngineType type, db::IDb& db) std::unique_ptr<IEngine> createEngine(db::RecommendationEngineType type, db::IDb& db)
{ {
switch (type) switch (type)
{ {
case db::ScanSettings::RecommendationEngineType::Clusters: case db::RecommendationEngineType::Clusters:
return std::make_unique<TagsEngine>(db); return std::make_unique<TagsEngine>(db);
case db::ScanSettings::RecommendationEngineType::AudioSimilarity: case db::RecommendationEngineType::AudioSimilarity:
return std::make_unique<MusicNNEmbeddingEngine>(db); return std::make_unique<MusicNNEmbeddingEngine>(db);
case db::ScanSettings::RecommendationEngineType::None: case db::RecommendationEngineType::None:
return nullptr; return nullptr;
} }
return nullptr; return nullptr;
@@ -139,7 +139,7 @@ namespace lms::recommendation
return false; return false;
} }
db::ScanSettings::RecommendationEngineType RecommendationService::prepareReload() db::RecommendationEngineType RecommendationService::prepareReload()
{ {
const auto type{ getRecommendationEngineType(_db.getTLSSession()) }; const auto type{ getRecommendationEngineType(_db.getTLSSession()) };
std::unique_lock lock{ _mutex }; std::unique_lock lock{ _mutex };
@@ -59,7 +59,7 @@ 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 findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const override; TrackResults findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const override;
db::ScanSettings::RecommendationEngineType prepareReload(); db::RecommendationEngineType prepareReload();
db::IDb& _db; db::IDb& _db;
mutable std::shared_mutex _mutex; mutable std::shared_mutex _mutex;
@@ -126,7 +126,7 @@ namespace lms::scanner
settings->allowArtistMBIDFallback = scanSettings->getAllowMBIDArtistMerge(); settings->allowArtistMBIDFallback = scanSettings->getAllowMBIDArtistMerge();
settings->artistImageFallbackToRelease = scanSettings->getArtistImageFallbackToRelease(); settings->artistImageFallbackToRelease = scanSettings->getArtistImageFallbackToRelease();
settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::ScanSettings::RecommendationEngineType::AudioSimilarity; settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::RecommendationEngineType::AudioSimilarity;
settings->musicnnModelPath = core::Service<core::IConfig>::get()->getPath("musicnn-model-path", "/usr/share/lms/models/MSD_musicnn_embedding.onnx"); settings->musicnnModelPath = core::Service<core::IConfig>::get()->getPath("musicnn-model-path", "/usr/share/lms/models/MSD_musicnn_embedding.onnx");
settings->musicnnMaxPatchCountPerTrack = core::Service<core::IConfig>::get()->getULong("musicnn-max-patch-count-per-track", 20); settings->musicnnMaxPatchCountPerTrack = core::Service<core::IConfig>::get()->getULong("musicnn-max-patch-count-per-track", 20);
+2
View File
@@ -36,6 +36,7 @@
#include "core/SystemPaths.hpp" #include "core/SystemPaths.hpp"
#include "audio/IAudioOutput.hpp" #include "audio/IAudioOutput.hpp"
#include "audio/IMusicNNEmbeddingExtractor.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/profiling/IQueryProfiler.hpp" #include "database/profiling/IQueryProfiler.hpp"
@@ -436,6 +437,7 @@ namespace lms
db::Session session{ *database }; db::Session session{ *database };
session.prepareTablesIfNeeded(); session.prepareTablesIfNeeded();
bool migrationPerformed{ session.migrateSchemaIfNeeded() }; bool migrationPerformed{ session.migrateSchemaIfNeeded() };
session.createScanSettingsIfNeeded(audio::canExtractMusicNNEmbeddings() ? db::RecommendationEngineType::AudioSimilarity : db::RecommendationEngineType::Clusters);
session.createIndexesIfNeeded(); session.createIndexesIfNeeded();
// As this may be quite long, we only do it during startup // As this may be quite long, we only do it during startup
+6 -6
View File
@@ -44,7 +44,7 @@ namespace lms::ui
{ {
namespace namespace
{ {
using RecommendationEngineTypeModel = ValueStringModel<db::ScanSettings::RecommendationEngineType>; using RecommendationEngineTypeModel = ValueStringModel<db::RecommendationEngineType>;
class RecommendationEngineValidator : public Wt::WValidator class RecommendationEngineValidator : public Wt::WValidator
{ {
@@ -63,7 +63,7 @@ namespace lms::ui
std::string inputStr{ input.toUTF8() }; std::string inputStr{ input.toUTF8() };
const auto row{ _model->getRowFromString(inputStr) }; const auto row{ _model->getRowFromString(inputStr) };
if (row && _model->getValue(*row) == db::ScanSettings::RecommendationEngineType::AudioSimilarity) if (row && _model->getValue(*row) == db::RecommendationEngineType::AudioSimilarity)
{ {
if (!core::Service<recommendation::IRecommendationService>::get()->isEngineTypeSupported(recommendation::EngineType::AudioSimilarity)) if (!core::Service<recommendation::IRecommendationService>::get()->isEngineTypeSupported(recommendation::EngineType::AudioSimilarity))
return Wt::WValidator::Result{ Wt::ValidationState::Invalid, Wt::WString::tr("Lms.Admin.Database.recommendation-engine-not-supported") }; return Wt::WValidator::Result{ Wt::ValidationState::Invalid, Wt::WString::tr("Lms.Admin.Database.recommendation-engine-not-supported") };
@@ -247,10 +247,10 @@ namespace lms::ui
_updateStartTimeModel->add(time.toString(), time); _updateStartTimeModel->add(time.toString(), time);
} }
_recommendationEngineTypeModel = std::make_shared<ValueStringModel<db::ScanSettings::RecommendationEngineType>>(); _recommendationEngineTypeModel = std::make_shared<ValueStringModel<db::RecommendationEngineType>>();
_recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.audio-similarity"), db::ScanSettings::RecommendationEngineType::AudioSimilarity); _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.audio-similarity"), db::RecommendationEngineType::AudioSimilarity);
_recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.clusters"), db::ScanSettings::RecommendationEngineType::Clusters); _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.clusters"), db::RecommendationEngineType::Clusters);
_recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.none"), db::ScanSettings::RecommendationEngineType::None); _recommendationEngineTypeModel->add(Wt::WString::tr("Lms.Admin.Database.recommendation-engine-type.none"), db::RecommendationEngineType::None);
} }
std::shared_ptr<UpdatePeriodModel> _updatePeriodModel; std::shared_ptr<UpdatePeriodModel> _updatePeriodModel;
@@ -282,6 +282,7 @@ int main(int argc, char* argv[])
db::Session session{ *db }; db::Session session{ *db };
session.prepareTablesIfNeeded(); session.prepareTablesIfNeeded();
session.migrateSchemaIfNeeded(); session.migrateSchemaIfNeeded();
session.createScanSettingsIfNeeded();
session.createIndexesIfNeeded(); session.createIndexesIfNeeded();
std::cout << "Starting generation..." << std::endl; std::cout << "Starting generation..." << std::endl;