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

pull/520/head
Adam Treat 1 year ago
parent 73df702abe
commit c086a45173

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

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

@ -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;
} }

@ -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 {
id: delegateItem
width: modelList.width
height: 70
objectName: "delegateItem"
property bool downloading: false
Rectangle {
anchors.fill: parent
color: index % 2 === 0 ? theme.backgroundLight : theme.backgroundLighter
}
Text {
id: modelName
objectName: "modelName"
property string filename: modelData.filename
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")
}
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 { ListView {
text: modelData.filesize id: modelList
anchors.verticalCenter: parent.verticalCenter model: Download.modelList
anchors.left: isDefault.visible ? isDefault.right : modelName.right boundsBehavior: Flickable.StopAtBounds
anchors.leftMargin: 10
color: theme.textColor delegate: Item {
Accessible.role: Accessible.Paragraph id: delegateItem
Accessible.name: qsTr("File size") width: modelList.width
Accessible.description: qsTr("The size of the file") height: 70
} objectName: "delegateItem"
property bool downloading: false
Label { Rectangle {
id: speedLabel anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter color: index % 2 === 0 ? theme.backgroundLight : theme.backgroundLighter
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 { Text {
id: installedLabel id: modelName
anchors.verticalCenter: parent.verticalCenter objectName: "modelName"
anchors.right: parent.right property string filename: modelData.filename
anchors.rightMargin: 15 text: filename.slice(5, filename.length - 4)
objectName: "installedLabel" anchors.verticalCenter: parent.verticalCenter
color: theme.textColor anchors.left: parent.left
text: qsTr("Already installed") anchors.leftMargin: 10
visible: modelData.installed color: theme.textColor
Accessible.role: Accessible.Paragraph Accessible.role: Accessible.Paragraph
Accessible.name: text Accessible.name: qsTr("Model file")
Accessible.description: qsTr("Whether the file is already installed on your system") Accessible.description: qsTr("Model file to be downloaded")
} }
Button { Text {
id: downloadButton id: isDefault
text: downloading ? "Cancel" : "Download" text: qsTr("(default)")
anchors.verticalCenter: parent.verticalCenter visible: modelData.isDefault
anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 10 anchors.left: modelName.right
visible: !modelData.installed anchors.leftMargin: 10
padding: 10 color: theme.textColor
onClicked: { Accessible.role: Accessible.Paragraph
if (!downloading) { Accessible.name: qsTr("Default file")
downloading = true; Accessible.description: qsTr("Whether the file is the default model")
Download.downloadModel(modelData.filename);
} else {
downloading = false;
Download.cancelDownload(modelData.filename);
}
} }
background: Rectangle {
opacity: .5 Text {
border.color: theme.backgroundLightest text: modelData.filesize
border.width: 1 anchors.verticalCenter: parent.verticalCenter
radius: 10 anchors.left: isDefault.visible ? isDefault.right : modelName.right
color: theme.backgroundLight anchors.leftMargin: 10
color: theme.textColor
Accessible.role: Accessible.Paragraph
Accessible.name: qsTr("File size")
Accessible.description: qsTr("The size of the file")
} }
Accessible.role: Accessible.Button
Accessible.name: text
Accessible.description: qsTr("Cancel/Download button to stop/start the download")
} 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")
}
Component.onCompleted: { ProgressBar {
Download.downloadProgress.connect(updateProgress); id: itemProgressBar
Download.downloadFinished.connect(resetProgress); 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")
}
property var lastUpdate: ({}) 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")
}
function updateProgress(bytesReceived, bytesTotal, modelName) { Button {
let currentTime = new Date().getTime(); 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
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")
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 Component.onCompleted: {
if (lastUpdate[modelName] && lastUpdate[modelName].timestamp) { Download.downloadProgress.connect(updateProgress);
let timeDifference = currentTime - lastUpdate[modelName].timestamp; Download.downloadFinished.connect(resetProgress);
let bytesDifference = bytesReceived - lastUpdate[modelName].bytesReceived; }
let speed = (bytesDifference / timeDifference) * 1000; // bytes per second
// Update the speed label property var lastUpdate: ({})
let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel");
if (speed < 1024) { function updateProgress(bytesReceived, bytesTotal, modelName) {
speedLabel.text = speed.toFixed(2) + " B/s"; let currentTime = new Date().getTime();
} else if (speed < 1024 * 1024) {
speedLabel.text = (speed / 1024).toFixed(2) + " KB/s"; for (let i = 0; i < modelList.contentItem.children.length; i++) {
} else { let delegateItem = modelList.contentItem.children[i];
speedLabel.text = (speed / (1024 * 1024)).toFixed(2) + " MB/s"; 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 // Update the lastUpdate object for the current model
lastUpdate[modelName] = {"timestamp": currentTime, "bytesReceived": bytesReceived}; lastUpdate[modelName] = {"timestamp": currentTime, "bytesReceived": bytesReceived};
break; 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.")
}
} }
} }

Loading…
Cancel
Save