jarvis-cloud / backend /voice /emergency_playback.py
Jarvis2345's picture
Squash history — remove all prior commits (secret hygiene, S4)
a31f556
Raw
History Blame
2.96 kB
import logging
import re
import io
import wave
# WARNING: Holding 60 seconds of uncompressed ambient audio in active memory
# has privacy and memory implications. This is an explicit tradeoff to enable
# the instant "emergency replay" feature.
EMERGENCY_BUFFER_WINDOW_SECONDS = 60
# Assuming 16000Hz, 16-bit PCM, Mono
# 16000 * 2 bytes = 32000 bytes per second
BYTES_PER_SECOND = 32000
MAX_BUFFER_BYTES = EMERGENCY_BUFFER_WINDOW_SECONDS * BYTES_PER_SECOND
class LiveAudioBuffer:
def __init__(self):
self.buffer = bytearray()
def append(self, data: bytes):
self.buffer.extend(data)
if len(self.buffer) > MAX_BUFFER_BYTES:
# Slice to keep only the most recent bytes
self.buffer = self.buffer[-MAX_BUFFER_BYTES:]
async def get_ring_buffer_tail(self, seconds_back: int) -> bytes:
bytes_needed = seconds_back * BYTES_PER_SECOND
if len(self.buffer) >= bytes_needed:
return bytes(self.buffer[-bytes_needed:])
return bytes(self.buffer)
# Global active recording session buffer
active_recording_session = LiveAudioBuffer()
async def get_live_audio_buffer(seconds_back: int = 30) -> bytes:
"""Pulls the last N seconds directly from the in-memory ring buffer."""
return await active_recording_session.get_ring_buffer_tail(seconds_back)
def matches_emergency_replay_intent(transcript: str) -> bool:
"""Uses lightweight keyword spotting to trap emergency playback requests."""
if not transcript:
return False
target = transcript.lower()
phrases = ["emergency replay", "play back the last", "what just happened", "play that back"]
return any(phrase in target for phrase in phrases)
def extract_duration(transcript: str, default: int = 30) -> int:
"""Extracts requested seconds from the transcript, e.g., 'play back the last 15 seconds'."""
if not transcript:
return default
match = re.search(r"(?:last|past)\s+(\d+)\s+seconds", transcript.lower())
if match:
try:
return int(match.group(1))
except ValueError:
pass
return default
async def play_audio_immediately(audio_bytes: bytes):
"""
Plays back the RAW 16kHz PCM audio immediately through the native pygame mixer.
Bypasses TTS entirely.
"""
from backend.voice.activation import _play_tts_bytes
if not audio_bytes:
logging.warning("No audio to playback in emergency replay.")
return
wav_io = io.BytesIO()
with wave.open(wav_io, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(16000)
wf.writeframes(audio_bytes)
wav_bytes = wav_io.getvalue()
logging.info(f"Playing back {len(audio_bytes) / BYTES_PER_SECOND} seconds of RAW emergency audio.")
# _play_tts_bytes expects a WAV payload and handles the pygame playback safely
await _play_tts_bytes(wav_bytes)