Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ audio_cache/
# vscode
.vscode/launch.json

# Jetbrains
.idea/

# Local voice samples
voice_samples/
Expand Down Expand Up @@ -103,4 +105,8 @@ inject_derived
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.apdisk

# FRP binary

frpc_*
40 changes: 27 additions & 13 deletions owl/core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]


Expand Down Expand Up @@ -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
###################################################################################################
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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."""
Expand All @@ -202,6 +207,7 @@ def create_migration(config: Configuration, message: str):

console.log(f"[bold green]Migration script generated with message: '{message}'")


####################################################################################################
# Server
####################################################################################################
Expand All @@ -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:
Expand All @@ -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()
cli()
3 changes: 2 additions & 1 deletion owl/server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .app_state import AppState
from .main import create_server_app
from .main import create_server_app
from .networking import setup_tunnel
20 changes: 20 additions & 0 deletions owl/server/networking.py
Original file line number Diff line number Diff line change
@@ -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
136 changes: 136 additions & 0 deletions owl/server/tunneling.py
Original file line number Diff line number Diff line change
@@ -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
11 changes: 6 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down