mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
chore(backend): Ruff lint pass — deprecated APIs, exception leaks, dead patterns
Mechanical sweep of items called out in the PR review: - qwen_llm_backend: AutoModelForCausalLM.from_pretrained(torch_dtype=…) is deprecated in transformers ≥4.41 in favor of dtype=. Renamed. - routes/llm: try/except around backend.generate() raised HTTPException(500, detail=str(e)) which leaks stack traces / paths to clients and trips Ruff B904. Now logs the original exception server-side and hands the client a generic message; chained via `from e` to preserve traceback context. - mcp_bindings + mcp_server/context: datetime.utcnow() is deprecated since 3.12. Switched the two assignment sites to datetime.now(timezone.utc). The schema-level `default=datetime.utcnow` defaults in database/models.py are left for a later schema-aware pass. - routes/generations: `logger = …` sat between two import blocks (Ruff E402). Moved below imports. - mcp_server/server + tests/test_refinement_samples: typing.Callable / typing.Iterable have been preferred-via collections.abc since 3.9 (Ruff UP035). - routes/events: `except asyncio.TimeoutError` aliases plain `TimeoutError` since 3.11 (UP041). - services/captures: hoisted WHISPER_NATIVE_FORMATS to module scope (was a function-local UPPER_SNAKE that tripped N806) and replaced the raw_path.unlink try/except OSError-pass with contextlib.suppress (SIM105). Semantic equivalence preserved — written_files.remove(raw_path) still only runs when unlink succeeds because it sits inside the suppressed block after the unlink call. - database/migrations: hoisted the duplicate `import sqlite3` from inside two helper bodies to a single module-level import.
This commit is contained in:
@@ -108,7 +108,7 @@ class PyTorchQwenLLMBackend:
|
||||
dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32
|
||||
self.model = AutoModelForCausalLM.from_pretrained(
|
||||
repo,
|
||||
torch_dtype=dtype,
|
||||
dtype=dtype,
|
||||
)
|
||||
self.model.to(self.device)
|
||||
self.model.eval()
|
||||
|
||||
@@ -18,6 +18,7 @@ Adding a new migration:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
@@ -266,8 +267,6 @@ def _migrate_mcp_bindings(engine, inspector, tables: set[str]) -> None:
|
||||
# Leaving the unused column in place is harmless — the ORM
|
||||
# only maps declared columns, so a stray one does no work
|
||||
# and gets no reads or writes.
|
||||
import sqlite3
|
||||
|
||||
logger.warning(
|
||||
"SQLite %s too old to DROP COLUMN (need 3.35+); leaving unused default_intent column on mcp_client_bindings in place.",
|
||||
sqlite3.sqlite_version,
|
||||
@@ -280,8 +279,6 @@ def _supports_drop_column(engine) -> bool:
|
||||
decades; SQLite only gained the feature in 3.35."""
|
||||
if engine.dialect.name != "sqlite":
|
||||
return True
|
||||
import sqlite3
|
||||
|
||||
return tuple(int(p) for p in sqlite3.sqlite_version.split(".")[:3]) >= (3, 35, 0)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import asyncio
|
||||
import ipaddress
|
||||
import logging
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
@@ -141,7 +141,7 @@ def _stamp_last_seen(client_id: str) -> None:
|
||||
if row is None:
|
||||
row = MCPClientBinding(client_id=client_id)
|
||||
db.add(row)
|
||||
row.last_seen_at = datetime.utcnow()
|
||||
row.last_seen_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
except Exception:
|
||||
logger.debug(
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastmcp import FastMCP
|
||||
|
||||
@@ -34,7 +34,7 @@ async def speak_events(request: Request):
|
||||
return
|
||||
try:
|
||||
event = await asyncio.wait_for(queue.get(), timeout=15.0)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
# Heartbeat so proxies don't reap idle streams.
|
||||
yield {"event": "ping", "data": "{}"}
|
||||
continue
|
||||
|
||||
@@ -8,8 +8,6 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .. import models
|
||||
from ..services import history, personality, profiles, tts
|
||||
from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db
|
||||
@@ -17,6 +15,8 @@ from ..services.generation import run_generation
|
||||
from ..services.task_queue import cancel_generation as cancel_generation_job, enqueue_generation
|
||||
from ..utils.tasks import get_task_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""LLM inference endpoints."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
@@ -9,6 +11,8 @@ from ..services import llm
|
||||
from ..services.task_queue import create_background_task
|
||||
from ..utils.tasks import get_task_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -70,4 +74,7 @@ async def llm_generate(request: models.LLMGenerateRequest):
|
||||
)
|
||||
return models.LLMGenerateResponse(text=text, model_size=model_size)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
# The backend exception text can include filesystem paths and stack
|
||||
# frames — log it server-side and hand the client a generic message.
|
||||
logger.exception("LLM generate failed")
|
||||
raise HTTPException(status_code=500, detail="LLM generation failed") from e
|
||||
|
||||
@@ -6,7 +6,7 @@ column is the same value the MCP client sends in ``X-Voicebox-Client-Id``
|
||||
(or the stdio shim pulls from ``VOICEBOX_CLIENT_ID``).
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -56,7 +56,7 @@ async def upsert_mcp_binding(
|
||||
row.profile_id = data.profile_id
|
||||
row.default_engine = data.default_engine
|
||||
row.default_personality = data.default_personality
|
||||
row.updated_at = datetime.utcnow()
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return models.MCPClientBindingResponse.model_validate(row)
|
||||
|
||||
@@ -7,6 +7,7 @@ uploaded file). Storage mirrors the generations flow: audio lives under
|
||||
``data/captures/<id>.wav`` and rows live in the ``captures`` table.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
@@ -27,6 +28,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VALID_SOURCES = {"dictation", "recording", "file"}
|
||||
# Suffixes whisper's miniaudio loader can read directly. Anything outside
|
||||
# this set has to go through librosa for decode + a soundfile transcode
|
||||
# before whisper sees it.
|
||||
WHISPER_NATIVE_FORMATS = (".wav", ".mp3", ".flac", ".ogg")
|
||||
|
||||
|
||||
def _to_response(row: DBCapture) -> CaptureResponse:
|
||||
@@ -91,13 +96,11 @@ async def create_capture(
|
||||
audio, sr = None, None
|
||||
duration_ms = None
|
||||
|
||||
_WHISPER_NATIVE_FORMATS = (".wav", ".mp3", ".flac", ".ogg")
|
||||
|
||||
if audio is None or sr is None:
|
||||
# Decode failed. Only pass the file straight to whisper if the
|
||||
# source is a format its miniaudio loader can still read — webm,
|
||||
# m4a, etc. would just 500 later. Surface a clean error instead.
|
||||
if suffix not in _WHISPER_NATIVE_FORMATS:
|
||||
if suffix not in WHISPER_NATIVE_FORMATS:
|
||||
raise ValueError(
|
||||
f"Could not decode {suffix} audio — the recording may be empty or corrupt"
|
||||
)
|
||||
@@ -110,11 +113,8 @@ async def create_capture(
|
||||
audio_path = config.get_captures_dir() / f"{capture_id}.wav"
|
||||
sf.write(str(audio_path), audio, sr, format="WAV")
|
||||
written_files.append(audio_path)
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
raw_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
written_files.remove(raw_path)
|
||||
|
||||
whisper = get_whisper_model()
|
||||
|
||||
@@ -34,7 +34,8 @@ import sys
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Optional
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
Reference in New Issue
Block a user