Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/app_info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ version: "0.1.0"
description: "AI/ML Research -> Clinical Applications (Speech Pathology)"
help_url: "https://voxkit-web.vercel.app/help"

# Logging: rolling file at ~/.voxkit/logs/voxkit.log
log_max_bytes: 5242880 # 5 MB per file before rotation
log_backup_count: 3 # number of rotated files to retain

# Introduction text displayed to users
introduction: |
VoxKit bridges advanced ML alignment tools and clinical speech pathology research.
Expand Down
4 changes: 4 additions & 0 deletions config/profiles/default/app_info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ version: "0.1.0"
description: "AI/ML Research -> Clinical Applications (Speech Pathology)"
help_url: "https://voxkit-web.vercel.app/help"

# Logging: rolling file at ~/.voxkit/logs/voxkit.log
log_max_bytes: 5242880 # 5 MB per file before rotation
log_backup_count: 3 # number of rotated files to retain

# Introduction text displayed to users
introduction: |
VoxKit bridges advanced ML alignment tools and clinical speech pathology research.
Expand Down
4 changes: 4 additions & 0 deletions config/profiles/explanatory/app_info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ version: "0.1.0"
description: "AI/ML Research -> Clinical Applications (Speech Pathology)"
help_url: "https://voxkit-web.vercel.app/help"

# Logging: rolling file at ~/.voxkit/logs/voxkit.log
log_max_bytes: 5242880 # 5 MB per file before rotation
log_backup_count: 3 # number of rotated files to retain

# Introduction text displayed to users
introduction: |
VoxKit bridges advanced ML alignment tools and clinical speech pathology research.
Expand Down
24 changes: 23 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
import faulthandler
import logging
import os
import multiprocessing

Expand All @@ -8,7 +9,8 @@
import _frozen_patch

from voxkit.config.pipeline_config import PipelineConfig
from voxkit.config.app_config import AppConfig, get_profile_config_path
from voxkit.config.app_config import AppConfig, get_app_config, get_profile_config_path
from voxkit.config.logging_config import setup_logging

# Disable Qt emoji support to prevent crashes in frozen builds

Expand Down Expand Up @@ -91,10 +93,29 @@


def main():
# Initialize logging as early as possible so startup work is captured.
# Use config values when available; fall back to defaults otherwise.
try:
_cfg = get_app_config()
setup_logging(
max_bytes=_cfg.log_max_bytes,
backup_count=_cfg.log_backup_count,
)
except Exception:
setup_logging()

# Attach the Qt-aware log handler so live viewers can subscribe.
from voxkit.gui.components.log_handler import get_gui_log_handler
get_gui_log_handler()

log = logging.getLogger("voxkit.main")
log.info("VoxKit starting (frozen=%s)", bool(getattr(sys, "frozen", False)))

app = QApplication(sys.argv)
app.setStyle("Fusion")

# Execute startup script on first launch (before GUI initialization)
log.info("Running startup script")
execute_startup_script(STARTUP_SCRIPT, app)

app_config = None
Expand All @@ -109,6 +130,7 @@ def main():

window = AlignmentGUI(pipeline_config=pipeline_config, app_config=app_config)
window.show()
log.info("Main window shown, entering Qt event loop")
sys.exit(app.exec())


Expand Down
4 changes: 4 additions & 0 deletions src/voxkit/config/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ class AppConfig:
help_url: str = "https://voxkit-web.vercel.app/help"
release_date: Optional[str] = None
release_notes: Optional[str] = None
log_max_bytes: int = 5 * 1024 * 1024
log_backup_count: int = 3

@classmethod
def from_yaml(cls, config_path: Path) -> "AppConfig":
Expand Down Expand Up @@ -160,6 +162,8 @@ def from_yaml(cls, config_path: Path) -> "AppConfig":
help_url=data.get("help_url", "https://voxkit-web.vercel.app/help"),
release_date=data.get("release_date"),
release_notes=data.get("release_notes"),
log_max_bytes=int(data.get("log_max_bytes", 5 * 1024 * 1024)),
log_backup_count=int(data.get("log_backup_count", 3)),
)

