diff --git a/com.jeffser.Alpaca.json b/com.jeffser.Alpaca.json
index f089fc5..67bbc84 100644
--- a/com.jeffser.Alpaca.json
+++ b/com.jeffser.Alpaca.json
@@ -85,6 +85,20 @@
}
]
},
+ {
+ "name": "python3-pytube",
+ "buildsystem": "simple",
+ "build-commands": [
+ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pytube\" --no-build-isolation"
+ ],
+ "sources": [
+ {
+ "type": "file",
+ "url": "https://files.pythonhosted.org/packages/51/64/bcf8632ed2b7a36bbf84a0544885ffa1d0b4bcf25cc0903dba66ec5fdad9/pytube-15.0.0-py3-none-any.whl",
+ "sha256": "07b9904749e213485780d7eb606e5e5b8e4341aa4dccf699160876da00e12d78"
+ }
+ ]
+ },
{
"name": "ollama",
"buildsystem": "simple",
diff --git a/src/alpaca.gresource.xml b/src/alpaca.gresource.xml
index 20aaea4..e2c88ca 100644
--- a/src/alpaca.gresource.xml
+++ b/src/alpaca.gresource.xml
@@ -23,6 +23,7 @@
icons/brain-augemnted-symbolic.svg
icons/chain-link-loose-symbolic.svg
icons/document-text-symbolic.svg
+ icons/play-symbolic.svg
window.ui
gtk/help-overlay.ui
diff --git a/src/dialogs.py b/src/dialogs.py
index 62dc281..3939578 100644
--- a/src/dialogs.py
+++ b/src/dialogs.py
@@ -182,8 +182,8 @@ def remove_attached_file_response(self, dialog, task, button):
def remove_attached_file(self, button):
dialog = Adw.AlertDialog(
- heading=_("Remove File"),
- body=_("Are you sure you want to remove file?"),
+ heading=_("Remove Attachment"),
+ body=_("Are you sure you want to remove attachment?"),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
@@ -290,3 +290,40 @@ def attach_file_response(self, file_dialog, result):
def attach_file(self, filter):
file_dialog = Gtk.FileDialog(default_filter=filter)
file_dialog.open(self, None, lambda file_dialog, result: attach_file_response(self, file_dialog, result))
+
+
+# YouTube caption |
+
+def youtube_caption_response(self, dialog, task, video_url, caption_drop_down):
+ if dialog.choose_finish(task) == "accept":
+ buffer = self.message_text_view.get_buffer()
+ text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).replace(video_url, "")
+ buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
+ buffer.insert(buffer.get_start_iter(), text, len(text))
+ self.attach_file("{}&caption_lang={}".format(video_url, caption_drop_down.get_selected_item().get_string().split(' | ')[1]), 'youtube')
+
+def youtube_caption(self, video_title, video_url, captions):
+ if len(captions) == 0:
+ self.show_toast("error", 9, self.main_overlay)
+ return
+ caption_list = Gtk.StringList()
+ for caption in captions: caption_list.append("{} | {}".format(caption.name, caption.code))
+ caption_drop_down = Gtk.DropDown(
+ enable_search=True,
+ model=caption_list
+ )
+ dialog = Adw.AlertDialog(
+ heading=_("Attach YouTube Video"),
+ body=_("{}\n\nPlease select a transcript to include").format(video_title),
+ extra_child=caption_drop_down,
+ close_response="cancel"
+ )
+ dialog.add_response("cancel", _("Cancel"))
+ dialog.add_response("accept", _("Accept"))
+ dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
+ dialog.choose(
+ parent = self,
+ cancellable = None,
+ callback = lambda dialog, task, video_url = video_url, caption_drop_down = caption_drop_down: youtube_caption_response(self, dialog, task, video_url, caption_drop_down)
+ )
+
diff --git a/src/icons/play-symbolic.svg b/src/icons/play-symbolic.svg
new file mode 100644
index 0000000..64cb6ce
--- /dev/null
+++ b/src/icons/play-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/src/window.py b/src/window.py
index 68e69e4..22d80ab 100644
--- a/src/window.py
+++ b/src/window.py
@@ -22,6 +22,7 @@ gi.require_version('GtkSource', '5')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
import json, requests, threading, os, re, base64, sys, gettext, locale, webbrowser, subprocess, uuid, shutil, tarfile, tempfile #, docx
+from pytube import YouTube
from time import sleep
from io import BytesIO
from PIL import Image
@@ -122,7 +123,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
_("Cannot open image"),
_("Cannot delete chat because it's the only one left"),
_("There was an error with the local Ollama instance, so it has been reset"),
- _("Image recognition is only available on specific models")
+ _("Image recognition is only available on specific models"),
+ _("This video does not have any transcriptions"),
+ _("This video is not available")
],
"info": [
_("Please select a model before chatting"),
@@ -182,10 +185,15 @@ class AlpacaWindow(Adw.ApplicationWindow):
can_use_images = self.verify_if_image_can_be_used()
for name, content in self.attachments.items():
if content["type"] == 'image' and can_use_images: attached_images.append(name)
- else: attached_files[name] = content['type']
+ else:
+ if content["type"] == 'youtube':
+ attached_files[content['path']] = content['type']
+ else:
+ attached_files[name] = content['type']
if not os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)):
os.makedirs(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id))
- shutil.copy(content['path'], os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name))
+ if content["type"] != 'youtube':
+ shutil.copy(content['path'], os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name))
content["button"].get_parent().remove(content["button"])
self.attachments = {}
self.attachment_box.set_visible(False)
@@ -420,7 +428,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
buffer = self.file_preview_text_view.get_buffer()
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), content, len(content))
- self.file_preview_dialog.set_title(os.path.basename(file_path))
+ if file_type == 'youtube':
+ self.file_preview_dialog.set_title(YouTube(file_path).title)
+ else:
+ self.file_preview_dialog.set_title(os.path.basename(file_path))
self.file_preview_dialog.present(self)
def convert_history_to_ollama(self):
@@ -431,7 +442,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
del new_message['files']
new_message['content'] = ''
for name, file_type in message['files'].items():
- file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name)
+ if file_type == 'youtube':
+ file_path = name
+ else:
+ file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name)
file_data = self.get_content_of_file(file_path, file_type)
if file_data: new_message['content'] += f"```[{name}]\n{file_data}\n```"
new_message['content'] += message['content']
@@ -531,11 +545,15 @@ class AlpacaWindow(Adw.ApplicationWindow):
child=file_container
)
for name, file_type in files.items():
- shown_name='.'.join(name.split(".")[:-1])[:20] + (name[20:] and '..') + f".{name.split('.')[-1]}"
+ if file_type == 'youtube':
+ yt = YouTube(name)
+ shown_name=yt.title[:20] + (yt.title[20:] and '..')
+ else:
+ shown_name='.'.join(name.split(".")[:-1])[:20] + (name[20:] and '..') + f".{name.split('.')[-1]}"
button_content = Adw.ButtonContent(
label=shown_name,
- icon_name="document-text-symbolic"
+ icon_name="play-symbolic" if file_type=='youtube' else "document-text-symbolic"
)
button = Gtk.Button(
vexpand=False,
@@ -545,7 +563,11 @@ class AlpacaWindow(Adw.ApplicationWindow):
tooltip_text=name,
child=button_content
)
- button.connect("clicked", lambda button, file_path=os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name), file_type=file_type: self.preview_file(file_path, file_type))
+ if file_type == 'youtube':
+ file_path = name
+ else:
+ file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name)
+ button.connect("clicked", lambda button, file_path=file_path, file_type=file_type: self.preview_file(file_path, file_type))
file_container.append(button)
message_box.append(file_scroller)
@@ -1112,7 +1134,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.verify_connection()
def get_content_of_file(self, file_path, file_type):
- if not os.path.exists(file_path): return None
+ if file_type != 'youtube' and not os.path.exists(file_path): return None
if file_type == 'image':
try:
with Image.open(file_path) as img:
@@ -1141,6 +1163,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
for i, page in enumerate(reader.pages):
text += f"\n- Page {i}\n{page.extract_text()}\n"
return text
+ elif file_type == 'youtube':
+ yt = YouTube(file_path)
+ text = "{}\n{}\n\n".format(yt.title, yt.author)
+ for event in yt.captions[file_path.split('&caption_lang=')[1]].json_captions['events']:
+ text += "{}\n".format(event['segs'][0]['utf8'].replace('\n', '\\n'))
+ return text
#elif file_type == 'docx':
#document = docx.Document(file_path)
#if len(document.paragraphs) == 0: return None
@@ -1155,17 +1183,23 @@ class AlpacaWindow(Adw.ApplicationWindow):
if len(self.attachments) == 0: self.attachment_box.set_visible(False)
def attach_file(self, file_path, file_type):
- name = self.generate_numbered_name(os.path.basename(file_path), self.attachments.keys())
+ if file_type == "youtube":
+ name = YouTube(file_path).title
+ else:
+ name = self.generate_numbered_name(os.path.basename(file_path), self.attachments.keys())
content = self.get_content_of_file(file_path, file_type)
if content:
- shown_name='.'.join(name.split(".")[:-1])[:20] + (name[20:] and '..') + f".{name.split('.')[-1]}"
-
+ if file_type == "youtube":
+ shown_name=name[:20] + (name[20:] and '..')
+ else:
+ shown_name='.'.join(name.split(".")[:-1])[:20] + (name[20:] and '..') + f".{name.split('.')[-1]}"
button_content = Adw.ButtonContent(
label=shown_name,
icon_name={
"image": "image-x-generic-symbolic",
"plain_text": "document-text-symbolic",
"pdf": "document-text-symbolic",
+ "youtube": "play-symbolic",
#"docx": "document-text-symbolic"
}[file_type]
)
@@ -1195,6 +1229,22 @@ class AlpacaWindow(Adw.ApplicationWindow):
elif action_name == 'export_chat':
self.export_chat(chat_name)
+ def text_received(self, clipboard, result):
+ text = clipboard.read_text_finish(result)
+ #Check if text is a Youtube URL
+ youtube_regex = re.compile(
+ r'(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/'
+ r'(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})')
+ if youtube_regex.match(text):
+ try:
+ yt = YouTube(text)
+ dialogs.youtube_caption(self, yt.title, text, yt.captions)
+ except Exception as e:
+ self.show_toast("error", 10, self.main_overlay)
+
+ def on_clipboard_paste(self, textview):
+ clipboard = Gdk.Display.get_default().get_clipboard()
+ clipboard.read_text_async(None, self.text_received)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -1214,6 +1264,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.get_application().create_action('delete_chat', self.chat_actions)
self.get_application().create_action('rename_chat', self.chat_actions)
self.get_application().create_action('export_chat', self.chat_actions)
+ self.message_text_view.connect("paste-clipboard", self.on_clipboard_paste)
self.add_chat_button.connect("clicked", lambda button : self.new_chat())
self.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialogs.attach_file(self, file_filter))
self.create_model_name.get_delegate().connect("insert-text", self.check_alphanumeric)