# SonicForge Runtime Profile — phát hiện môi trường chạy (desktop Windows / # docker headless) và khả năng của máy, để app TỰ CHỌN cách xử lý: # - Windows desktop (server + client cùng 1 máy): native_bridge render VSTi # - Docker / Linux headless (server + browser UI): soundfont + VSTi mở được # từ storage mount; preset upload qua browser; không có GUI local. # # Ưu tiên: SF_RUNTIME env override > heuristic (platform + display + docker). # Module CHỈ dùng stdlib + app.config (không kéo numpy) để import # nhẹ và hoạt động trong mọi tiến trình (web/worker/celery). import os import sys import socket import functools from app.config import settings SF_RUNTIME_ENV = "SF_RUNTIME" # auto | desktop | headless SF_DOCKER_ENV = "SF_DOCKER" # 1 = chạy trong container Docker def _is_windows() -> bool: return os.name == "nt" def _is_macos() -> bool: return sys.platform == "darwin" def _is_linux() -> bool: return not _is_windows() and not _is_macos() def _in_docker() -> bool: if os.environ.get(SF_DOCKER_ENV) == "1": return True # Marker chuẩn của Docker (không tồn tại trên máy host thường) try: return os.path.exists("/.dockerenv") except Exception: return False def _has_display() -> bool: """Có môi trường GUI hiển thị được hay không. Windows/macOS luôn có (desktop). Linux cần biến DISPLAY (X11) — nếu chạy Docker không có DISPLAY → headless.""" if _is_windows() or _is_macos(): return True return bool(os.environ.get("DISPLAY")) def _tauri_bridge_ready() -> bool: """App desktop Tauri viết marker file lúc setup (xem plugins.py _pick_dir_via_tauri_bridge) — dùng để nhận diện bản desktop app.""" try: root = os.environ.get("APPDATA") or os.path.expanduser("~") return os.path.exists(os.path.join(root, "SonicForgeDAW", "ipc", "tauri_bridge_ready")) except Exception: return False def _detect_platform() -> str: if _is_windows(): return "windows" if _is_macos(): return "darwin" return "linux" def _detect_runtime() -> str: """auto → desktop nếu có GUI (Windows/macOS luôn; Linux cần DISPLAY và không phải docker), ngược lại headless. SF_RUNTIME override thắng.""" override = (os.environ.get(SF_RUNTIME_ENV) or "auto").strip().lower() if override in ("desktop", "headless"): return override if _is_windows() or _is_macos(): return "desktop" if _in_docker() or not _has_display(): return "headless" return "desktop" def default_vst_dirs() -> list: """Thư mục VST mặc định theo platform (vẫn ưu tiên env VST_DIR). Windows: thư mục chuẩn VST3 của hệ thống; user thường khai báo thêm qua Plugins Manager (plugin_dirs.json).""" if settings.VST_DIR and settings.VST_DIR != "/opt/daw_engine/vst3": return [settings.VST_DIR] if _is_windows(): pf = os.environ.get("ProgramFiles", r"C:\Program Files") return [os.path.join(pf, "Common Files", "VST3"), os.path.join(pf, "VSTPlugins")] if _is_macos(): return ["/Library/Audio/Plug-Ins/VST3", os.path.expanduser("~/Library/Audio/Plug-Ins/VST3")] return ["/opt/daw_engine/vst3", os.path.expanduser("~/.vst3")] def default_soundfont_dirs() -> list: if settings.SOUNDFONT_DIR and settings.SOUNDFONT_DIR != "/opt/daw_engine/soundfonts": return [settings.SOUNDFONT_DIR] if _is_windows(): root = os.environ.get("APPDATA") or os.path.expanduser("~") return [os.path.join(root, "SonicForgeDAW", "soundfonts")] if _is_macos(): return [os.path.expanduser("~/Music/SonicForgeDAW/soundfonts")] return ["/opt/daw_engine/soundfonts", os.path.expanduser("~/.sf2")] def _lan_ips() -> list: """IPv4 của máy trên mạng LAN (bỏ loopback) — để client browser ở máy khác mở app qua địa chỉ này. Stdlib only (không kéo thư viện ngoài).""" ips = [] try: # IP ra mạng theo default route — không gửi gói tin thật with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] if ip and not ip.startswith("127.") and ip not in ips: ips.append(ip) except Exception: pass # Enumerate thêm các interface khác (multi-NIC) try: for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET): ip = info[4][0] if ip and not ip.startswith("127.") and ip not in ips: ips.append(ip) except Exception: pass return ips @functools.lru_cache(maxsize=1) def detect() -> dict: """Detect 1 lần (cache toàn cục) — kết quả bất biến trong 1 tiến trình.""" platform = _detect_platform() runtime = _detect_runtime() docker = _in_docker() return { "platform": platform, "runtime": runtime, # "desktop" | "headless" "docker": docker, "environment": "docker" if docker else "standalone", # luồng âm instrument: docker→FluidSynthWASM, standalone→native OS "has_display": _has_display(), "tauri_bridge": _tauri_bridge_ready(), "vst_render": _vst_render_available(), "default_vst_dirs": default_vst_dirs(), "default_soundfont_dirs": default_soundfont_dirs(), "preset_dir": os.path.join(settings.STORAGE_DIR, "presets"), } def _vst_render_available() -> bool: """native_bridge có sẵn không: SF_BRIDGE_PATH trỏ file, hoặc exe trong install/ (không import app.core.native_render để tránh kéo numpy — mirror logic find_bridge_exe).""" env = os.getenv("SF_BRIDGE_PATH", "").strip() if env and os.path.isfile(env): return True exe = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge" if os.path.isfile(os.path.join(settings.BASE_DIR, "install", exe)): return True # Frozen onedir: bridge sidecar nam canh app root (ngoai _MEIPASS) if getattr(sys, "frozen", False): exe_dir = os.path.dirname(os.path.abspath(sys.executable)) for cand in (os.path.join(exe_dir, exe), os.path.join(os.path.dirname(exe_dir), exe)): if os.path.isfile(cand): return True return False def _lan_info() -> dict: """Địa chỉ + port để client LAN mở app. Port: SF_PORT (desktop engine chọn trước, 8000-8010) hoặc 8000 mặc định.""" try: port = int(os.environ.get("SF_PORT") or "8000") except ValueError: port = 8000 ips = _lan_ips() return { "ips": ips, "port": port, "urls": [f"http://{ip}:{port}" for ip in ips], } def capabilities() -> dict: """Capabilities API — frontend gọi 1 lần lúc boot để bật/tắt tính năng.""" d = detect() features = { "preset_upload": True, # cả 2 mode đều upload preset được "vst_render": d["vst_render"], "tauri_bridge": d["tauri_bridge"], # preview VSTi: quick_render (native_bridge render clip ngắn — âm thật, # giống export) là chuẩn cho cả 2 mode; không có realtime trong web UI. "preview_mode": "quick_render" if d["vst_render"] else "wasm", } return { "success": True, "runtime": d["runtime"], "platform": d["platform"], "docker": d["docker"], "environment": d["environment"], # LAN access (chỉ standalone): URL để client browser ở máy khác trên # mạng mở app. Docker (container) → bỏ qua (IP container không dùng được). **({"lan": _lan_info()} if not d["docker"] else {}), "features": features, "default_dirs": { "vst": d["default_vst_dirs"], "soundfont": d["default_soundfont_dirs"], "preset": d["preset_dir"], }, } def is_desktop() -> bool: return detect()["runtime"] == "desktop" def is_headless() -> bool: return detect()["runtime"] == "headless" if __name__ == "__main__": # python -m app.core.runtime → in capabilities để kiểm tra detect import json print(json.dumps(capabilities(), ensure_ascii=False, indent=2))