Files
VoiceStudio/backend/api/routers/projects.py
T
Palash Debnath 34610ca091 feat: real-time WebSocket event bus + sidebar reactivity fixes (#27)
## Core Infrastructure
- Add backend event bus (core/event_bus.py) — in-memory pub/sub with
  emit(), subscribe(), unsubscribe()
- Add WebSocket endpoint /ws/events (api/routers/events.py) with 25s
  keepalive pings and auto-cleanup on disconnect
- Add frontend hook useRealtimeEvents.js — single WS connection with
  exponential backoff reconnect (2s→60s)

## Backend Event Integration
- projects.py: emit on create/update/delete
- profiles.py: emit on create/update/lock/unlock/delete
- dub_core.py: emit on clear/delete history
- dub_pipeline.py: emit on save_job (every pipeline write)
- exports.py: emit on export/record
- generation.py: emit on generate/clear/delete
- gallery.py: emit on save-as-profile/to-profile

## Frontend Improvements
- Replace 45s polling interval with instant WS-based invalidation
- Fix critical bug: apiModelStatus was undefined, causing loadAll()
  to loop forever — sidebar data never loaded on startup
- Add websockets to main deps (was optional, got removed by uv sync)
- Reduce model/status polling from 5s to 10s, disable background
  polling for logs
- Add ReadinessChecklist and FloatingPill components
- Default UI scale changed from S (1.0) to M (1.3)

## Dependencies
- Add websockets>=16.0 to main dependencies for uvicorn WS support

Closes #3 (native desktop app exists via Tauri)
Closes #5 (Dockerfile already uses root bun.lock)
Resolves #26 (Triton workaround documented)
2026-04-27 22:16:28 +05:30

77 lines
2.7 KiB
Python

import uuid
import time
import json
from fastapi import APIRouter, HTTPException
from core.db import get_db
from core import event_bus
from schemas.requests import ProjectSaveRequest
router = APIRouter()
@router.get("/projects")
async def list_projects():
conn = get_db()
rows = conn.execute(
"SELECT id, name, video_path, duration, created_at, updated_at FROM studio_projects ORDER BY updated_at DESC"
).fetchall()
conn.close()
return [dict(r) for r in rows]
@router.get("/projects/{project_id}")
async def get_project(project_id: str):
conn = get_db()
row = conn.execute("SELECT * FROM studio_projects WHERE id=?", (project_id,)).fetchone()
conn.close()
if not row:
raise HTTPException(status_code=404, detail="Project not found")
result = dict(row)
if result.get("state_json"):
try:
result["state"] = json.loads(result["state_json"])
except Exception:
result["state"] = {}
else:
result["state"] = {}
return result
@router.post("/projects")
async def create_project(req: ProjectSaveRequest):
project_id = str(uuid.uuid4())[:8]
now = time.time()
conn = get_db()
conn.execute(
"INSERT INTO studio_projects (id, name, video_path, audio_path, duration, state_json, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
(project_id, req.name, req.video_path, req.audio_path, req.duration, json.dumps(req.state), now, now),
)
conn.commit()
conn.close()
event_bus.emit("projects", {"action": "created", "id": project_id})
return {"id": project_id, "name": req.name, "created_at": now}
@router.put("/projects/{project_id}")
async def update_project(project_id: str, req: ProjectSaveRequest):
conn = get_db()
row = conn.execute("SELECT id FROM studio_projects WHERE id=?", (project_id,)).fetchone()
if not row:
conn.close()
raise HTTPException(status_code=404, detail="Project not found")
now = time.time()
conn.execute(
"UPDATE studio_projects SET name=?, video_path=?, audio_path=?, duration=?, state_json=?, updated_at=? WHERE id=?",
(req.name, req.video_path, req.audio_path, req.duration, json.dumps(req.state), now, project_id),
)
conn.commit()
conn.close()
event_bus.emit("projects", {"action": "updated", "id": project_id})
return {"id": project_id, "name": req.name, "updated_at": now}
@router.delete("/projects/{project_id}")
async def delete_project(project_id: str):
conn = get_db()
conn.execute("DELETE FROM studio_projects WHERE id=?", (project_id,))
conn.commit()
conn.close()
event_bus.emit("projects", {"action": "deleted", "id": project_id})
return {"deleted": project_id}