Restored recommendations based on acoustic similarities (using musicnn), fixes #301
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
#include "steps/ScanStepCheckForRemovedFiles.hpp"
|
||||
#include "steps/ScanStepCompact.hpp"
|
||||
#include "steps/ScanStepComputeClusterStats.hpp"
|
||||
#include "steps/ScanStepExtractMusicNNEmbeddings.hpp"
|
||||
#include "steps/ScanStepOptimize.hpp"
|
||||
#include "steps/ScanStepRemoveOrphanedDbEntries.hpp"
|
||||
#include "steps/ScanStepScanFiles.hpp"
|
||||
@@ -123,6 +124,10 @@ namespace lms::scanner
|
||||
settings->allowArtistMBIDFallback = scanSettings->getAllowMBIDArtistMerge();
|
||||
settings->artistImageFallbackToRelease = scanSettings->getArtistImageFallbackToReleaseField();
|
||||
|
||||
settings->extractMusicNNEmbeddings = scanSettings->getRecommendationEngineType() == db::ScanSettings::RecommendationEngineType::AudioSimilarity;
|
||||
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);
|
||||
|
||||
// TODO, store this in DB + expose in UI
|
||||
settings->skipDuplicateTrackMBID = core::Service<core::IConfig>::get()->getBool("scanner-skip-duplicate-mbid", false);
|
||||
|
||||
@@ -164,6 +169,8 @@ namespace lms::scanner
|
||||
, _jobScheduler{ core::createJobScheduler("Scanner", getScannerThreadCount()) }
|
||||
, _cachePath{ cachePath }
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO, "Starting service...");
|
||||
|
||||
_ioService.setThreadCount(1);
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO, "Using " << _jobScheduler->getThreadCount() << " thread(s) for jobs");
|
||||
@@ -186,6 +193,8 @@ namespace lms::scanner
|
||||
refreshScanSettings();
|
||||
|
||||
start();
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO, "Service started!");
|
||||
}
|
||||
|
||||
ScannerService::~ScannerService()
|
||||
@@ -389,7 +398,7 @@ namespace lms::scanner
|
||||
}
|
||||
|
||||
refreshTracingLoggerStats();
|
||||
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.getChangesCount() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << ", failures = " << stats.failures << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errorsCount << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
|
||||
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.getChangesCount() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << ", failures = " << stats.failures << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errorsCount << "), audio features extracted = " << stats.featureExtractions << ", duplicates = " << stats.duplicates.size());
|
||||
|
||||
{
|
||||
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||
@@ -518,6 +527,10 @@ namespace lms::scanner
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepOptimize>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepComputeClusterStats>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
|
||||
|
||||
// Audio extraction scan step must be last as it is the most long running
|
||||
if (_settings.extractMusicNNEmbeddings)
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepExtractMusicNNEmbeddings>(params, _settings.musicnnModelPath, _settings.musicnnMaxPatchCountPerTrack));
|
||||
}
|
||||
|
||||
void ScannerService::notifyInProgress(const ScanStepStats& stepStats)
|
||||
|
||||
@@ -47,6 +47,9 @@ namespace lms::scanner
|
||||
bool skipSingleReleasePlayLists{};
|
||||
bool allowArtistMBIDFallback{ true };
|
||||
bool artistImageFallbackToRelease{};
|
||||
bool extractMusicNNEmbeddings{};
|
||||
std::filesystem::path musicnnModelPath;
|
||||
std::size_t musicnnMaxPatchCountPerTrack{};
|
||||
|
||||
std::vector<MediaLibraryInfo> mediaLibraries;
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ namespace lms::scanner
|
||||
unsigned ScanStepStats::progress() const
|
||||
{
|
||||
const unsigned res{ static_cast<unsigned>((processedElems / static_cast<float>(totalElems ? totalElems : 1)) * 100) };
|
||||
// can technically be above 100% since we may add files while iterating the filesystem
|
||||
return res;
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
#include "database/objects/TrackArtistLink.hpp"
|
||||
#include "database/objects/TrackEmbeddedImage.hpp"
|
||||
#include "database/objects/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/objects/TrackFeatures.hpp"
|
||||
#include "database/objects/TrackLyrics.hpp"
|
||||
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
|
||||
@@ -554,7 +554,7 @@ namespace lms::scanner
|
||||
info.type = image.type;
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ImageHash");
|
||||
info.hash = core::xxHash3_64(image.data);
|
||||
info.hash = core::XxHash3_64::hash(image.data);
|
||||
}
|
||||
info.size = image.data.size();
|
||||
info.mimeType = image.mimeType;
|
||||
@@ -581,6 +581,28 @@ namespace lms::scanner
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if any value actually changed.
|
||||
bool updateAudioProperties(db::Track::pointer& track, const audio::AudioProperties& props)
|
||||
{
|
||||
const bool changed{ track->getDuration() != props.duration
|
||||
|| track->getContainer() != props.container
|
||||
|| track->getCodec() != props.codec
|
||||
|| track->getBitrate() != props.bitrate
|
||||
|| track->getChannelCount() != props.channelCount
|
||||
|| track->getSampleRate() != props.sampleRate
|
||||
|| track->getBitsPerSample() != props.bitsPerSample };
|
||||
|
||||
track.modify()->setDuration(props.duration);
|
||||
track.modify()->setContainer(props.container);
|
||||
track.modify()->setCodec(props.codec);
|
||||
track.modify()->setBitrate(props.bitrate);
|
||||
track.modify()->setChannelCount(props.channelCount);
|
||||
track.modify()->setSampleRate(props.sampleRate);
|
||||
track.modify()->setBitsPerSample(props.bitsPerSample);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
AudioFileScanOperation::OperationResult AudioFileScanOperation::processResult()
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
||||
@@ -700,13 +722,7 @@ namespace lms::scanner
|
||||
track.modify()->setScanVersion(getScannerSettings().audioScanVersion);
|
||||
|
||||
// Audio properties
|
||||
track.modify()->setDuration(_file->audioProperties.duration);
|
||||
track.modify()->setContainer(_file->audioProperties.container);
|
||||
track.modify()->setCodec(_file->audioProperties.codec);
|
||||
track.modify()->setBitrate(_file->audioProperties.bitrate);
|
||||
track.modify()->setChannelCount(_file->audioProperties.channelCount);
|
||||
track.modify()->setSampleRate(_file->audioProperties.sampleRate);
|
||||
track.modify()->setBitsPerSample(_file->audioProperties.bitsPerSample);
|
||||
const bool audioPropertiesChanged{ updateAudioProperties(track, _file->audioProperties) };
|
||||
|
||||
track.modify()->setFileSize(getFileSize());
|
||||
track.modify()->setLastWriteTime(getLastWriteTime());
|
||||
@@ -772,8 +788,11 @@ namespace lms::scanner
|
||||
|
||||
track.modify()->setRecordingMBID(_file->track.recordingMBID);
|
||||
track.modify()->setTrackMBID(_file->track.mbid);
|
||||
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||
if (audioPropertiesChanged)
|
||||
{
|
||||
if (auto musicnnEmbedding{ db::TrackMusicNNEmbeddings::find(dbSession, track->getId()) })
|
||||
musicnnEmbedding.remove();
|
||||
}
|
||||
track.modify()->setCopyright(_file->track.copyright);
|
||||
track.modify()->setCopyrightURL(_file->track.copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_file->track.advisory));
|
||||
|
||||
@@ -90,4 +90,9 @@ namespace lms::scanner
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse playlist " << error.path << ": all entries are missing");
|
||||
}
|
||||
|
||||
void ScanErrorLogger::visit(const MusicNNEmbeddingsExtractError& error)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to extract MusicNN embeddings from " << error.path << ": " << error.errorMsg);
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -23,21 +23,22 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class ScanErrorLogger : public scanner::ScanErrorVisitor
|
||||
class ScanErrorLogger : public ScanErrorVisitor
|
||||
{
|
||||
private:
|
||||
void visit(const scanner::ScanError&) override;
|
||||
void visit(const scanner::IOScanError& error) override;
|
||||
void visit(const scanner::AudioFileScanError& error) override;
|
||||
void visit(const scanner::EmbeddedImageScanError& error) override;
|
||||
void visit(const scanner::NoAudioTrackFoundError& error) override;
|
||||
void visit(const scanner::BadAudioDurationError& error) override;
|
||||
void visit(const scanner::ArtistInfoFileScanError& error) override;
|
||||
void visit(const scanner::MissingArtistNameError& error) override;
|
||||
void visit(const scanner::ImageFileScanError& error) override;
|
||||
void visit(const scanner::LyricsFileScanError& error) override;
|
||||
void visit(const scanner::PlayListFileScanError& error) override;
|
||||
void visit(const scanner::PlayListFilePathMissingError& error) override;
|
||||
void visit(const scanner::PlayListFileAllPathesMissingError& error) override;
|
||||
void visit(const ScanError& error) override;
|
||||
void visit(const IOScanError& error) override;
|
||||
void visit(const AudioFileScanError& error) override;
|
||||
void visit(const EmbeddedImageScanError& error) override;
|
||||
void visit(const NoAudioTrackFoundError& error) override;
|
||||
void visit(const BadAudioDurationError& error) override;
|
||||
void visit(const ArtistInfoFileScanError& error) override;
|
||||
void visit(const MissingArtistNameError& error) override;
|
||||
void visit(const ImageFileScanError& error) override;
|
||||
void visit(const LyricsFileScanError& error) override;
|
||||
void visit(const PlayListFileScanError& error) override;
|
||||
void visit(const PlayListFilePathMissingError& error) override;
|
||||
void visit(const PlayListFileAllPathesMissingError& error) override;
|
||||
void visit(const MusicNNEmbeddingsExtractError& error) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -229,7 +229,7 @@ namespace lms::scanner
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 50 };
|
||||
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty())
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || (!forceFullBatch && !imageAssociations.empty()))
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace lms::scanner
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 50 };
|
||||
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty())
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || (!forceFullBatch && !imageAssociations.empty()))
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ namespace lms::scanner
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 5 };
|
||||
|
||||
while ((forceFullBatch && playListFileAssociations.size() >= writeBatchSize) || !playListFileAssociations.empty())
|
||||
while ((forceFullBatch && playListFileAssociations.size() >= writeBatchSize) || (!forceFullBatch && !playListFileAssociations.empty()))
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace lms::scanner
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 50 };
|
||||
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty())
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || (!forceFullBatch && !imageAssociations.empty()))
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace lms::scanner
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 50 };
|
||||
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || !imageAssociations.empty())
|
||||
while ((forceFullBatch && imageAssociations.size() >= writeBatchSize) || (!forceFullBatch && !imageAssociations.empty()))
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace lms::scanner
|
||||
constexpr std::size_t writeBatchSize{ 50 };
|
||||
|
||||
std::vector<typename Object::IdType> ids;
|
||||
while ((forceFullBatch && objectIdsToRemove.size() >= writeBatchSize) || !objectIdsToRemove.empty())
|
||||
while ((forceFullBatch && objectIdsToRemove.size() >= writeBatchSize) || (!forceFullBatch && !objectIdsToRemove.empty()))
|
||||
{
|
||||
for (std::size_t i{}; !objectIdsToRemove.empty() && i < writeBatchSize; ++i)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
*/
|
||||
|
||||
#include "ScanStepComputeClusterStats.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/Cluster.hpp"
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "ScanStepExtractMusicNNEmbeddings.hpp"
|
||||
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
|
||||
#include "core/IJob.hpp"
|
||||
#include "core/IJobScheduler.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
#include "audio/IMusicNNEmbeddingExtractor.hpp"
|
||||
#include "audio/MusicNNEmbeddings.hpp"
|
||||
#include "database/IDb.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/objects/ScanSettings.hpp"
|
||||
#include "database/objects/Track.hpp"
|
||||
#include "database/objects/TrackMusicNNEmbeddings.hpp"
|
||||
#include "services/scanner/ScanErrors.hpp"
|
||||
|
||||
#include "JobQueue.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "TrackLocation.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct TrackEmbeddingAssociation
|
||||
{
|
||||
db::TrackId trackId;
|
||||
std::optional<audio::TrackMusicNNEmbeddings> embeddings;
|
||||
};
|
||||
using TrackEmbeddingAssociationContainer = std::deque<TrackEmbeddingAssociation>;
|
||||
|
||||
db::Track::FindParameters createFindTrackParams(db::TrackId lastRetrievedTrackId = {})
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
params.setHasMusicNNEmbeddings(false);
|
||||
params.setSortMethod(db::TrackSortMethod::Id);
|
||||
params.setLastTrackId(lastRetrievedTrackId);
|
||||
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
bool fetchNextTrackWithoutEmbeddings(db::Session& session, db::TrackId& lastRetrievedTrackId, TrackLocation& trackLocation)
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const db::Track::FindParameters params{ createFindTrackParams(lastRetrievedTrackId) };
|
||||
|
||||
trackLocation.track = db::TrackId{};
|
||||
trackLocation.trackPath.clear();
|
||||
db::Track::findAbsoluteFilePath(session, params, [&](db::TrackId trackId, const std::filesystem::path& absoluteFilePath) {
|
||||
trackLocation.track = trackId;
|
||||
trackLocation.trackPath = absoluteFilePath;
|
||||
});
|
||||
lastRetrievedTrackId = trackLocation.track;
|
||||
return trackLocation.track.isValid();
|
||||
}
|
||||
|
||||
class ExtractMusicNNEmbeddingsJob : public core::IJob
|
||||
{
|
||||
public:
|
||||
ExtractMusicNNEmbeddingsJob(const audio::IMusicNNEmbeddingExtractor& extractor, const TrackLocation& trackLocation)
|
||||
: _extractor{ extractor }
|
||||
, _trackLocation{ trackLocation }
|
||||
{
|
||||
}
|
||||
~ExtractMusicNNEmbeddingsJob() override = default;
|
||||
ExtractMusicNNEmbeddingsJob(const ExtractMusicNNEmbeddingsJob&) = delete;
|
||||
ExtractMusicNNEmbeddingsJob& operator=(const ExtractMusicNNEmbeddingsJob&) = delete;
|
||||
|
||||
const TrackLocation& getTrackLocation() const { return _trackLocation; }
|
||||
const audio::TrackMusicNNEmbeddings* getEmbeddings() const { return _embeddings ? &_embeddings.value() : nullptr; }
|
||||
std::string_view getErrorMessage() const { return _errorMessage; }
|
||||
|
||||
private:
|
||||
core::LiteralString getName() const override { return "Extract MusicNN Embeddings"; }
|
||||
|
||||
void run() override
|
||||
{
|
||||
try
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Extracting MusicNN embeddings for " << _trackLocation.trackPath);
|
||||
const auto result{ _extractor.extract(_trackLocation.trackPath) };
|
||||
if (result.patchCount > 0)
|
||||
_embeddings.emplace(result.embeddings);
|
||||
LMS_LOG(DBUPDATER, DEBUG, "MusicNN extraction complete for " << _trackLocation.trackPath << " (" << result.patchCount << " patches)");
|
||||
}
|
||||
catch (const audio::Exception& e)
|
||||
{
|
||||
_errorMessage = e.what();
|
||||
}
|
||||
}
|
||||
|
||||
const audio::IMusicNNEmbeddingExtractor& _extractor;
|
||||
const TrackLocation _trackLocation;
|
||||
std::optional<audio::TrackMusicNNEmbeddings> _embeddings;
|
||||
std::string _errorMessage;
|
||||
};
|
||||
|
||||
void writeEmbedding(db::Session& session, const TrackEmbeddingAssociation& assoc)
|
||||
{
|
||||
db::Track::pointer track{ db::Track::find(session, assoc.trackId) };
|
||||
assert(track);
|
||||
|
||||
std::vector<std::byte> blob(sizeof(audio::TrackMusicNNEmbeddings));
|
||||
audio::trackMusicNNEmbeddingsToBlob(*assoc.embeddings, blob);
|
||||
db::TrackMusicNNEmbeddings::pointer entry{ session.create<db::TrackMusicNNEmbeddings>(track) };
|
||||
entry.modify()->setData(blob);
|
||||
}
|
||||
|
||||
void writeEmbeddings(ScanContext& context, db::Session& session, TrackEmbeddingAssociationContainer& pendingAssocs, bool forceFullBatch)
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 10 };
|
||||
|
||||
while ((forceFullBatch && pendingAssocs.size() >= writeBatchSize) || (!forceFullBatch && !pendingAssocs.empty()))
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
for (std::size_t i{}; !pendingAssocs.empty() && i < writeBatchSize; ++i)
|
||||
{
|
||||
writeEmbedding(session, pendingAssocs.front());
|
||||
pendingAssocs.pop_front();
|
||||
context.stats.featureExtractions += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ScanStepExtractMusicNNEmbeddings::ScanStepExtractMusicNNEmbeddings(InitParams& initParams, const std::filesystem::path& modelPath, std::size_t musicnnMaxPatchCountPerTrack)
|
||||
: ScanStepBase{ initParams }
|
||||
, _embeddingExtractor{ audio::createMusicNNEmbeddingExtractor(modelPath, musicnnMaxPatchCountPerTrack) }
|
||||
{
|
||||
}
|
||||
|
||||
ScanStepExtractMusicNNEmbeddings::~ScanStepExtractMusicNNEmbeddings() = default;
|
||||
|
||||
bool ScanStepExtractMusicNNEmbeddings::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScanStepExtractMusicNNEmbeddings::process(ScanContext& context)
|
||||
{
|
||||
db::Session& dbSession{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
const std::string fileIdentifier{ audio::getMusicNNModelIdentifier(_settings.musicnnModelPath) };
|
||||
if (fileIdentifier.empty())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, WARNING, "Cannot identify MusicNN model file, skipping embedding extraction");
|
||||
return;
|
||||
}
|
||||
const std::string identifier{ fileIdentifier + "|" + std::to_string(_settings.musicnnMaxPatchCountPerTrack) };
|
||||
|
||||
auto transaction{ dbSession.createWriteTransaction() };
|
||||
db::ScanSettings::pointer settings{ db::ScanSettings::find(dbSession) };
|
||||
assert(settings);
|
||||
if (settings->getMusicNNModelIdentifier() != identifier)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO, "MusicNN model changed, clearing embeddings");
|
||||
db::TrackMusicNNEmbeddings::removeAll(dbSession);
|
||||
settings.modify()->setMusicNNModelIdentifier(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
db::Track::FindParameters params{ createFindTrackParams() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
context.currentStepStats.totalElems = db::Track::getCount(dbSession, params);
|
||||
}
|
||||
|
||||
TrackEmbeddingAssociationContainer pendingAssocs;
|
||||
|
||||
auto processResults{ [&](std::span<std::unique_ptr<core::IJob>> jobs) {
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
for (const auto& job : jobs)
|
||||
{
|
||||
const auto& extractJob{ static_cast<const ExtractMusicNNEmbeddingsJob&>(*job) };
|
||||
|
||||
if (const audio::TrackMusicNNEmbeddings * embeddings{ extractJob.getEmbeddings() })
|
||||
pendingAssocs.push_back(TrackEmbeddingAssociation{ .trackId = extractJob.getTrackLocation().track, .embeddings = *embeddings });
|
||||
else
|
||||
addError<MusicNNEmbeddingsExtractError>(context, extractJob.getTrackLocation().trackPath, extractJob.getErrorMessage());
|
||||
}
|
||||
|
||||
context.currentStepStats.processedElems += jobs.size();
|
||||
writeEmbeddings(context, dbSession, pendingAssocs, true);
|
||||
_progressCallback(context.currentStepStats);
|
||||
} };
|
||||
|
||||
{
|
||||
JobQueue queue{ getJobScheduler(), 50, processResults, 1, 0.85F };
|
||||
|
||||
db::TrackId lastRetrievedTrackId;
|
||||
TrackLocation trackLocation;
|
||||
while (!_abortScan && fetchNextTrackWithoutEmbeddings(dbSession, lastRetrievedTrackId, trackLocation))
|
||||
queue.push(std::make_unique<ExtractMusicNNEmbeddingsJob>(*_embeddingExtractor, trackLocation));
|
||||
}
|
||||
|
||||
writeEmbeddings(context, dbSession, pendingAssocs, false);
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 "ScanStepBase.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
class IMusicNNEmbeddingExtractor;
|
||||
}
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class ScanStepExtractMusicNNEmbeddings : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
ScanStepExtractMusicNNEmbeddings(InitParams& initParams, const std::filesystem::path& modelPath, std::size_t maxPatchCountPerTrack);
|
||||
~ScanStepExtractMusicNNEmbeddings() override;
|
||||
ScanStepExtractMusicNNEmbeddings(const ScanStepExtractMusicNNEmbeddings&) = delete;
|
||||
ScanStepExtractMusicNNEmbeddings& operator=(const ScanStepExtractMusicNNEmbeddings&) = delete;
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ExtractMusicNNEmbeddings; }
|
||||
core::LiteralString getStepName() const override { return "Extract MusicNN embeddings"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
std::unique_ptr<audio::IMusicNNEmbeddingExtractor> _embeddingExtractor;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -181,7 +181,7 @@ namespace lms::scanner
|
||||
constexpr std::size_t filesPerScanJob{ 10 };
|
||||
constexpr std::size_t scanQueueMaxSize{ 50 };
|
||||
constexpr std::size_t processFileResultsBatchSize{ 1 };
|
||||
constexpr float drainRatio{ 0.85 };
|
||||
constexpr float drainRatio{ 0.85F };
|
||||
|
||||
std::deque<std::unique_ptr<IFileScanOperation>> operations;
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 <filesystem>
|
||||
|
||||
#include "database/objects/TrackId.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct TrackLocation
|
||||
{
|
||||
db::TrackId track;
|
||||
std::filesystem::path trackPath;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
Reference in New Issue
Block a user