diff --git a/MANIFEST.in b/MANIFEST.in index aba83f6a2..f3c7fa884 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ recursive-include mlx_audio/tts *.tsx recursive-include mlx_audio/tts *.mjs recursive-include mlx_audio/tts *.css recursive-include mlx_audio/tts *.md +recursive-include mlx_audio/ui * # Exclude large directories prune mlx_audio/ui/node_modules diff --git a/README.md b/README.md index e711c57b2..5b386e500 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,24 @@ The best audio processing library built on Apple's MLX framework, providing fast - Quantization support (3-bit, 4-bit, 6-bit, 8-bit, and more) for optimized performance - Swift package for iOS/macOS integration +## Quickstart (macOS, Apple Silicon) + +If you want the web UI + API server in one command: + +```bash +# Install prerequisites (Homebrew must be /opt/homebrew on Apple Silicon) +brew install ffmpeg uv + +# Run everything (API server + web UI) +uvx --from "mlx-audio[app]" mlx_audio.dev +``` + +**Homebrew note:** on Apple Silicon, `brew --prefix` should return `/opt/homebrew` +(not the Intel `/usr/local`). If it doesn’t, fix Homebrew first or you may hit +missing dependencies. + +If you cloned the repo, you can also double‑click `mlx_audio_dev.command`. + ## Installation ### Using pip @@ -335,6 +353,12 @@ MLX-Audio includes a modern web interface and OpenAI-compatible API. ### Starting the Server ```bash +# One-command dev server + UI +mlx_audio.dev + +# Or run via uvx (installs server + STS deps) +uvx --from "mlx-audio[app]" mlx_audio.dev + # Start API server mlx_audio.server --host 0.0.0.0 --port 8000 diff --git a/mlx_audio/dev.py b/mlx_audio/dev.py new file mode 100644 index 000000000..7e43d05d4 --- /dev/null +++ b/mlx_audio/dev.py @@ -0,0 +1,160 @@ +"""Launch MLX-Audio API server and web UI dev server.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import signal +import subprocess +import sys +import time +from pathlib import Path +from typing import Iterable, Optional, Sequence + + +def _iter_ui_candidates(ui_dir: Optional[str]) -> Iterable[Path]: + if ui_dir: + yield Path(ui_dir) + + env_dir = os.getenv("MLX_AUDIO_UI_DIR") + if env_dir: + yield Path(env_dir) + + cwd = Path.cwd() + yield cwd / "mlx_audio" / "ui" + yield cwd / "ui" + + here = Path(__file__).resolve().parent + yield here / "ui" + yield here.parent / "mlx_audio" / "ui" + + +def _resolve_ui_dir(ui_dir: Optional[str]) -> Path: + seen = set() + for candidate in _iter_ui_candidates(ui_dir): + if not candidate: + continue + if candidate in seen: + continue + seen.add(candidate) + if (candidate / "package.json").exists(): + return candidate + + raise FileNotFoundError( + "Could not find the UI directory. Set MLX_AUDIO_UI_DIR or pass --ui-dir." + ) + + +def _require_npm() -> str: + npm = shutil.which("npm") + if npm is None: + raise RuntimeError("npm is required to run the web UI. Install Node.js first.") + return npm + + +def _spawn(cmd: Sequence[str], cwd: Optional[Path] = None) -> subprocess.Popen: + env = os.environ.copy() + env.setdefault("PYTHONUNBUFFERED", "1") + return subprocess.Popen( + cmd, + cwd=str(cwd) if cwd else None, + env=env, + start_new_session=True, + ) + + +def _terminate(proc: subprocess.Popen, name: str) -> None: + if proc.poll() is not None: + return + + try: + if os.name != "nt": + os.killpg(proc.pid, signal.SIGTERM) + else: + proc.terminate() + except ProcessLookupError: + return + + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + if os.name != "nt": + os.killpg(proc.pid, signal.SIGKILL) + else: + proc.kill() + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run MLX-Audio API server and web UI dev server together." + ) + parser.add_argument("--host", default="0.0.0.0", help="Server host") + parser.add_argument("--port", type=int, default=8000, help="Server port") + parser.add_argument( + "--ui-dir", + default=None, + help="Path to the UI directory (defaults to mlx_audio/ui)", + ) + parser.add_argument( + "--skip-npm-install", + action="store_true", + help="Skip running npm install", + ) + + args = parser.parse_args() + + ui_dir = _resolve_ui_dir(args.ui_dir) + npm = _require_npm() + + server_cmd = [ + sys.executable, + "-m", + "mlx_audio.server", + "--host", + args.host, + "--port", + str(args.port), + ] + + server_proc = _spawn(server_cmd) + + try: + if not args.skip_npm_install: + subprocess.run([npm, "install"], cwd=ui_dir, check=True) + ui_proc = _spawn([npm, "run", "dev"], cwd=ui_dir) + except Exception: + _terminate(server_proc, "server") + raise + + print( + f"Server running on http://{args.host}:{args.port} (API). " + "UI dev server starting on http://localhost:3000" + ) + print("Press Ctrl+C to stop.") + + exit_code = 0 + try: + while True: + server_ret = server_proc.poll() + ui_ret = ui_proc.poll() + if server_ret is not None: + print(f"Server exited with code {server_ret}.") + exit_code = server_ret or 1 + break + if ui_ret is not None: + print(f"UI dev server exited with code {ui_ret}.") + exit_code = ui_ret or 1 + break + time.sleep(0.5) + except KeyboardInterrupt: + exit_code = 130 + finally: + _terminate(ui_proc, "ui") + _terminate(server_proc, "server") + + return exit_code + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/mlx_audio_dev.command b/mlx_audio_dev.command new file mode 100755 index 000000000..ce5ce19cf --- /dev/null +++ b/mlx_audio_dev.command @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "This launcher is intended for macOS." + read -r -p "Press Enter to close..." _ + exit 1 +fi + +if ! command -v uvx >/dev/null 2>&1; then + echo "uvx is not installed. Install uv first: brew install uv" + read -r -p "Press Enter to close..." _ + exit 1 +fi + +if command -v brew >/dev/null 2>&1; then + brew_prefix=$(brew --prefix 2>/dev/null || true) + if [[ -n "$brew_prefix" && "$brew_prefix" != "/opt/homebrew" ]]; then + echo "Warning: Homebrew prefix is '$brew_prefix'." + echo "On Apple Silicon, use Homebrew at /opt/homebrew (not the Intel /usr/local)." + echo "This may cause missing deps if you are on Apple Silicon." + echo + fi +fi + +echo "Starting MLX-Audio API server and UI dev server..." +uvx --from "mlx-audio[app]" mlx_audio.dev + +echo +read -r -p "Press Enter to close..." _ diff --git a/pyproject.toml b/pyproject.toml index ea619da00..0f8a55d6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,28 @@ all = [ "webrtcvad>=2.0.10", ] +# App/dev server dependencies (API + STS + dev helpers) +app = [ + "tiktoken>=0.9.0", + "mistral-common[audio]", + + "misaki>=0.9.4", + "num2words>=0.5.14", + "spacy>=3.8.4", + "phonemizer-fork>=3.3.2", + "espeakng-loader>=0.2.4", + "sentencepiece>=0.2.0", + + "fastapi>=0.95.0", + "uvicorn>=0.22.0", + "python-multipart>=0.0.22", + + "webrtcvad>=2.0.10", + + "pytest>=7.0.0", + "pytest-asyncio>=1.0.0", +] + # Development dependencies dev = [ "pytest>=7.0.0", @@ -103,6 +125,7 @@ dev = [ "mlx_audio.stt.generate" = "mlx_audio.stt.generate:main" "mlx_audio.tts.generate" = "mlx_audio.tts.generate:main" "mlx_audio.server" = "mlx_audio.server:main" +"mlx_audio.dev" = "mlx_audio.dev:main" [project.urls] Homepage = "https://github.com/Blaizzy/mlx-audio" @@ -128,6 +151,7 @@ mlx_audio = [ "tts/*.css", "tts/**/*.json", "tts/static/**/*", + "ui/**/*", ] [tool.black]