From e10c8f4c1aa2c0fee6c5590daf8841493c945d30 Mon Sep 17 00:00:00 2001 From: Alhdo Date: Tue, 5 Mar 2024 16:21:18 +0100 Subject: [PATCH] Fix conflict with poetry.lock and pyproject.toml --- .gitignore | 8 ++- owl/core/cli.py | 40 ++++++++---- owl/server/__init__.py | 3 +- owl/server/networking.py | 20 ++++++ owl/server/tunneling.py | 136 +++++++++++++++++++++++++++++++++++++++ poetry.lock | 11 ++-- pyproject.toml | 1 + 7 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 owl/server/networking.py create mode 100644 owl/server/tunneling.py diff --git a/.gitignore b/.gitignore index 4e22b63c..bd8e8e85 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ audio_cache/ # vscode .vscode/launch.json +# Jetbrains +.idea/ # Local voice samples voice_samples/ @@ -103,4 +105,8 @@ inject_derived .AppleDesktop Network Trash Folder Temporary Items -.apdisk \ No newline at end of file +.apdisk + +# FRP binary + +frpc_* \ No newline at end of file diff --git a/owl/core/cli.py b/owl/core/cli.py index 12822ce2..43c2bb1a 100644 --- a/owl/core/cli.py +++ b/owl/core/cli.py @@ -32,15 +32,19 @@ def load_config_yaml(ctx, param, value) -> Configuration: return Configuration.load_config_yaml(value.name) + def add_options(options): def _add_options(func): for option in reversed(options): func = option(func) return func + return _add_options + _config_options = [ - click.option("--config", default="owl/sample_config.yaml", help="Configuration file", type=click.File(mode="r"), callback=load_config_yaml) + click.option("--config", default="owl/sample_config.yaml", help="Configuration file", type=click.File(mode="r"), + callback=load_config_yaml) ] @@ -87,7 +91,6 @@ def transcribe(config: Configuration, main_audio_filepath: str, voice_sample_fil console.log(f"[bold green]Transcription complete! Time taken: {time.time() - start_time:.2f} seconds") - ################################################################################################### # Summarize File ################################################################################################### @@ -151,9 +154,9 @@ def upload(config: Configuration, file: str, timestamp: datetime | None, device_ # Timestamp if timestamp is not None: try: - timestamp = datetime.strptime(timestamp, "%Y%m%d-%H%M%S.%f") + timestamp = datetime.strptime(timestamp, "%Y%m%d-%H%M%S.%f") except: - raise ValueError("'timestamp' string does not conform to YYYYmmdd-HHMMSS.fff format") + raise ValueError("'timestamp' string does not conform to YYYYmmdd-HHMMSS.fff format") else: timestamp = datetime.now(timezone.utc) @@ -162,9 +165,9 @@ def upload(config: Configuration, file: str, timestamp: datetime | None, device_ "Authorization": f"Bearer {config.user.client_token}" } data = { - "capture_uuid": capture_uuid, - "timestamp": timestamp.strftime("%Y%m%d-%H%M%S.%f")[:-3], - "device_type": device_type if device_type else "unknown" + "capture_uuid": capture_uuid, + "timestamp": timestamp.strftime("%Y%m%d-%H%M%S.%f")[:-3], + "device_type": device_type if device_type else "unknown" } files = { "file": (os.path.basename(file), file_contents) @@ -175,17 +178,19 @@ def upload(config: Configuration, file: str, timestamp: datetime | None, device_ if response.status_code != 200: print(f"Error {response.status_code}: {response.content}") else: - response = requests.post(url=f"http://{host}:{port}/capture/process_capture", data={ "capture_uuid": capture_uuid }, headers=headers) + response = requests.post(url=f"http://{host}:{port}/capture/process_capture", + data={"capture_uuid": capture_uuid}, headers=headers) if response.status_code != 200: print(f"Error {response.status_code}: {response.content}") print(response.content) + #################################################################################################### # Database #################################################################################################### - + @cli.command() -@add_options(_config_options) +@add_options(_config_options) @click.option('--message', '-m', required=True, help='Migration message') def create_migration(config: Configuration, message: str): """Generate a new migration script for schema changes.""" @@ -202,6 +207,7 @@ def create_migration(config: Configuration, message: str): console.log(f"[bold green]Migration script generated with message: '{message}'") + #################################################################################################### # Server #################################################################################################### @@ -211,9 +217,10 @@ def create_migration(config: Configuration, message: str): @click.option('--host', default='127.0.0.1', help='The interface to bind to.') @click.option('--port', default=8000, help='The port to bind to.') @click.option('--web', is_flag=True, help='Build and start the web frontend.') -def serve(config: Configuration, host, port, web): +@click.option('--share', is_flag=True, default=False, help='Create a public share url') +def serve(config: Configuration, host, port, web, share): """Start the server.""" - from .. import server + from .. import server console = Console() if web: @@ -231,7 +238,14 @@ def serve(config: Configuration, host, port, web): console.log(f"[bold green]Starting Python server at http://{host}:{port}...") app = server.create_server_app(config=config) + if share: + try: + share_url = server.setup_tunnel(host, port) + console.log(f"[bold green]Running on public URL: {share_url}") + except Exception as e: + console.log(e) uvicorn.run(app, host=host, port=port, log_level="info", ws_ping_interval=None, ws_ping_timeout=None) + if __name__ == '__main__': - cli() \ No newline at end of file + cli() diff --git a/owl/server/__init__.py b/owl/server/__init__.py index 674f5cdd..1e8e7d78 100644 --- a/owl/server/__init__.py +++ b/owl/server/__init__.py @@ -1,2 +1,3 @@ from .app_state import AppState -from .main import create_server_app \ No newline at end of file +from .main import create_server_app +from .networking import setup_tunnel \ No newline at end of file diff --git a/owl/server/networking.py b/owl/server/networking.py new file mode 100644 index 00000000..088c42de --- /dev/null +++ b/owl/server/networking.py @@ -0,0 +1,20 @@ +""" +Defines helper methods useful for creating tunnels. +""" +OWL_SHARE_SERVER_ADDRESS = "live.owlai.dev:7000" +from .tunneling import Tunnel +import secrets + +def setup_tunnel( + local_host: str, local_port: int +) -> str: + share_server_address = OWL_SHARE_SERVER_ADDRESS + remote_host, remote_port = share_server_address.split(":") + remote_port = int(remote_port) + + try: + tunnel = Tunnel(remote_host, remote_port, local_host, local_port, secrets.token_urlsafe(32)) + address = tunnel.start_tunnel() + return address + except Exception as e: + raise RuntimeError(str(e)) from e diff --git a/owl/server/tunneling.py b/owl/server/tunneling.py new file mode 100644 index 00000000..c719b4bd --- /dev/null +++ b/owl/server/tunneling.py @@ -0,0 +1,136 @@ +import atexit +import os +import platform +import re +import stat +import subprocess +import sys +import time +from pathlib import Path +from typing import List +import logging + +import httpx + +VERSION = "0.2" +CURRENT_TUNNELS: List["Tunnel"] = [] + +machine = platform.machine() +if machine == "x86_64": + machine = "amd64" + +BINARY_REMOTE_NAME = f"frpc_{platform.system().lower()}_{machine.lower()}" +EXTENSION = ".exe" if os.name == "nt" else "" +BINARY_URL = f"https://cdn-media.huggingface.co/frpc-gradio-{VERSION}/{BINARY_REMOTE_NAME}{EXTENSION}" + +BINARY_FILENAME = f"{BINARY_REMOTE_NAME}_v{VERSION}" +BINARY_FOLDER = Path(__file__).parent +BINARY_PATH = f"{BINARY_FOLDER / BINARY_FILENAME}" + +TUNNEL_TIMEOUT_SECONDS = 30 +TUNNEL_ERROR_MESSAGE = ( + "Could not create share URL. " + "Please check the appended log from frpc for more information:" +) + +logger = logging.getLogger(__name__) + +class Tunnel: + def __init__(self, remote_host, remote_port, local_host, local_port, share_token) -> None: + self.proc = None + self.url = None + self.remote_host = remote_host + self.remote_port = remote_port + self.local_host = local_host + self.local_port = local_port + self.share_token = share_token + @staticmethod + def download_binary(): + if not Path(BINARY_PATH).exists(): + resp = httpx.get(BINARY_URL, timeout=30) + + if resp.status_code == 403: + raise OSError( + f"Cannot set up a share link as this platform is incompatible. Please " + f"create a GitHub issue with information about your platform: {platform.uname()}" + ) + resp.raise_for_status() + + # Save file data to local copy + with open(BINARY_PATH, "wb") as file: + file.write(resp.content) + st = os.stat(BINARY_PATH) + os.chmod(BINARY_PATH, st.st_mode | stat.S_IEXEC) + + def start_tunnel(self) -> str: + self.download_binary() + self.url = self._start_tunnel(BINARY_PATH) + return self.url + + def kill(self): + if self.proc is not None: + logger.info(f"Killing tunnel {self.local_host}:{self.local_port} <> {self.url}") + self.proc.terminate() + self.proc = None + + def _start_tunnel(self, binary: str) -> str: + CURRENT_TUNNELS.append(self) + command = [ + binary, + "http", + "-n", + self.share_token, + "-l", + str(self.local_port), + "-i", + self.local_host, + "--uc", + "--sd", + "random", + "--ue", + "--server_addr", + f"{self.remote_host}:{self.remote_port}", + "--disable_log_color" + ] + self.proc = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + atexit.register(self.kill) + return self._read_url_from_tunnel_stream() + + def _read_url_from_tunnel_stream(self) -> str: + start_timestamp = time.time() + + log = [] + url = "" + + def _raise_tunnel_error(): + log_text = "\n".join(log) + logger.error(log_text) + raise ValueError(f"{TUNNEL_ERROR_MESSAGE}\n{log_text}") + + while url == "": + if time.time() - start_timestamp >= TUNNEL_TIMEOUT_SECONDS: + _raise_tunnel_error() + + assert self.proc is not None + if self.proc.stdout is None: + continue + + line = self.proc.stdout.readline() + line = line.decode("utf-8") + + if line == "": + continue + + log.append(line.strip()) + + if "start proxy success" in line: + result = re.search("start proxy success: (.+)\n", line) + if result is None: + _raise_tunnel_error() + else: + url = result.group(1) + elif "login to server failed" in line: + _raise_tunnel_error() + return url \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 0de9cc90..dc23ce35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1115,13 +1115,13 @@ trio = ["trio (>=0.22.0,<0.23.0)"] [[package]] name = "httpx" -version = "0.26.0" +version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, - {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, ] [package.dependencies] @@ -3130,6 +3130,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -4886,4 +4887,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "5b5a34049529f11ecc432fea0956d2b8b14d5942130ca7e2372f21a9c425fdcb" +content-hash = "20545a973bc61abca400deea04dd3b252a23e78a34e338fa74d7bb2c7de24321" diff --git a/pyproject.toml b/pyproject.toml index eb16164d..167d50c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ halo = "^0.0.31" alembic = "^1.13.1" faster-whisper = "^1.0.0" whisperx = {git = "https://github.com/m-bain/whisperx.git"} +httpx = "^0.27.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.4"