From 5cc05d90f0de4e4c093f0917381d726817f7dc4e Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:46:31 -0700 Subject: [PATCH] fix: avoid blocking audio writes TTS responses were written synchronously from the async presentation pipeline, blocking concurrent requests. Offload byte writes to a worker thread and add a regression test that keeps the event loop responsive. --- .../app/agents/video_presentation/nodes.py | 4 +-- surfsense_backend/app/utils/file_io.py | 7 +++++ .../agents/video_presentation/test_file_io.py | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 surfsense_backend/app/utils/file_io.py create mode 100644 surfsense_backend/tests/unit/agents/video_presentation/test_file_io.py diff --git a/surfsense_backend/app/agents/video_presentation/nodes.py b/surfsense_backend/app/agents/video_presentation/nodes.py index dca89059f..e52d1f4ce 100644 --- a/surfsense_backend/app/agents/video_presentation/nodes.py +++ b/surfsense_backend/app/agents/video_presentation/nodes.py @@ -17,6 +17,7 @@ from app.config import config as app_config from app.services.kokoro_tts_service import get_kokoro_tts_service from app.services.llm_service import get_agent_llm from app.utils.content_utils import extract_text_content, strip_markdown_fences +from app.utils.file_io import write_bytes from .configuration import Configuration from .prompts import ( @@ -137,8 +138,7 @@ async def create_slide_audio(state: State, config: RunnableConfig) -> dict[str, kwargs["api_base"] = app_config.TTS_SERVICE_API_BASE response = await aspeech(**kwargs) - with open(chunk_path, "wb") as f: - f.write(response.content) + await write_bytes(chunk_path, response.content) return chunk_path diff --git a/surfsense_backend/app/utils/file_io.py b/surfsense_backend/app/utils/file_io.py new file mode 100644 index 000000000..2e5700955 --- /dev/null +++ b/surfsense_backend/app/utils/file_io.py @@ -0,0 +1,7 @@ +import asyncio +from pathlib import Path + + +async def write_bytes(path: str, content: bytes) -> None: + """Write bytes without blocking the event loop.""" + await asyncio.to_thread(Path(path).write_bytes, content) diff --git a/surfsense_backend/tests/unit/agents/video_presentation/test_file_io.py b/surfsense_backend/tests/unit/agents/video_presentation/test_file_io.py new file mode 100644 index 000000000..c85cef19c --- /dev/null +++ b/surfsense_backend/tests/unit/agents/video_presentation/test_file_io.py @@ -0,0 +1,29 @@ +import asyncio +import threading + +import pytest + +from app.utils.file_io import write_bytes + +pytestmark = pytest.mark.unit + + +@pytest.mark.asyncio +async def test_write_bytes_does_not_block_event_loop(monkeypatch, tmp_path): + write_started = threading.Event() + release_write = threading.Event() + + def blocking_write(_path, _content): + write_started.set() + release_write.wait(timeout=1) + return 5 + + monkeypatch.setattr("pathlib.Path.write_bytes", blocking_write) + + task = asyncio.create_task(write_bytes(str(tmp_path / "audio.mp3"), b"audio")) + while not write_started.is_set(): + await asyncio.sleep(0) + + assert not task.done() + release_write.set() + await task