127 lines
3.9 KiB
Python
127 lines
3.9 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Headless entry point for the Local Transcription backend.
|
||
|
|
|
||
|
|
Runs the transcription engine + API server without any GUI (no PySide6).
|
||
|
|
Designed to be launched as a Tauri sidecar or run standalone for development.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python -m backend.main_headless [--port PORT] [--host HOST]
|
||
|
|
|
||
|
|
The backend prints the actual port to stdout as JSON on startup:
|
||
|
|
{"event": "ready", "port": 8080}
|
||
|
|
|
||
|
|
This allows the Tauri shell to discover which port the backend bound to.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import multiprocessing
|
||
|
|
import os
|
||
|
|
import signal
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
# Must be called before anything else for PyInstaller compatibility
|
||
|
|
multiprocessing.freeze_support()
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
multiprocessing.set_start_method('spawn', force=True)
|
||
|
|
except RuntimeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Add project root to path
|
||
|
|
project_root = Path(__file__).resolve().parent.parent
|
||
|
|
sys.path.insert(0, str(project_root))
|
||
|
|
os.chdir(project_root)
|
||
|
|
|
||
|
|
from client.instance_lock import InstanceLock
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(description="Local Transcription headless backend")
|
||
|
|
parser.add_argument("--host", default="127.0.0.1", help="API server host (default: 127.0.0.1)")
|
||
|
|
parser.add_argument("--port", type=int, default=8080, help="API server port (default: 8080)")
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
instance_lock = InstanceLock()
|
||
|
|
if not instance_lock.acquire():
|
||
|
|
print(json.dumps({"event": "error", "message": "Another instance is already running"}),
|
||
|
|
flush=True)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
def handle_shutdown(signum, frame):
|
||
|
|
print(json.dumps({"event": "shutdown"}), flush=True)
|
||
|
|
if controller:
|
||
|
|
controller.shutdown()
|
||
|
|
instance_lock.release()
|
||
|
|
sys.exit(0)
|
||
|
|
|
||
|
|
signal.signal(signal.SIGTERM, handle_shutdown)
|
||
|
|
signal.signal(signal.SIGINT, handle_shutdown)
|
||
|
|
|
||
|
|
controller = None
|
||
|
|
|
||
|
|
try:
|
||
|
|
from backend.app_controller import AppController
|
||
|
|
from backend.api_server import APIServer
|
||
|
|
|
||
|
|
# Override web server port from CLI arg
|
||
|
|
from client.config import Config
|
||
|
|
config = Config()
|
||
|
|
config.set('web_server.host', args.host)
|
||
|
|
config.set('web_server.port', args.port)
|
||
|
|
|
||
|
|
# Create controller and initialize
|
||
|
|
controller = AppController(config=config)
|
||
|
|
|
||
|
|
# Wire a state callback that prints the ready event
|
||
|
|
def on_state_changed(state, message):
|
||
|
|
event = {"event": "state", "state": state, "message": message}
|
||
|
|
print(json.dumps(event), flush=True)
|
||
|
|
|
||
|
|
controller.on_state_changed = on_state_changed
|
||
|
|
|
||
|
|
# Initialize engine + web server
|
||
|
|
controller.initialize()
|
||
|
|
|
||
|
|
# Create API server wrapping the controller
|
||
|
|
api_server = APIServer(controller)
|
||
|
|
|
||
|
|
# Determine actual port (web server may have shifted if port was in use)
|
||
|
|
actual_port = controller.actual_web_port or args.port
|
||
|
|
|
||
|
|
# Print ready event so Tauri can discover the port
|
||
|
|
print(json.dumps({"event": "ready", "port": actual_port}), flush=True)
|
||
|
|
|
||
|
|
# Run the API server (blocks)
|
||
|
|
import uvicorn
|
||
|
|
import logging
|
||
|
|
|
||
|
|
logging.getLogger("uvicorn").setLevel(logging.ERROR)
|
||
|
|
logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
|
||
|
|
|
||
|
|
uvicorn.run(
|
||
|
|
api_server.app,
|
||
|
|
host=args.host,
|
||
|
|
port=actual_port + 1, # API on port+1, OBS display on the main port
|
||
|
|
log_level="error",
|
||
|
|
access_log=False,
|
||
|
|
)
|
||
|
|
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print(json.dumps({"event": "shutdown", "reason": "keyboard_interrupt"}), flush=True)
|
||
|
|
except Exception as e:
|
||
|
|
print(json.dumps({"event": "error", "message": str(e)}), flush=True)
|
||
|
|
import traceback
|
||
|
|
traceback.print_exc()
|
||
|
|
sys.exit(1)
|
||
|
|
finally:
|
||
|
|
if controller:
|
||
|
|
controller.shutdown()
|
||
|
|
instance_lock.release()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|