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 73df702abe
commit c086a45173
4 changed files with 286 additions and 195 deletions

View File

@ -8,6 +8,7 @@
#include <QJsonArray>
#include <QUrl>
#include <QDir>
#include <QStandardPaths>
class MyDownload: public Download { };
Q_GLOBAL_STATIC(MyDownload, downloadInstance)
@ -38,6 +39,26 @@ QList<ModelInfo> Download::modelList() const
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()
{
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);
}
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + modelFilename;
QString filePath = downloadLocalModelsPath() + modelFilename;
QFileInfo info(filePath);
ModelInfo modelInfo;
modelInfo.filename = modelFilename;
@ -164,7 +185,6 @@ void Download::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
return;
QString modelFilename = modelReply->url().fileName();
// qDebug() << "handleDownloadProgress" << bytesReceived << bytesTotal << modelFilename;
emit downloadProgress(bytesReceived, bytesTotal, modelFilename);
}
@ -179,7 +199,6 @@ void Download::handleModelDownloadFinished()
return;
QString modelFilename = modelReply->url().fileName();
// qDebug() << "handleModelDownloadFinished" << modelFilename;
m_activeDownloads.removeAll(modelReply);
if (modelReply->error()) {
@ -210,10 +229,18 @@ void Download::handleModelDownloadFinished()
}
// Save the model file to disk
QFile file(QCoreApplication::applicationDirPath() + QDir::separator() + modelFilename);
QFile file(downloadLocalModelsPath() + modelFilename);
if (file.open(QIODevice::WriteOnly)) {
file.write(modelData);
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();

View File

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

88
llm.cpp
View File

@ -17,6 +17,23 @@ LLM *LLM::globalInstance()
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()
: QObject{nullptr}
, m_llmodel(nullptr)
@ -31,14 +48,15 @@ LLMObject::LLMObject()
bool LLMObject::loadModel()
{
if (modelList().isEmpty()) {
const QList<QString> models = modelList();
if (models.isEmpty()) {
// try again when we get a list of models
connect(Download::globalInstance(), &Download::modelListChanged, this,
&LLMObject::loadModel, Qt::SingleShotConnection);
return false;
}
return loadModelPrivate(modelList().first());
return loadModelPrivate(models.first());
}
bool LLMObject::loadModelPrivate(const QString &modelName)
@ -54,8 +72,7 @@ bool LLMObject::loadModelPrivate(const QString &modelName)
}
bool isGPTJ = false;
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() +
"ggml-" + modelName + ".bin";
QString filePath = modelFilePath(modelName);
QFileInfo info(filePath);
if (info.exists()) {
@ -169,28 +186,57 @@ void LLMObject::modelNameChangeRequested(const QString &modelName)
QList<QString> LLMObject::modelList() const
{
QDir dir(QCoreApplication::applicationDirPath());
dir.setNameFilters(QStringList() << "ggml-*.bin");
QStringList fileNames = dir.entryList();
if (fileNames.isEmpty()) {
qWarning() << "ERROR: Could not find any applicable models in directory"
<< QCoreApplication::applicationDirPath();
return QList<QString>();
// 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");
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;
for (QString f : fileNames) {
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + 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);
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;
}

View File

@ -7,7 +7,7 @@ import llm
Dialog {
id: modelDownloaderDialog
width: 1024
height: 400
height: 435
modal: true
opacity: 0.9
closePolicy: LLM.modelList.length === 0 ? Popup.NoAutoClose : (Popup.CloseOnEscape | Popup.CloseOnPressOutside)
@ -28,7 +28,7 @@ Dialog {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 10
spacing: 30
Label {
id: listLabel
@ -38,199 +38,216 @@ Dialog {
color: theme.textColor
}
ListView {
id: modelList
ScrollView {
id: scrollView
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
Layout.fillWidth: true
Layout.fillHeight: true
model: Download.modelList
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
}
ListView {
id: modelList
model: Download.modelList
boundsBehavior: Flickable.StopAtBounds
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 {
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);
}
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
}
background: Rectangle {
opacity: .5
border.color: theme.backgroundLightest
border.width: 1
radius: 10
color: theme.backgroundLight
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")
}
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: {
Download.downloadProgress.connect(updateProgress);
Download.downloadFinished.connect(resetProgress);
}
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")
}
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) {
let currentTime = new Date().getTime();
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")
}
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;
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")
}
// 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";
}
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
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) {
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 = 0;
delegateItem.downloading = false;
function resetProgress(modelName) {
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 = 0;
delegateItem.downloading = false;
// Remove speed label text
let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel");
speedLabel.text = "";
// Remove speed label text
let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel");
speedLabel.text = "";
// Remove the lastUpdate object for the canceled model
delete lastUpdate[modelName];
break;
// Remove the lastUpdate object for the canceled model
delete lastUpdate[modelName];
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.")
}
}
}