Files
local-transcription/backend/main_headless.py
Developer a8de39de84
All checks were successful
Release / Bump version and tag (push) Successful in 12s
Sidecar Release / Bump sidecar version and tag (push) Successful in 6s
Fix OBS display and Start button not working
Three issues fixed:

1. Port mismatch: The sidecar reported the OBS port (8080) in the
   ready event but the frontend needs the API port (8081). Now reports
   the API port so WebSocket/REST connects to the right place.

2. Broadcast from wrong thread: Engine init fires state_changed from
   a background thread, but _broadcast_control used get_event_loop()
   which returns the wrong loop. Now captures the uvicorn event loop
   at startup via on_event("startup").

3. Missed ready state: If the engine finishes before the WebSocket
   client connects, the "ready" state_changed was never received.
   Added status polling (GET /api/status) on WebSocket connect that
   retries every 2s while appState is "initializing".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:35:41 -07:00

132 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)
# OBS display runs on the configured port, API server on port+1
obs_port = controller.actual_web_port or args.port
api_port = obs_port + 1
# Print ready event so Tauri can discover the API port
print(json.dumps({
"event": "ready",
"port": api_port,
"obs_port": obs_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=api_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()