diff --git a/surfsense_backend/alembic/versions/158_evolve_podcasts_lifecycle.py b/surfsense_backend/alembic/versions/158_evolve_podcasts_lifecycle.py index 15cf04f9d..ae595cc1b 100644 --- a/surfsense_backend/alembic/versions/158_evolve_podcasts_lifecycle.py +++ b/surfsense_backend/alembic/versions/158_evolve_podcasts_lifecycle.py @@ -52,7 +52,9 @@ def upgrade() -> None: "ALTER TABLE podcasts ADD COLUMN IF NOT EXISTS storage_backend VARCHAR(32);" ) op.execute("ALTER TABLE podcasts ADD COLUMN IF NOT EXISTS storage_key TEXT;") - op.execute("ALTER TABLE podcasts ADD COLUMN IF NOT EXISTS duration_seconds INTEGER;") + op.execute( + "ALTER TABLE podcasts ADD COLUMN IF NOT EXISTS duration_seconds INTEGER;" + ) op.execute("ALTER TABLE podcasts ADD COLUMN IF NOT EXISTS error TEXT;") diff --git a/surfsense_backend/app/podcasts/api/routes.py b/surfsense_backend/app/podcasts/api/routes.py index 0a9a8e659..80e5e1c64 100644 --- a/surfsense_backend/app/podcasts/api/routes.py +++ b/surfsense_backend/app/podcasts/api/routes.py @@ -9,6 +9,8 @@ then enqueues the matching Celery task; lifecycle errors map to 409/422. from __future__ import annotations import os +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, Response @@ -27,10 +29,10 @@ from app.db import ( from app.podcasts.generation.brief import propose_brief from app.podcasts.persistence import Podcast, PodcastRepository from app.podcasts.service import ( - InvalidTransition, + InvalidTransitionError, PodcastService, - PreconditionFailed, - SpecConflict, + PreconditionFailedError, + SpecConflictError, ) from app.podcasts.storage import open_audio_stream, purge_audio from app.podcasts.tasks import draft_transcript_task @@ -324,19 +326,12 @@ async def _load( return podcast -class _lifecycle_errors: +@asynccontextmanager +async def _lifecycle_errors() -> AsyncIterator[None]: """Map service lifecycle errors onto HTTP responses.""" - - async def __aenter__(self) -> None: - return None - - async def __aexit__(self, exc_type, exc, tb) -> bool: - if exc is None: - return False - if isinstance(exc, SpecConflict): - raise HTTPException(status_code=409, detail=str(exc)) from exc - if isinstance(exc, InvalidTransition): - raise HTTPException(status_code=409, detail=str(exc)) from exc - if isinstance(exc, PreconditionFailed): - raise HTTPException(status_code=422, detail=str(exc)) from exc - return False + try: + yield + except (SpecConflictError, InvalidTransitionError) as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except PreconditionFailedError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc diff --git a/surfsense_backend/app/podcasts/generation/structured.py b/surfsense_backend/app/podcasts/generation/structured.py index bcc03a6c7..08132e776 100644 --- a/surfsense_backend/app/podcasts/generation/structured.py +++ b/surfsense_backend/app/podcasts/generation/structured.py @@ -23,7 +23,9 @@ class StructuredOutputError(RuntimeError): """The model reply could not be parsed into the expected shape.""" -async def invoke_json(llm, messages: list[BaseMessage], model: type[T]) -> T: +async def invoke_json[T: BaseModel]( + llm, messages: list[BaseMessage], model: type[T] +) -> T: """Invoke ``llm`` and validate its reply as ``model``.""" response = await llm.ainvoke(messages) content = strip_markdown_fences(extract_text_content(response.content)) diff --git a/surfsense_backend/app/podcasts/generation/transcript/nodes.py b/surfsense_backend/app/podcasts/generation/transcript/nodes.py index b4a3e6541..44d6b219d 100644 --- a/surfsense_backend/app/podcasts/generation/transcript/nodes.py +++ b/surfsense_backend/app/podcasts/generation/transcript/nodes.py @@ -18,7 +18,7 @@ from app.services.llm_service import get_agent_llm from ..prompts import draft_segment_prompt, plan_outline_prompt from ..structured import invoke_json from .config import TranscriptConfig -from .planning import Outline, OutlineSegment, SegmentDraft +from .planning import Outline, SegmentDraft from .state import TranscriptState # Average speaking rate; converts target minutes to a target word count. diff --git a/surfsense_backend/app/podcasts/rendering/merge.py b/surfsense_backend/app/podcasts/rendering/merge.py index 48771d17c..223295349 100644 --- a/surfsense_backend/app/podcasts/rendering/merge.py +++ b/surfsense_backend/app/podcasts/rendering/merge.py @@ -32,7 +32,7 @@ async def concat_to_mp3(segment_paths: list[Path], output_path: Path) -> None: .output(str(output_path), {"c:a": "libmp3lame"}) ) await ffmpeg.execute() - except Exception as exc: # noqa: BLE001 - normalise ffmpeg failures + except Exception as exc: raise RenderError(f"audio merge failed: {exc}") from exc finally: list_file.unlink(missing_ok=True) diff --git a/surfsense_backend/app/podcasts/rendering/renderer.py b/surfsense_backend/app/podcasts/rendering/renderer.py index 89a4e6b7d..44071c060 100644 --- a/surfsense_backend/app/podcasts/rendering/renderer.py +++ b/surfsense_backend/app/podcasts/rendering/renderer.py @@ -77,9 +77,7 @@ class PodcastRenderer: await concat_to_mp3(list(segment_paths), output_path) return RenderedPodcast(data=output_path.read_bytes(), container="mp3") - def _request_for( - self, spec: PodcastSpec, turn: TranscriptTurn - ) -> SynthesisRequest: + def _request_for(self, spec: PodcastSpec, turn: TranscriptTurn) -> SynthesisRequest: try: speaker = spec.speaker_for(turn.speaker) except KeyError as exc: @@ -132,7 +130,7 @@ class _SegmentSynthesizer: if owner: try: path = await self._synthesize(request, key) - except BaseException as exc: # noqa: BLE001 - relayed to all waiters + except BaseException as exc: future.set_exception(exc) else: future.set_result(path) diff --git a/surfsense_backend/app/podcasts/schemas/spec.py b/surfsense_backend/app/podcasts/schemas/spec.py index a2c68e359..1ef3dcfff 100644 --- a/surfsense_backend/app/podcasts/schemas/spec.py +++ b/surfsense_backend/app/podcasts/schemas/spec.py @@ -70,7 +70,9 @@ class SpeakerSpec(BaseModel): model_config = ConfigDict(extra="forbid") - slot: int = Field(..., ge=0, description="Stable index a transcript turn references") + slot: int = Field( + ..., ge=0, description="Stable index a transcript turn references" + ) name: str = Field(..., min_length=1, max_length=120) role: SpeakerRole voice_id: str = Field( diff --git a/surfsense_backend/app/podcasts/service.py b/surfsense_backend/app/podcasts/service.py index 1a2f3677b..165bc77a4 100644 --- a/surfsense_backend/app/podcasts/service.py +++ b/surfsense_backend/app/podcasts/service.py @@ -59,11 +59,11 @@ class PodcastError(RuntimeError): """Base class for lifecycle errors.""" -class InvalidTransition(PodcastError): +class InvalidTransitionError(PodcastError): """A requested status change is not permitted from the current state.""" -class SpecConflict(PodcastError): +class SpecConflictError(PodcastError): """A spec edit raced another: the expected version is stale.""" def __init__(self, expected: int, actual: int) -> None: @@ -74,7 +74,7 @@ class SpecConflict(PodcastError): self.actual = actual -class PreconditionFailed(PodcastError): +class PreconditionFailedError(PodcastError): """A transition's data precondition (brief/transcript present) is unmet.""" @@ -110,12 +110,12 @@ class PodcastService: ) -> Podcast: """Edit the brief at the gate, guarded by optimistic concurrency.""" if _status(podcast) is not PodcastStatus.AWAITING_BRIEF: - raise InvalidTransition( + raise InvalidTransitionError( f"the brief can only be edited while awaiting_brief, " f"not {_status(podcast).value}" ) if expected_version != podcast.spec_version: - raise SpecConflict(expected_version, podcast.spec_version) + raise SpecConflictError(expected_version, podcast.spec_version) podcast.spec = spec.model_dump(mode="json") podcast.spec_version += 1 await self._session.flush() @@ -124,7 +124,7 @@ class PodcastService: async def begin_drafting(self, podcast: Podcast) -> Podcast: """Approve the brief and start transcript drafting.""" if podcast.spec is None: - raise PreconditionFailed("cannot draft without a brief") + raise PreconditionFailedError("cannot draft without a brief") self._transition(podcast, PodcastStatus.DRAFTING) await self._session.flush() return podcast @@ -145,13 +145,13 @@ class PodcastService: async def regenerate(self, podcast: Podcast) -> Podcast: """Reopen the brief gate; the saved spec becomes the new starting point.""" if _status(podcast) not in self._REGENERABLE: - raise InvalidTransition( + raise InvalidTransitionError( f"nothing to regenerate from {_status(podcast).value}" ) # Legacy episodes finished before briefs existed; a gate with nothing # to review would strand them. if podcast.spec is None: - raise PreconditionFailed("cannot regenerate without a brief") + raise PreconditionFailedError("cannot regenerate without a brief") self._transition(podcast, PodcastStatus.AWAITING_BRIEF) await self._session.flush() return podcast @@ -164,7 +164,7 @@ class PodcastService: has no regeneration to revert and is rejected. """ if not has_stored_episode(podcast): - raise InvalidTransition("no finished episode to fall back to") + raise InvalidTransitionError("no finished episode to fall back to") self._transition(podcast, PodcastStatus.READY) await self._session.flush() return podcast @@ -200,7 +200,7 @@ class PodcastService: backing out goes through revert_regeneration instead. """ if has_stored_episode(podcast): - raise InvalidTransition( + raise InvalidTransitionError( "a finished episode exists; revert the regeneration instead" ) self._transition(podcast, PodcastStatus.CANCELLED) @@ -210,7 +210,7 @@ class PodcastService: def _transition(self, podcast: Podcast, target: PodcastStatus) -> None: current = _status(podcast) if target not in _ALLOWED[current]: - raise InvalidTransition( + raise InvalidTransitionError( f"{current.value} -> {target.value} is not allowed" ) podcast.status = target diff --git a/surfsense_backend/app/podcasts/tasks/draft.py b/surfsense_backend/app/podcasts/tasks/draft.py index 8779f6ce1..c5b489571 100644 --- a/surfsense_backend/app/podcasts/tasks/draft.py +++ b/surfsense_backend/app/podcasts/tasks/draft.py @@ -36,9 +36,10 @@ def draft_transcript_task(self, podcast_id: int, search_space_id: int) -> dict: return run_async_celery_task( lambda: _draft_transcript(podcast_id, search_space_id) ) - except Exception as exc: # noqa: BLE001 - record and report, never crash worker + except Exception as exc: logger.error("Podcast %s drafting failed: %s", podcast_id, exc) - run_async_celery_task(lambda: mark_failed(podcast_id, str(exc))) + message = str(exc) + run_async_celery_task(lambda: mark_failed(podcast_id, message)) return {"status": "failed", "podcast_id": podcast_id} diff --git a/surfsense_backend/app/podcasts/tasks/render.py b/surfsense_backend/app/podcasts/tasks/render.py index 8afe53b83..2e550a868 100644 --- a/surfsense_backend/app/podcasts/tasks/render.py +++ b/surfsense_backend/app/podcasts/tasks/render.py @@ -15,7 +15,7 @@ from app.celery_app import celery_app from app.podcasts.persistence import PodcastRepository from app.podcasts.rendering import PodcastRenderer from app.podcasts.service import ( - InvalidTransition, + InvalidTransitionError, PodcastService, read_spec, read_transcript, @@ -36,9 +36,10 @@ _WORKDIR_BASE = Path(tempfile.gettempdir()) / "surfsense_podcasts" def render_audio_task(self, podcast_id: int) -> dict: try: return run_async_celery_task(lambda: _render_audio(podcast_id)) - except Exception as exc: # noqa: BLE001 - record and report, never crash worker + except Exception as exc: logger.error("Podcast %s render failed: %s", podcast_id, exc) - run_async_celery_task(lambda: mark_failed(podcast_id, str(exc))) + message = str(exc) + run_async_celery_task(lambda: mark_failed(podcast_id, message)) return {"status": "failed", "podcast_id": podcast_id} @@ -75,7 +76,7 @@ async def _render_audio(podcast_id: int) -> dict: podcast, storage_backend=backend_name, storage_key=key ) await session.commit() - except InvalidTransition: + except InvalidTransitionError: # A user back-out won the race (e.g. the regeneration was # reverted): drop the stale render and leave the row alone. await purge_audio_object(key) diff --git a/surfsense_backend/app/podcasts/tts/adapters/kokoro.py b/surfsense_backend/app/podcasts/tts/adapters/kokoro.py index 031b48e86..2ef0069c5 100644 --- a/surfsense_backend/app/podcasts/tts/adapters/kokoro.py +++ b/surfsense_backend/app/podcasts/tts/adapters/kokoro.py @@ -50,9 +50,7 @@ class KokoroTextToSpeech(TextToSpeech): async def synthesize(self, request: SynthesisRequest) -> SynthesizedAudio: if not isinstance(request.voice, str): - raise TextToSpeechError( - "Kokoro voices are named by string, not a mapping" - ) + raise TextToSpeechError("Kokoro voices are named by string, not a mapping") pipeline = self._pipeline_for(request.language) loop = asyncio.get_event_loop() @@ -67,7 +65,7 @@ class KokoroTextToSpeech(TextToSpeech): ), ) segments = [audio for _gs, _ps, audio in generator] - except Exception as exc: # noqa: BLE001 - normalise provider errors + except Exception as exc: raise TextToSpeechError(f"Kokoro synthesis failed: {exc}") from exc if not segments: diff --git a/surfsense_backend/app/podcasts/tts/adapters/litellm.py b/surfsense_backend/app/podcasts/tts/adapters/litellm.py index 181973b47..d0014c5cd 100644 --- a/surfsense_backend/app/podcasts/tts/adapters/litellm.py +++ b/surfsense_backend/app/podcasts/tts/adapters/litellm.py @@ -57,10 +57,8 @@ class LiteLlmTextToSpeech(TextToSpeech): try: response = await aspeech(**kwargs) - except Exception as exc: # noqa: BLE001 - normalise provider errors - raise TextToSpeechError( - f"{self._model} synthesis failed: {exc}" - ) from exc + except Exception as exc: + raise TextToSpeechError(f"{self._model} synthesis failed: {exc}") from exc data = getattr(response, "content", None) if not data: diff --git a/surfsense_backend/app/podcasts/voices/catalog.py b/surfsense_backend/app/podcasts/voices/catalog.py index 28914e742..c36313a0c 100644 --- a/surfsense_backend/app/podcasts/voices/catalog.py +++ b/surfsense_backend/app/podcasts/voices/catalog.py @@ -36,9 +36,7 @@ class VoiceCatalog: """All voices offered by ``provider``, in catalog order.""" return list(self._by_provider.get(provider, ())) - def for_language( - self, provider: TtsProvider, language: str - ) -> list[CatalogVoice]: + def for_language(self, provider: TtsProvider, language: str) -> list[CatalogVoice]: """``provider`` voices that can render ``language``, in catalog order.""" return [v for v in self.for_provider(provider) if v.speaks(language)] @@ -50,6 +48,4 @@ class VoiceCatalog: @lru_cache(maxsize=1) def get_voice_catalog() -> VoiceCatalog: """The process-wide catalog assembled from every provider's roster.""" - return VoiceCatalog( - (*KOKORO_VOICES, *OPENAI_VOICES, *AZURE_VOICES, *VERTEX_VOICES) - ) + return VoiceCatalog((*KOKORO_VOICES, *OPENAI_VOICES, *AZURE_VOICES, *VERTEX_VOICES)) diff --git a/surfsense_backend/app/podcasts/voices/data/vertex.py b/surfsense_backend/app/podcasts/voices/data/vertex.py index 8452a00ff..99477eb21 100644 --- a/surfsense_backend/app/podcasts/voices/data/vertex.py +++ b/surfsense_backend/app/podcasts/voices/data/vertex.py @@ -30,10 +30,52 @@ def _voice( VERTEX_VOICES: tuple[CatalogVoice, ...] = ( - _voice("en-US-Studio-O", "en-US", "en-US", "en-US-Studio-O", "Studio O (US)", VoiceGender.FEMALE), - _voice("en-US-Studio-M", "en-US", "en-US", "en-US-Studio-M", "Studio M (US)", VoiceGender.MALE), - _voice("en-GB-Studio-A", "en-GB", "en-UK", "en-UK-Studio-A", "Studio A (UK)", VoiceGender.FEMALE), - _voice("en-GB-Studio-B", "en-GB", "en-UK", "en-UK-Studio-B", "Studio B (UK)", VoiceGender.MALE), - _voice("en-AU-Studio-A", "en-AU", "en-AU", "en-AU-Studio-A", "Studio A (AU)", VoiceGender.FEMALE), - _voice("en-AU-Studio-B", "en-AU", "en-AU", "en-AU-Studio-B", "Studio B (AU)", VoiceGender.MALE), + _voice( + "en-US-Studio-O", + "en-US", + "en-US", + "en-US-Studio-O", + "Studio O (US)", + VoiceGender.FEMALE, + ), + _voice( + "en-US-Studio-M", + "en-US", + "en-US", + "en-US-Studio-M", + "Studio M (US)", + VoiceGender.MALE, + ), + _voice( + "en-GB-Studio-A", + "en-GB", + "en-UK", + "en-UK-Studio-A", + "Studio A (UK)", + VoiceGender.FEMALE, + ), + _voice( + "en-GB-Studio-B", + "en-GB", + "en-UK", + "en-UK-Studio-B", + "Studio B (UK)", + VoiceGender.MALE, + ), + _voice( + "en-AU-Studio-A", + "en-AU", + "en-AU", + "en-AU-Studio-A", + "Studio A (AU)", + VoiceGender.FEMALE, + ), + _voice( + "en-AU-Studio-B", + "en-AU", + "en-AU", + "en-AU-Studio-B", + "Studio B (AU)", + VoiceGender.MALE, + ), ) diff --git a/surfsense_backend/app/podcasts/voices/preview.py b/surfsense_backend/app/podcasts/voices/preview.py index cb70a9f0b..868504a91 100644 --- a/surfsense_backend/app/podcasts/voices/preview.py +++ b/surfsense_backend/app/podcasts/voices/preview.py @@ -30,7 +30,7 @@ _SAMPLE_TEXTS = { "it": "Ciao! Questa è la mia voce quando racconto il tuo podcast.", "ja": "こんにちは。ポッドキャストをお届けするときの私の声です。", "pt": "Olá! É assim que eu soo ao narrar o seu podcast.", - "zh": "你好!这就是我为你播报播客时的声音。", + "zh": "你好!这就是我为你播报播客时的声音。", # noqa: RUF001 } _CONTENT_TYPES = {"mp3": "audio/mpeg", "wav": "audio/wav"} @@ -40,9 +40,7 @@ async def render_voice_preview( voice: CatalogVoice, tts: TextToSpeech ) -> tuple[bytes, str]: """Return ``(audio_bytes, content_type)`` for a sample spoken by ``voice``.""" - language = ( - _FALLBACK_LANGUAGE if voice.language == ANY_LANGUAGE else voice.language - ) + language = _FALLBACK_LANGUAGE if voice.language == ANY_LANGUAGE else voice.language request = SynthesisRequest( text=_sample_text(language), voice=voice.native_ref, language=language ) diff --git a/surfsense_backend/app/tasks/chat/streaming/handlers/tools/deliverables/generate_podcast/emission.py b/surfsense_backend/app/tasks/chat/streaming/handlers/tools/deliverables/generate_podcast/emission.py index 86f453bc7..b21357b50 100644 --- a/surfsense_backend/app/tasks/chat/streaming/handlers/tools/deliverables/generate_podcast/emission.py +++ b/surfsense_backend/app/tasks/chat/streaming/handlers/tools/deliverables/generate_podcast/emission.py @@ -36,7 +36,11 @@ def iter_completion_emission_frames( "success", ) elif status in ("failed", "error"): - error_msg = out.get("error", "Unknown error") if isinstance(out, dict) else "Unknown error" + error_msg = ( + out.get("error", "Unknown error") + if isinstance(out, dict) + else "Unknown error" + ) yield ctx.streaming_service.format_terminal_info( f"Podcast generation failed: {error_msg}", "error", diff --git a/surfsense_backend/tests/integration/podcasts/conftest.py b/surfsense_backend/tests/integration/podcasts/conftest.py index 330bcbef0..f244c17d2 100644 --- a/surfsense_backend/tests/integration/podcasts/conftest.py +++ b/surfsense_backend/tests/integration/podcasts/conftest.py @@ -26,7 +26,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.app import app, limiter from app.config import config as app_config from app.db import SearchSpace, User, get_async_session -from app.routes.search_spaces_routes import create_default_roles_and_membership from app.podcasts.persistence import Podcast, PodcastStatus from app.podcasts.schemas import ( DurationTarget, @@ -39,6 +38,7 @@ from app.podcasts.schemas import ( ) from app.podcasts.service import PodcastService from app.podcasts.tts import SynthesisRequest, SynthesizedAudio, TextToSpeech +from app.routes.search_spaces_routes import create_default_roles_and_membership from app.users import current_active_user pytestmark = pytest.mark.integration @@ -128,12 +128,8 @@ class FakeStorageBackend: def fake_storage(monkeypatch) -> FakeStorageBackend: """Route audio storage to an in-memory backend for the stream routes.""" backend = FakeStorageBackend() - monkeypatch.setattr( - "app.podcasts.storage.get_storage_backend", lambda: backend - ) - monkeypatch.setattr( - "app.file_storage.factory.get_storage_backend", lambda: backend - ) + monkeypatch.setattr("app.podcasts.storage.get_storage_backend", lambda: backend) + monkeypatch.setattr("app.file_storage.factory.get_storage_backend", lambda: backend) return backend @@ -159,9 +155,7 @@ def bind_task_session(db_session: AsyncSession, monkeypatch) -> AsyncSession: "app.podcasts.tasks.render", "app.podcasts.tasks.runtime", ): - monkeypatch.setattr( - f"{module}.get_celery_session_maker", lambda: _make_session - ) + monkeypatch.setattr(f"{module}.get_celery_session_maker", lambda: _make_session) return db_session @@ -213,8 +207,12 @@ def build_spec( language=language, style=PodcastStyle.CONVERSATIONAL, speakers=[ - SpeakerSpec(slot=0, name="Host", role=SpeakerRole.HOST, voice_id=voice_ids[0]), - SpeakerSpec(slot=1, name="Guest", role=SpeakerRole.GUEST, voice_id=voice_ids[1]), + SpeakerSpec( + slot=0, name="Host", role=SpeakerRole.HOST, voice_id=voice_ids[0] + ), + SpeakerSpec( + slot=1, name="Guest", role=SpeakerRole.GUEST, voice_id=voice_ids[1] + ), ], duration=DurationTarget(min_minutes=10, max_minutes=20), ) @@ -237,7 +235,7 @@ def make_podcast(db_session: AsyncSession): session, so the endpoint under test reads a realistically-built row. """ - _LADDER = [ + ladder = [ PodcastStatus.AWAITING_BRIEF, PodcastStatus.DRAFTING, PodcastStatus.RENDERING, @@ -259,7 +257,7 @@ def make_podcast(db_session: AsyncSession): await db_session.flush() return podcast - targets = _LADDER[: _LADDER.index(status) + 1] + targets = ladder[: ladder.index(status) + 1] for target in targets: if target is PodcastStatus.AWAITING_BRIEF: await service.attach_brief(podcast, build_spec()) diff --git a/surfsense_backend/tests/integration/podcasts/test_cancel.py b/surfsense_backend/tests/integration/podcasts/test_cancel.py index e4d78031d..4fe4cfc55 100644 --- a/surfsense_backend/tests/integration/podcasts/test_cancel.py +++ b/surfsense_backend/tests/integration/podcasts/test_cancel.py @@ -15,9 +15,7 @@ pytestmark = pytest.mark.integration BASE = "/api/v1/podcasts" -async def test_cancel_from_a_live_state_succeeds( - client, db_search_space, make_podcast -): +async def test_cancel_from_a_live_state_succeeds(client, db_search_space, make_podcast): podcast = await make_podcast( search_space_id=db_search_space.id, status=PodcastStatus.AWAITING_BRIEF ) diff --git a/surfsense_backend/tests/integration/podcasts/test_voice_preview.py b/surfsense_backend/tests/integration/podcasts/test_voice_preview.py index 729b77be4..113172bee 100644 --- a/surfsense_backend/tests/integration/podcasts/test_voice_preview.py +++ b/surfsense_backend/tests/integration/podcasts/test_voice_preview.py @@ -23,18 +23,12 @@ BASE = "/api/v1/podcasts" def preview_tts(monkeypatch, tmp_path) -> FakeTextToSpeech: """Route preview synthesis to the fake provider and an isolated cache.""" provider = FakeTextToSpeech() - monkeypatch.setattr( - "app.podcasts.api.routes.get_text_to_speech", lambda: provider - ) - monkeypatch.setattr( - "app.podcasts.voices.preview.PREVIEW_CACHE_ROOT", tmp_path - ) + monkeypatch.setattr("app.podcasts.api.routes.get_text_to_speech", lambda: provider) + monkeypatch.setattr("app.podcasts.voices.preview.PREVIEW_CACHE_ROOT", tmp_path) return provider -async def test_preview_returns_playable_audio_for_a_catalog_voice( - client, preview_tts -): +async def test_preview_returns_playable_audio_for_a_catalog_voice(client, preview_tts): resp = await client.get(f"{BASE}/voices/openai:alloy/preview") assert resp.status_code == 200 @@ -42,9 +36,7 @@ async def test_preview_returns_playable_audio_for_a_catalog_voice( assert resp.content == b"segment-audio" -async def test_preview_is_synthesised_once_then_served_from_cache( - client, preview_tts -): +async def test_preview_is_synthesised_once_then_served_from_cache(client, preview_tts): first = await client.get(f"{BASE}/voices/openai:alloy/preview") second = await client.get(f"{BASE}/voices/openai:alloy/preview") @@ -69,9 +61,7 @@ async def test_preview_voice_of_inactive_provider_is_404(client, preview_tts): assert preview_tts.requests == [] -async def test_preview_without_tts_provider_is_503( - client, preview_tts, monkeypatch -): +async def test_preview_without_tts_provider_is_503(client, preview_tts, monkeypatch): monkeypatch.setattr(app_config, "TTS_SERVICE", None) resp = await client.get(f"{BASE}/voices/openai:alloy/preview") diff --git a/surfsense_backend/tests/unit/podcasts/conftest.py b/surfsense_backend/tests/unit/podcasts/conftest.py index a3836f689..5eb4d8457 100644 --- a/surfsense_backend/tests/unit/podcasts/conftest.py +++ b/surfsense_backend/tests/unit/podcasts/conftest.py @@ -38,7 +38,10 @@ def make_spec(): if speakers is None: speakers = [ SpeakerSpec( - slot=0, name="Host", role=SpeakerRole.HOST, voice_id="kokoro:am_adam" + slot=0, + name="Host", + role=SpeakerRole.HOST, + voice_id="kokoro:am_adam", ), SpeakerSpec( slot=1, diff --git a/surfsense_backend/tests/unit/podcasts/test_renderer.py b/surfsense_backend/tests/unit/podcasts/test_renderer.py index f80e2a4c4..2bcdff967 100644 --- a/surfsense_backend/tests/unit/podcasts/test_renderer.py +++ b/surfsense_backend/tests/unit/podcasts/test_renderer.py @@ -71,7 +71,9 @@ def _spec(voice_id: str) -> PodcastSpec: async def test_render_rejects_a_turn_for_an_unknown_speaker(tmp_path): - renderer = PodcastRenderer(tts=_UnusedTTS(), catalog=_catalog_with("kokoro:am_adam")) + renderer = PodcastRenderer( + tts=_UnusedTTS(), catalog=_catalog_with("kokoro:am_adam") + ) transcript = Transcript(turns=[TranscriptTurn(speaker=5, text="Who am I?")]) with pytest.raises(RenderError): @@ -81,7 +83,9 @@ async def test_render_rejects_a_turn_for_an_unknown_speaker(tmp_path): async def test_render_rejects_a_speaker_whose_voice_is_not_in_the_catalog(tmp_path): - renderer = PodcastRenderer(tts=_UnusedTTS(), catalog=_catalog_with("kokoro:am_adam")) + renderer = PodcastRenderer( + tts=_UnusedTTS(), catalog=_catalog_with("kokoro:am_adam") + ) transcript = Transcript(turns=[TranscriptTurn(speaker=0, text="Hello.")]) with pytest.raises(RenderError): diff --git a/surfsense_backend/tests/unit/podcasts/test_resolution.py b/surfsense_backend/tests/unit/podcasts/test_resolution.py index 48e834096..aab44f8fb 100644 --- a/surfsense_backend/tests/unit/podcasts/test_resolution.py +++ b/surfsense_backend/tests/unit/podcasts/test_resolution.py @@ -64,7 +64,9 @@ def test_a_preferred_voice_invalid_for_the_language_is_replaced(): speaker_count=1, preferred=["kokoro:does-not-exist"], ) - assert voices[0].voice_id in {v.voice_id for v in catalog.for_provider(TtsProvider.KOKORO)} + assert voices[0].voice_id in { + v.voice_id for v in catalog.for_provider(TtsProvider.KOKORO) + } def test_resolution_fails_when_no_voice_speaks_the_language(): diff --git a/surfsense_backend/tests/unit/podcasts/test_voice_catalog.py b/surfsense_backend/tests/unit/podcasts/test_voice_catalog.py index d94c85922..861d8768c 100644 --- a/surfsense_backend/tests/unit/podcasts/test_voice_catalog.py +++ b/surfsense_backend/tests/unit/podcasts/test_voice_catalog.py @@ -52,7 +52,9 @@ def test_for_provider_returns_only_that_providers_voices(): def test_for_language_matches_on_the_primary_subtag(): """A request for 'en' should match an 'en-US' voice (region-insensitive).""" catalog = VoiceCatalog([_voice("k1", language="en-US")]) - assert [v.voice_id for v in catalog.for_language(TtsProvider.KOKORO, "en")] == ["k1"] + assert [v.voice_id for v in catalog.for_language(TtsProvider.KOKORO, "en")] == [ + "k1" + ] def test_for_language_excludes_other_languages(): diff --git a/surfsense_web/app/(home)/changelog/page.tsx b/surfsense_web/app/(home)/changelog/page.tsx index 42bac512a..b7aa14d20 100644 --- a/surfsense_web/app/(home)/changelog/page.tsx +++ b/surfsense_web/app/(home)/changelog/page.tsx @@ -3,10 +3,7 @@ import type { MDXComponents } from "mdx/types"; import type { Metadata } from "next"; import type { ComponentType } from "react"; import { changelog } from "@/.source/server"; -import { - ChangelogTimeline, - type ChangelogTimelineEntry, -} from "@/components/ui/changelog-timeline"; +import { ChangelogTimeline, type ChangelogTimelineEntry } from "@/components/ui/changelog-timeline"; import { formatDate } from "@/lib/utils"; import { getMDXComponents } from "@/mdx-components"; diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PurchaseHistoryContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PurchaseHistoryContent.tsx index 081ce358e..263a286c1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PurchaseHistoryContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PurchaseHistoryContent.tsx @@ -13,11 +13,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import type { - CreditPurchase, - PagePurchase, - PurchaseStatus, -} from "@/contracts/types/stripe.types"; +import type { CreditPurchase, PagePurchase, PurchaseStatus } from "@/contracts/types/stripe.types"; import { stripeApiService } from "@/lib/apis/stripe-api.service"; import { cn } from "@/lib/utils"; diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index f6c7f3e15..3785dc649 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -48,8 +48,8 @@ import { isCommentReplyMetadata, isConnectorIndexingMetadata, isDocumentProcessingMetadata, - isNewMentionMetadata, isInsufficientCreditsMetadata, + isNewMentionMetadata, } from "@/contracts/types/inbox.types"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import type { InboxItem } from "@/hooks/use-inbox"; diff --git a/surfsense_web/components/layout/ui/sidebar/index.ts b/surfsense_web/components/layout/ui/sidebar/index.ts index e9256cc3a..fcfe2252d 100644 --- a/surfsense_web/components/layout/ui/sidebar/index.ts +++ b/surfsense_web/components/layout/ui/sidebar/index.ts @@ -1,9 +1,9 @@ export { AllChatsSidebar, AllChatsSidebarContent } from "./AllChatsSidebar"; export { ChatListItem } from "./ChatListItem"; +export { CreditBalanceDisplay } from "./CreditBalanceDisplay"; export { DocumentsSidebar } from "./DocumentsSidebar"; export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar"; export { MobileSidebar, MobileSidebarTrigger } from "./MobileSidebar"; -export { CreditBalanceDisplay } from "./CreditBalanceDisplay"; export { NavSection } from "./NavSection"; export { Sidebar } from "./Sidebar"; export { SidebarCollapseButton } from "./SidebarCollapseButton"; diff --git a/surfsense_web/components/new-chat/chat-example-prompts.tsx b/surfsense_web/components/new-chat/chat-example-prompts.tsx index 98d95b98b..61041cc29 100644 --- a/surfsense_web/components/new-chat/chat-example-prompts.tsx +++ b/surfsense_web/components/new-chat/chat-example-prompts.tsx @@ -2,9 +2,9 @@ import { FilePlus2, + type LucideIcon, Search, Settings2, - type LucideIcon, WandSparkles, Workflow, X, diff --git a/surfsense_web/components/pricing/pricing-section.tsx b/surfsense_web/components/pricing/pricing-section.tsx index bf4067336..1e11e95d5 100644 --- a/surfsense_web/components/pricing/pricing-section.tsx +++ b/surfsense_web/components/pricing/pricing-section.tsx @@ -286,8 +286,8 @@ function PricingFAQ() { Frequently Asked Questions

- Everything you need to know about SurfSense credits and billing. - Can't find what you need? Reach out at{" "} + Everything you need to know about SurfSense credits and billing. Can't find what you + need? Reach out at{" "} rohan@surfsense.com diff --git a/surfsense_web/components/settings/earn-credits-content.tsx b/surfsense_web/components/settings/earn-credits-content.tsx index 21b7f8a5b..731ea7726 100644 --- a/surfsense_web/components/settings/earn-credits-content.tsx +++ b/surfsense_web/components/settings/earn-credits-content.tsx @@ -77,9 +77,7 @@ export function EarnCreditsContent() {

Earn Credits

-

- Earn bonus credits by completing tasks -

+

Earn bonus credits by completing tasks