Files
reflector/server/reflector/processors/audio_padding_pyav.py
Juan Diego García a682846645 feat: 3-mode selfhosted refactoring (--gpu, --cpu, --hosted) + audio token auth fallback (#896)
* fix: local processing instead of http server for cpu

* add fallback token if service worker doesnt work

* chore: rename processors to keep processor pattern up to date and allow other processors to be createed and used with env vars
2026-03-04 16:31:08 -05:00

134 lines
4.5 KiB
Python

"""
PyAV audio padding processor.
Pads audio tracks with silence directly in-process (no HTTP).
Reuses the shared PyAV utilities from reflector.utils.audio_padding.
"""
import asyncio
import os
import tempfile
import av
from reflector.logger import logger
from reflector.processors.audio_padding import AudioPaddingProcessor, PaddingResponse
from reflector.processors.audio_padding_auto import AudioPaddingAutoProcessor
from reflector.utils.audio_padding import apply_audio_padding_to_file
S3_TIMEOUT = 60
class AudioPaddingPyavProcessor(AudioPaddingProcessor):
"""Audio padding processor using PyAV (no HTTP backend)."""
async def pad_track(
self,
track_url: str,
output_url: str,
start_time_seconds: float,
track_index: int,
) -> PaddingResponse:
"""Pad audio track with silence via PyAV.
Args:
track_url: Presigned GET URL for source audio track
output_url: Presigned PUT URL for output WebM
start_time_seconds: Amount of silence to prepend
track_index: Track index for logging
"""
if not track_url:
raise ValueError("track_url cannot be empty")
if start_time_seconds <= 0:
raise ValueError(
f"start_time_seconds must be positive, got {start_time_seconds}"
)
log = logger.bind(track_index=track_index, padding_seconds=start_time_seconds)
log.info("Starting local PyAV padding")
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None,
self._pad_track_blocking,
track_url,
output_url,
start_time_seconds,
track_index,
)
def _pad_track_blocking(
self,
track_url: str,
output_url: str,
start_time_seconds: float,
track_index: int,
) -> PaddingResponse:
"""Blocking padding work: download, pad with PyAV, upload."""
import requests
log = logger.bind(track_index=track_index, padding_seconds=start_time_seconds)
temp_dir = tempfile.mkdtemp()
input_path = None
output_path = None
try:
# Download source audio
log.info("Downloading track for local padding")
response = requests.get(track_url, stream=True, timeout=S3_TIMEOUT)
response.raise_for_status()
input_path = os.path.join(temp_dir, "track.webm")
total_bytes = 0
with open(input_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
total_bytes += len(chunk)
log.info("Track downloaded", bytes=total_bytes)
# Apply padding using shared PyAV utility
output_path = os.path.join(temp_dir, "padded.webm")
with av.open(input_path) as in_container:
apply_audio_padding_to_file(
in_container,
output_path,
start_time_seconds,
track_index,
logger=logger,
)
file_size = os.path.getsize(output_path)
log.info("Local padding complete", size=file_size)
# Upload padded track
log.info("Uploading padded track to S3")
with open(output_path, "rb") as f:
upload_response = requests.put(output_url, data=f, timeout=S3_TIMEOUT)
upload_response.raise_for_status()
log.info("Upload complete", size=file_size)
return PaddingResponse(size=file_size)
except Exception as e:
log.error("Local padding failed", error=str(e), exc_info=True)
raise
finally:
if input_path and os.path.exists(input_path):
try:
os.unlink(input_path)
except Exception as e:
log.warning("Failed to cleanup input file", error=str(e))
if output_path and os.path.exists(output_path):
try:
os.unlink(output_path)
except Exception as e:
log.warning("Failed to cleanup output file", error=str(e))
try:
os.rmdir(temp_dir)
except Exception as e:
log.warning("Failed to cleanup temp directory", error=str(e))
AudioPaddingAutoProcessor.register("pyav", AudioPaddingPyavProcessor)