upload path: extra form fields, MIME by file suffix, spool-only start (node-26#45)
UploadMeta.extra carries backend-specific /upload fields through the spool sidecar; post_recording picks the MIME from the file suffix so a spooled WAV is re-sent as audio/wav; call_recorder.start(capture=False) runs only the spool, for a backend that records without PulseAudio. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5983ae27be
commit
fa3eac3b28
@@ -64,7 +64,7 @@ from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from app.config import settings
|
||||
from app.internal import audio_trim, pcm, pulse, upload_spool
|
||||
@@ -335,10 +335,15 @@ class CallRecorder:
|
||||
# Lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the persistent capture. Call once from app lifespan."""
|
||||
self._stream_task = asyncio.create_task(self._capture_loop())
|
||||
logger.info("PulseAudio ring-buffer starting.")
|
||||
async def start(self, capture: bool = True) -> None:
|
||||
"""Start the persistent capture. Call once from app lifespan.
|
||||
|
||||
capture=False starts only the upload spool: with the trunk-recorder
|
||||
backend (node-26#45) TR does the recording, and a second recorder on
|
||||
the same audio would upload every call twice."""
|
||||
if capture:
|
||||
self._stream_task = asyncio.create_task(self._capture_loop())
|
||||
logger.info("PulseAudio ring-buffer starting.")
|
||||
if settings.c2_url:
|
||||
self.spool.start()
|
||||
|
||||
@@ -766,14 +771,15 @@ class CallRecorder:
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _post(self, file_path: Path, meta: UploadMeta) -> UploadResult:
|
||||
return await upload_spool.post_recording(file_path, meta, AUDIO_MIME)
|
||||
return await upload_spool.post_recording(file_path, meta, upload_spool.mime_for(file_path, AUDIO_MIME))
|
||||
|
||||
async def _post_inline(self, file_path: Path, meta: UploadMeta) -> UploadResult:
|
||||
# metadata_watcher awaits on_call_end, so this attempt blocks the poll
|
||||
# loop. With the spool behind it, it can afford a short timeout; the
|
||||
# background retries get the full UPLOAD_TIMEOUT_SECONDS.
|
||||
return await upload_spool.post_recording(
|
||||
file_path, meta, AUDIO_MIME, timeout=settings.upload_inline_timeout_seconds,
|
||||
file_path, meta, upload_spool.mime_for(file_path, AUDIO_MIME),
|
||||
timeout=settings.upload_inline_timeout_seconds,
|
||||
)
|
||||
|
||||
async def upload_recording(
|
||||
@@ -785,6 +791,7 @@ class CallRecorder:
|
||||
system_id: Optional[str] = None,
|
||||
audio_start_epoch: Optional[float] = None,
|
||||
audio_end_epoch: Optional[float] = None,
|
||||
extra: Optional[Dict[str, str]] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Upload once, inline. Returns the storage URL on success.
|
||||
@@ -802,6 +809,7 @@ class CallRecorder:
|
||||
meta = UploadMeta(
|
||||
call_id=call_id, talkgroup_id=talkgroup_id, talkgroup_name=talkgroup_name,
|
||||
system_id=system_id, audio_start_epoch=audio_start_epoch, audio_end_epoch=audio_end_epoch,
|
||||
extra=extra,
|
||||
)
|
||||
result = await self._post_inline(file_path, meta)
|
||||
self.spool.last_result = {"call_id": call_id, "kind": result.kind, "status": result.status,
|
||||
|
||||
@@ -71,6 +71,18 @@ class UploadMeta:
|
||||
system_id: Optional[str] = None
|
||||
audio_start_epoch: Optional[float] = None
|
||||
audio_end_epoch: Optional[float] = None
|
||||
# Further form fields, sent as-is (Trunk Recorder calls carry units,
|
||||
# emergency, encryption — node-26#45). Kept in the sidecar like the rest.
|
||||
extra: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
# By extension, so a spooled file is re-sent with the type it was first sent
|
||||
# with — the PulseAudio recorder writes FLAC, Trunk Recorder WAV.
|
||||
AUDIO_MIME_BY_SUFFIX = {".flac": "audio/flac", ".wav": "audio/wav", ".mp3": "audio/mpeg", ".m4a": "audio/mp4"}
|
||||
|
||||
|
||||
def mime_for(path: Path, default: str = "application/octet-stream") -> str:
|
||||
return AUDIO_MIME_BY_SUFFIX.get(path.suffix.lower(), default)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -108,6 +120,9 @@ def _form(meta: UploadMeta) -> Dict[str, str]:
|
||||
form["audio_start_epoch"] = f"{meta.audio_start_epoch:.3f}"
|
||||
if meta.audio_end_epoch is not None:
|
||||
form["audio_end_epoch"] = f"{meta.audio_end_epoch:.3f}"
|
||||
for key, value in (meta.extra or {}).items():
|
||||
if value is not None and key not in form:
|
||||
form[key] = str(value)
|
||||
return form
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user