diff --git a/assets/vk.ico b/assets/vk.ico new file mode 100644 index 0000000..6925d0b Binary files /dev/null and b/assets/vk.ico differ diff --git a/assets/vk.png b/assets/vk.png new file mode 100644 index 0000000..a4c6580 Binary files /dev/null and b/assets/vk.png differ diff --git a/main.py b/main.py index bcfc042..757b811 100644 --- a/main.py +++ b/main.py @@ -110,8 +110,10 @@ log.info("[FROZEN] Environment configured for frozen app") +from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QApplication from voxkit.config import STARTUP_SCRIPT +from voxkit.config.app_config import get_app_icon_path from voxkit.gui import VoxKitGUI from voxkit.gui.workers.startup import execute_mfa_provisioning, execute_startup_script @@ -135,6 +137,7 @@ def main(): app = QApplication(sys.argv) app.setStyle("Fusion") + app.setWindowIcon(QIcon(str(get_app_icon_path()))) # Execute startup script on first launch (before GUI initialization) log.info("Running startup script") diff --git a/scripts/build.py b/scripts/build.py index 70811ed..364467f 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -106,11 +106,11 @@ def build(args): # This is important because PyInstaller resolves paths relative to CWD project_root = Path(__file__).parent.parent original_cwd = Path.cwd() - + if original_cwd != project_root: print(f"[INFO] Changing working directory to project root: {project_root}") os.chdir(project_root) - + opts = [] # Basic options @@ -184,6 +184,14 @@ def build(args): print("[INFO] Adding vendor folder to build assets") opts.append(f'--add-data={vendor_dir}{sep}vendor') + # Add assets folder if it exists (branding images used for the in-app + # window icon and first-launch splash logo, resolved at runtime via + # voxkit.config.app_config.get_assets_root()) + assets_dir = project_root / "assets" + if assets_dir.exists() and assets_dir.is_dir(): + print("[INFO] Adding assets folder to build assets") + opts.append(f'--add-data={assets_dir}{sep}assets') + for ad in args.add_data: if sep in ad: opts.append(f'--add-data={ad}') @@ -217,7 +225,7 @@ def build(args): print(f" Run with: ./dist/{args.name}/{args.name}") else: print(f"\n⚠️ Expected build output not found at {app_path}") - + # Restore original working directory if original_cwd != project_root: os.chdir(original_cwd) diff --git a/src/voxkit/config/app_config.py b/src/voxkit/config/app_config.py index c6376a9..1455e3b 100644 --- a/src/voxkit/config/app_config.py +++ b/src/voxkit/config/app_config.py @@ -36,6 +36,36 @@ def get_config_root() -> Path: return Path(__file__).parent.parent.parent.parent / "config" +def get_assets_root() -> Path: + """Get the path to the assets root directory. + + Returns the correct assets path whether running from source or as a + PyInstaller bundle. + + Returns: + Path to the assets directory + """ + if getattr(sys, "_MEIPASS", None): + return Path(getattr(sys, "_MEIPASS")) / "assets" + else: + return Path(__file__).parent.parent.parent.parent / "assets" + + +def get_app_icon_path() -> Path: + """Get the best available runtime icon file for QIcon/QPixmap use. + + Prefers the Windows .ico (multi-resolution, sharper in the taskbar/title + bar) when on Windows, falling back to the PNG everywhere else. + + Returns: + Path to the icon file + """ + root = get_assets_root() + if sys.platform == "win32" and (root / "vk.ico").exists(): + return root / "vk.ico" + return root / "vk.png" + + def get_active_profile() -> str: """Get the active configuration profile name. diff --git a/src/voxkit/engines/w2tg_engine.py b/src/voxkit/engines/w2tg_engine.py index 8a5b4d2..e7e0efa 100644 --- a/src/voxkit/engines/w2tg_engine.py +++ b/src/voxkit/engines/w2tg_engine.py @@ -85,7 +85,7 @@ name="use_speaker_adaptation", label="Use Speaker Adaptation", field_type=FieldType.CHECKBOX, - default_value=False, + default_value=True, tooltip="Enable speaker adaptation for better alignment results.", ), FieldConfig( diff --git a/src/voxkit/gui/__init__.py b/src/voxkit/gui/__init__.py index 0878851..1758972 100644 --- a/src/voxkit/gui/__init__.py +++ b/src/voxkit/gui/__init__.py @@ -411,7 +411,10 @@ def open_feedback(self): webbrowser.open(mailto_url) def init_ui(self): + from voxkit.config.app_config import get_app_icon_path + self.setWindowTitle(self.app_config.app_name) + self.setWindowIcon(QIcon(str(get_app_icon_path()))) self.setMinimumSize(1200, 800) # Set application-wide stylesheet diff --git a/src/voxkit/gui/components/loading_dialog.py b/src/voxkit/gui/components/loading_dialog.py index 96f2a7c..ed4dc4c 100644 --- a/src/voxkit/gui/components/loading_dialog.py +++ b/src/voxkit/gui/components/loading_dialog.py @@ -14,7 +14,7 @@ QWidget, ) -from voxkit.gui.styles import Colors +from voxkit.gui.styles import Colors, Labels class _WaveformStrip(QWidget): @@ -193,6 +193,16 @@ def init_ui(self, message: str, title: str, subtitle: str | None): self.subtitle_label.setVisible(bool(subtitle)) layout.addWidget(self.subtitle_label) + # Fine-print institutional credit + credit_label = QLabel( + "VoxKit is brought to you by the University of Wisconsin-Madison " + "and Arizona State University" + ) + credit_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + credit_label.setWordWrap(True) + credit_label.setStyleSheet(Labels.CREDIT) + layout.addWidget(credit_label) + # Card background: subtle vertical gradient (light tints of the app's # primary blue) with a primary-blue rounded border. Painted on the child # frame (not the translucent window) so it renders reliably. @@ -210,7 +220,7 @@ def init_ui(self, message: str, title: str, subtitle: str | None): """ ) - self.setFixedSize(420, 260) + self.setFixedSize(420, 290) def center_on_screen(self): """Center the dialog on the primary screen.""" diff --git a/src/voxkit/gui/pages/pipeline/viewer_stacker.py b/src/voxkit/gui/pages/pipeline/viewer_stacker.py index dbff89a..9edf9e3 100644 --- a/src/voxkit/gui/pages/pipeline/viewer_stacker.py +++ b/src/voxkit/gui/pages/pipeline/viewer_stacker.py @@ -410,7 +410,7 @@ def __init__(self, parent: QWidget | None = None) -> None: self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.setFixedHeight(self.RULER_HEIGHT) - _TIER_ORDER = {"phones": 0, "words": 1} + _TIER_ORDER = {"words": 0, "phones": 1} def set_data(self, tiers: list[dict], duration: float) -> None: self._tiers = sorted( @@ -1839,7 +1839,11 @@ def _on_position_changed(self, position_ms: int): def _update_active_segment_label(self, secs: float) -> None: if self._active_label and self._active_label.isVisible() and self._loaded_tiers: parts = [] - for tier in self._loaded_tiers: + ordered_tiers = sorted( + self._loaded_tiers, + key=lambda t: TextGridTimeline._TIER_ORDER.get(t["name"].lower(), 2), + ) + for tier in ordered_tiers: if tier["class"] == "IntervalTier": for iv in tier["intervals"]: if iv["start"] <= secs < iv["end"] and iv["label"] not in _SILENCE_LABELS: