diff --git a/com.jeffser.Alpaca.json b/com.jeffser.Alpaca.json
index 7c78bd9..3cea016 100644
--- a/com.jeffser.Alpaca.json
+++ b/com.jeffser.Alpaca.json
@@ -57,6 +57,20 @@
}
]
},
+ {
+ "name": "python3-pillow",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pillow\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/ef/43/c50c17c5f7d438e836c169e343695534c38c77f60e7c90389bd77981bc21/pillow-10.3.0.tar.gz",
+ "sha256": "9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"
+ }
+ ]
+ },
{
"name" : "alpaca",
"builddir" : true,
diff --git a/src/available_models.py b/src/available_models.py
index 0da1f72..a99ef5d 100644
--- a/src/available_models.py
+++ b/src/available_models.py
@@ -89,3 +89,4 @@ available_models = {
"llava-phi3":"A new small LLaVA model fine-tuned from Phi 3 Mini."
}
+
diff --git a/src/window.py b/src/window.py
index a82c6d8..bf50d75 100644
--- a/src/window.py
+++ b/src/window.py
@@ -19,8 +19,11 @@
import gi
gi.require_version('GtkSource', '5')
-from gi.repository import Adw, Gtk, GLib, GtkSource
-import json, requests, threading, os, re, markdown
+gi.require_version('GdkPixbuf', '2.0')
+from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
+import json, requests, threading, os, re, base64
+from io import BytesIO
+from PIL import Image
from datetime import datetime
from .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake
from .available_models import available_models
@@ -35,6 +38,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
#In the future I will at multiple chats, for now I'll save it like this so that past chats don't break in the future
current_chat_id="0"
chats = {"chats": {"0": {"messages": []}}}
+ attached_image = {"path": None, "base64": None}
#Elements
bot_message : Gtk.TextBuffer = None
@@ -51,8 +55,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
connection_overlay = Gtk.Template.Child()
chat_container = Gtk.Template.Child()
chat_window = Gtk.Template.Child()
- message_entry = Gtk.Template.Child()
+ message_text_view = Gtk.Template.Child()
send_button = Gtk.Template.Child()
+ image_button = Gtk.Template.Child()
+ file_filter_image = Gtk.Template.Child()
model_drop_down = Gtk.Template.Child()
model_string_list = Gtk.Template.Child()
@@ -70,7 +76,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
"Failed to connect to server",
"Could not list local models",
"Could not delete model",
- "Could not pull model"
+ "Could not pull model",
+ "Cannot open image"
],
"info": [
"Please select a model before chatting",
@@ -92,7 +99,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
)
overlay.add_toast(toast)
- def show_message(self, msg:str, bot:bool, footer:str=None):
+ def show_message(self, msg:str, bot:bool, footer:str=None, image_base64:str=None):
message_text = Gtk.TextView(
editable=False,
focusable=False,
@@ -112,14 +119,42 @@ class AlpacaWindow(Adw.ApplicationWindow):
orientation=1,
css_classes=[None if bot else "card"]
)
- message_box.append(message_text)
message_text.set_valign(Gtk.Align.CENTER)
self.chat_container.append(message_box)
+
+ if image_base64 is not None:
+ image_data = base64.b64decode(image_base64)
+ loader = GdkPixbuf.PixbufLoader.new()
+ loader.write(image_data)
+ loader.close()
+
+ pixbuf = loader.get_pixbuf()
+ texture = Gdk.Texture.new_for_pixbuf(pixbuf)
+
+ image = Gtk.Image.new_from_paintable(texture)
+ image.set_size_request(360, 360)
+ message_box.append(image)
+
+ message_box.append(message_text)
+
if bot:
self.bot_message = message_buffer
self.bot_message_view = message_text
self.bot_message_box = message_box
+ def verify_if_image_can_be_used(self, pspec=None, user_data=None):
+ if self.model_drop_down.get_selected_item() == None: return True
+ selected = self.model_drop_down.get_selected_item().get_string().split(":")[0]
+ if selected in ['llava']:
+ self.image_button.set_sensitive(True)
+ return True
+ else:
+ self.image_button.set_sensitive(False)
+ self.image_button.set_css_classes([])
+ self.image_button.get_child().set_icon_name("image-x-generic-symbolic")
+ self.attached_image = {"path": None, "base64": None}
+ return False
+
def update_list_local_models(self):
self.local_models = []
response = simple_get(self.ollama_url + "/api/tags")
@@ -130,6 +165,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.model_string_list.append(model["name"])
self.local_models.append(model["name"])
self.model_drop_down.set_selected(0)
+ self.verify_if_image_can_be_used()
return
else:
self.show_connection_dialog(True)
@@ -140,13 +176,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
if response['status'] == 'ok':
if "Ollama is running" in response['text']:
with open(os.path.join(self.config_dir, "server.conf"), "w+") as f: f.write(self.ollama_url)
- self.message_entry.grab_focus_without_selecting()
+ #self.message_text_view.grab_focus_without_selecting()
self.update_list_local_models()
return True
return False
def add_code_blocks(self):
- print('a')
text = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), True)
GLib.idle_add(self.bot_message_view.get_parent().remove, self.bot_message_view)
# Define a regular expression pattern to match code blocks
@@ -181,7 +216,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
css_classes=["flat"]
)
message_buffer = message_text.get_buffer()
- message_buffer.insert(message_buffer.get_end_iter(), part['text'])
+ message_buffer.insert(message_buffer.get_end_iter(), f"\n\n{part['text']}" if part['text'] == parts[-1]['text'] else part['text'])
self.bot_message_box.append(message_text)
else:
language = GtkSource.LanguageManager.get_default().get_language(part['language'])
@@ -219,13 +254,17 @@ class AlpacaWindow(Adw.ApplicationWindow):
response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=self.update_bot_message)
GLib.idle_add(self.add_code_blocks)
GLib.idle_add(self.send_button.set_sensitive, True)
- GLib.idle_add(self.message_entry.set_sensitive, True)
+ GLib.idle_add(self.image_button.set_sensitive, True)
+ GLib.idle_add(self.image_button.set_css_classes, [])
+ GLib.idle_add(self.image_button.get_child().set_icon_name, "image-x-generic-symbolic")
+ self.attached_image = {"path": None, "base64": None}
+ GLib.idle_add(self.message_text_view.set_sensitive, True)
if response['status'] == 'error':
GLib.idle_add(self.show_toast, 'error', 1, self.connection_overlay)
GLib.idle_add(self.show_connection_dialog, True)
def send_message(self, button):
- if not self.message_entry.get_text(): return
+ if not self.message_text_view.get_buffer().get_text(self.message_text_view.get_buffer().get_start_iter(), self.message_text_view.get_buffer().get_end_iter(), False): return
current_model = self.model_drop_down.get_selected_item()
if current_model is None:
self.show_toast("info", 0, self.main_overlay)
@@ -235,16 +274,19 @@ class AlpacaWindow(Adw.ApplicationWindow):
"role": "user",
"model": "User",
"date": formated_datetime,
- "content": self.message_entry.get_text()
+ "content": self.message_text_view.get_buffer().get_text(self.message_text_view.get_buffer().get_start_iter(), self.message_text_view.get_buffer().get_end_iter(), False)
})
data = {
"model": current_model.get_string(),
"messages": self.chats["chats"][self.current_chat_id]["messages"]
}
- self.message_entry.set_sensitive(False)
+ if self.verify_if_image_can_be_used() and self.attached_image["base64"] is not None:
+ data["messages"][-1]["images"] = [self.attached_image["base64"]]
+ self.message_text_view.set_sensitive(False)
self.send_button.set_sensitive(False)
- self.show_message(self.message_entry.get_text(), False, f"\n\n{formated_datetime}")
- self.message_entry.get_buffer().set_text("", 0)
+ self.image_button.set_sensitive(False)
+ self.show_message(self.message_text_view.get_buffer().get_text(self.message_text_view.get_buffer().get_start_iter(), self.message_text_view.get_buffer().get_end_iter(), False), False, f"\n\n{formated_datetime}", self.attached_image["base64"])
+ self.message_text_view.get_buffer().set_text("", 0)
self.show_message("", True)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model']))
thread.start()
@@ -323,7 +365,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
title = model_name,
subtitle = model_description,
)
- model_name += ":latest"
+ if ":" not in model_name: model_name += ":latest"
button = Gtk.Button(
icon_name = "folder-download-symbolic" if model_name not in self.local_models else "user-trash-symbolic",
vexpand = False,
@@ -334,7 +376,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.model_list_box.append(model)
def manage_models_button_activate(self, button):
-
self.manage_models_dialog.present(self)
self.update_list_available_models()
@@ -406,7 +447,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.chats = {"chats": {"0": {"messages": []}}}
for message in self.chats['chats'][self.current_chat_id]['messages']:
if message['role'] == 'user':
- self.show_message(message['content'], False, f"\n\n{message['date']}")
+ self.show_message(message['content'], False, f"\n\n{message['date']}", message['images'][0])
else:
self.show_message(message['content'], True, f"\n\n{message['model']}\t|\t{message['date']}")
self.add_code_blocks()
@@ -448,14 +489,68 @@ class AlpacaWindow(Adw.ApplicationWindow):
callback = self.closing_connection_dialog_response
)
+ def load_image(self, file_dialog, result):
+ try: file = file_dialog.open_finish(result)
+ except: return
+ try:
+ self.attached_image["path"] = file.get_path()
+ '''with open(self.attached_image["path"], "rb") as image_file:
+ self.attached_image["base64"] = base64.b64encode(image_file.read()).decode("utf-8")'''
+ with Image.open(self.attached_image["path"]) as img:
+ width, height = img.size
+ max_size = 240
+ if width > height:
+ new_width = max_size
+ new_height = int((max_size / width) * height)
+ else:
+ new_height = max_size
+ new_width = int((max_size / height) * width)
+ resized_img = img.resize((new_width, new_height), Image.LANCZOS)
+ with BytesIO() as output:
+ resized_img.save(output, format="JPEG")
+ image_data = output.getvalue()
+ self.attached_image["base64"] = base64.b64encode(image_data).decode("utf-8")
+
+ self.image_button.set_css_classes(["destructive-action"])
+ self.image_button.get_child().set_icon_name("edit-delete-symbolic")
+ except Exception as e:
+ print(e)
+ self.show_toast("error", 5, self.main_overlay)
+
+ def remove_image(self, dialog, task):
+ if dialog.choose_finish(task) == 'remove':
+ self.image_button.set_css_classes([])
+ self.image_button.get_child().set_icon_name("image-x-generic-symbolic")
+ self.attached_image = {"path": None, "base64": None}
+
+ def open_image(self, button):
+ if "destructive-action" in button.get_css_classes():
+ dialog = Adw.AlertDialog(
+ heading=f"Remove Image?",
+ body=f"Are you sure you want to remove image?",
+ close_response="cancel"
+ )
+ dialog.add_response("cancel", "Cancel")
+ dialog.add_response("remove", "Remove")
+ dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE)
+ dialog.choose(
+ parent = self,
+ cancellable = None,
+ callback = self.remove_image
+ )
+ else:
+ file_dialog = Gtk.FileDialog(default_filter=self.file_filter_image)
+ file_dialog.open(self, None, self.load_image)
+
def __init__(self, **kwargs):
super().__init__(**kwargs)
GtkSource.init()
self.manage_models_button.connect("clicked", self.manage_models_button_activate)
self.send_button.connect("clicked", self.send_message)
+ self.image_button.connect("clicked", self.open_image)
self.set_default_widget(self.send_button)
- self.message_entry.set_activates_default(self.send_button)
- self.message_entry.set_text("Could you give me a hello world for python?")
+ self.model_drop_down.connect("notify", self.verify_if_image_can_be_used)
+ #self.message_text_view.set_activates_default(self.send_button)
self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed)
self.connection_previous_button.connect("clicked", self.connection_previous_button_activate)
self.connection_next_button.connect("clicked", self.connection_next_button_activate)
diff --git a/src/window.ui b/src/window.ui
index f93addb..3b85e75 100644
--- a/src/window.ui
+++ b/src/window.ui
@@ -67,8 +67,6 @@
1
true
@@ -92,19 +90,54 @@
0
12
-
-
-
-
+
+
+ 1
+ 12
+
+
+
+
+
+ Send
+ send-to-symbolic
+
+
+
+
+
+
+ false
+ Requires model 'llava' to be selected
+
+
+ Image
+ image-x-generic-symbolic
+
+
@@ -323,4 +356,14 @@
+
+
+
+ image/svg+xml
+ image/png
+ image/jpeg
+ image/webp
+ image/gif
+
+