Download plus/sse_proxy.py from Nokia123543/muscriptor-large-fork: direct link, hf CLI and curl.
- Browser
- Download file 28.4 kB
-
https://huggingface.co/Nokia123543/muscriptor-large-fork/resolve/main/plus/sse_proxy.py
- Command line
-
hf download hf://Nokia123543/muscriptor-large-fork/plus/sse_proxy.py
-
curl -L -o sse_proxy.py https://huggingface.co/Nokia123543/muscriptor-large-fork/resolve/main/plus/sse_proxy.py
28.4 kB
| """Прокси с heartbeat для штатного интерфейса MuScriptor. | |
| Зачем: /transcribe отдаёт SSE-поток, который на бесплатном туннеле рвётся | |
| по простою (провайдер закрывает соединение, если данные не идут ~60 с). | |
| Прокси читает поток с сервера отдельным потоком и, пока он молчит, | |
| отправляет браузеру комментарий-пульс ": ping", поэтому туннель видит трафик | |
| и не рвёт соединение. Интерфейс остаётся штатным, в пакет не лезем. | |
| Запуск: python plus/sse_proxy.py --listen 8223 --target 8222 | |
| """ | |
| import argparse | |
| import http.client | |
| import json | |
| import os | |
| import re | |
| import queue | |
| import sys | |
| import threading | |
| import time | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| MIDI_TYPES = ("audio/midi", "audio/x-midi", "application/x-midi", | |
| "application/octet-stream", "application/midi") | |
| def guess_audio(body: bytes, content_type: str) -> tuple[str, bytes] | None: | |
| """Вытащить аудио из multipart-запроса, чтобы потом мерить по нему громкость.""" | |
| marker = "boundary=" | |
| if "multipart/form-data" not in content_type or marker not in content_type: | |
| return None | |
| boundary = content_type.split(marker, 1)[1].split(";")[0].strip().strip('"') | |
| parts = body.split(b"--" + boundary.encode()) | |
| for part in parts: | |
| if b"filename=" not in part: | |
| continue | |
| head, _, tail = part.partition(b"\r\n\r\n") | |
| if not tail: | |
| continue | |
| name = "" | |
| for line in head.decode("latin-1").split("\r\n"): | |
| if "filename=" in line: | |
| found = re.search(r'filename="([^"]+)"', line) | |
| if found: | |
| name = found.group(1) | |
| else: | |
| name = line.split("filename=")[-1].strip().strip('"').split(";")[0] | |
| suffix = ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else "" | |
| if suffix in (".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aiff", ".aif", ".opus"): | |
| data = tail.rstrip(b"\r\n") | |
| data = data[:-2] if data.endswith(b"\r\n") else data | |
| return name or ("audio" + suffix), data | |
| return None | |
| def apply_velocity(audio: tuple[str, bytes] | None, midi: bytes) -> tuple[bytes, dict]: | |
| """Проставить измеренную громкость вместо константы 100.""" | |
| if not audio: | |
| return midi, {"skipped": "нет аудио для замера"} | |
| import tempfile | |
| import velocity as vel | |
| name, blob = audio | |
| suffix = ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else ".wav" | |
| try: | |
| with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as handle: | |
| handle.write(blob) | |
| handle.flush() | |
| return vel.apply(handle.name, midi) | |
| except Exception as problem: | |
| return midi, {"skipped": "замер не вышел: %s" % problem} | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| import fusion # noqa: E402 | |
| import verify_notes # noqa: E402 | |
| import articulation # noqa: E402 | |
| 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: пресет band, беру 8 проверенных групп" % 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, беру band" | |
| % (label, ", ".join(unknown[:4]))) | |
| return names, "%s: выкинул неизвестные группы %s" % (label, ", ".join(unknown[:4])) | |
| if len(names) > MAX_GROUPS: | |
| return list(BAND_LIST), ( | |
| "%s: %d групп — модель размазывается (замер: 35 групп дали 0 нот," | |
| " 12 групп хуже). Беру проверенный band из %d групп." | |
| % (label or "набор", len(names), len(BAND_LIST))) | |
| return names, "" | |
| def replace_instruments(body: bytes, content_type: str, names: list[str]) -> bytes: | |
| """Заменить поля instruments на нужный набор (для второго прохода).""" | |
| marker = "boundary=" | |
| if "multipart/form-data" not in content_type or marker not in content_type: | |
| return body | |
| boundary = content_type.split(marker, 1)[1].split(";")[0].strip().strip('"') | |
| chunks = body.split(b"--" + boundary.encode()) | |
| kept = [] | |
| for chunk in chunks: | |
| if b'name="instruments"' in chunk: | |
| continue | |
| kept.append(chunk) | |
| rebuilt = (b"--" + boundary.encode()).join(kept) | |
| if names: | |
| rebuilt = add_instruments(rebuilt, content_type, names) | |
| return rebuilt | |
| def run_pass(host: str, port: int, path: str, body: bytes, content_type: str, | |
| instruments: list[str]) -> str: | |
| """Один проход транскрипции к серверу, ответ как текст (SSE).""" | |
| payload = replace_instruments(body, content_type, instruments) | |
| connection = http.client.HTTPConnection(host, port, timeout=3600) | |
| headers = {"Content-Type": content_type, "Content-Length": str(len(payload)), | |
| "Accept-Encoding": "identity"} | |
| connection.request("POST", path, body=payload, headers=headers) | |
| answer = connection.getresponse() | |
| text = answer.read().decode("utf-8", "ignore") | |
| connection.close() | |
| return text | |
| def build_midi(host: str, port: int, notes: list[dict]) -> bytes: | |
| """Собрать MIDI из слитых нот силами самого сервера (/export).""" | |
| payload = json.dumps(fusion.notes_to_export(notes)).encode("utf-8") | |
| connection = http.client.HTTPConnection(host, port, timeout=600) | |
| connection.request("POST", "/export", body=payload, | |
| headers={"Content-Type": "application/json"}) | |
| answer = connection.getresponse() | |
| data = answer.read() | |
| connection.close() | |
| return data | |
| WARNING_BYPASS = """<script>/*serveo-warning-bypass*/ | |
| (function(){var H="serveo-skip-browser-warning",V="true"; | |
| try{var f=window.fetch; | |
| if(f){window.fetch=function(i,o){try{ | |
| if(typeof i==="string"||i instanceof URL){o=o||{};var h=new Headers(o.headers||{});h.set(H,V);o.headers=h;} | |
| else if(i instanceof Request){var g=new Headers(i.headers||{});g.set(H,V);i=new Request(i,{headers:g});} | |
| }catch(e){}return f.call(this,i,o);};} | |
| var sd=XMLHttpRequest.prototype.send; | |
| XMLHttpRequest.prototype.send=function(){try{this.setRequestHeader(H,V);}catch(e){} | |
| return sd.apply(this,arguments);};}catch(e){}})(); | |
| </script>""" | |
| def inject_warning_bypass(data: bytes) -> tuple[bytes, bool]: | |
| """Вставить обход предупреждения serveo в HTML интерфейса.""" | |
| if os.environ.get("PX_NO_INJECT") == "1" or b"serveo-warning-bypass" in data: | |
| return data, False | |
| low = data.lower() | |
| for needle in (b"</head>", b"<body"): | |
| at = low.find(needle) | |
| if at >= 0: | |
| return data[:at] + WARNING_BYPASS.encode("utf-8") + data[at:], True | |
| return data, False | |
| HOP_HEADERS = {"connection", "keep-alive", "proxy-authenticate", "proxy-authorization", | |
| "te", "trailers", "transfer-encoding", "upgrade", "host", | |
| "content-length", "accept-encoding"} | |
| PING_SECONDS = 10.0 | |
| # Звуковой банк (38 МБ) не гоняем через туннель: браузер берёт его с CDN. | |
| FONT_CDN = ("https://huggingface.co/Nokia123543/muscriptor-large-fork" | |
| "/resolve/main/soundfonts/MuseScore_General.sf3") | |
| def read_body(handler) -> bytes: | |
| length = handler.headers.get("Content-Length") | |
| if length: | |
| return handler.rfile.read(int(length)) | |
| if (handler.headers.get("Transfer-Encoding") or "").lower() == "chunked": | |
| parts = [] | |
| while True: | |
| line = handler.rfile.readline().strip() | |
| size = int(line.split(b";")[0] or b"0", 16) | |
| if size == 0: | |
| handler.rfile.readline() | |
| break | |
| parts.append(handler.rfile.read(size)) | |
| handler.rfile.readline() | |
| return b"".join(parts) | |
| return b"" | |
| BAND = ("drums,electric_bass,acoustic_bass,chromatic_percussion,acoustic_piano," | |
| "electric_piano,clean_electric_guitar,distorted_electric_guitar," | |
| "string_ensemble,synth_pad,synth_lead,voice") | |
| def add_instruments(body: bytes, content_type: str, names: list[str]) -> bytes: | |
| """Дописать поля instruments в multipart, если их там нет.""" | |
| if not names or b'name="instruments"' in body: | |
| return body | |
| marker = "boundary=" | |
| if marker not in content_type: | |
| return body | |
| boundary = content_type.split(marker, 1)[1].split(";")[0].strip().strip('"') | |
| closing = b"--" + boundary.encode() + b"--" | |
| if closing not in body: | |
| return body | |
| parts = b"" | |
| for name in names: | |
| parts += (b"--" + boundary.encode() + b"\r\n" | |
| b'Content-Disposition: form-data; name="instruments"\r\n\r\n' | |
| + name.encode() + b"\r\n") | |
| return body.replace(closing, parts + closing, 1) | |
| POST_ONLY = ("/transcribe", "/transcribe/midi", "/export", "/auralize", "/sheets") | |
| def serve_alias(handler, target: str, method: str = "GET") -> bool: | |
| """Отдать другой путь с того же сервера (например favicon).""" | |
| try: | |
| upstream = http.client.HTTPConnection(handler.target_host, handler.target_port, | |
| timeout=30) | |
| upstream.request(method, target, headers={"Accept-Encoding": "identity"}) | |
| answer = upstream.getresponse() | |
| data = answer.read() | |
| except Exception as problem: | |
| print("алиас %s не вышел: %s" % (target, problem), flush=True) | |
| return False | |
| handler.send_response(answer.status) | |
| for key, value in answer.getheaders(): | |
| if key.lower() in HOP_HEADERS or key.lower() == "content-length": | |
| continue | |
| handler.send_header(key, value) | |
| handler.send_header("Content-Length", str(len(data))) | |
| handler.end_headers() | |
| if method != "HEAD": | |
| handler.wfile.write(data) | |
| upstream.close() | |
| return True | |
| class Proxy(BaseHTTPRequestHandler): | |
| protocol_version = "HTTP/1.1" | |
| target_host = "127.0.0.1" | |
| target_port = 8222 | |
| instruments: list[str] = [] | |
| measure_velocity = True | |
| ping_seconds = PING_SECONDS | |
| multi_pass: list[str] = [] | |
| energy_threshold = 6.0 | |
| refine_articulation = True | |
| last_audio: tuple[str, bytes] | None = None | |
| font_cdn = os.environ.get("MUSCRIPTOR_FONT_CDN", FONT_CDN) | |
| def log_message(self, fmt, *args): | |
| pass | |
| def do_GET(self): | |
| self.pass_through("GET") | |
| def do_HEAD(self): | |
| self.pass_through("HEAD") | |
| def do_POST(self): | |
| self.pass_through("POST") | |
| def do_PUT(self): | |
| self.pass_through("PUT") | |
| def do_DELETE(self): | |
| self.pass_through("DELETE") | |
| def do_OPTIONS(self): | |
| self.pass_through("OPTIONS") | |
| def pass_through(self, method: str): | |
| path_only = self.path.split("?")[0] | |
| self._started = time.time() | |
| declared = self.headers.get("Content-Length") or "-" | |
| print("ЗАПРОС %s %s | %s байт | клиент %s" | |
| % (method, path_only, declared, self.client_address[0]), flush=True) | |
| if (method in ("GET", "HEAD") and self.font_cdn | |
| and path_only.startswith("/soundfonts/") | |
| and "local=1" not in self.path): | |
| # тяжёлый банк отдаёт CDN, туннель отдыхает | |
| print("банк: редирект на CDN (%s)" % path_only, flush=True) | |
| self.send_response(302) | |
| self.send_header("Location", self.font_cdn) | |
| self.send_header("Content-Length", "0") | |
| self.send_header("Cache-Control", "public, max-age=86400") | |
| self.end_headers() | |
| return | |
| # 1. браузер всегда просит иконку: отдаём png вместо JSON-404 | |
| if path_only == "/favicon.ico" and method in ("GET", "HEAD"): | |
| if serve_alias(self, "/favicon.png", method): | |
| return | |
| # 2. GET на POST-маршрут: понятный текст вместо {"detail":"Not Found"} | |
| if method in ("GET", "HEAD") and path_only in POST_ONLY: | |
| note = ("Этот адрес принимает только POST.\n" | |
| "Открой интерфейс: %s/\n" % self.headers.get("Host", "ссылка")) | |
| payload = note.encode("utf-8") | |
| self.send_response(405) | |
| self.send_header("Content-Type", "text/plain; charset=utf-8") | |
| self.send_header("Allow", "POST") | |
| self.send_header("Content-Length", str(len(payload))) | |
| self.end_headers() | |
| if method != "HEAD": | |
| self.wfile.write(payload) | |
| print("ПОДСКАЗКА %s %s | 405 текстом" % (method, path_only), flush=True) | |
| return | |
| # 3. лишний слэш в конце: /transcribe/ → /transcribe | |
| if method == "POST" and path_only != "/" and path_only.endswith("/"): | |
| trimmed = path_only.rstrip("/") | |
| if trimmed in POST_ONLY: | |
| query = self.path[len(path_only):] | |
| self.path = trimmed + query | |
| path_only = trimmed | |
| body = read_body(self) | |
| content_type = self.headers.get("Content-Type") or "" | |
| audio = guess_audio(body, content_type) | |
| if os.environ.get("PXDEBUG"): | |
| print("PXDEBUG path=%s body=%d ctype=%s filename=%s audio=%s" | |
| % (self.path, len(body), content_type[:60], | |
| b"filename=" in body, bool(audio)), flush=True) | |
| if audio: | |
| Proxy.last_audio = audio | |
| if self.quality_applies(method): | |
| self.serve_quality(body) | |
| return | |
| if (self.instruments and method == "POST" | |
| and self.path.split("?")[0] in ("/transcribe", "/transcribe/midi")): | |
| body = add_instruments(body, self.headers.get("Content-Type") or "", | |
| self.instruments) | |
| upstream = http.client.HTTPConnection(self.target_host, self.target_port, timeout=900) | |
| headers = {k: v for k, v in self.headers.items() | |
| if k.lower() not in HOP_HEADERS} | |
| headers["Accept-Encoding"] = "identity" | |
| try: | |
| upstream.request(method, self.path, body=body, headers=headers) | |
| answer = upstream.getresponse() | |
| except Exception as problem: | |
| self.send_error(502, "upstream: %s" % problem) | |
| return | |
| content_type = (answer.getheader("Content-Type") or "") | |
| self.send_response(answer.status) | |
| stream = content_type.startswith("text/event-stream") | |
| for key, value in answer.getheaders(): | |
| low = key.lower() | |
| if low in HOP_HEADERS or low in ("server", "date"): | |
| continue | |
| if stream and low in ("cache-control", "x-accel-buffering"): | |
| continue | |
| self.send_header(key, value) | |
| if stream: | |
| self.begin_stream() | |
| try: | |
| self.stream_events(answer) | |
| finally: | |
| self.end_stream() | |
| else: | |
| data = answer.read() | |
| content_type = (answer.getheader("Content-Type") or "").lower() | |
| if "text/html" in content_type: | |
| data, injected = inject_warning_bypass(data) | |
| if injected: | |
| print("интерфейс: добавлен обход предупреждения провайдера", flush=True) | |
| looks_midi = content_type.startswith(MIDI_TYPES) or data[:4] == b"MThd" | |
| if (self.measure_velocity and method != "HEAD" and looks_midi | |
| and data[:4] == b"MThd"): | |
| data, stats = apply_velocity(self.last_audio, data) | |
| print("velocity: %s" % stats, flush=True) | |
| if method == "HEAD": | |
| length = answer.getheader("Content-Length") | |
| self.send_header("Content-Length", length if length is not None else "0") | |
| elif answer.status not in (204, 304) and answer.status >= 200: | |
| self.send_header("Content-Length", str(len(data))) | |
| self.end_headers() | |
| if method != "HEAD" and answer.status not in (204, 304): | |
| self.wfile.write(data) | |
| if path_only != "/health": | |
| print("ОТВЕТ %s | статус %d | %d байт | %.2f с" | |
| % (path_only, answer.status, len(data), | |
| time.time() - getattr(self, "_started", time.time())), flush=True) | |
| upstream.close() | |
| def begin_stream(self) -> None: | |
| """Заголовки SSE: chunked + Connection: close, чтобы туннель (HTTP/2) | |
| видел конец ответа и не рвал поток с ERR_HTTP2_PROTOCOL_ERROR.""" | |
| self.send_header("Cache-Control", "no-cache, no-transform") | |
| self.send_header("X-Accel-Buffering", "no") | |
| self.send_header("Transfer-Encoding", "chunked") | |
| self.send_header("Connection", "close") | |
| self.end_headers() | |
| self.close_connection = True | |
| self.emit(b": open\n\n") | |
| def emit(self, data: bytes) -> None: | |
| if not data: | |
| return | |
| self.wfile.write(b"%x\r\n" % len(data) + data + b"\r\n") | |
| self.wfile.flush() | |
| def end_stream(self) -> None: | |
| self.close_connection = True | |
| try: | |
| self.wfile.write(b"0\r\n\r\n") | |
| self.wfile.flush() | |
| except Exception: | |
| pass | |
| def refine(self, notes: list[dict], skip_filter: bool = False) -> list[dict]: | |
| """Пересчитать концы нот и артикуляцию по записи.""" | |
| if not notes or not self.last_audio: | |
| return notes | |
| import tempfile | |
| name, blob = self.last_audio | |
| suffix = ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else ".wav" | |
| try: | |
| with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as handle: | |
| handle.write(blob) | |
| handle.flush() | |
| refined, stats = articulation.refine([dict(n) for n in notes], handle.name) | |
| except Exception as problem: | |
| print("артикуляция не вышла: %s" % problem, flush=True) | |
| return notes | |
| print("артикуляция: %s" % stats, flush=True) | |
| for note in refined: | |
| note.pop("measured_end", None) | |
| return self.filter_by_audio(refined) if skip_filter else refined | |
| def filter_by_audio(self, notes: list[dict]) -> list[dict]: | |
| """Оставить только то, что подтверждается энергией в записи.""" | |
| if not notes or not self.last_audio or self.energy_threshold <= 0: | |
| return notes | |
| import tempfile | |
| name, blob = self.last_audio | |
| suffix = ("." + name.rsplit(".", 1)[-1].lower()) if "." in name else ".wav" | |
| try: | |
| with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as handle: | |
| handle.write(blob) | |
| handle.flush() | |
| kept, dropped, stats = verify_notes.verify( | |
| notes, handle.name, threshold=self.energy_threshold) | |
| except Exception as problem: | |
| print("проверка по аудио не вышла: %s" % problem, flush=True) | |
| return notes | |
| print("проверка по аудио: %s" % stats, flush=True) | |
| for note in kept: | |
| note.pop("energy_ratio", None) | |
| return kept | |
| def quality_applies(self, method: str) -> bool: | |
| """Нужен ли конвейер качества для этого запроса.""" | |
| if method != "POST" or not self.last_audio: | |
| return False | |
| if self.path.split("?")[0] != "/transcribe": | |
| return False | |
| return bool(self.multi_pass) or self.refine_articulation | |
| def serve_quality(self, body: bytes) -> None: | |
| """Ответ потоком с пульсом сразу, конвейер считается в фоне.""" | |
| outcome: queue.Queue = queue.Queue() | |
| def worker(): | |
| try: | |
| text = run_pass(self.target_host, self.target_port, self.path, body, | |
| self.headers.get("Content-Type") or "", self.instruments) | |
| notes = fusion.parse_sse(text) | |
| if self.multi_pass: | |
| second = run_pass(self.target_host, self.target_port, self.path, body, | |
| self.headers.get("Content-Type") or "", self.multi_pass) | |
| notes, stats = fusion.merge([notes, fusion.parse_sse(second)]) | |
| print("слияние: %s" % stats, flush=True) | |
| notes = self.filter_by_audio(notes) | |
| if self.refine_articulation: | |
| notes = self.refine(notes, skip_filter=True) | |
| elif self.refine_articulation: | |
| notes = self.refine(notes) | |
| outcome.put(fusion.to_sse(notes)) | |
| except Exception as problem: | |
| print("конвейер упал: %s" % problem, flush=True) | |
| outcome.put(None) | |
| threading.Thread(target=worker, daemon=True).start() | |
| self.send_response(200) | |
| self.send_header("Content-Type", "text/event-stream") | |
| try: | |
| self.begin_stream() | |
| except Exception: | |
| return | |
| payload = None | |
| try: | |
| while True: | |
| try: | |
| payload = outcome.get(timeout=self.ping_seconds) | |
| break | |
| except queue.Empty: | |
| self.emit(b": ping\n\n") | |
| if payload is None: | |
| payload = fusion.to_sse([]) | |
| print("ОТВЕТ %s | конвейер %.1f с | %d байт" | |
| % (self.path.split("?")[0], | |
| time.time() - getattr(self, "_started", time.time()), | |
| len(payload)), flush=True) | |
| self.emit(payload) | |
| self.emit(b": done\n\n") | |
| except Exception: | |
| pass | |
| finally: | |
| self.end_stream() | |
| def stream_events(self, answer): | |
| """Переливаем SSE, вставляя ": ping" во время молчания модели.""" | |
| lines: queue.Queue = queue.Queue() | |
| def pump(): | |
| try: | |
| while True: | |
| line = answer.readline() | |
| if not line: | |
| break | |
| lines.put(line) | |
| except Exception: | |
| pass | |
| finally: | |
| lines.put(None) | |
| threading.Thread(target=pump, daemon=True).start() | |
| while True: | |
| try: | |
| line = lines.get(timeout=PING_SECONDS) | |
| except queue.Empty: | |
| try: | |
| self.emit(b": ping\n\n") | |
| except Exception: | |
| return | |
| continue | |
| if line is None: | |
| return | |
| try: | |
| self.emit(line) | |
| except Exception: | |
| return | |
| def main(): | |
| parser = argparse.ArgumentParser(description="SSE-прокси MuScriptor с heartbeat") | |
| parser.add_argument("--listen", type=int, default=8223) | |
| parser.add_argument("--target", type=int, default=8222) | |
| parser.add_argument("--ping", type=float, default=PING_SECONDS) | |
| parser.add_argument("--font-cdn", default=os.environ.get("MUSCRIPTOR_FONT_CDN", FONT_CDN), | |
| help="адрес звукового банка на CDN; none — отдавать с сервера") | |
| parser.add_argument("--multi-pass", default="", | |
| help="второй проход с этим набором инструментов и слияние," | |
| " например: chromatic_percussion,electric_bass,acoustic_bass") | |
| parser.add_argument("--no-articulation", action="store_true", | |
| help="не пересчитывать концы нот и не ставить легато/стакато") | |
| parser.add_argument("--energy-threshold", type=float, default=6.0, | |
| help="во сколько раз тон должен превышать соседние полутона;" | |
| " 0 = не проверять") | |
| parser.add_argument("--no-velocity", action="store_true", | |
| help="не мерить громкость, оставить константу 100") | |
| parser.add_argument("--instruments", default="band", | |
| help="какие инструменты просить у модели: auto (по умолчанию," | |
| " модель решает сама), band (8 проверенных групп) или список") | |
| args = parser.parse_args() | |
| Proxy.target_port = args.target | |
| Proxy.ping_seconds = args.ping | |
| Proxy.font_cdn = "" if (args.font_cdn or "").lower() in ("none", "off", "-") else args.font_cdn | |
| Proxy.measure_velocity = not args.no_velocity | |
| Proxy.multi_pass, warning = guard_instruments( | |
| expand_instruments(args.multi_pass), "--multi-pass") | |
| if warning: | |
| print(warning, flush=True) | |
| Proxy.energy_threshold = args.energy_threshold | |
| Proxy.refine_articulation = not args.no_articulation | |
| Proxy.instruments, warning = guard_instruments( | |
| expand_instruments(args.instruments), "--instruments") | |
| if warning: | |
| print(warning, flush=True) | |
| server = ThreadingHTTPServer(("0.0.0.0", args.listen), Proxy) | |
| server.daemon_threads = True | |
| print("прокси: 0.0.0.0:%d -> 127.0.0.1:%d" % (args.listen, args.target), flush=True) | |
| server.serve_forever() | |
| if __name__ == "__main__": | |
| main() | |