Download plus/launch_studio.py from Nokia123543/muscriptor-large-fork: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/Nokia123543/muscriptor-large-fork/resolve/main/plus/launch_studio.py
- Command line
-
hf download hf://Nokia123543/muscriptor-large-fork/plus/launch_studio.py
-
curl -L -o launch_studio.py https://huggingface.co/Nokia123543/muscriptor-large-fork/resolve/main/plus/launch_studio.py
12 kB
| """Пускач PLUS Studio: сервер + публичная ссылка. | |
| Одна строка в Colab или Kaggle: | |
| !python3 plus/launch_studio.py | |
| Что делает: | |
| 1. ставит зависимости (fastapi, uvicorn, python-multipart, soundfile, pretty_midi, ...); | |
| 2. если рядом нет plus_app.py — скачивает plus.zip с Hugging Face и распаковывает; | |
| 3. поднимает plus_app.py на локальном порту и ждёт /health; | |
| 4. перебирает туннели по очереди (localhost.run, serveo, pinggy) и берёт первый, | |
| который выдал адрес и реально ответил; | |
| 5. печатает ссылку, кладёт её в plus_links.txt и держит всё живым, | |
| перезапуская сервер или туннель, если они умерли. | |
| Флаги: | |
| --port 8900 локальный порт | |
| --device auto auto | cuda | cpu | |
| --weights PATH свои веса; иначе модель скачается с HF при первом запросе | |
| --no-tunnel только сервер, без ссылки (для локального запуска) | |
| --tunnel NAME начать с конкретного туннеля: localhostrun | serveo | pinggy | |
| --keep 0 сколько секунд держать (0 = бесконечно) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import collections | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import threading | |
| import time | |
| import urllib.request | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| REPO = "Nokia123543/muscriptor-large-fork" | |
| ZIP_URL = "https://huggingface.co/%s/resolve/main/plus.zip" % REPO | |
| PACKAGES = ("fastapi", "uvicorn", "python-multipart", "soundfile", "pretty_midi", | |
| "librosa", "huggingface_hub", "numpy", "scipy") | |
| LINKS_FILE = "plus_links.txt" | |
| def step(index: int, total: int, text: str) -> None: | |
| print("[%d/%d] %s" % (index, total, text), flush=True) | |
| def ensure_deps() -> None: | |
| missing = [] | |
| for name in PACKAGES: | |
| import importlib.util | |
| if importlib.util.find_spec(name.replace("-", "_")) is None: | |
| missing.append(name) | |
| if not missing: | |
| return | |
| print("ставлю: " + ", ".join(missing), flush=True) | |
| subprocess.run([sys.executable, "-m", "pip", "install", "-q", *missing], check=False) | |
| def ensure_pack() -> str: | |
| target = os.path.join(HERE, "plus_app.py") | |
| if os.path.isfile(target): | |
| return HERE | |
| print("скачиваю пакет с Hugging Face", flush=True) | |
| import zipfile | |
| archive = os.path.join(HERE, "plus.zip") | |
| urllib.request.urlretrieve(ZIP_URL, archive) | |
| with zipfile.ZipFile(archive) as bundle: | |
| bundle.extractall(HERE) | |
| for candidate in (os.path.join(HERE, "plus"), HERE): | |
| if os.path.isfile(os.path.join(candidate, "plus_app.py")): | |
| return candidate | |
| raise SystemExit("в архиве нет plus_app.py") | |
| def healthy(port: int) -> bool: | |
| try: | |
| with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=3) as answer: | |
| return answer.status == 200 | |
| except Exception: | |
| return False | |
| def start_server(pack: str, port: int, device: str, weights: str | None) -> subprocess.Popen: | |
| command = [sys.executable, os.path.join(pack, "plus_app.py"), | |
| "--port", str(port), "--device", device, "--host", "0.0.0.0"] | |
| if weights: | |
| command += ["--weights", weights] | |
| log = open("plus_studio.log", "a") | |
| log.write("\n=== %s ===\n" % time.strftime("%Y-%m-%d %H:%M:%S")) | |
| process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT, | |
| stdin=subprocess.DEVNULL) | |
| for _ in range(90): | |
| if healthy(port): | |
| return process | |
| if process.poll() is not None: | |
| raise SystemExit("сервер умер при старте, смотри plus_studio.log") | |
| time.sleep(2) | |
| raise SystemExit("сервер не ответил за 3 минуты, смотри plus_studio.log") | |
| class Tunnel: | |
| """Туннель по ssh -R или по http-клиенту; нужен только адрес в stdout.""" | |
| def __init__(self, name: str, argv: list[str], pattern: str): | |
| self.name, self.pattern, self.tail = name, re.compile(pattern), collections.deque(maxlen=60) | |
| self.url: str | None = None | |
| self.proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | |
| stdin=subprocess.DEVNULL, text=True, bufsize=1) | |
| threading.Thread(target=self._drain, daemon=True).start() | |
| def _drain(self) -> None: | |
| for line in self.proc.stdout: | |
| self.tail.append(line.rstrip()) | |
| if self.url is None: | |
| found = self.pattern.search(line) | |
| if found: | |
| self.url = found.group(0) | |
| def alive(self) -> bool: | |
| return self.proc.poll() is None | |
| def kill(self) -> None: | |
| try: | |
| self.proc.kill() | |
| except Exception: | |
| pass | |
| SSH_OPTS = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", | |
| "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3", | |
| "-o", "TCPKeepAlive=yes", "-o", "ExitOnForwardFailure=yes"] | |
| def tunnel_specs(port: int) -> list[tuple[str, list[str], str]]: | |
| target = "127.0.0.1:%d" % port | |
| specs = [("localhostrun", | |
| ["ssh", "-T", *SSH_OPTS, "-R", "80:" + target, "nokey@localhost.run"], | |
| r"https://[a-z0-9]+\.lhr\.life"), | |
| ("serveo", | |
| ["ssh", "-T", *SSH_OPTS, "-R", "80:" + target, "serveo.net"], | |
| r"https://[a-z0-9-]+(?:\.[a-z0-9-]+)*\.(?:serveousercontent|serveo)\.[a-z]+")] | |
| if shutil.which("npx"): | |
| specs.append(("localtunnel", | |
| ["npx", "-y", "localtunnel", "--port", str(port)], | |
| r"https://[a-z0-9-]+\.loca\.lt")) | |
| return specs | |
| def reachable(url: str, attempts: int = 5) -> bool: | |
| for _ in range(attempts): | |
| try: | |
| with urllib.request.urlopen(url + "/health", timeout=8) as answer: | |
| if answer.status == 200: | |
| return True | |
| except Exception: | |
| pass | |
| time.sleep(3) | |
| return False | |
| def open_tunnel(port: int, order: list[str]) -> Tunnel: | |
| specs = {name: (argv, pattern) for name, argv, pattern in tunnel_specs(port)} | |
| tried = [] | |
| for name in order: | |
| if name not in specs: | |
| continue | |
| argv, pattern = specs[name] | |
| if not shutil.which(argv[0]): | |
| print(" [%s] нет команды %s, пропускаю" % (name, argv[0]), flush=True) | |
| continue | |
| print(" [%s] открываю" % name, flush=True) | |
| tunnel = Tunnel(name, argv, pattern) | |
| deadline = time.time() + 60 | |
| started = time.time() | |
| while time.time() < deadline and tunnel.url is None and tunnel.alive(): | |
| time.sleep(1) | |
| waited = int(time.time() - started) | |
| if waited and waited % 10 == 0: | |
| print(" [%s] ждём адрес, %d с, жив: %s" | |
| % (name, waited, tunnel.alive()), flush=True) | |
| if tunnel.url and reachable(tunnel.url): | |
| print(" [%s] работает: %s" % (name, tunnel.url), flush=True) | |
| return tunnel | |
| tried.append((name, tunnel.url, list(tunnel.tail)[-3:])) | |
| tunnel.kill() | |
| for name, url, tail in tried: | |
| print(" [%s] не вышло: %s | %s" % (name, url or "нет адреса", " / ".join(tail)), flush=True) | |
| raise SystemExit("ни один туннель не поднялся: ssh или npx недоступны из этой среды") | |
| def write_links(lines: list[str]) -> None: | |
| text = "\n".join(lines) + "\n" | |
| with open(LINKS_FILE, "w", encoding="utf-8") as handle: | |
| handle.write(text) | |
| print("\n" + text, flush=True) | |
| def main(argv: list[str] | None = None) -> int: | |
| print("логи: %s (сервер) и %s (ссылки) в %s" | |
| % ("plus_studio.log", LINKS_FILE, os.getcwd()), flush=True) | |
| parser = argparse.ArgumentParser(description="PLUS Studio: сервер и ссылка") | |
| parser.add_argument("--port", type=int, default=8900) | |
| parser.add_argument("--device", default="auto") | |
| parser.add_argument("--weights", default=os.environ.get("MUSCRIPTOR_WEIGHTS")) | |
| parser.add_argument("--no-tunnel", action="store_true") | |
| parser.add_argument("--tunnel", default=None) | |
| parser.add_argument("--keep", type=int, default=0, help="секунд держать, 0 = бесконечно") | |
| args = parser.parse_args(argv) | |
| step(1, 5, "зависимости") | |
| ensure_deps() | |
| step(2, 5, "пакет PLUS") | |
| pack = ensure_pack() | |
| step(3, 5, "сервер на порту %d" % args.port) | |
| server = start_server(pack, args.port, args.device, args.weights) | |
| print(" сервер отвечает: http://127.0.0.1:%d" % args.port, flush=True) | |
| order = ["localhostrun", "serveo", "local" "tunnel"] | |
| if args.tunnel: | |
| order = [args.tunnel] + [name for name in order if name != args.tunnel] | |
| tunnel = None | |
| if args.no_tunnel: | |
| write_links(["PLUS Studio " + time.strftime("%H:%M:%S"), | |
| "локально: http://127.0.0.1:%d" % args.port]) | |
| print(">>> LOCAL LINK: http://127.0.0.1:%d" % args.port, flush=True) | |
| else: | |
| step(4, 5, "публичная ссылка") | |
| tunnel = open_tunnel(args.port, order) | |
| write_links(["PLUS Studio " + time.strftime("%H:%M:%S"), | |
| "ссылка: " + tunnel.url, | |
| "локально: http://127.0.0.1:%d" % args.port]) | |
| if tunnel.url: | |
| print("\n>>> OPEN THIS LINK: " + tunnel.url, flush=True) | |
| step(5, 5, "держу живым, Stop останавливает") | |
| started = time.time() | |
| fails = 0 | |
| beat = time.time() | |
| try: | |
| while True: | |
| time.sleep(10) | |
| if time.time() - beat > 60: | |
| beat = time.time() | |
| print(" жив: %.1f мин | сервер %s | ссылка %s" | |
| % ((time.time() - started) / 60, | |
| "ок" if server.poll() is None else "упал", | |
| tunnel.url if tunnel else "нет (локально)"), flush=True) | |
| if args.keep and time.time() - started > args.keep: | |
| break | |
| if server.poll() is not None: | |
| print("сервер упал, поднимаю заново", flush=True) | |
| server = start_server(pack, args.port, args.device, args.weights) | |
| if tunnel is None: | |
| continue | |
| if not tunnel.alive(): | |
| print("туннель умер, открываю новый", flush=True) | |
| tunnel.kill() | |
| tunnel = open_tunnel(args.port, order) | |
| write_links(["PLUS Studio " + time.strftime("%H:%M:%S"), | |
| "новая ссылка: " + tunnel.url]) | |
| if tunnel.url: | |
| print(">>> OPEN THIS LINK: " + tunnel.url, flush=True) | |
| fails = 0 | |
| continue | |
| if not reachable(tunnel.url, attempts=1): | |
| fails += 1 | |
| if fails >= 3: | |
| print("ссылка перестала отвечать, переоткрываю", flush=True) | |
| tunnel.kill() | |
| fails = 0 | |
| except KeyboardInterrupt: | |
| pass | |
| if tunnel is not None: | |
| tunnel.kill() | |
| if server.poll() is None: | |
| server.terminate() | |
| print("остановлено", flush=True) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |