@@ -41,11 +41,14 @@ def setup_environment():
4141 app_support_dir = home_dir / "Library" / "Application Support" / "Applio"
4242 logs_dir = home_dir / "Library" / "Logs" / "Applio"
4343
44- # Create directories
44+ # Create directories (user data root so models/data persist across builds/versions)
45+ (app_support_dir / "logs" ).mkdir (parents = True , exist_ok = True )
4546 for directory in [app_support_dir , logs_dir ]:
4647 directory .mkdir (parents = True , exist_ok = True )
4748 print (f"Created/verified directory: { directory } " )
4849
50+ os .environ ["APPLIO_APP_SUPPORT" ] = str (app_support_dir )
51+
4952 # IMPORTANT: Set environment variables for Apple MPS optimization
5053 # These settings optimize for Apple's unified memory architecture
5154 os .environ ["PYTORCH_ENABLE_MPS_FALLBACK" ] = "1" # Enable Metal Performance Shaders with CPU fallback
@@ -116,21 +119,46 @@ def cleanup():
116119 print ("Goodbye!" )
117120
118121def wait_for_server (url = 'http://127.0.0.1:6969' , timeout = 30 ):
119- """Wait for the server to be ready by polling the endpoint."""
120- print ("Waiting for Applio server to start..." )
122+ """Wait for the server to be ready by polling. Returns True when ready."""
121123 start_time = time .time ()
122124 while time .time () - start_time < timeout :
123125 try :
124126 response = urllib .request .urlopen (url , timeout = 1 )
125127 if response .getcode () == 200 :
126- print ("Applio server is ready!" )
127128 return True
128129 except (urllib .error .URLError , ConnectionError , OSError ):
129- # Server not ready yet, wait a bit
130130 time .sleep (0.5 )
131- print (f"Warning: Server did not respond within { timeout } seconds" )
132131 return False
133132
133+ # Shown immediately so user sees progress; we redirect to the real UI when server is up (no second instance).
134+ LOADING_HTML = '''<!DOCTYPE html>
135+ <html>
136+ <head>
137+ <meta charset="utf-8">
138+ <meta name="viewport" content="width=device-width, initial-scale=1">
139+ <title>Applio - Starting</title>
140+ <style>
141+ * { box-sizing: border-box; }
142+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #1a1a1a; color: #e5e5e5; margin: 0; padding: 2rem; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; }
143+ .box { max-width: 520px; text-align: center; }
144+ h1 { font-size: 1.5rem; margin-bottom: 1rem; }
145+ p { color: #a3a3a3; line-height: 1.6; margin: 0.5rem 0; }
146+ .spinner { width: 40px; height: 40px; border: 3px solid #333; border-top-color: #0ea5e9; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 1.5rem; }
147+ @keyframes spin { to { transform: rotate(360deg); } }
148+ .log-path { font-size: 0.85rem; word-break: break-all; color: #737373; margin-top: 1.5rem; }
149+ </style>
150+ </head>
151+ <body>
152+ <div class="box">
153+ <div class="spinner"></div>
154+ <h1>Applio is starting</h1>
155+ <p>Checking and downloading prerequisites if needed. This may take several minutes on first run.</p>
156+ <p>You will be switched to the main interface when ready.</p>
157+ <p class="log-path">Log: ~/Library/Logs/Applio/console.log</p>
158+ </div>
159+ </body>
160+ </html>'''
161+
134162def launch_gradio (app_dir , logs_dir ):
135163 """Launch the Gradio app."""
136164 global _gradio_thread
@@ -180,6 +208,10 @@ def flush(self):
180208 self .original_stream .flush ()
181209 except Exception :
182210 pass
211+
212+ def isatty (self ):
213+ """Required by uvicorn/logging when configuring formatters; we are not a TTY."""
214+ return False
183215
184216 # Redirect stdout and stderr to our tee
185217 sys .stdout = TeeOutput (log_file_path , sys .stdout )
@@ -191,63 +223,65 @@ def flush(self):
191223 print (f"All output will be captured to the Console tab in the UI" )
192224 print ("=" * 60 )
193225
194- # Run the Gradio app in a thread so pywebview can take control of main thread
195- def run_gradio ():
226+ # Single background thread: import app (runs prerequisites) then start Gradio server.
227+ # We never start a second Applio process.
228+ def run_app_and_server ():
196229 try :
197- # Import the app module which will launch Gradio
198230 import app
231+ app .launch_gradio ("127.0.0.1" , 6969 )
199232 except Exception as e :
200233 print (f"ERROR: Failed to start Applio: { e } " )
201234 import traceback
202235 traceback .print_exc ()
203236
204- _gradio_thread = threading .Thread (target = run_gradio , daemon = True )
237+ _gradio_thread = threading .Thread (target = run_app_and_server , daemon = True )
205238 _gradio_thread .start ()
206239
207- # Wait for server to be ready
208- wait_for_server ()
209-
210- # Launch pywebview window
211240 try :
212241 import webview
213- print ("Opening Applio window..." )
214-
215- # Create window and keep reference so we can treat close-as-quit
242+ print ("Opening Applio window (loading page first)..." )
243+ # Show loading page immediately; redirect to real UI when server is ready (no second instance).
216244 window = webview .create_window (
217245 'Applio' ,
218- 'http://127.0.0.1:6969' ,
246+ html = LOADING_HTML ,
219247 width = 1400 ,
220248 height = 900 ,
221249 resizable = True ,
222250 fullscreen = False ,
223251 min_size = (800 , 600 ),
224252 background_color = '#1a1a1a' ,
225253 text_select = True ,
226- on_top = False , # Keep in foreground but not always on top
227- focus = True # Get focus on creation
254+ on_top = False ,
255+ focus = True ,
228256 )
229257
230- # On macOS, closing the window (red X) does not quit the app by default;
231- # the Cocoa run loop keeps running and the app can "reopen". Treat window
232- # close as an explicit quit: cleanup and exit so we don't respawn.
258+ # Redirect this same window when server is up (long timeout for first-run download).
259+ def redirect_when_ready ():
260+ print ("Waiting for Applio server (prerequisites may be downloading)..." )
261+ if wait_for_server (timeout = 600 ):
262+ print ("Applio server is ready — switching to main interface." )
263+ try :
264+ window .load_url ("http://127.0.0.1:6969" )
265+ except Exception as e :
266+ print (f"Redirect failed: { e } " )
267+ else :
268+ print ("Server did not start in time. Check ~/Library/Logs/Applio/console.log" )
269+
270+ threading .Thread (target = redirect_when_ready , daemon = True ).start ()
271+
272+ # Quit fully on close so macOS does not reopen the app.
233273 if window is not None :
234274 def on_window_closed ():
235- print ("\n Window closed by user — quitting." )
275+ print ("\n Window closed — quitting." )
236276 cleanup ()
237277 os ._exit (0 )
238278 try :
239279 window .events .closed += on_window_closed
240280 except Exception :
241- pass # older pywebview may not have events.closed
281+ pass
242282
243- # Register cleanup handler for graceful shutdown (e.g. Dock Quit, SIGTERM)
244283 atexit .register (cleanup )
245-
246- # Start the webview - this blocks until the GUI run loop exits
247- # When window is closed, on_window_closed() runs and we os._exit(0)
248- webview .start (gui = 'cocoa' ) # Explicitly use Cocoa for macOS
249-
250- # If start() returns without on_window_closed firing (e.g. other backends)
284+ webview .start (gui = 'cocoa' )
251285 print ("\n Window closed by user" )
252286
253287 except ImportError :
0 commit comments