diff --git a/app/main.py b/app/main.py index e69de29..e40a838 100644 --- a/app/main.py +++ b/app/main.py @@ -0,0 +1,21 @@ +# app/main.py +import sys +import PySide6.QtAsyncio as QtAsyncio +from PySide6.QtWidgets import QApplication + +from app.ui.main_window import MainWindow + + +def main(): + app = QApplication(sys.argv) + app.setApplicationName("Spanish Assistant") + + window = MainWindow() + window.show() + + # QtAsyncio handles the combined Qt and Python asyncio event loop + QtAsyncio.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app/ui/chat_tab.py b/app/ui/chat_tab.py index e69de29..8925231 100644 --- a/app/ui/chat_tab.py +++ b/app/ui/chat_tab.py @@ -0,0 +1,79 @@ +#app/ui/chat_tab.py + +import asyncio +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + + +class ChatTab(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._init_ui() + + def _init_ui(self): + layout = QVBoxLayout(self) + + # 1. Chat History Display + self.chat_display = QTextEdit(self) + self.chat_display.setReadOnly(True) + self.chat_display.setPlaceholderText( + "Su conversación aparecerá aquí... (Your conversation will appear here...)" + ) + layout.addWidget(self.chat_display, stretch=4) + + # 2. Feedback Panel (Pronunciation & WER Metrics) + self.feedback_panel = QFrame(self) + self.feedback_panel.setFrameShape(QFrame.Shape.StyledPanel) + feedback_layout = QVBoxLayout(self.feedback_panel) + + self.feedback_label = QLabel("Análisis de Pronunciación:", self) + self.feedback_content = QLabel("Mantén presionado el botón o habla para evaluar.", self) + + feedback_layout.addWidget(self.feedback_label) + feedback_layout.addWidget(self.feedback_content) + layout.addWidget(self.feedback_panel, stretch=1) + + # 3. User Input Controls + input_layout = QHBoxLayout() + + self.text_input = QLineEdit(self) + self.text_input.setPlaceholderText("Escribe un mensaje o habla...") + self.text_input.returnPressed.connect(self._on_send) + + self.btn_send = QPushButton("Enviar", self) + self.btn_send.clicked.connect(self._on_send) + + self.btn_record = QPushButton("🎤 Hablar (Hold Space)", self) + self.btn_record.clicked.connect(self._on_record_clicked) + + input_layout.addWidget(self.text_input, stretch=3) + input_layout.addWidget(self.btn_send) + input_layout.addWidget(self.btn_record) + + layout.addLayout(input_layout) + + def _on_send(self): + text = self.text_input.text().strip() + if text: + self.chat_display.append(f"Tú: {text}") + self.text_input.clear() + # Schedule asynchronous processing via asyncio + asyncio.create_task(self._simulate_assistant_response(text)) + + def _on_record_clicked(self): + self.chat_display.append("[Escuchando audio local vía mlx-whisper...]") + + async def _simulate_assistant_response(self, user_text: str): + # Async coroutine demonstrating QtAsyncio responsiveness + await asyncio.sleep(1.0) + response = f"¡Hola! Recibí tu mensaje: '{user_text}'. ¿Cómo te va?" + self.chat_display.append(f"Asistente: {response}") \ No newline at end of file diff --git a/app/ui/control_tab.py b/app/ui/control_tab.py index e69de29..4016048 100644 --- a/app/ui/control_tab.py +++ b/app/ui/control_tab.py @@ -0,0 +1,43 @@ + +# app/ui/control_tab.py +from PySide6.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + + +class ControlTab(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._init_ui() + + def _init_ui(self): + layout = QVBoxLayout(self) + + # Server Control Group + server_group = QGroupBox("Estado del Servidor LLM Local (llama.cpp / Gemma 4)", self) + server_layout = QHBoxLayout(server_group) + + self.status_label = QLabel("Estado: Detenido (Stopped)", self) + self.btn_toggle_server = QPushButton("Iniciar Servidor", self) + + server_layout.addWidget(self.status_label) + server_layout.addWidget(self.btn_toggle_server) + layout.addWidget(server_group) + + # Log Output Viewer + log_group = QGroupBox("Registros del Sistema (System Logs)", self) + log_layout = QVBoxLayout(log_group) + + self.log_viewer = QTextEdit(self) + self.log_viewer.setReadOnly(True) + self.log_viewer.append("[SYSTEM] Entorno PySide6 inicializado correctamente.") + self.log_viewer.append("[SYSTEM] Esperando conexión con base de datos SQLite...") + + log_layout.addWidget(self.log_viewer) + layout.addWidget(log_group) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index e69de29..71e2b07 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -0,0 +1,33 @@ +# app/ui/main_window.py +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QMainWindow, QStatusBar, QTabWidget, QVBoxLayout, QWidget + +from app.ui.chat_tab import ChatTab +from app.ui.control_tab import ControlTab + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("Spanish Voice Practice Assistant") + self.resize(1000, 700) + + # Main Layout Setup + central_widget = QWidget(self) + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout(central_widget) + + # Tab Widget Initialization + self.tabs = QTabWidget(self) + self.chat_tab = ChatTab(self) + self.control_tab = ControlTab(self) + + self.tabs.addTab(self.chat_tab, "Conversación") + self.tabs.addTab(self.control_tab, "Control & Logs") + main_layout.addWidget(self.tabs) + + # Status Bar Setup + self.status_bar = QStatusBar(self) + self.setStatusBar(self.status_bar) + self.status_bar.showMessage("Listo (Ready) | Connected to Local Environment") \ No newline at end of file diff --git a/doc/images/image-01.png b/doc/images/image-01.png new file mode 100644 index 0000000..c213bb6 Binary files /dev/null and b/doc/images/image-01.png differ diff --git a/doc/notes.md b/doc/notes.md index 567b21f..dee2187 100644 --- a/doc/notes.md +++ b/doc/notes.md @@ -22,7 +22,13 @@ - [13.1. Errors and Fixes](#131-errors-and-fixes) - [13.1.1. Step-by-Step Resolution Commands](#1311-step-by-step-resolution-commands) - [13.1.2. I got the following errors uv sync](#1312-i-got-the-following-errors-uv-sync) -- [Create basic PySide6 boilerplate for main.py, main\_window.py, and the tab modules](#create--basic-pyside6-boilerplate-for-mainpy-main_windowpy-and-the-tab-modules) +- [14. Create basic PySide6 boilerplate for main.py, main\_window.py, and the tab modules](#14-create--basic-pyside6-boilerplate-for-mainpy-main_windowpy-and-the-tab-modules) + - [14.1. Entry Point: app/main.py](#141-entry-point-appmainpy) + - [14.2. Main Window: app/ui/main\_window.py](#142-main-window-appuimain_windowpy) + - [14.3. Chat Tab: app/ui/chat\_tab.py](#143-chat-tab-appuichat_tabpy) + - [14.4. Control Tab: app/ui/control\_tab.py](#144-control-tab-appuicontrol_tabpy) + - [14.5. Verification](#145-verification) + - [14.6. GUI First run](#146-gui-first-run) # 1. Executive Overview @@ -788,11 +794,222 @@ You should see All core modules imported successfully!, with zero warnings or er git tag -a v0.1.1 -m "uv set all modules loaded " -# Create basic PySide6 boilerplate for main.py, main_window.py, and the tab modules +# 14. Create basic PySide6 boilerplate for main.py, main_window.py, and the tab modules Here is a complete set of boilerplate code tailored specifically for 141_Spanish_Assistant. It uses PySide6.QtAsyncio for non-blocking execution (allowing STT, TTS, and LLM calls to run seamlessly in the background without freezing the GUI). -1. Entry Point: app/main.py +## 14.1. Entry Point: app/main.py This file initializes the PySide6 application, creates the MainWindow, and delegates execution to QtAsyncio. + +```zsh +# app/main.py +import sys +import PySide6.QtAsyncio as QtAsyncio +from PySide6.QtWidgets import QApplication + +from app.ui.main_window import MainWindow + + +def main(): + app = QApplication(sys.argv) + app.setApplicationName("Spanish Assistant") + + window = MainWindow() + window.show() + + # QtAsyncio handles the combined Qt and Python asyncio event loop + QtAsyncio.run() + + +if __name__ == "__main__": + main() +``` +## 14.2. Main Window: app/ui/main_window.py +Sets up the primary container with a QTabWidget hosting Tab 1 (Conversación) and Tab 2 (Control & Database). + +```zsh +# app/ui/main_window.py +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QMainWindow, QStatusBar, QTabWidget, QVBoxLayout, QWidget + +from app.ui.chat_tab import ChatTab +from app.ui.control_tab import ControlTab + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("Spanish Voice Practice Assistant") + self.resize(1000, 700) + + # Main Layout Setup + central_widget = QWidget(self) + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout(central_widget) + + # Tab Widget Initialization + self.tabs = QTabWidget(self) + self.chat_tab = ChatTab(self) + self.control_tab = ControlTab(self) + + self.tabs.addTab(self.chat_tab, "Conversación") + self.tabs.addTab(self.control_tab, "Control & Logs") + main_layout.addWidget(self.tabs) + + # Status Bar Setup + self.status_bar = QStatusBar(self) + self.setStatusBar(self.status_bar) + self.status_bar.showMessage("Listo (Ready) | Connected to Local Environment") + ``` + ## 14.3. Chat Tab: app/ui/chat_tab.py +Contains the conversation log, feedback display panel, and voice/text user input triggers. + +```zsh +#app/ui/chat_tab.py + +import asyncio +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + + +class ChatTab(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._init_ui() + + def _init_ui(self): + layout = QVBoxLayout(self) + + # 1. Chat History Display + self.chat_display = QTextEdit(self) + self.chat_display.setReadOnly(True) + self.chat_display.setPlaceholderText( + "Su conversación aparecerá aquí... (Your conversation will appear here...)" + ) + layout.addWidget(self.chat_display, stretch=4) + + # 2. Feedback Panel (Pronunciation & WER Metrics) + self.feedback_panel = QFrame(self) + self.feedback_panel.setFrameShape(QFrame.Shape.StyledPanel) + feedback_layout = QVBoxLayout(self.feedback_panel) + + self.feedback_label = QLabel("Análisis de Pronunciación:", self) + self.feedback_content = QLabel("Mantén presionado el botón o habla para evaluar.", self) + + feedback_layout.addWidget(self.feedback_label) + feedback_layout.addWidget(self.feedback_content) + layout.addWidget(self.feedback_panel, stretch=1) + + # 3. User Input Controls + input_layout = QHBoxLayout() + + self.text_input = QLineEdit(self) + self.text_input.setPlaceholderText("Escribe un mensaje o habla...") + self.text_input.returnPressed.connect(self._on_send) + + self.btn_send = QPushButton("Enviar", self) + self.btn_send.clicked.connect(self._on_send) + + self.btn_record = QPushButton("🎤 Hablar (Hold Space)", self) + self.btn_record.clicked.connect(self._on_record_clicked) + + input_layout.addWidget(self.text_input, stretch=3) + input_layout.addWidget(self.btn_send) + input_layout.addWidget(self.btn_record) + + layout.addLayout(input_layout) + + def _on_send(self): + text = self.text_input.text().strip() + if text: + self.chat_display.append(f"Tú: {text}") + self.text_input.clear() + # Schedule asynchronous processing via asyncio + asyncio.create_task(self._simulate_assistant_response(text)) + + def _on_record_clicked(self): + self.chat_display.append("[Escuchando audio local vía mlx-whisper...]") + + async def _simulate_assistant_response(self, user_text: str): + # Async coroutine demonstrating QtAsyncio responsiveness + await asyncio.sleep(1.0) + response = f"¡Hola! Recibí tu mensaje: '{user_text}'. ¿Cómo te va?" + self.chat_display.append(f"Asistente: {response}") + ``` + + ## 14.4. Control Tab: app/ui/control_tab.py +Dedicated to system logs, llama.cpp server configuration, and local SQLite data inspection. + +```zsh + +# app/ui/control_tab.py +from PySide6.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + + +class ControlTab(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._init_ui() + + def _init_ui(self): + layout = QVBoxLayout(self) + + # Server Control Group + server_group = QGroupBox("Estado del Servidor LLM Local (llama.cpp / Gemma 4)", self) + server_layout = QHBoxLayout(server_group) + + self.status_label = QLabel("Estado: Detenido (Stopped)", self) + self.btn_toggle_server = QPushButton("Iniciar Servidor", self) + + server_layout.addWidget(self.status_label) + server_layout.addWidget(self.btn_toggle_server) + layout.addWidget(server_group) + + # Log Output Viewer + log_group = QGroupBox("Registros del Sistema (System Logs)", self) + log_layout = QVBoxLayout(log_group) + + self.log_viewer = QTextEdit(self) + self.log_viewer.setReadOnly(True) + self.log_viewer.append("[SYSTEM] Entorno PySide6 inicializado correctamente.") + self.log_viewer.append("[SYSTEM] Esperando conexión con base de datos SQLite...") + + log_layout.addWidget(self.log_viewer) + layout.addWidget(log_group) +``` + +## 14.5. Verification +Run the boilerplate using uv: + +```zsh +uv run python -m app.main +``` +This will launch a GUI window featuring tabbed navigation, text interaction, and async-ready buttons. + +If you'd like to get a sense of how asynchronous Qt event loops handle UI reactivity without freezing, this tutorial provides a great hands-on walkthrough. + +Async event loop integration with Qt +This video demonstrates how to run an infinite or asynchronous task inside a Qt application without locking up the user interface. +## 14.6. GUI First run + +![GUI](./images/image-01.png) \ No newline at end of file diff --git a/doc/notes.pdf b/doc/notes.pdf index 120070a..ca8eb06 100644 Binary files a/doc/notes.pdf and b/doc/notes.pdf differ