@classmethod
Expand Down
81 changes: 81 additions & 0 deletions src/voxkit/config/logging_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Application logging setup.

Configures a rotating file log at ``~/.voxkit/logs/voxkit.log`` and exposes
a hook for attaching a GUI handler. ``VOXKIT_DEBUG=1`` in the environment
raises the file log level to DEBUG.
"""

import logging
import os
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional

DEFAULT_MAX_BYTES = 5 * 1024 * 1024
DEFAULT_BACKUP_COUNT = 3

LOG_DIR = Path.home() / ".voxkit" / "logs"
LOG_FILE = LOG_DIR / "voxkit.log"

_LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"

_configured = False


def setup_logging(
max_bytes: int = DEFAULT_MAX_BYTES,
backup_count: int = DEFAULT_BACKUP_COUNT,
log_file: Optional[Path] = None,
) -> RotatingFileHandler:
"""Configure the root logger with a rotating file handler.

Idempotent — calling more than once has no effect beyond the first call.

Args:
max_bytes: Max size in bytes before rotation.
backup_count: Number of rotated files to retain.
log_file: Override the log file path (primarily for tests).

Returns:
The installed RotatingFileHandler.
"""
global _configured

target = log_file or LOG_FILE
target.parent.mkdir(parents=True, exist_ok=True)

root = logging.getLogger()
debug_enabled = os.environ.get("VOXKIT_DEBUG") == "1"
root.setLevel(logging.DEBUG if debug_enabled else logging.INFO)

if _configured:
for handler in root.handlers:
if isinstance(handler, RotatingFileHandler):
return handler

handler = RotatingFileHandler(
target,
maxBytes=max_bytes,
backupCount=backup_count,
encoding="utf-8",
)
handler.setFormatter(logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT))
handler.setLevel(logging.DEBUG if debug_enabled else logging.INFO)
root.addHandler(handler)

_configured = True
logging.getLogger(__name__).info(
"Logging initialized (debug=%s, file=%s)", debug_enabled, target
)
return handler


def reset_logging() -> None:
"""Remove handlers installed by :func:`setup_logging`. Test helper."""
global _configured
root = logging.getLogger()
for handler in list(root.handlers):
root.removeHandler(handler)
handler.close()
_configured = False
90 changes: 84 additions & 6 deletions src/voxkit/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,31 @@
- Styling is centralized in the styles module for consistency
"""

import logging
import webbrowser
from typing import Optional

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtWidgets import QHBoxLayout, QMainWindow, QStackedWidget, QToolBar, QWidget
from PyQt6.QtWidgets import (
QHBoxLayout,
QMainWindow,
QStackedWidget,
QToolBar,
QToolButton,
QWidget,
)
from rich import print as rprint

from voxkit.config.app_config import AppConfig, get_app_config
from voxkit.config.pipeline_config import PipelineConfig, get_pipeline_config
from voxkit.gui.components import DNAStrandWidget
from voxkit.gui.components import DNAStrandWidget, LogViewerDialog
from voxkit.gui.pages.datasets import DatasetsPage
from voxkit.gui.pages.models import ManageAlignersWidget
from voxkit.gui.pages.pipeline import PipelineFormStack as PipelineContainer

logger = logging.getLogger(__name__)

