Add Tauri v2 + Svelte 5 frontend and headless Python backend
Scaffold the cross-platform rewrite from PySide6/Qt to Tauri + Svelte, following the same architecture as voice-to-notes. The Python backend runs headless as a sidecar, with a FastAPI control API that the Svelte frontend connects to via REST and WebSocket. New files: - backend/app_controller.py: Headless orchestration (extracted from MainWindow) - backend/api_server.py: FastAPI control endpoints + /ws/control WebSocket - backend/main_headless.py: Headless entry point for sidecar mode - src-tauri/: Tauri v2 Rust shell with sidecar and dialog plugins - src/: Svelte 5 frontend (App, Settings, Controls, TranscriptionDisplay) - src/lib/stores/: Reactive stores for backend connection, config, transcriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
backend/main_headless.py
Normal file
126
backend/main_headless.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user