muscriptor-large-fork / plus /launch_studio.py
Nokia123543's picture
PLUS: пускач печатает пульс и пути логов
6a61282 verified
Raw History Blame Contribute Delete
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())