"""Локальная WAV-запись обеих сторон учебного голосового вызова.""" import os import time import wave from dataclasses import dataclass from pathlib import Path from uuid import UUID import numpy as np from app.config import get_settings TARGET_RATE = 16_000 def recording_path(session_id: UUID) -> Path: return Path(get_settings().recordings_dir).resolve() / f"{session_id}.wav" @dataclass(frozen=True) class _Segment: offset: int samples: np.ndarray class CallRecorder: """Смешивает PCM16 разных частот на монотаймлайн 16 кГц. Вход курсанта приходит по 16 кГц, TTS звонящего — по 24 кГц. Метка monotonic сохраняет паузы и взаимное расположение реплик; системные часы и изменение времени на хосте на запись не влияют. """ def __init__(self, path: Path, *, clock=time.monotonic) -> None: self.path = path self._clock = clock self._started = clock() self._segments: list[_Segment] = [] self._finalized = False def add_pcm(self, pcm: bytes, *, sample_rate: int) -> None: if self._finalized or not pcm or sample_rate <= 0 or len(pcm) % 2: return source = np.frombuffer(pcm, dtype=" Path | None: if self._finalized: return self.path if self.path.is_file() else None self._finalized = True if not self._segments: return None total = max(item.offset + item.samples.size for item in self._segments) mixed = np.zeros(total, dtype=np.int32) for item in self._segments: mixed[item.offset:item.offset + item.samples.size] += item.samples pcm = np.clip(mixed, -32768, 32767).astype(" CallRecorder | None: if not get_settings().record_calls: return None return CallRecorder(recording_path(session_id))