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();

88
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,28 +186,57 @@ 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
dir.setNameFilters(QStringList() << "ggml-*.bin"); QList<QString> list;
QStringList fileNames = dir.entryList();
if (fileNames.isEmpty()) { QString exePath = QCoreApplication::applicationDirPath() + QDir::separator();
qWarning() << "ERROR: Could not find any applicable models in directory" QString localPath = Download::globalInstance()->downloadLocalModelsPath();
<< QCoreApplication::applicationDirPath();
return QList<QString>(); {
QDir dir(exePath);
dir.setNameFilters(QStringList() << "ggml-*.bin");
QStringList fileNames = dir.entryList();
for (QString f : fileNames) {
QString filePath = exePath + f;
QFileInfo info(filePath);
QString name = info.completeBaseName().remove(0, 5);
if (info.exists()) {
if (name == m_modelName)
list.prepend(name);
else
list.append(name);
}
}
} }
QList<QString> list; if (localPath != exePath) {
for (QString f : fileNames) { QDir dir(localPath);
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + f; dir.setNameFilters(QStringList() << "ggml-*.bin");
QFileInfo info(filePath); QStringList fileNames = dir.entryList();
QString name = info.completeBaseName().remove(0, 5); for (QString f : fileNames) {
if (info.exists()) { QString filePath = localPath + f;
if (name == m_modelName) QFileInfo info(filePath);
list.prepend(name); QString name = info.completeBaseName().remove(0, 5);
else if (info.exists() && !list.contains(name)) { // don't allow duplicates
list.append(name); 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,199 +38,216 @@ 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
boundsBehavior: Flickable.StopAtBounds
delegate: Item { ListView {
id: delegateItem id: modelList
width: modelList.width model: Download.modelList
height: 70 boundsBehavior: Flickable.StopAtBounds
objectName: "delegateItem"
property bool downloading: false
Rectangle {
anchors.fill: parent
color: index % 2 === 0 ? theme.backgroundLight : theme.backgroundLighter
}
Text { delegate: Item {
id: modelName id: delegateItem
objectName: "modelName" width: modelList.width
property string filename: modelData.filename height: 70
text: filename.slice(5, filename.length - 4) objectName: "delegateItem"
anchors.verticalCenter: parent.verticalCenter property bool downloading: false
anchors.left: parent.left Rectangle {
anchors.leftMargin: 10 anchors.fill: parent
color: theme.textColor color: index % 2 === 0 ? theme.backgroundLight : theme.backgroundLighter
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Model file")
Accessible.description: qsTr("Model file to be downloaded")
}
Text {
id: isDefault
text: qsTr("(default)")
visible: modelData.isDefault
anchors.verticalCenter: parent.verticalCenter
anchors.left: modelName.right
anchors.leftMargin: 10
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Default file")
Accessible.description: qsTr("Whether the file is the default model")
}
Text {
text: modelData.filesize
anchors.verticalCenter: parent.verticalCenter
anchors.left: isDefault.visible ? isDefault.right : modelName.right
anchors.leftMargin: 10
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("File size")
Accessible.description: qsTr("The size of the file")
}
Label {
id: speedLabel
anchors.verticalCenter: parent.verticalCenter
anchors.right: itemProgressBar.left
anchors.rightMargin: 10
objectName: "speedLabel"
color: theme.textColor
text: ""
visible: downloading
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Download speed")
Accessible.description: qsTr("Download speed in bytes/kilobytes/megabytes per second")
}
ProgressBar {
id: itemProgressBar
objectName: "itemProgressBar"
anchors.verticalCenter: parent.verticalCenter
anchors.right: downloadButton.left
anchors.rightMargin: 10
width: 100
visible: downloading
Accessible.role: Accessible.ProgressBar
Accessible.name: qsTr("Download progressBar")
Accessible.description: qsTr("Shows the progress made in the download")
}
Label {
id: installedLabel
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 15
objectName: "installedLabel"
color: theme.textColor
text: qsTr("Already installed")
visible: modelData.installed
Accessible.role: Accessible.Paragraph
Accessible.name: text
Accessible.description: qsTr("Whether the file is already installed on your system")
}
Button {
id: downloadButton
text: downloading ? "Cancel" : "Download"
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 10
visible: !modelData.installed
padding: 10
onClicked: {
if (!downloading) {
downloading = true;
Download.downloadModel(modelData.filename);
} else {
downloading = false;
Download.cancelDownload(modelData.filename);
}
} }
background: Rectangle {
opacity: .5 Text {
border.color: theme.backgroundLightest id: modelName
border.width: 1 objectName: "modelName"
radius: 10 property string filename: modelData.filename
color: theme.backgroundLight text: filename.slice(5, filename.length - 4)
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 10
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Model file")
Accessible.description: qsTr("Model file to be downloaded")
} }
Accessible.role: Accessible.Button
Accessible.name: text
Accessible.description: qsTr("Cancel/Download button to stop/start the download")
} Text {
} id: isDefault
text: qsTr("(default)")
visible: modelData.isDefault
anchors.verticalCenter: parent.verticalCenter
anchors.left: modelName.right
anchors.leftMargin: 10
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Default file")
Accessible.description: qsTr("Whether the file is the default model")
}
Component.onCompleted: { Text {
Download.downloadProgress.connect(updateProgress); text: modelData.filesize
Download.downloadFinished.connect(resetProgress); anchors.verticalCenter: parent.verticalCenter
} anchors.left: isDefault.visible ? isDefault.right : modelName.right
anchors.leftMargin: 10
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("File size")
Accessible.description: qsTr("The size of the file")
}
property var lastUpdate: ({}) Label {
id: speedLabel
anchors.verticalCenter: parent.verticalCenter
anchors.right: itemProgressBar.left
anchors.rightMargin: 10
objectName: "speedLabel"
color: theme.textColor
text: ""
visible: downloading
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("Download speed")
Accessible.description: qsTr("Download speed in bytes/kilobytes/megabytes per second")
}
function updateProgress(bytesReceived, bytesTotal, modelName) { ProgressBar {
let currentTime = new Date().getTime(); id: itemProgressBar
objectName: "itemProgressBar"
anchors.verticalCenter: parent.verticalCenter
anchors.right: downloadButton.left
anchors.rightMargin: 10
width: 100
visible: downloading
Accessible.role: Accessible.ProgressBar
Accessible.name: qsTr("Download progressBar")
Accessible.description: qsTr("Shows the progress made in the download")
}
for (let i = 0; i < modelList.contentItem.children.length; i++) { Label {
let delegateItem = modelList.contentItem.children[i]; id: installedLabel
if (delegateItem.objectName === "delegateItem") { anchors.verticalCenter: parent.verticalCenter
let modelNameText = delegateItem.children.find(child => child.objectName === "modelName").filename; anchors.right: parent.right
if (modelNameText === modelName) { anchors.rightMargin: 15
let progressBar = delegateItem.children.find(child => child.objectName === "itemProgressBar"); objectName: "installedLabel"
progressBar.value = bytesReceived / bytesTotal; color: theme.textColor
text: qsTr("Already installed")
visible: modelData.installed
Accessible.role: Accessible.Paragraph
Accessible.name: text
Accessible.description: qsTr("Whether the file is already installed on your system")
}
// Calculate the download speed Button {
if (lastUpdate[modelName] && lastUpdate[modelName].timestamp) { id: downloadButton
let timeDifference = currentTime - lastUpdate[modelName].timestamp; text: downloading ? "Cancel" : "Download"
let bytesDifference = bytesReceived - lastUpdate[modelName].bytesReceived; anchors.verticalCenter: parent.verticalCenter
let speed = (bytesDifference / timeDifference) * 1000; // bytes per second anchors.right: parent.right
anchors.rightMargin: 10
// Update the speed label visible: !modelData.installed
let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel"); padding: 10
if (speed < 1024) { onClicked: {
speedLabel.text = speed.toFixed(2) + " B/s"; if (!downloading) {
} else if (speed < 1024 * 1024) { downloading = true;
speedLabel.text = (speed / 1024).toFixed(2) + " KB/s"; Download.downloadModel(modelData.filename);
} else { } else {
speedLabel.text = (speed / (1024 * 1024)).toFixed(2) + " MB/s"; downloading = false;
} Download.cancelDownload(modelData.filename);
} }
}
background: Rectangle {
opacity: .5
border.color: theme.backgroundLightest
border.width: 1
radius: 10
color: theme.backgroundLight
}
Accessible.role: Accessible.Button
Accessible.name: text
Accessible.description: qsTr("Cancel/Download button to stop/start the download")
// Update the lastUpdate object for the current model }
lastUpdate[modelName] = {"timestamp": currentTime, "bytesReceived": bytesReceived}; }
break;
Component.onCompleted: {
Download.downloadProgress.connect(updateProgress);
Download.downloadFinished.connect(resetProgress);
}
property var lastUpdate: ({})
function updateProgress(bytesReceived, bytesTotal, modelName) {
let currentTime = new Date().getTime();
for (let i = 0; i < modelList.contentItem.children.length; i++) {
let delegateItem = modelList.contentItem.children[i];
if (delegateItem.objectName === "delegateItem") {
let modelNameText = delegateItem.children.find(child => child.objectName === "modelName").filename;
if (modelNameText === modelName) {
let progressBar = delegateItem.children.find(child => child.objectName === "itemProgressBar");
progressBar.value = bytesReceived / bytesTotal;
// Calculate the download speed
if (lastUpdate[modelName] && lastUpdate[modelName].timestamp) {
let timeDifference = currentTime - lastUpdate[modelName].timestamp;
let bytesDifference = bytesReceived - lastUpdate[modelName].bytesReceived;
let speed = (bytesDifference / timeDifference) * 1000; // bytes per second
// Update the speed label
let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel");
if (speed < 1024) {
speedLabel.text = speed.toFixed(2) + " B/s";
} else if (speed < 1024 * 1024) {
speedLabel.text = (speed / 1024).toFixed(2) + " KB/s";
} else {
speedLabel.text = (speed / (1024 * 1024)).toFixed(2) + " MB/s";
}
}
// Update the lastUpdate object for the current model
lastUpdate[modelName] = {"timestamp": currentTime, "bytesReceived": bytesReceived};
break;
}
} }
} }
} }
}
function resetProgress(modelName) { function resetProgress(modelName) {
for (let i = 0; i < modelList.contentItem.children.length; i++) { for (let i = 0; i < modelList.contentItem.children.length; i++) {
let delegateItem = modelList.contentItem.children[i]; let delegateItem = modelList.contentItem.children[i];
if (delegateItem.objectName === "delegateItem") { if (delegateItem.objectName === "delegateItem") {
let modelNameText = delegateItem.children.find(child => child.objectName === "modelName").filename; let modelNameText = delegateItem.children.find(child => child.objectName === "modelName").filename;
if (modelNameText === modelName) { if (modelNameText === modelName) {
let progressBar = delegateItem.children.find(child => child.objectName === "itemProgressBar"); let progressBar = delegateItem.children.find(child => child.objectName === "itemProgressBar");
progressBar.value = 0; progressBar.value = 0;
delegateItem.downloading = false; delegateItem.downloading = false;
// Remove speed label text // Remove speed label text
let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel"); let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel");
speedLabel.text = ""; speedLabel.text = "";
// Remove the lastUpdate object for the canceled model // Remove the lastUpdate object for the canceled model
delete lastUpdate[modelName]; delete lastUpdate[modelName];
break; break;
}
} }
} }
} }
} }
} }
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.")
}
} }
} }