Added YouTube integration (yeah)

This commit is contained in:
jeffser 2024-06-24 00:18:55 -06:00
parent 9d332a0d1d
commit 01608696d6
5 changed files with 119 additions and 14 deletions

View File

@ -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", "name": "ollama",
"buildsystem": "simple", "buildsystem": "simple",

View File

@ -23,6 +23,7 @@
<file alias="icons/scalable/status/brain-augemnted-symbolic.svg">icons/brain-augemnted-symbolic.svg</file> <file alias="icons/scalable/status/brain-augemnted-symbolic.svg">icons/brain-augemnted-symbolic.svg</file>
<file alias="icons/scalable/status/chain-link-loose-symbolic.svg">icons/chain-link-loose-symbolic.svg</file> <file alias="icons/scalable/status/chain-link-loose-symbolic.svg">icons/chain-link-loose-symbolic.svg</file>
<file alias="icons/scalable/status/document-text-symbolic.svg">icons/document-text-symbolic.svg</file> <file alias="icons/scalable/status/document-text-symbolic.svg">icons/document-text-symbolic.svg</file>
<file alias="icons/scalable/status/play-symbolic.svg">icons/play-symbolic.svg</file>
<file preprocess="xml-stripblanks">window.ui</file> <file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file> <file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource> </gresource>

View File

@ -182,8 +182,8 @@ def remove_attached_file_response(self, dialog, task, button):
def remove_attached_file(self, button): def remove_attached_file(self, button):
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=_("Remove File"), heading=_("Remove Attachment"),
body=_("Are you sure you want to remove file?"), body=_("Are you sure you want to remove attachment?"),
close_response="cancel" close_response="cancel"
) )
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
@ -290,3 +290,40 @@ def attach_file_response(self, file_dialog, result):
def attach_file(self, filter): def attach_file(self, filter):
file_dialog = Gtk.FileDialog(default_filter=filter) file_dialog = Gtk.FileDialog(default_filter=filter)
file_dialog.open(self, None, lambda file_dialog, result: attach_file_response(self, file_dialog, result)) 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)
)

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 4.003906 4.070312 v 7.859376 c 0 1.070312 0.90625 1.066406 0.90625 1.066406 h 0.09375 c 0.171875 0 0.347656 -0.039063 0.5 -0.125 l 7 -4 c 0.308594 -0.171875 0.46875 -0.523438 0.46875 -0.875 c 0 -0.351563 -0.160156 -0.703125 -0.46875 -0.875 l -7 -4 c -0.152344 -0.085938 -0.328125 -0.125 -0.5 -0.125 h -0.09375 s -0.90625 0 -0.90625 1.074218 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -22,6 +22,7 @@ gi.require_version('GtkSource', '5')
gi.require_version('GdkPixbuf', '2.0') gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf 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 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 time import sleep
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
@ -122,7 +123,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
_("Cannot open image"), _("Cannot open image"),
_("Cannot delete chat because it's the only one left"), _("Cannot delete chat because it's the only one left"),
_("There was an error with the local Ollama instance, so it has been reset"), _("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": [ "info": [
_("Please select a model before chatting"), _("Please select a model before chatting"),
@ -182,10 +185,15 @@ class AlpacaWindow(Adw.ApplicationWindow):
can_use_images = self.verify_if_image_can_be_used() can_use_images = self.verify_if_image_can_be_used()
for name, content in self.attachments.items(): for name, content in self.attachments.items():
if content["type"] == 'image' and can_use_images: attached_images.append(name) 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)): 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)) 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"]) content["button"].get_parent().remove(content["button"])
self.attachments = {} self.attachments = {}
self.attachment_box.set_visible(False) self.attachment_box.set_visible(False)
@ -420,7 +428,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
buffer = self.file_preview_text_view.get_buffer() buffer = self.file_preview_text_view.get_buffer()
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter()) buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), content, len(content)) 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) self.file_preview_dialog.present(self)
def convert_history_to_ollama(self): def convert_history_to_ollama(self):
@ -431,7 +442,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
del new_message['files'] del new_message['files']
new_message['content'] = '' new_message['content'] = ''
for name, file_type in message['files'].items(): 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) file_data = self.get_content_of_file(file_path, file_type)
if file_data: new_message['content'] += f"```[{name}]\n{file_data}\n```" if file_data: new_message['content'] += f"```[{name}]\n{file_data}\n```"
new_message['content'] += message['content'] new_message['content'] += message['content']
@ -531,11 +545,15 @@ class AlpacaWindow(Adw.ApplicationWindow):
child=file_container child=file_container
) )
for name, file_type in files.items(): 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( button_content = Adw.ButtonContent(
label=shown_name, label=shown_name,
icon_name="document-text-symbolic" icon_name="play-symbolic" if file_type=='youtube' else "document-text-symbolic"
) )
button = Gtk.Button( button = Gtk.Button(
vexpand=False, vexpand=False,
@ -545,7 +563,11 @@ class AlpacaWindow(Adw.ApplicationWindow):
tooltip_text=name, tooltip_text=name,
child=button_content 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) file_container.append(button)
message_box.append(file_scroller) message_box.append(file_scroller)
@ -1112,7 +1134,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.verify_connection() self.verify_connection()
def get_content_of_file(self, file_path, file_type): 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': if file_type == 'image':
try: try:
with Image.open(file_path) as img: with Image.open(file_path) as img:
@ -1141,6 +1163,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
for i, page in enumerate(reader.pages): for i, page in enumerate(reader.pages):
text += f"\n- Page {i}\n{page.extract_text()}\n" text += f"\n- Page {i}\n{page.extract_text()}\n"
return text 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': #elif file_type == 'docx':
#document = docx.Document(file_path) #document = docx.Document(file_path)
#if len(document.paragraphs) == 0: return None #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) if len(self.attachments) == 0: self.attachment_box.set_visible(False)
def attach_file(self, file_path, file_type): 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) content = self.get_content_of_file(file_path, file_type)
if content: 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( button_content = Adw.ButtonContent(
label=shown_name, label=shown_name,
icon_name={ icon_name={
"image": "image-x-generic-symbolic", "image": "image-x-generic-symbolic",
"plain_text": "document-text-symbolic", "plain_text": "document-text-symbolic",
"pdf": "document-text-symbolic", "pdf": "document-text-symbolic",
"youtube": "play-symbolic",
#"docx": "document-text-symbolic" #"docx": "document-text-symbolic"
}[file_type] }[file_type]
) )
@ -1195,6 +1229,22 @@ class AlpacaWindow(Adw.ApplicationWindow):
elif action_name == 'export_chat': elif action_name == 'export_chat':
self.export_chat(chat_name) 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): def __init__(self, **kwargs):
super().__init__(**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('delete_chat', self.chat_actions)
self.get_application().create_action('rename_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.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.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.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) self.create_model_name.get_delegate().connect("insert-text", self.check_alphanumeric)