GlobalStyleSheet = """
QMainWindow {
background-color: transparent;
Expand Down Expand Up @@ -174,6 +185,12 @@ def __init__(
self.app_config = app_config or get_app_config()
self.pipeline_config = pipeline_config or get_pipeline_config()

logger.info(
"AlignmentGUI initialized: app=%s version=%s",
self.app_config.app_name,
self.app_config.version,
)

# DEBUG
rprint("[bold green]App Configuration:[/bold green]")
rprint(self.app_config)
Expand Down Expand Up @@ -295,7 +312,7 @@ def update_active_tab_style(self, active_button):

def open_datasets(self):
"""Switch to Datasets view"""
print("Open Datasets...")
logger.info("Navigate: Datasets page")
# Remember current pipeline page
self.last_pipeline_page = self.pipeline_container.get_current_page_index()
self.pipeline_container.menu_list.setVisible(False)
Expand All @@ -307,7 +324,7 @@ def open_datasets(self):

def open_models_dashboard(self):
"""Switch to Pipeline view with menu and stacked pages"""
print("Open Models Dashboard...")
logger.info("Navigate: Pipeline page")
self.pipeline_container.reload() # Ensure models are reloaded
self.pipeline_container.menu_list.setVisible(True)
self.content_stack.setCurrentIndex(0) # Show pipeline stack
Expand All @@ -318,7 +335,7 @@ def open_models_dashboard(self):

def open_preferences(self):
"""Switch to Manage view with CategoricalListWidget"""
print("Open Preferences...")
logger.info("Navigate: Models page")
self.pipeline_container.reload() # Ensure models are reloaded
# Remember current pipeline page
self.last_pipeline_page = self.pipeline_container.get_current_page_index()
Expand All @@ -328,8 +345,8 @@ def open_preferences(self):
self.update_active_tab_style("manage")

def open_help(self):
logger.info("Opening help URL: %s", self.app_config.help_url)
webbrowser.open(self.app_config.help_url)
print("Open Help...")

def init_ui(self):
self.setWindowTitle(self.app_config.app_name)
Expand Down Expand Up @@ -371,5 +388,66 @@ def init_ui(self):
# Set initial active tab style
self.update_active_tab_style("pipeline")

# Subtle status-bar entry point for the log viewer
self._init_log_status_entry()

def _init_log_status_entry(self) -> None:
"""Attach a low-visibility log viewer button floating in the bottom-right."""
central = self.centralWidget()
if central is None:
return

self._log_button = QToolButton(central)
self._log_button.setText("\u2630") # trigram glyph — subtle, monochrome
self._log_button.setToolTip("View application log")
self._log_button.setCursor(Qt.CursorShape.PointingHandCursor)
self._log_button.setStyleSheet(
"QToolButton {"
" background: transparent;"
" color: #9aa0a6;"
" border: none;"
" padding: 0 4px;"
" font-size: 12px;"
"}"
"QToolButton:hover { color: #5f6368; }"
)
self._log_button.clicked.connect(self._open_log_viewer)
self._log_button.adjustSize()
self._log_button.raise_()
self._log_viewer: Optional[LogViewerDialog] = None

central.installEventFilter(self)
self._reposition_log_button()

def _reposition_log_button(self) -> None:
central = self.centralWidget()
if central is None or not hasattr(self, "_log_button"):
return
btn = self._log_button
btn.adjustSize()
# Bottom-right, aligned to the existing 20px content margin.
x = central.width() - btn.width() - 4
y = central.height() - btn.height() - 4
btn.move(max(0, x), max(0, y))

def eventFilter(self, obj, event): # noqa: N802 (Qt API)
from PyQt6.QtCore import QEvent

if obj is self.centralWidget() and event.type() in (
QEvent.Type.Resize,
QEvent.Type.Show,
):
self._reposition_log_button()
return super().eventFilter(obj, event)

def _open_log_viewer(self) -> None:
logger.info("Opening log viewer")
if self._log_viewer is None or not self._log_viewer.isVisible():
self._log_viewer = LogViewerDialog(self)
self._log_viewer.show()
else:
self._log_viewer.raise_()
self._log_viewer.activateWindow()


__all__ = ["AlignmentGUI"]
2 changes: 2 additions & 0 deletions src/voxkit/gui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from .grip_splitter import GripSplitter
from .huggingface_button import HuggingFaceButton
from .loading_dialog import LoadingDialog
from .log_viewer_dialog import LogViewerDialog
from .model_selection_panel import ModelSelectionPanel
from .overlay_effects import OverlayWidget
from .toggle_switch import ToggleSwitch
Expand All @@ -40,6 +41,7 @@
"GripSplitter",
"HuggingFaceButton",
"LoadingDialog",
"LogViewerDialog",
"ModelSelectionPanel",
"MultiColumnComboBox",
"OverlayWidget",
Expand Down
Loading
Loading