99import asyncio
1010import io
1111import os
12+ import subprocess
1213import time
14+ import webbrowser
15+ from pathlib import Path
1316from typing import Any , Dict , List , Optional
1417from urllib .parse import unquote
1518
@@ -125,6 +128,13 @@ class SpeechRequest(BaseModel):
125128model_provider = ModelProvider ()
126129
127130
131+ @app .get ("/" )
132+ async def root ():
133+ return {
134+ "message" : "Welcome to the MLX Audio API server! Visit https://localhost:3000 for the UI."
135+ }
136+
137+
128138@app .get ("/v1/models" )
129139async def list_models ():
130140 """
@@ -235,63 +245,62 @@ async def stt_transcriptions(
235245 return result
236246
237247
238- def main ():
239- parser = argparse .ArgumentParser (description = "MLX Audio API server" )
240- parser .add_argument (
241- "--allowed-origins" ,
242- nargs = "+" ,
243- default = ["*" ],
244- help = "List of allowed origins for CORS" ,
245- )
246- parser .add_argument (
247- "--host" , type = str , default = "0.0.0.0" , help = "Host to run the server on"
248- )
249- parser .add_argument (
250- "--port" , type = int , default = 8000 , help = "Port to run the server on"
251- )
252- parser .add_argument (
253- "--reload" ,
254- type = bool ,
255- default = False ,
256- help = "Enable auto-reload of the server. Only works when 'workers' is set to None." ,
257- )
258-
259- parser .add_argument (
260- "--workers" ,
261- type = int_or_float ,
262- default = calculate_default_workers (),
263- help = """Number of workers. Overrides the `MLX_AUDIO_NUM_WORKERS` env variable.
264- Can be either an int or a float.
265- If an int, it will be the number of workers to use.
266- If a float, number of workers will be this fraction of the number of CPU cores available, with a minimum of 1.
267- Defaults to the `MLX_AUDIO_NUM_WORKERS` env variable if set and to 2 if not.
268- To use all available CPU cores, set it to 1.0.
269-
270- Examples:
271- --workers 1 (will use 1 worker)
272- --workers 1.0 (will use all available CPU cores)
273- --workers 0.5 (will use half the number of CPU cores available)
274- --workers 0.0 (will use 1 worker)""" ,
275- )
248+ class MLXAudioStudioServer :
249+ def __init__ (self , start_ui = False , log_dir = "logs" ):
250+ self .start_ui = start_ui
251+ self .ui_process = None
252+ self .log_dir = Path (log_dir )
253+ self .log_dir .mkdir (exist_ok = True )
276254
277- args = parser . parse_args ()
278- if isinstance ( args . workers , float ):
279- args . workers = max ( 1 , int ( os . cpu_count () * args . workers ))
255+ def start_ui_background ( self ):
256+ """Start UI with logs redirected to file"""
257+ ui_path = Path ( __file__ ). parent / "ui"
280258
281- setup_cors (app , args .allowed_origins )
259+ # Install deps silently
260+ subprocess .run (
261+ ["npm" , "install" ],
262+ cwd = str (ui_path ),
263+ stdout = subprocess .DEVNULL ,
264+ stderr = subprocess .DEVNULL ,
265+ )
282266
283- uvicorn .run (
284- "mlx_audio.server:app" ,
285- host = args .host ,
286- port = args .port ,
287- reload = args .reload ,
288- workers = args .workers ,
289- loop = "asyncio" ,
290- )
267+ # Start UI with logs to file
268+ ui_log = open (self .log_dir / "ui.log" , "w" )
269+ self .ui_process = subprocess .Popen (
270+ ["npm" , "run" , "dev" ],
271+ cwd = str (ui_path ),
272+ stdout = ui_log ,
273+ stderr = subprocess .STDOUT ,
274+ )
275+ print (f"✓ UI started (logs: { self .log_dir } /ui.log)" )
291276
277+ def start_server (self , host = "localhost" , port = 8000 , reload = False , workers = 2 ):
278+ if self .start_ui :
279+ self .start_ui_background ()
280+ time .sleep (2 )
281+ webbrowser .open ("http://localhost:3000" )
282+ print (f"✓ API server starting on http://{ host } :{ port } " )
283+ print (f"✓ Studio UI available at http://localhost:3000" )
284+ print ("\n Press Ctrl+C to stop both servers" )
292285
293- if __name__ == "__main__" :
294- main ()
286+ try :
287+ uvicorn .run (
288+ "mlx_audio.server:app" ,
289+ host = host ,
290+ port = port ,
291+ reload = reload ,
292+ workers = workers ,
293+ loop = "asyncio" ,
294+ )
295+ finally :
296+ if self .ui_process :
297+ self .ui_process .terminate ()
298+ print ("✓ UI server stopped" )
299+
300+ ui_log_path = self .log_dir / "ui.log"
301+ if ui_log_path .exists ():
302+ ui_log_path .unlink ()
303+ print (f"✓ UI logs deleted from { ui_log_path } " )
295304
296305
297306def main ():
@@ -303,7 +312,7 @@ def main():
303312 help = "List of allowed origins for CORS" ,
304313 )
305314 parser .add_argument (
306- "--host" , type = str , default = "0.0.0.0 " , help = "Host to run the server on"
315+ "--host" , type = str , default = "localhost " , help = "Host to run the server on"
307316 )
308317 parser .add_argument (
309318 "--port" , type = int , default = 8000 , help = "Port to run the server on"
@@ -332,20 +341,30 @@ def main():
332341 --workers 0.5 (will use half the number of CPU cores available)
333342 --workers 0.0 (will use 1 worker)""" ,
334343 )
344+ parser .add_argument (
345+ "--start-ui" ,
346+ action = "store_true" ,
347+ help = "Start the Studio UI alongside the API server" ,
348+ )
349+ parser .add_argument (
350+ "--log-dir" ,
351+ type = str ,
352+ default = "logs" ,
353+ help = "Directory to save server logs" ,
354+ )
335355
336356 args = parser .parse_args ()
337357 if isinstance (args .workers , float ):
338358 args .workers = max (1 , int (os .cpu_count () * args .workers ))
339359
340360 setup_cors (app , args .allowed_origins )
341361
342- uvicorn . run (
343- "mlx_audio.server:app" ,
362+ client = MLXAudioStudioServer ( start_ui = args . start_ui , log_dir = args . log_dir )
363+ client . start_server (
344364 host = args .host ,
345365 port = args .port ,
346- reload = args .reload ,
366+ reload = args .reload if args . workers is None else False ,
347367 workers = args .workers ,
348- loop = "asyncio" ,
349368 )
350369
351370
0 commit comments