diff --git a/src/app.py b/src/app.py index 00151be..c37aa44 100644 --- a/src/app.py +++ b/src/app.py @@ -1,5 +1,3 @@ -from audio.textual_transcription_textarea import TranscriptionTextArea -import datetime from typing import Optional from rich.panel import Panel @@ -9,12 +7,12 @@ from textual.app import App, ComposeResult from textual.widgets import Static, ListView, ListItem from textual.containers import Grid -from textual.widgets import Header, Static, Footer, TextArea, Button, Label +from textual.widgets import Header, Static, Footer, Button, Label from textual.screen import Screen, ModalScreen -from textual.binding import Binding -from llm.model import LanguageModel from notes.manager import NoteManager +from notes_editor_components import NoteEditScreen, LiveNoteEditScreen +from settings_screen import SettingsScreen class NotePanel(Static): @@ -55,98 +53,6 @@ def compose(self): ) -class NoteTextArea(TextArea): - BINDINGS = [] - - def __init__(self, uuid: str, content: str, *args, **kwargs): - super().__init__(*args, **kwargs) - self.language = "markdown" - self.note_manager = NoteManager() - self.content = content - self.uuid = uuid - self.text = self.content - - def on_mount(self): - if self.text and len(self.text) > 0: - lines = self.text.split("\n") - self.cursor_location = (len(lines) - 1, len(lines[-1])) - - def on_text_area_changed(self, changed: TextArea.Changed): - self.note_manager.update_note_content(self.uuid, changed.text_area.text) - - -class NoteEditScreen(Screen): - BINDINGS = [ - ("escape", "quit", "Quit"), - Binding("ctrl+l", "run_llm", "Run LLM", show=True, priority=True), - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.note_manager = NoteManager() - if self.note_manager.selected_note is None: - today = datetime.datetime.now().strftime("%Y-%m-%d") - self.title = today - # add todays date to the title - self.content = f"# Notes {today}\n\n" - self.uuid = self.note_manager.create_note(self.title, self.content) - else: - note = self.note_manager.selected_note - self.title = note["title"] - self.content = self.note_manager.read_note(note["uuid"]) - self.uuid = note["uuid"] - - def compose(self): - yield Static(f"Title: {self.title}") - yield NoteTextArea(self.uuid, self.content, id="note_text") - yield Footer() - - def action_quit(self): - self.app.notify("Note Saved", timeout=1) - new_content = self.query_one("#note_text").text - self.note_manager.update_note_content(self.uuid, new_content) - self.dismiss() - - @work - async def action_run_llm(self): - self.app.notify("Running LLM") - textArea = self.query_one("#note_text") - note_content = textArea.text - textArea.text += f"\n\n# Response\n\n" - for response in LanguageModel().generate_response(note_content): - self.log.info(response) - textArea.text += response - - -class LiveNoteEditScreen(Screen): - BINDINGS = [ - ("escape", "quit", "Quit"), - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.note_manager = NoteManager() - today = datetime.datetime.now().strftime("%Y-%m-%d") - self.title = today - # add todays date to the title - self.content = f"# Notes {today}\n\n" - self.uuid = self.note_manager.create_note(self.title, self.content) - - def compose(self): - yield Static(f"Title: {self.title}") - yield TranscriptionTextArea(id="Transcription") - text_area = NoteTextArea(self.uuid, self.content, id="note_text") - text_area.focus() - yield text_area - - def action_quit(self): - self.app.notify("Note Saved") - note_content = self.query_one("#note_text").text - transcription = self.query_one("#Transcription").text - combined = f"{note_content}\n\n# Transcription\n\n{transcription}" - self.note_manager.update_note_content(self.uuid, combined) - self.dismiss() - class DeleteScreen(ModalScreen): def compose(self) -> ComposeResult: @@ -171,6 +77,7 @@ class RichNoteTakingScreen(Screen): ("n", "new_note", "New"), ("l", "live_note", "New Live"), ("d", "delete_note", "Delete"), + ("ctrl-.", "settings", "Settings"), ] CSS_PATH = "main.tcss" @@ -201,6 +108,11 @@ async def action_live_note(self): self.app.pop_screen() self.app.push_screen(RichNoteTakingScreen()) + async def action_settings(self): + await self.app.push_screen(SettingsScreen()) + self.app.pop_screen() + self.app.push_screen(RichNoteTakingScreen()) + def action_quit(self): self.app.exit() diff --git a/src/audio/Transcriber.py b/src/audio/Transcriber.py index fc2a0b3..c6010eb 100644 --- a/src/audio/Transcriber.py +++ b/src/audio/Transcriber.py @@ -1,5 +1,5 @@ import threading -from simpler_whisper.whisper import ThreadedWhisperModel, set_log_callback, LogLevel +from simpler_whisper.whisper import ThreadedWhisperModel, set_log_callback from utils import resource_path import os diff --git a/src/llm/model.py b/src/llm/model.py index 2d373a6..f842692 100644 --- a/src/llm/model.py +++ b/src/llm/model.py @@ -1,4 +1,9 @@ -from llama_cpp import Llama +from llama_cpp import Llama, llama_log_set +import ctypes + + +def my_log_callback(level, message, user_data): + pass class LanguageModel: @@ -11,6 +16,10 @@ def __new__(cls, *args, **kwargs): def __init__(self): if not hasattr(self, "llm"): + log_callback = ctypes.CFUNCTYPE( + None, ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p + )(my_log_callback) + llama_log_set(log_callback, ctypes.c_void_p()) self.llm = Llama.from_pretrained( repo_id="bartowski/Llama-3.2-1B-Instruct-GGUF", filename="Llama-3.2-1B-Instruct-Q4_K_M.gguf", diff --git a/src/main.py b/src/main.py index 79e0738..ff0d4f6 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,8 @@ -import multiprocessing from dotenv import load_dotenv from app import RichNoteTakingApp from audio.Transcriber import Transcriber from llm.model import LanguageModel from utils import resource_path -import os if __name__ == "__main__": diff --git a/src/notes_editor_components.py b/src/notes_editor_components.py new file mode 100644 index 0000000..cc139cd --- /dev/null +++ b/src/notes_editor_components.py @@ -0,0 +1,105 @@ +import datetime + +from textual.widgets import Static +from textual.widgets import Static, Footer, TextArea +from textual.screen import Screen +from textual.binding import Binding +from textual import work + +from llm.model import LanguageModel +from notes.manager import NoteManager + +from audio.textual_transcription_textarea import TranscriptionTextArea + + +class NoteTextArea(TextArea): + BINDINGS = [] + + def __init__(self, uuid: str, content: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self.language = "markdown" + self.note_manager = NoteManager() + self.content = content + self.uuid = uuid + self.text = self.content + + def on_mount(self): + if self.text and len(self.text) > 0: + lines = self.text.split("\n") + self.cursor_location = (len(lines) - 1, len(lines[-1])) + + def on_text_area_changed(self, changed: TextArea.Changed): + self.note_manager.update_note_content(self.uuid, changed.text_area.text) + + +class NoteEditScreen(Screen): + BINDINGS = [ + ("escape", "quit", "Quit"), + Binding("ctrl+l", "run_llm", "Run LLM", show=True, priority=True), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.note_manager = NoteManager() + if self.note_manager.selected_note is None: + today = datetime.datetime.now().strftime("%Y-%m-%d") + self.title = today + # add todays date to the title + self.content = f"# Notes {today}\n\n" + self.uuid = self.note_manager.create_note(self.title, self.content) + else: + note = self.note_manager.selected_note + self.title = note["title"] + self.content = self.note_manager.read_note(note["uuid"]) + self.uuid = note["uuid"] + + def compose(self): + yield Static(f"Title: {self.title}") + yield NoteTextArea(self.uuid, self.content, id="note_text") + yield Footer() + + def action_quit(self): + self.app.notify("Note Saved", timeout=1) + new_content = self.query_one("#note_text").text + self.note_manager.update_note_content(self.uuid, new_content) + self.dismiss() + + @work + async def action_run_llm(self): + self.app.notify("Running LLM") + textArea = self.query_one("#note_text") + note_content = textArea.text + textArea.text += f"\n\n# Response\n\n" + for response in LanguageModel().generate_response(note_content): + self.log.info(response) + textArea.text += response + + +class LiveNoteEditScreen(Screen): + BINDINGS = [ + ("escape", "quit", "Quit"), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.note_manager = NoteManager() + today = datetime.datetime.now().strftime("%Y-%m-%d") + self.title = today + # add todays date to the title + self.content = f"# Notes {today}\n\n" + self.uuid = self.note_manager.create_note(self.title, self.content) + + def compose(self): + yield Static(f"Title: {self.title}") + yield TranscriptionTextArea(id="Transcription") + text_area = NoteTextArea(self.uuid, self.content, id="note_text") + text_area.focus() + yield text_area + + def action_quit(self): + self.app.notify("Note Saved") + note_content = self.query_one("#note_text").text + transcription = self.query_one("#Transcription").text + combined = f"{note_content}\n\n# Transcription\n\n{transcription}" + self.note_manager.update_note_content(self.uuid, combined) + self.dismiss() diff --git a/src/settings_screen.py b/src/settings_screen.py new file mode 100644 index 0000000..f9c8386 --- /dev/null +++ b/src/settings_screen.py @@ -0,0 +1,68 @@ +from textual.containers import Grid, Vertical +from textual.widgets import Select, TextArea, Button, Label +from textual.screen import ModalScreen +from textual.message import Message + +class SettingsScreen(ModalScreen): + """A modal screen for managing LLM settings.""" + + class SettingsChanged(Message): + """Settings changed message.""" + def __init__(self, prompt: str, model: str) -> None: + self.prompt = prompt + self.model = model + super().__init__() + + def __init__(self, current_prompt: str = "", current_model: str = "gpt-3.5-turbo"): + super().__init__() + self.current_prompt = current_prompt + self.current_model = current_model + + BINDINGS = [("escape", "cancel", "Cancel")] + + def compose(self): + yield Grid( + Vertical( + Label("LLM Settings", id="settings-title", classes="settings-header"), + Label("System Prompt:"), + TextArea( + id="prompt-input", + language="markdown", + value=self.current_prompt, + classes="settings-prompt" + ), + Label("Model:"), + Select( + [(model, model) for model in [ + "gpt-3.5-turbo", + "gpt-4", + "claude-3-opus", + "claude-3-sonnet", + "claude-3-haiku" + ]], + id="model-select", + value=self.current_model, + classes="settings-select" + ), + Grid( + Button("Save", variant="primary", id="save"), + Button("Cancel", variant="default", id="cancel"), + id="button-container", + classes="settings-buttons" + ), + id="settings-container" + ), + id="settings-dialog" + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "save": + prompt = self.query_one("#prompt-input").text + model = self.query_one("#model-select").value + self.post_message(self.SettingsChanged(prompt, model)) + self.dismiss(True) + else: + self.dismiss(False) + + def action_cancel(self): + self.dismiss(False)