Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 3478a899e1 |
7 changed files with 396 additions and 3 deletions
21
app/main.py
21
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()
|
||||||
|
|
@ -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("<b>Análisis de Pronunciación:</b>", 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"<b>Tú:</b> {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("<i>[Escuchando audio local vía mlx-whisper...]</i>")
|
||||||
|
|
||||||
|
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"<b>Asistente:</b> {response}")
|
||||||
|
|
@ -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: <b>Detenido (Stopped)</b>", 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)
|
||||||
|
|
@ -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")
|
||||||
BIN
doc/images/image-01.png
Normal file
BIN
doc/images/image-01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
223
doc/notes.md
223
doc/notes.md
|
|
@ -22,7 +22,13 @@
|
||||||
- [13.1. Errors and Fixes](#131-errors-and-fixes)
|
- [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.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)
|
- [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
|
# 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 "
|
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.
|
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).
|
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.
|
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("<b>Análisis de Pronunciación:</b>", 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"<b>Tú:</b> {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("<i>[Escuchando audio local vía mlx-whisper...]</i>")
|
||||||
|
|
||||||
|
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"<b>Asistente:</b> {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: <b>Detenido (Stopped)</b>", 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
|
||||||
|
|
||||||
|

|
||||||
BIN
doc/notes.pdf
BIN
doc/notes.pdf
Binary file not shown.
Loading…
Reference in a new issue