mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 15:15:27 -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
|
dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32
|
||||||
self.model = AutoModelForCausalLM.from_pretrained(
|
self.model = AutoModelForCausalLM.from_pretrained(
|
||||||
repo,
|
repo,
|
||||||
torch_dtype=dtype,
|
dtype=dtype,
|
||||||
)
|
)
|
||||||
self.model.to(self.device)
|
self.model.to(self.device)
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Adding a new migration:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
from sqlalchemy import inspect, text
|
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
|
# Leaving the unused column in place is harmless — the ORM
|
||||||
# only maps declared columns, so a stray one does no work
|
# only maps declared columns, so a stray one does no work
|
||||||
# and gets no reads or writes.
|
# and gets no reads or writes.
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"SQLite %s too old to DROP COLUMN (need 3.35+); leaving unused default_intent column on mcp_client_bindings in place.",
|
"SQLite %s too old to DROP COLUMN (need 3.35+); leaving unused default_intent column on mcp_client_bindings in place.",
|
||||||
sqlite3.sqlite_version,
|
sqlite3.sqlite_version,
|
||||||
@@ -280,8 +279,6 @@ def _supports_drop_column(engine) -> bool:
|
|||||||
decades; SQLite only gained the feature in 3.35."""
|
decades; SQLite only gained the feature in 3.35."""
|
||||||
if engine.dialect.name != "sqlite":
|
if engine.dialect.name != "sqlite":
|
||||||
return True
|
return True
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
return tuple(int(p) for p in sqlite3.sqlite_version.split(".")[:3]) >= (3, 35, 0)
|
return tuple(int(p) for p in sqlite3.sqlite_version.split(".")[:3]) >= (3, 35, 0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import asyncio
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
@@ -141,7 +141,7 @@ def _stamp_last_seen(client_id: str) -> None:
|
|||||||
if row is None:
|
if row is None:
|
||||||
row = MCPClientBinding(client_id=client_id)
|
row = MCPClientBinding(client_id=client_id)
|
||||||
db.add(row)
|
db.add(row)
|
||||||
row.last_seen_at = datetime.utcnow()
|
row.last_seen_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from contextlib import AsyncExitStack, asynccontextmanager
|
from contextlib import AsyncExitStack, asynccontextmanager
|
||||||
from typing import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ async def speak_events(request: Request):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
event = await asyncio.wait_for(queue.get(), timeout=15.0)
|
event = await asyncio.wait_for(queue.get(), timeout=15.0)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
# Heartbeat so proxies don't reap idle streams.
|
# Heartbeat so proxies don't reap idle streams.
|
||||||
yield {"event": "ping", "data": "{}"}
|
yield {"event": "ping", "data": "{}"}
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from .. import models
|
from .. import models
|
||||||
from ..services import history, personality, profiles, tts
|
from ..services import history, personality, profiles, tts
|
||||||
from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db
|
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 ..services.task_queue import cancel_generation as cancel_generation_job, enqueue_generation
|
||||||
from ..utils.tasks import get_task_manager
|
from ..utils.tasks import get_task_manager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""LLM inference endpoints."""
|
"""LLM inference endpoints."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
@@ -9,6 +11,8 @@ from ..services import llm
|
|||||||
from ..services.task_queue import create_background_task
|
from ..services.task_queue import create_background_task
|
||||||
from ..utils.tasks import get_task_manager
|
from ..utils.tasks import get_task_manager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@@ -70,4 +74,7 @@ async def llm_generate(request: models.LLMGenerateRequest):
|
|||||||
)
|
)
|
||||||
return models.LLMGenerateResponse(text=text, model_size=model_size)
|
return models.LLMGenerateResponse(text=text, model_size=model_size)
|
||||||
except Exception as e:
|
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``).
|
(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 fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -56,7 +56,7 @@ async def upsert_mcp_binding(
|
|||||||
row.profile_id = data.profile_id
|
row.profile_id = data.profile_id
|
||||||
row.default_engine = data.default_engine
|
row.default_engine = data.default_engine
|
||||||
row.default_personality = data.default_personality
|
row.default_personality = data.default_personality
|
||||||
row.updated_at = datetime.utcnow()
|
row.updated_at = datetime.now(timezone.utc)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(row)
|
db.refresh(row)
|
||||||
return models.MCPClientBindingResponse.model_validate(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.
|
``data/captures/<id>.wav`` and rows live in the ``captures`` table.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
@@ -27,6 +28,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
VALID_SOURCES = {"dictation", "recording", "file"}
|
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:
|
def _to_response(row: DBCapture) -> CaptureResponse:
|
||||||
@@ -91,13 +96,11 @@ async def create_capture(
|
|||||||
audio, sr = None, None
|
audio, sr = None, None
|
||||||
duration_ms = None
|
duration_ms = None
|
||||||
|
|
||||||
_WHISPER_NATIVE_FORMATS = (".wav", ".mp3", ".flac", ".ogg")
|
|
||||||
|
|
||||||
if audio is None or sr is None:
|
if audio is None or sr is None:
|
||||||
# Decode failed. Only pass the file straight to whisper if the
|
# Decode failed. Only pass the file straight to whisper if the
|
||||||
# source is a format its miniaudio loader can still read — webm,
|
# source is a format its miniaudio loader can still read — webm,
|
||||||
# m4a, etc. would just 500 later. Surface a clean error instead.
|
# 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(
|
raise ValueError(
|
||||||
f"Could not decode {suffix} audio — the recording may be empty or corrupt"
|
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"
|
audio_path = config.get_captures_dir() / f"{capture_id}.wav"
|
||||||
sf.write(str(audio_path), audio, sr, format="WAV")
|
sf.write(str(audio_path), audio, sr, format="WAV")
|
||||||
written_files.append(audio_path)
|
written_files.append(audio_path)
|
||||||
try:
|
with contextlib.suppress(OSError):
|
||||||
raw_path.unlink()
|
raw_path.unlink()
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
written_files.remove(raw_path)
|
written_files.remove(raw_path)
|
||||||
|
|
||||||
whisper = get_whisper_model()
|
whisper = get_whisper_model()
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, Optional
|
from collections.abc import Iterable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user