feat: API router + pipeline — toggle, status, transcripts, WebSocket, auth stub
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
||||
import asyncio
|
||||
import os
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
|
||||
|
||||
from api.state import state, Status
|
||||
from config import load as load_config
|
||||
from output import list_transcripts
|
||||
|
||||
router = APIRouter()
|
||||
_ws_clients: list[WebSocket] = []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auth stub — replaced by auth.py in Task 13.
|
||||
# current_user() returns a dict with at least {"username": str, "output_dir": str}.
|
||||
# Until auth is wired up, every request runs as an anonymous guest using the
|
||||
# global output path from config.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _guest_user() -> dict:
|
||||
cfg = load_config()
|
||||
return {"username": "guest", "output_dir": cfg["output"]["path"]}
|
||||
|
||||
|
||||
def current_user(user: dict = Depends(_guest_user)) -> dict:
|
||||
return user
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status():
|
||||
return {"status": state.status}
|
||||
|
||||
|
||||
@router.post("/toggle")
|
||||
async def toggle_recording(user: dict = Depends(current_user)):
|
||||
from api.pipeline import run_pipeline
|
||||
if state.status == Status.RECORDING:
|
||||
asyncio.create_task(run_pipeline())
|
||||
return {"action": "stopped"}
|
||||
if state.status == Status.IDLE:
|
||||
from audio import AudioRecorder
|
||||
state._recorder = AudioRecorder()
|
||||
state._recorder.start()
|
||||
state.recording_user = user["username"]
|
||||
state._recording_output_dir = user["output_dir"]
|
||||
state._recording_instructions = user.get("instructions", "")
|
||||
await state.set_status(Status.RECORDING)
|
||||
return {"action": "started"}
|
||||
return {"action": "busy", "status": state.status}
|
||||
|
||||
|
||||
@router.post("/instructions")
|
||||
async def set_instructions(body: dict, user: dict = Depends(current_user)):
|
||||
# Instructions are per-user; stored on state only for the active recording.
|
||||
# Full per-user persistence comes in Task 13.
|
||||
user["instructions"] = body.get("instructions", "")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/transcripts")
|
||||
async def get_transcripts(user: dict = Depends(current_user)):
|
||||
return list_transcripts(user["output_dir"])
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
async def get_config():
|
||||
return load_config()
|
||||
|
||||
|
||||
@router.put("/config")
|
||||
async def put_config(body: dict):
|
||||
cfg = load_config()
|
||||
cfg.update(body)
|
||||
return cfg
|
||||
|
||||
|
||||
@router.post("/open")
|
||||
async def open_file(body: dict):
|
||||
import subprocess
|
||||
path = body.get("path", "")
|
||||
if path and os.path.exists(path):
|
||||
subprocess.Popen(["xdg-open", path])
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(ws: WebSocket):
|
||||
await ws.accept()
|
||||
_ws_clients.append(ws)
|
||||
try:
|
||||
while True:
|
||||
await ws.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
if ws in _ws_clients:
|
||||
_ws_clients.remove(ws)
|
||||
|
||||
|
||||
async def broadcast(message: dict):
|
||||
for ws in list(_ws_clients):
|
||||
try:
|
||||
await ws.send_json(message)
|
||||
except Exception:
|
||||
if ws in _ws_clients:
|
||||
_ws_clients.remove(ws)
|
||||
Reference in New Issue
Block a user