"""MuScriptor-сервер и публичная ссылка, одной командой. python3 plus/serve_muscriptor.py # наши веса с HF, порт 8222 python3 plus/serve_muscriptor.py --device cuda # на T4 python3 plus/serve_muscriptor.py --no-tunnel # только локально Поднимает ШТАТНЫЙ сервер MuScriptor (python -m muscriptor serve), тот самый, что отдаёт веб-приложение «MuScriptor: Music to MIDI and Sheet Music», и открывает туннель, печатая ссылку. Своего интерфейса здесь нет. """ from __future__ import annotations import argparse import collections import json import os import pathlib import re import shutil import subprocess import sys import threading import time import urllib.error import urllib.request LINKS_FILE = "muscriptor_links.txt" DEFAULT_MODEL = "hf://Nokia123543/muscriptor-large-fork/model.safetensors" PACKAGE = "muscriptor" TUNNEL_ORDER = ("serveo", "localhostrun", "pinggy", "cloudflared", "tuns", "tunnl", "tunnl-stable") # serveo первым: постоянный поддомен, без заглушки def say(text: str) -> None: print(text, flush=True) def ensure_package() -> None: import importlib.util if importlib.util.find_spec(PACKAGE) is not None: return say("ставлю пакет %s" % PACKAGE) subprocess.run([sys.executable, "-m", "pip", "install", "-q", PACKAGE], check=False) def ensure_ssh() -> str | None: """ssh есть в большинстве образов; если нет, пробуем доставить.""" found = shutil.which("ssh") if found: return found say("ssh не найден, пробую доставить openssh-client") for command in (["apt-get", "install", "-y", "-q", "openssh-client"], [sys.executable, "-m", "pip", "install", "-q", "sshtunnel"]): try: subprocess.run(command, check=False, timeout=300, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception: pass found = shutil.which("ssh") if found: say("ssh установлен: " + found) return found say("ssh поставить не удалось: останутся http-туннели") return None def ensure_key() -> bool: """Ключ нужен для стабильной ссылки (один адрес при переподключении).""" home = pathlib.Path.home() / ".ssh" if any((home / name).exists() for name in ("id_ed25519", "id_rsa", "id_ecdsa")): return True home.mkdir(parents=True, exist_ok=True) try: subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(home / "id_ed25519")], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60) except Exception: return False if (home / "id_ed25519").exists(): say("сгенерирован ssh-ключ: " + str(home / "id_ed25519")) return True return False def healthy(port: int, timeout: float = 3.0) -> bool: try: with urllib.request.urlopen("http://127.0.0.1:%d/health" % port, timeout=timeout) as answer: return answer.status == 200 except Exception: return False def low_memory_env(threads: int | None = None) -> dict: """Убрать раздувание памяти аллокаторами, не трогая точность модели. Веса остаются float32, никакого квантования: экономия идёт за счёт числа потоков (у каждого своя арена malloc) и возврата освобождённых страниц ОС. """ cores = os.cpu_count() or 4 count = threads or max(1, min(4, cores // 2)) env = dict(os.environ) env.update({ "OMP_NUM_THREADS": str(count), "MKL_NUM_THREADS": str(count), "OPENBLAS_NUM_THREADS": str(count), "NUMEXPR_NUM_THREADS": str(count), "MALLOC_ARENA_MAX": "2", "MALLOC_TRIM_THRESHOLD_": "1000000", "MALLOC_MMAP_THRESHOLD_": "131072", "TOKENIZERS_PARALLELISM": "false", "PYTORCH_CPU_ALLOC_CONF": "garbage_collection_threshold:0.6", }) return env def start_server(port: int, model: str, device: str, dtype: str | None, env: dict | None = None) -> subprocess.Popen: command = [sys.executable, "-m", "muscriptor", "serve", "--model", model, "--device", device, "--host", "0.0.0.0", "--port", str(port)] if dtype: command += ["--dtype", dtype] log = open("muscriptor_server.log", "a") say("запускаю: " + " ".join(command[1:])) process = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT, stdin=open(os.devnull, "rb"), env=env or dict(os.environ)) deadline = time.time() + 600 started = time.time() while time.time() < deadline: if process.poll() is not None: raise SystemExit("сервер умер при старте, смотри muscriptor_server.log") if healthy(port): return process waited = int(time.time() - started) if waited and waited % 15 == 0: say(" жду загрузки модели, %d с (первый раз качаются веса)" % waited) time.sleep(1) time.sleep(2) raise SystemExit("сервер не ответил за 10 минут, смотри muscriptor_server.log") RESERVED_HOSTS = ("console.", "www.", "admin.", "docs.", "blog.", "status.") def pick_url(pattern: "re.Pattern[str]", line: str, subdomain: str = "") -> str | None: """Выбрать настоящую ссылку туннеля, пропустив служебные адреса провайдера.""" for found in pattern.finditer(line): url = found.group(0) host = url.split("://", 1)[-1].lower() if host.startswith(RESERVED_HOSTS): continue return url return None class Tunnel: def __init__(self, name: str, argv: list[str], pattern: str): self.name, self.pattern = name, re.compile(pattern) self.url: str | None = None self.tail: collections.deque[str] = collections.deque(maxlen=40) self.failed = False self.proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=open(os.devnull, "rb"), text=True, bufsize=1) threading.Thread(target=self._drain, daemon=True).start() def _drain(self) -> None: assert self.proc.stdout is not None for line in self.proc.stdout: self.tail.append(line.rstrip()) low = line.lower() if "forwarding failed" in low or "administratively prohibited" in low: self.failed = True continue candidate = pick_url(self.pattern, line, SUBDOMAIN) if not candidate: continue if self.url is None: self.url = candidate elif SUBDOMAIN and SUBDOMAIN not in self.url and SUBDOMAIN in candidate: self.url = candidate # свой поддомен важнее случайного def alive(self) -> bool: return self.proc.poll() is None def kill(self) -> None: try: self.proc.kill() except Exception: pass DEFAULT_SUBDOMAIN = "nokia1709" SUBDOMAIN = os.environ.get("MUSCRIPTOR_SUBDOMAIN", DEFAULT_SUBDOMAIN).strip() def forward_spec(port: int, wants_name: bool) -> str: """Строка проброса: с постоянным поддоменом, если он задан.""" if wants_name and SUBDOMAIN: return "%s:80:127.0.0.1:%d" % (SUBDOMAIN, port) return "80:127.0.0.1:%d" % port SSH_OPTS = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3", "-o", "TCPKeepAlive=yes", "-o", "ExitOnForwardFailure=yes"] SSH_USER = os.environ.get("MUSCRIPTOR_SSH_USER", "nokey") CUSTOM_SSH = os.environ.get("MUSCRIPTOR_SSH", "") def custom_spec(port: int) -> tuple[str, list[str], str] | None: """Свой ssh-адрес: пользователь@хост или пользователь@хост:порт.""" if not CUSTOM_SSH: return None target, ssh_port = CUSTOM_SSH, None if ":" in target.rsplit("@", 1)[-1]: target, ssh_port = target.rsplit(":", 1) argv = ["ssh", "-T", *SSH_OPTS] if ssh_port: argv += ["-p", ssh_port] argv += ["-R", "80:127.0.0.1:%d" % port, target] return ("custom", argv, r"https?://[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}") CLOUDFLARED_URL = ("https://github.com/cloudflare/cloudflared/releases/latest/" "download/cloudflared-linux-amd64") def ensure_cloudflared() -> bool: """Резерв без ssh: бинарник тянем один раз, регистрация не нужна.""" if shutil.which("cloudflared"): return True target = pathlib.Path("/usr/local/bin/cloudflared") say("резервный бинарник не найден, качаю (около 40 МБ)") try: urllib.request.urlretrieve(CLOUDFLARED_URL, str(target)) except Exception as problem: say("скачать не удалось: %s" % problem) return False try: target.chmod(0o755) except Exception: pass return target.exists() def specs(port: int) -> list[tuple[str, list[str], str]]: head = [custom_spec(port)] if custom_spec(port) else [] return head + [ ("tunnl", ["ssh", "-T", *SSH_OPTS, "-R", "80:127.0.0.1:%d" % port, "proxy.tunnl.gg"], r"https://[a-z0-9-]+\.tunnl\.gg"), ("tunnl-stable", ["ssh", "-T", *SSH_OPTS, "-R", "80:127.0.0.1:%d" % port, "stable@proxy.tunnl.gg"], r"https://[a-z0-9-]+\.tunnl\.gg"), ("tuns", ["ssh", "-T", *SSH_OPTS, "-R", "80:127.0.0.1:%d" % port, "tuns.sh"], r"https://[a-z0-9-]+\.tuns\.sh"), ("localhostrun", ["ssh", "-T", *SSH_OPTS, "-R", "80:127.0.0.1:%d" % port, SSH_USER + "@localhost.run"], r"https://[a-z0-9]+\.lhr\.life"), ("serveo", ["ssh", "-T", *SSH_OPTS, "-R", forward_spec(port, True), "serveo.net"], r"https://(?!console\.|www\.)[a-z0-9-]+(?:\.[a-z0-9-]+)*\." r"(?:serveousercontent|serveo)\.[a-z]+"), ("pinggy", ["ssh", "-T", *SSH_OPTS, "-p", "443", "-R0:127.0.0.1:%d" % port, "a.pinggy.io"], r"https://[a-z0-9-]+\.(?:a\.)?pinggy\.(?:link|online)"), ("cloudflared", ["cloudflared", "tunnel", "--url", "http://127.0.0.1:%d" % port, "--no-autoupdate", "--protocol", "http2"], r"https://[a-z0-9-]+\.trycloudflare\.com"), ] BROWSER_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/128.0 Safari/537.36") BYPASS_HEADERS = {"serveo-skip-browser-warning": "true"} PAGE_MARKS = ("continue to", "continue to site", "proceed", "phishing", "browser warning", "serveo") def probe_api(url: str, timeout: float = 10.0) -> tuple[bool, str]: """API через туннель: JSON со списком инструментов, заглушка — не поломка.""" request = urllib.request.Request(url + "/instruments", headers=BYPASS_HEADERS) try: with urllib.request.urlopen(request, timeout=timeout) as answer: status = answer.status body = answer.read().decode("utf-8", "ignore") except urllib.error.HTTPError as problem: return False, "API код %d" % problem.code except Exception as problem: return False, "API не ответил (%s)" % type(problem).__name__ if status != 200: return False, "API код %d" % status try: data = json.loads(body) except Exception: low = body.lower() if any(mark in low for mark in PAGE_MARKS): return True, "API за страницей-предупреждением провайдера" return False, "API отдал не JSON" names = data.get("instruments") if isinstance(data, dict) else None if not names: return False, "API отдал пустой список инструментов" return True, "API: %d инструментов" % len(names) def probe_page(url: str, timeout: float = 15.0) -> tuple[int, str]: """Что реально увидит браузер: код и начало страницы.""" request = urllib.request.Request(url + "/", headers={ "User-Agent": BROWSER_UA, "Accept": "text/html,application/xhtml+xml", "Accept-Language": "ru,en;q=0.8", }) try: with urllib.request.urlopen(request, timeout=timeout) as answer: body = answer.read(4000).decode("utf-8", "ignore") return answer.status, body except urllib.error.HTTPError as problem: try: body = problem.read(800).decode("utf-8", "ignore") except Exception: body = "" return problem.code, body except Exception as problem: return 0, str(problem) INTERSTITIAL_MARKS = ("continue", "warning", "proceed", "restricted", "phishing", "confirm you are human", "продолж") def is_interstitial(body: str) -> bool: low = body.lower() provider = any(name in low for name in ("tunnl", "pinggy", "localhost.run", "serveo", "lhr.life")) return provider and any(mark in low for mark in INTERSTITIAL_MARKS) def looks_like_app(status: int, body: str) -> tuple[bool, str]: if status != 200: return False, "код %s" % (status or "нет ответа") low = body.lower() if "muscriptor" in low: return True, "страница MuScriptor" if is_interstitial(body): return True, "страница-предупреждение провайдера, приложение за ней" if " 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: tuple[str, ...]) -> Tunnel: table = {name: (argv, pattern) for name, argv, pattern in specs(port)} fails = [] for name in order: if name not in table: continue argv, pattern = table[name] if not shutil.which(argv[0]) and name == "cloudflared": ensure_cloudflared() if not shutil.which(argv[0]): say(" [%s] нет команды %s, пропускаю" % (name, argv[0])) continue say(" [%s] открываю" % name) tunnel = Tunnel(name, argv, pattern) deadline, started = time.time() + 60, time.time() while (time.time() < deadline and tunnel.url is None and tunnel.alive() and not tunnel.failed): time.sleep(1) waited = int(time.time() - started) if waited and waited % 10 == 0: say(" [%s] ждём адрес, %d с" % (name, waited)) if tunnel.url: status, body = probe_page(tunnel.url) fine, note = looks_like_app(status, body) if not fine and reachable(tunnel.url): fine, note = True, "жив, страницу проверить не вышло" if fine: say(" [%s] работает: %s (%s)" % (name, tunnel.url, note)) return tunnel say(" [%s] адрес есть, приложение не отдаёт: %s" % (name, note)) fails.append((name, tunnel.url, list(tunnel.tail)[-2:])) tunnel.kill() for name, url, tail in fails: say(" [%s] не вышло: %s | %s" % (name, url or "нет адреса", " / ".join(tail))) raise SystemExit("ни один туннель не поднялся") BAND_INSTRUMENTS = ("drums,electric_bass,acoustic_bass,chromatic_percussion," "acoustic_piano,electric_piano,clean_electric_guitar,synth_pad") def ensure_audio_tools() -> bool: """fluidsynth рендерит MIDI в WAV на сервере; pip его не ставит.""" if shutil.which("fluidsynth"): return True say(" fluidsynth не найден, ставлю (нужен для рендера аудио)") env = dict(os.environ, DEBIAN_FRONTEND="noninteractive") steps = (["apt-get", "update", "-qq"], ["apt-get", "install", "-y", "--no-install-recommends", "fluidsynth"]) for argv in steps: try: run = subprocess.run(argv, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=600) if run.returncode != 0: tail = run.stdout.decode("utf-8", "replace").strip().splitlines()[-2:] say(" %s: %s" % (argv[0], " / ".join(tail) or "код %d" % run.returncode)) except Exception as problem: say(" %s не сработал: %s" % (argv[0], problem)) if shutil.which("fluidsynth"): say(" fluidsynth готов: %s" % shutil.which("fluidsynth")) return True say(" без fluidsynth транскрипция работает, рендер в WAV нет") return False WIDE_INSTRUMENTS = ("acoustic_piano,electric_piano,chromatic_percussion,organ,acoustic_guitar,clean_electric_guitar,acoustic_bass,electric_bass,string_ensemble,voice,synth_pad,drums") ALL_INSTRUMENTS = ("acoustic_piano,electric_piano,chromatic_percussion,organ,acoustic_guitar,clean_electric_guitar,distorted_electric_guitar,acoustic_bass,electric_bass,violin,viola,cello,contrabass,orchestral_harp,timpani,string_ensemble,synth_strings,voice,orchestra_hit,trumpet,trombone,tuba,french_horn,brass_section,soprano_and_alto_sax,tenor_sax,baritone_sax,oboe,english_horn,bassoon,clarinet,flutes,synth_lead,synth_pad,drums") def expand_instruments(text: str) -> list[str]: """Разбор набора: all → все группы, auto → пусто, иначе список через запятую.""" cleaned = (text or "").strip() if not cleaned or cleaned.lower() == "auto": return [] if cleaned.lower() in ("all", "все"): return ALL_INSTRUMENTS.split(",") if cleaned.lower() in ("wide", "широкий"): return WIDE_INSTRUMENTS.split(",") return [name.strip() for name in cleaned.split(",") if name.strip()] MAX_GROUPS = 10 # шире модель размазывается: замерено BAND_LIST = "drums,electric_bass,acoustic_bass,chromatic_percussion,acoustic_piano,electric_piano,clean_electric_guitar,synth_pad".split(",") def guard_instruments(names: list[str], label: str = "") -> tuple[list[str], str]: """Свести опасный набор к проверенному и объяснить почему.""" if not names: return names, "" band_alias = [n for n in names if n.lower() in ("band", "набор")] if band_alias: return list(BAND_LIST), "%s: это слово-псевдоним, беру проверенный набор" % label known = set(ALL_INSTRUMENTS.split(",")) unknown = [n for n in names if n not in known] if unknown: names = [n for n in names if n in known] if not names: return list(BAND_LIST), ("%s: неизвестные группы %s, беру проверенный набор" % (label, ", ".join(unknown[:4]))) return names, "%s: выкинул неизвестные группы %s" % (label, ", ".join(unknown[:4])) if len(names) > MAX_GROUPS: return list(BAND_LIST), ( "%s: %d групп из %d — модель размазывается (замер: 35 групп дали 0 нот," " 12 групп хуже на 9 попаданий). Беру проверенный набор из %d." % (label or "набор", len(names), len(names), len(BAND_LIST))) return names, "" def start_proxy(server_port: int, listen_port: int, instruments: str = "band", multi_pass: str = "", no_articulation: bool = False, local_font: bool = False) -> subprocess.Popen: """SSE-прокси: держит поток /transcribe живым через медленный туннель.""" script = pathlib.Path(__file__).with_name("sse_proxy.py") if not script.exists(): script = pathlib.Path(__file__).parent / "sse_proxy.py" proc = subprocess.Popen( [sys.executable, "-u", str(script), "--listen", str(listen_port), "--target", str(server_port), "--instruments", instruments] + (["--multi-pass", multi_pass] if multi_pass else []) + (["--font-cdn", "none"] if local_font else []), stdout=open("muscriptor_proxy.log", "a"), stderr=subprocess.STDOUT) for _ in range(20): time.sleep(0.5) if proc.poll() is not None: say("прокси не поднялся, работаю напрямую (поток может рваться)") return proc try: with urllib.request.urlopen("http://127.0.0.1:%d/health" % listen_port, timeout=3) as answer: if answer.status == 200: return proc except Exception: continue return proc def speed_probe(url: str, timeout: float = 25.0) -> tuple[float, float, int]: """Вернуть (ttfb, всего, байт) для страницы по ссылке.""" import time as _time started = _time.time() try: with urllib.request.urlopen(url.rstrip("/") + "/", timeout=timeout) as answer: first = answer.read(1) first_at = _time.time() rest = answer.read() return first_at - started, _time.time() - started, len(first) + len(rest) except Exception: return -1.0, _time.time() - started, 0 def write_link(url: str, port: int, public_port: int | None = None) -> None: lines = ["MuScriptor " + time.strftime("%H:%M:%S"), "ссылка: " + url, "локально: http://127.0.0.1:%d" % port] if public_port and public_port != port: lines.append("локально через прокси: http://127.0.0.1:%d" % public_port) with open(LINKS_FILE, "w", encoding="utf-8") as handle: handle.write("\n".join(lines) + "\n") say("\n" + "\n".join(lines)) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="MuScriptor: сервер и ссылка") parser.add_argument("--port", type=int, default=8222) parser.add_argument("--model", default=DEFAULT_MODEL, help="путь к safetensors, имя размера или hf://org/repo/file") parser.add_argument("--device", default="auto", help="auto | cpu | cuda | cuda:0") parser.add_argument("--dtype", default=None, help="float32 | float16 | bfloat16") parser.add_argument("--subdomain", default=DEFAULT_SUBDOMAIN, help="постоянный поддомен serveo, например nokia1709") parser.add_argument("--tunnel", default=None, help="только этот туннель") parser.add_argument("--ssh", default=None, help="свой ssh-туннель: пользователь@хост или пользователь@хост:порт") parser.add_argument("--ssh-user", default=None, help="имя пользователя для локального туннеля (по умолчанию nokey)") parser.add_argument("--no-tunnel", action="store_true", help="без публичной ссылки") parser.add_argument("--keep", type=int, default=0, help="держать N секунд и выйти") parser.add_argument("--low-memory", action="store_true", help="экономный режим для слабого ПК: меньше потоков и арен," " веса всё так же float32") parser.add_argument("--threads", type=int, default=None, help="сколько потоков дать модели (по умолчанию половина ядер)") parser.add_argument("--skip-audio", action="store_true", help="не ставить fluidsynth (рендер аудио будет недоступен)") parser.add_argument("--local-font", action="store_true", help="звуковой банк отдавать с сервера, а не с CDN" " (по умолчанию идёт с CDN: 38 МБ не через туннель)") parser.add_argument("--no-articulation", action="store_true", help="не пересчитывать концы нот и не ставить легато/стакато") parser.add_argument("--multi-pass", default="", help="второй проход с другим набором инструментов и слияние," " например: chromatic_percussion,electric_bass,acoustic_bass") parser.add_argument("--instruments", default="band", help="инструменты для модели: band (по умолчанию, 8 групп), auto" " или список через запятую") args = parser.parse_args(argv) global SSH_USER, CUSTOM_SSH, SUBDOMAIN if args.subdomain: SUBDOMAIN = args.subdomain.strip() os.environ["MUSCRIPTOR_SUBDOMAIN"] = SUBDOMAIN if args.ssh_user: SSH_USER = args.ssh_user if args.ssh: CUSTOM_SSH = args.ssh ensure_package() checked, warning = guard_instruments(expand_instruments(args.instruments), "--instruments") if warning: say(" " + warning) args.instruments = ",".join(checked) if args.multi_pass: checked_pass, warning = guard_instruments(expand_instruments(args.multi_pass), "--multi-pass") if warning: say(" " + warning) args.multi_pass = ",".join(checked_pass) if not args.skip_audio: ensure_audio_tools() ensure_ssh() ensure_key() env = None if args.low_memory or args.threads: env = low_memory_env(args.threads) say(" экономный режим: потоков %s, арен malloc 2, веса float32" % env["OMP_NUM_THREADS"]) server = start_server(args.port, args.model, args.device, args.dtype, env) say("[1/2] сервер отвечает: http://127.0.0.1:%d" % args.port) proxy_port = args.port + 1 proxy = start_proxy(args.port, proxy_port, args.instruments, args.multi_pass, args.no_articulation, args.local_font) if proxy.poll() is None: say(" поток защищён прокси: http://127.0.0.1:%d" % proxy_port) else: proxy_port = args.port if args.no_tunnel: write_link("нет (локальный режим)", args.port, proxy_port) say(">>> LOCAL LINK: http://127.0.0.1:%d" % args.port) tunnel, order = None, () else: say("[2/2] публичная ссылка") if SUBDOMAIN and "serveo" in (args.tunnel or "serveo"): say(" просим поддомен serveo: %s" % SUBDOMAIN) base = TUNNEL_ORDER if CUSTOM_SSH and "custom" not in base: base = ("custom",) + tuple(base) order = (args.tunnel,) + tuple(n for n in base if n != args.tunnel) \ if args.tunnel else base tunnel = open_tunnel(proxy_port, order) write_link(tunnel.url or "", args.port, proxy_port) say(">>> OPEN THIS LINK: " + str(tunnel.url)) if getattr(tunnel, "interstitial", False): say(" ВНИМАНИЕ: провайдер показывает страницу-предупреждение;") say(" из-за неё панель инструментов и транскрипция могут не работать.") say(" Перезапусти с --tunnel localhostrun") if tunnel.url and SUBDOMAIN and SUBDOMAIN not in tunnel.url: say(" провайдер выдал другой адрес (свой поддомен занят): %s" % tunnel.url) if tunnel.url: api_ok, api_note = probe_api(tunnel.url) say(" API снаружи: %s" % api_note) ttfb, total, size = speed_probe(tunnel.url) if ttfb >= 0: say(" скорость ссылки: первый байт %.1f с, страница %.1f с" % (ttfb, total)) if ttfb > 2.5 or total > 5.0: say(" медленная ссылка (%s): попробуй --tunnel pinggy" " или --tunnel serveo" % tunnel.name) else: say(" скорость ссылки померить не удалось (страница не открылась)") started, beat = time.time(), time.time() public_fails, checked = 0, time.time() reopens = 0 try: while True: time.sleep(10) if time.time() - beat > 60: beat = time.time() say(" жив: %.1f мин | сервер %s | ссылка %s" % ((time.time() - started) / 60, "ок" if server.poll() is None else "упал", tunnel.url if tunnel else "локально")) if args.keep and time.time() - started > args.keep: break if server.poll() is not None: say("сервер упал, поднимаю заново") server = start_server(args.port, args.model, args.device, args.dtype, env) if proxy.poll() is not None: say("прокси упал, поднимаю заново") proxy = start_proxy(args.port, proxy_port, args.instruments, args.multi_pass, args.no_articulation, args.local_font) if tunnel is not None and tunnel.url and time.time() - checked > 60: checked = time.time() status, body = probe_page(tunnel.url) fine, note = looks_like_app(status, body) api_fine, api_note = probe_api(tunnel.url) if fine and not api_fine: fine, note = False, api_note if fine: public_fails = 0 else: public_fails += 1 say(" ссылка снаружи: %s (%d подряд)" % (note, public_fails)) if public_fails >= 3 and reopens < 5: reopens += 1 say(" переоткрываю туннель: снаружи не отвечает" " (раз %d из 5)" % reopens) tunnel.kill() tunnel = open_tunnel(proxy_port, order) write_link(tunnel.url or "", args.port, proxy_port) if tunnel.url: say(">>> OPEN THIS LINK: " + tunnel.url) public_fails = 0 elif public_fails >= 3: say(" переоткрытий уже 5, оставляю этот туннель:" " проблема не в нём") if tunnel is not None and not tunnel.alive(): say("туннель умер, открываю новый") tunnel.kill() tunnel = open_tunnel(proxy_port, order) write_link(tunnel.url or "", args.port, proxy_port) if tunnel.url: say(">>> OPEN THIS LINK: " + tunnel.url) except KeyboardInterrupt: pass if tunnel is not None: tunnel.kill() if proxy.poll() is None: proxy.terminate() if server.poll() is None: server.terminate() say("остановлено") return 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: print("\nостановлено вручную", flush=True) sys.exit(130)