Provide a non-priviledged place for model downloads when exe is installed to root.

This commit is contained in:
Adam Treat 2023-04-23 11:28:17 -04:00
parent 09c27f6ec4
commit 29685b3eab
4 changed files with 286 additions and 195 deletions

View File

@ -8,6 +8,7 @@
#include <QJsonArray> #include <QJsonArray>
#include <QUrl> #include <QUrl>
#include <QDir> #include <QDir>
#include <QStandardPaths>
class MyDownload: public Download { }; class MyDownload: public Download { };
Q_GLOBAL_STATIC(MyDownload, downloadInstance) Q_GLOBAL_STATIC(MyDownload, downloadInstance)
@ -38,6 +39,26 @@ QList<ModelInfo> Download::modelList() const
return values; return values;
} }
QString Download::downloadLocalModelsPath() const
{
QString exePath = QCoreApplication::applicationDirPath() + QDir::separator();
QFileInfo infoExe(exePath);
if (infoExe.isWritable())
return exePath;
QString localPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
QDir localDir(localPath);
if (!localDir.exists())
localDir.mkpath(localPath);
QString localDownloadPath = localPath
+ QDir::separator();
QFileInfo infoLocal(localDownloadPath);
if (infoLocal.isWritable())
return localDownloadPath;
qWarning() << "ERROR: Local download path appears not writeable:" << localDownloadPath;
return localDownloadPath;
}
void Download::updateModelList() void Download::updateModelList()
{ {
QUrl jsonUrl("http://gpt4all.io/models/models.json"); QUrl jsonUrl("http://gpt4all.io/models/models.json");
@ -143,7 +164,7 @@ void Download::parseJsonFile(const QByteArray &jsonData)
modelFilesize = QString("%1 GB").arg(qreal(sz) / (1024 * 1024 * 1024), 0, 'g', 3); modelFilesize = QString("%1 GB").arg(qreal(sz) / (1024 * 1024 * 1024), 0, 'g', 3);
} }
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + modelFilename; QString filePath = downloadLocalModelsPath() + modelFilename;
QFileInfo info(filePath); QFileInfo info(filePath);
ModelInfo modelInfo; ModelInfo modelInfo;
modelInfo.filename = modelFilename; modelInfo.filename = modelFilename;
@ -164,7 +185,6 @@ void Download::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
return; return;
QString modelFilename = modelReply->url().fileName(); QString modelFilename = modelReply->url().fileName();
// qDebug() << "handleDownloadProgress" << bytesReceived << bytesTotal << modelFilename;
emit downloadProgress(bytesReceived, bytesTotal, modelFilename); emit downloadProgress(bytesReceived, bytesTotal, modelFilename);
} }
@ -179,7 +199,6 @@ void Download::handleModelDownloadFinished()
return; return;
QString modelFilename = modelReply->url().fileName(); QString modelFilename = modelReply->url().fileName();
// qDebug() << "handleModelDownloadFinished" << modelFilename;
m_activeDownloads.removeAll(modelReply); m_activeDownloads.removeAll(modelReply);
if (modelReply->error()) { if (modelReply->error()) {
@ -210,10 +229,18 @@ void Download::handleModelDownloadFinished()
} }
// Save the model file to disk // Save the model file to disk
QFile file(QCoreApplication::applicationDirPath() + QDir::separator() + modelFilename); QFile file(downloadLocalModelsPath() + modelFilename);
if (file.open(QIODevice::WriteOnly)) { if (file.open(QIODevice::WriteOnly)) {
file.write(modelData); file.write(modelData);
file.close(); file.close();
} else {
QFile::FileError error = file.error();
qWarning() << "ERROR: Could not save model to location:"
<< downloadLocalModelsPath() + modelFilename
<< "failed with code" << error;
modelReply->deleteLater();
emit downloadFinished(modelFilename);
return;
} }
modelReply->deleteLater(); modelReply->deleteLater();

View File

@ -36,6 +36,7 @@ public:
Q_INVOKABLE void updateModelList(); Q_INVOKABLE void updateModelList();
Q_INVOKABLE void downloadModel(const QString &modelFile); Q_INVOKABLE void downloadModel(const QString &modelFile);
Q_INVOKABLE void cancelDownload(const QString &modelFile); Q_INVOKABLE void cancelDownload(const QString &modelFile);
Q_INVOKABLE QString downloadLocalModelsPath() const;
private Q_SLOTS: private Q_SLOTS:
void handleJsonDownloadFinished(); void handleJsonDownloadFinished();

72
llm.cpp
View File

