-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
162 lines (134 loc) · 6.29 KB
/
main.py
File metadata and controls
162 lines (134 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import sys
import multiprocessing
import os
# CRITICAL: Must run before any heavy imports (voxkit, torch, PyQt6, etc.).
# When PyTorch DataLoader workers spawn on Windows, each child re-launches the
# frozen exe and re-imports this module. freeze_support() short-circuits the
# child before it re-runs main() and tries to open another GUI window.
if __name__ == "__main__":
multiprocessing.freeze_support()
# In a --windowed PyInstaller build there is no console, so sys.stdout and
# sys.stderr are None. Libraries like tqdm (used by transformers' Trainer)
# crash with AttributeError: 'NoneType' object has no attribute 'write' when
# they try to print progress. Redirect to devnull so writes silently no-op.
if sys.stdout is None:
sys.stdout = open(os.devnull, "w", encoding="utf-8")
if sys.stderr is None:
sys.stderr = open(os.devnull, "w", encoding="utf-8")
import faulthandler
import logging
# Windows: configure console and stdout/stderr for UTF-8 before any output.
# Without this, rich's legacy renderer falls back to cp1252 and chokes on
# Unicode characters (e.g., circled letters in pipeline config YAML).
if sys.platform == 'win32':
try:
import ctypes
ctypes.windll.kernel32.SetConsoleOutputCP(65001)
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
# Apply patches for frozen (PyInstaller) environment BEFORE other imports
if getattr(sys, 'frozen', False):
import _frozen_patch
from voxkit.config.pipeline_config import PipelineConfig
from voxkit.config.app_config import AppConfig, get_app_config, get_profile_config_path
from voxkit.config.logging_config import setup_logging
# Minimal early config so frozen-env messages below are emitted before
# setup_logging() runs in main(); setup_logging() will reconfigure handlers.
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger("voxkit.main")
# Enable detailed crash reports
faulthandler.enable()
# Apply environment patches for frozen (PyInstaller) environment
if getattr(sys, 'frozen', False):
# Define the minimal required environment
minimal_env = {
'HOME': os.environ.get('HOME') or os.path.expanduser('~'),
'USER': os.environ.get('USER') or os.getlogin(),
'TMPDIR': os.environ.get('TMPDIR') or '/tmp',
'QT_ENABLE_EMOJI': '0'
}
# Add conda to PATH if available (required for MFA alignment)
# Check common conda installation locations
home = os.path.expanduser('~')
conda_locations = [
os.path.join(home, 'miniforge3', 'bin'),
os.path.join(home, 'mambaforge', 'bin'),
os.path.join(home, 'anaconda3', 'bin'),
os.path.join(home, 'miniconda3', 'bin'),
os.path.join(home, 'opt', 'anaconda3', 'bin'),
os.path.join(home, 'opt', 'miniconda3', 'bin'),
]
# Find first available conda installation
conda_bin = None
for location in conda_locations:
if os.path.exists(os.path.join(location, 'conda')):
conda_bin = location
break
# Build PATH with conda if found
if conda_bin:
existing_path = os.environ.get('PATH', '/usr/bin:/bin:/usr/sbin:/sbin')
minimal_env['PATH'] = f"{conda_bin}:{existing_path}"
log.info("[FROZEN] Added conda to PATH: %s", conda_bin)
else:
minimal_env['PATH'] = os.environ.get('PATH', '/usr/bin:/bin:/usr/sbin:/sbin')
log.warning("[FROZEN] conda not found in standard locations. MFA alignment may fail.")
# PyInstaller-specific: Add Qt plugin paths
if getattr(sys, '_MEIPASS', None):
bundle_dir = sys._MEIPASS
qt_plugins = os.path.join(bundle_dir, 'PyQt6', 'Qt6', 'plugins')
if os.path.exists(qt_plugins):
minimal_env['QT_PLUGIN_PATH'] = qt_plugins
minimal_env['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(qt_plugins, 'platforms')
# Additional Qt environment for frozen apps
minimal_env['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
minimal_env['QT_LOGGING_RULES'] = '*.debug=false;qt.qpa.*=false'
log.info("[FROZEN] Qt plugins directory: %s", qt_plugins)
log.info("[FROZEN] Bundle directory: %s", bundle_dir)
# IMPORTANT: Don't clear os.environ - preserve system environment
# Just add/override our minimal required variables
for key, value in minimal_env.items():
if value:
os.environ[key] = value
log.info("[FROZEN] Environment configured for frozen app")
from PyQt6.QtWidgets import QApplication
from voxkit.config import STARTUP_SCRIPT
from voxkit.gui import VoxKitGUI
from voxkit.gui.workers.startup import execute_startup_script
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.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
pipeline_config = None
# Handle special '_MEIPASS' argument for frozen builds
# Uses profile system - reads from config/profile.txt to determine active profile
if getattr(sys, '_MEIPASS', None):
profile_path = get_profile_config_path()
app_config = AppConfig.from_yaml(profile_path / "app_info.yaml")
pipeline_config = PipelineConfig.from_yaml(profile_path / "pipeline_definitions.yaml")
window = VoxKitGUI(pipeline_config=pipeline_config, app_config=app_config)
window.show()
log.info("Main window shown, entering Qt event loop")
sys.exit(app.exec())
if __name__ == "__main__":
# Prevent multiprocessing from spawning new app windows in frozen builds
multiprocessing.set_start_method('spawn', force=True)
main()