From 2f993c1c6d0d4bb546a50caea516e6da4ceaa8fd Mon Sep 17 00:00:00 2001 From: Viet-Anh Nguyen Date: Sun, 29 Sep 2024 17:11:17 +0700 Subject: [PATCH] Add streaming for model response --- README.md | 2 +- llama_assistant/icons.py | 9 +++ llama_assistant/llama_assistant.py | 107 ++++++++++++++++--------- llama_assistant/loading_animation.py | 68 ---------------- llama_assistant/model_handler.py | 11 ++- llama_assistant/resources/mic_icon.png | Bin 19558 -> 0 bytes pyproject.toml | 4 +- setup.cfg | 2 +- 8 files changed, 91 insertions(+), 112 deletions(-) delete mode 100644 llama_assistant/loading_animation.py delete mode 100644 llama_assistant/resources/mic_icon.png diff --git a/README.md b/README.md index b72edd1..52de72b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ This assistant can run offline on your local machine, and it respects your priva - [x] Custom models: Add support for custom models. - [x] 📚 Support 5 other text models. - [x] 🖼️ Support 5 other multimodal models. +- [x] ⚡ Streaming support for response. - [ ] 🎙️ Add offline STT support: WhisperCPP. [Experimental Code](llama_assistant/speech_recognition_whisper_experimental.py). - [ ] 🧠 Knowledge database: Langchain or LlamaIndex?. - [ ] 🔌 Plugin system for extensibility. @@ -158,7 +159,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Acknowledgements -- [Radio icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/radio) - [Llama 3.2](https://github.com/facebookresearch/llama) by Meta AI Research ## Star History diff --git a/llama_assistant/icons.py b/llama_assistant/icons.py index 17fa01b..fc6730d 100644 --- a/llama_assistant/icons.py +++ b/llama_assistant/icons.py @@ -20,6 +20,15 @@ """ +microphone_icon_svg = """ + + + + + + +""" + def create_icon_from_svg(svg_string): svg_bytes = QByteArray(svg_string.encode("utf-8")) diff --git a/llama_assistant/llama_assistant.py b/llama_assistant/llama_assistant.py index e726c3c..57e14de 100644 --- a/llama_assistant/llama_assistant.py +++ b/llama_assistant/llama_assistant.py @@ -1,8 +1,6 @@ import json import markdown from pathlib import Path -from importlib import resources - from PyQt6.QtWidgets import ( QApplication, QMainWindow, @@ -21,6 +19,8 @@ QPoint, QSize, QTimer, + QThread, + pyqtSignal, ) from PyQt6.QtGui import ( QIcon, @@ -35,12 +35,11 @@ QDropEvent, QFont, QBitmap, + QTextCursor, ) from llama_assistant.wake_word_detector import WakeWordDetector - from llama_assistant.custom_plaintext_editor import CustomPlainTextEdit from llama_assistant.global_hotkey import GlobalHotkey -from llama_assistant.loading_animation import LoadingAnimation from llama_assistant.setting_dialog import SettingsDialog from llama_assistant.speech_recognition import SpeechRecognitionThread from llama_assistant.utils import image_to_base64_data_uri @@ -49,9 +48,34 @@ create_icon_from_svg, copy_icon_svg, clear_icon_svg, + microphone_icon_svg, ) +class ProcessingThread(QThread): + update_signal = pyqtSignal(str) + finished_signal = pyqtSignal() + + def __init__(self, model, prompt, image=None): + super().__init__() + self.model = model + self.prompt = prompt + self.image = image + + def run(self): + output = model_handler.chat_completion( + self.model, self.prompt, image=self.image, stream=True + ) + for chunk in output: + delta = chunk["choices"][0]["delta"] + if "role" in delta: + print(delta["role"], end=": ") + elif "content" in delta: + print(delta["content"], end="") + self.update_signal.emit(delta["content"]) + self.finished_signal.emit() + + class LlamaAssistant(QMainWindow): def __init__(self): super().__init__() @@ -67,6 +91,8 @@ def __init__(self): self.image_label = None self.current_text_model = self.settings.get("text_model") self.current_multimodal_model = self.settings.get("multimodal_model") + self.processing_thread = None + self.response_start_position = 0 def init_wake_word_detector(self): if self.wake_word_detector is not None: @@ -180,23 +206,19 @@ def init_ui(self): ) top_layout.addWidget(self.input_field) - # Load the mic icon from resources - with resources.path("llama_assistant.resources", "mic_icon.png") as path: - mic_icon = QIcon(str(path)) - self.mic_button = QPushButton(self) - self.mic_button.setIcon(mic_icon) + self.mic_button.setIcon(create_icon_from_svg(microphone_icon_svg)) self.mic_button.setIconSize(QSize(24, 24)) self.mic_button.setFixedSize(40, 40) self.mic_button.setStyleSheet( """ QPushButton { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.3); border: none; border-radius: 20px; } QPushButton:hover { - background-color: rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.5); } """ ) @@ -290,6 +312,7 @@ def init_ui(self): QScrollArea { border: none; background-color: transparent; + border-radius: 10px; } QScrollBar:vertical { border: none; @@ -315,10 +338,6 @@ def init_ui(self): self.scroll_area.hide() main_layout.addWidget(self.scroll_area) - self.loading_animation = LoadingAnimation(self) - self.loading_animation.setFixedSize(50, 50) - self.loading_animation.hide() - self.oldPos = self.pos() self.center_on_screen() @@ -354,7 +373,7 @@ def update_styles(self): self.chat_box.setStyleSheet( f"""QTextBrowser {{ {base_style} background-color: rgba{QColor(self.settings["color"]).lighter(120).getRgb()[:3] + (opacity,)}; - border-radius: 5px; + border-radius: 10px; }}""" ) button_style = f""" @@ -441,8 +460,6 @@ def toggle_visibility(self): def on_submit(self): message = self.input_field.toPlainText() self.input_field.clear() - self.loading_animation.move(self.width() // 2 - 25, self.height() // 2 - 25) - self.loading_animation.start_animation() if self.dropped_image: self.process_image_with_prompt(self.dropped_image, message) @@ -452,6 +469,7 @@ def on_submit(self): QTimer.singleShot(100, lambda: self.process_text(message)) def process_text(self, message, task="chat"): + self.show_chat_box() if task == "chat": prompt = message + " \n" + "Generate a short and simple response." elif task == "summarize": @@ -465,32 +483,49 @@ def process_text(self, message, task="chat"): elif task == "write email": prompt = f"Write an email about: {message}" - response = model_handler.chat_completion(self.current_text_model, prompt) - self.last_response = response - self.chat_box.append(f"You: {message}") - self.chat_box.append(f"AI ({task}): {markdown.markdown(response)}") - self.loading_animation.stop_animation() - self.show_chat_box() + self.chat_box.append(f"AI ({task}): ") + + self.processing_thread = ProcessingThread(self.current_text_model, prompt) + self.processing_thread.update_signal.connect(self.update_chat_box) + self.processing_thread.finished_signal.connect(self.on_processing_finished) + self.processing_thread.start() def process_image_with_prompt(self, image_path, prompt): - response = model_handler.chat_completion( - self.current_multimodal_model, prompt, image=image_to_base64_data_uri(image_path) - ) + self.show_chat_box() self.chat_box.append(f"You: [Uploaded an image: {image_path}]") self.chat_box.append(f"You: {prompt}") - self.chat_box.append( - f"AI: {markdown.markdown(response)}" if response else "No response" + self.chat_box.append("AI: ") + + image = image_to_base64_data_uri(image_path) + self.processing_thread = ProcessingThread( + self.current_multimodal_model, prompt, image=image ) - self.loading_animation.stop_animation() - self.show_chat_box() + self.processing_thread.update_signal.connect(self.update_chat_box) + self.processing_thread.finished_signal.connect(self.on_processing_finished) + self.processing_thread.start() + + def update_chat_box(self, text): + self.chat_box.textCursor().insertText(text) + self.chat_box.verticalScrollBar().setValue(self.chat_box.verticalScrollBar().maximum()) + self.last_response += text + + def on_processing_finished(self): + # Clear the last_response for the next interaction + self.last_response = "" + + # Reset the response start position + self.response_start_position = 0 + + # New line for the next interaction + self.chat_box.append("") def show_chat_box(self): if self.scroll_area.isHidden(): self.scroll_area.show() self.copy_button.show() self.clear_button.show() - self.setFixedHeight(600) # Increase this value if needed + self.setFixedHeight(500) # Increase this value if needed self.chat_box.verticalScrollBar().setValue(self.chat_box.verticalScrollBar().maximum()) def copy_result(self): @@ -617,12 +652,12 @@ def start_voice_input(self): self.mic_button.setStyleSheet( """ QPushButton { - background-color: rgba(255, 0, 0, 0.3); + background-color: rgba(255, 0, 0, 0.5); border: none; border-radius: 20px; } QPushButton:hover { - background-color: rgba(255, 0, 0, 0.5); + background-color: rgba(255, 0, 0, 0.6); } """ ) @@ -639,12 +674,12 @@ def stop_voice_input(self): self.mic_button.setStyleSheet( """ QPushButton { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.5); border: none; border-radius: 20px; } QPushButton:hover { - background-color: rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.6); } """ ) diff --git a/llama_assistant/loading_animation.py b/llama_assistant/loading_animation.py deleted file mode 100644 index 6832fa8..0000000 --- a/llama_assistant/loading_animation.py +++ /dev/null @@ -1,68 +0,0 @@ -import math - -from PyQt6.QtWidgets import ( - QWidget, -) -from PyQt6.QtCore import ( - Qt, - QPointF, - QPropertyAnimation, - QEasingCurve, -) -from PyQt6.QtGui import ( - QColor, - QPainter, -) - - -class LoadingAnimation(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.setFixedSize(60, 60) - self.rotation = 0 - self.dot_count = 8 - self.dot_radius = 3 - self.circle_radius = 20 - - self.animation = QPropertyAnimation(self, b"rotation") - self.animation.setDuration(1500) - self.animation.setStartValue(0) - self.animation.setEndValue(360) - self.animation.setLoopCount(-1) - self.animation.setEasingCurve(QEasingCurve.Type.Linear) - - def start_animation(self): - self.show() - self.animation.start() - - def stop_animation(self): - self.animation.stop() - self.hide() - - def paintEvent(self, event): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - painter.translate(self.width() / 2, self.height() / 2) - painter.rotate(self.rotation) - - for i in range(self.dot_count): - angle = 360 / self.dot_count * i - x = self.circle_radius * math.cos(math.radians(angle)) - y = self.circle_radius * math.sin(math.radians(angle)) - - opacity = (i + 1) / self.dot_count - color = QColor(255, 255, 255, int(255 * opacity)) - painter.setBrush(color) - painter.setPen(Qt.PenStyle.NoPen) - - painter.drawEllipse(QPointF(x, y), self.dot_radius, self.dot_radius) - - @property - def rotation(self): - return self._rotation - - @rotation.setter - def rotation(self, value): - self._rotation = value - self.update() diff --git a/llama_assistant/model_handler.py b/llama_assistant/model_handler.py index 8dfac3e..53123ac 100644 --- a/llama_assistant/model_handler.py +++ b/llama_assistant/model_handler.py @@ -148,7 +148,7 @@ def chat_completion( model_id: str, message: str, image: Optional[str] = None, - n_ctx: int = 2048, + stream: bool = False, ) -> str: model_data = self.load_model(model_id) if not model_data: @@ -168,12 +168,15 @@ def chat_completion( {"type": "image_url", "image_url": {"url": image}}, ], } - ] + ], + stream=stream, ) else: - response = model.create_chat_completion(messages=[{"role": "user", "content": message}]) + response = model.create_chat_completion( + messages=[{"role": "user", "content": message}], stream=stream + ) - return response["choices"][0]["message"]["content"] + return response def _schedule_unload(self): if self.unload_timer: diff --git a/llama_assistant/resources/mic_icon.png b/llama_assistant/resources/mic_icon.png deleted file mode 100644 index 1b5b9cac8aaad2e96d609ad981614c70ddfe1f44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19558 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.8 where = . [options.package_data] -llama_assistant.resources = *.png, *.onnx +llama_assistant.resources = *.onnx