@ -17,6 +17,23 @@ LLM *LLM::globalInstance()
static LLModel::PromptContext s_ctx; static LLModel::PromptContext s_ctx;
static QString modelFilePath(const QString &modelName)
{
QString appPath = QCoreApplication::applicationDirPath()
+ QDir::separator() + "ggml-" + modelName + ".bin";
QFileInfo infoAppPath(appPath);
if (infoAppPath.exists())
return appPath;
QString downloadPath = Download::globalInstance()->downloadLocalModelsPath()
+ QDir::separator() + "ggml-" + modelName + ".bin";
QFileInfo infoLocalPath(downloadPath);
if (infoLocalPath.exists())
return downloadPath;
return QString();
}
LLMObject::LLMObject() LLMObject::LLMObject()
: QObject{nullptr} : QObject{nullptr}
, m_llmodel(nullptr) , m_llmodel(nullptr)
@ -31,14 +48,15 @@ LLMObject::LLMObject()
bool LLMObject::loadModel() bool LLMObject::loadModel()
{ {
if (modelList().isEmpty()) { const QList<QString> models = modelList();
if (models.isEmpty()) {
// try again when we get a list of models // try again when we get a list of models
connect(Download::globalInstance(), &Download::modelListChanged, this, connect(Download::globalInstance(), &Download::modelListChanged, this,
&LLMObject::loadModel, Qt::SingleShotConnection); &LLMObject::loadModel, Qt::SingleShotConnection);
return false; return false;
} }
return loadModelPrivate(modelList().first()); return loadModelPrivate(models.first());
} }
bool LLMObject::loadModelPrivate(const QString &modelName) bool LLMObject::loadModelPrivate(const QString &modelName)
@ -54,8 +72,7 @@ bool LLMObject::loadModelPrivate(const QString &modelName)
} }
bool isGPTJ = false; bool isGPTJ = false;
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + QString filePath = modelFilePath(modelName);
"ggml-" + modelName + ".bin";
QFileInfo info(filePath); QFileInfo info(filePath);
if (info.exists()) { if (info.exists()) {
@ -169,18 +186,18 @@ void LLMObject::modelNameChangeRequested(const QString &modelName)
QList<QString> LLMObject::modelList() const QList<QString> LLMObject::modelList() const
{ {
QDir dir(QCoreApplication::applicationDirPath()); // Build a model list from exepath and from the localpath
QList<QString> list;
QString exePath = QCoreApplication::applicationDirPath() + QDir::separator();
QString localPath = Download::globalInstance()->downloadLocalModelsPath();
{
QDir dir(exePath);
dir.setNameFilters(QStringList() << "ggml-*.bin"); dir.setNameFilters(QStringList() << "ggml-*.bin");
QStringList fileNames = dir.entryList(); QStringList fileNames = dir.entryList();
if (fileNames.isEmpty()) {
qWarning() << "ERROR: Could not find any applicable models in directory"
<< QCoreApplication::applicationDirPath();
return QList<QString>();
}
QList<QString> list;
for (QString f : fileNames) { for (QString f : fileNames) {
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + f; QString filePath = exePath + f;
QFileInfo info(filePath); QFileInfo info(filePath);
QString name = info.completeBaseName().remove(0, 5); QString name = info.completeBaseName().remove(0, 5);
if (info.exists()) { if (info.exists()) {
@ -190,6 +207,35 @@ QList<QString> LLMObject::modelList() const
list.append(name); list.append(name);
} }
} }
}
if (localPath != exePath) {
QDir dir(localPath);
dir.setNameFilters(QStringList() << "ggml-*.bin");
QStringList fileNames = dir.entryList();
for (QString f : fileNames) {
QString filePath = localPath + f;
QFileInfo info(filePath);
QString name = info.completeBaseName().remove(0, 5);
if (info.exists() && !list.contains(name)) { // don't allow duplicates
if (name == m_modelName)
list.prepend(name);
else
list.append(name);
}
}
}
if (list.isEmpty()) {
if (exePath != localPath) {
qWarning() << "ERROR: Could not find any applicable models in"
<< exePath << "nor" << localPath;
} else {
qWarning() << "ERROR: Could not find any applicable models in"
<< exePath;
}
return QList<QString>();
}
return list; return list;
} }

View File

@ -7,7 +7,7 @@ import llm
Dialog { Dialog {
id: modelDownloaderDialog id: modelDownloaderDialog
width: 1024 width: 1024
height: 400 height: 435
modal: true modal: true
opacity: 0.9 opacity: 0.9
closePolicy: LLM.modelList.length === 0 ? Popup.NoAutoClose : (Popup.CloseOnEscape | Popup.CloseOnPressOutside) closePolicy: LLM.modelList.length === 0 ? Popup.NoAutoClose : (Popup.CloseOnEscape | Popup.CloseOnPressOutside)
@ -28,7 +28,7 @@ Dialog {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 20 anchors.margins: 20
spacing: 10 spacing: 30
Label { Label {
id: listLabel id: listLabel
@ -38,12 +38,16 @@ Dialog {
color: theme.textColor color: theme.textColor
} }
ListView { ScrollView {
id: modelList id: scrollView
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
model: Download.modelList
clip: true clip: true
ListView {
id: modelList
model: Download.modelList
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
delegate: Item { delegate: Item {
@ -233,4 +237,17 @@ Dialog {
} }
} }
} }
Label {
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
text: qsTr("NOTE: models will be downloaded to\n") + Download.downloadLocalModelsPath()
wrapMode: Text.WrapAnywhere
horizontalAlignment: Text.AlignHCenter
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Model download path")
Accessible.description: qsTr("The path where downloaded models will be saved.")
}
}
} }