116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
import wave
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
import recording
|
|
|
|
|
|
class FakeUser:
|
|
def __init__(self, user_id, display_name=None, name=None):
|
|
self.id = user_id
|
|
if display_name is not None:
|
|
self.display_name = display_name
|
|
if name is not None:
|
|
self.name = name
|
|
|
|
|
|
class FakeData:
|
|
def __init__(self, pcm):
|
|
self.pcm = pcm
|
|
|
|
|
|
def _read_wav_frames(path):
|
|
with wave.open(str(path), "rb") as w:
|
|
return w.readframes(w.getnframes())
|
|
|
|
|
|
SPEECH = b"\x10\x20" * 100 # non-silent PCM
|
|
SILENCE = b"\x00\x00" * 100
|
|
|
|
|
|
def test_meeting_sink_writes_mixed_and_per_speaker_tracks(tmp_path):
|
|
mixed = tmp_path / "meeting.wav"
|
|
tracks_dir = tmp_path / "tracks"
|
|
sink = recording.MeetingSink(str(mixed), str(tracks_dir))
|
|
|
|
alice = FakeUser(111, display_name="Alice")
|
|
bob = FakeUser(222, display_name="Bob")
|
|
|
|
sink.write(alice, FakeData(SPEECH))
|
|
sink.write(bob, FakeData(SPEECH))
|
|
sink.write(alice, FakeData(SPEECH))
|
|
sink.cleanup()
|
|
|
|
# Mixed archive is a valid, finalized WAV containing all frames.
|
|
assert mixed.exists()
|
|
mixed_frames = _read_wav_frames(mixed)
|
|
assert mixed_frames == SPEECH * 3
|
|
|
|
# Each speaker has their own track.
|
|
alice_track = tracks_dir / "track-111.wav"
|
|
bob_track = tracks_dir / "track-222.wav"
|
|
assert _read_wav_frames(alice_track) == SPEECH * 2
|
|
assert _read_wav_frames(bob_track) == SPEECH
|
|
|
|
# Manifest maps tracks back to display names.
|
|
manifest = json.loads((tracks_dir / "tracks.json").read_text())
|
|
names = {t["display_name"]: t["user_id"] for t in manifest["tracks"]}
|
|
assert names == {"Alice": 111, "Bob": 222}
|
|
|
|
|
|
def test_meeting_sink_excludes_silence_from_speaker_tracks(tmp_path):
|
|
mixed = tmp_path / "meeting.wav"
|
|
tracks_dir = tmp_path / "tracks"
|
|
sink = recording.MeetingSink(str(mixed), str(tracks_dir))
|
|
|
|
alice = FakeUser(111, display_name="Alice")
|
|
sink.write(alice, FakeData(SPEECH))
|
|
sink.write(alice, FakeData(SILENCE)) # synthetic silence frame
|
|
sink.cleanup()
|
|
|
|
# Mixed gets both frames (faithful timeline), speaker track only the speech.
|
|
assert _read_wav_frames(mixed) == SPEECH + SILENCE
|
|
assert _read_wav_frames(tracks_dir / "track-111.wav") == SPEECH
|
|
|
|
|
|
def test_meeting_sink_ignores_silence_only_and_unknown_users(tmp_path):
|
|
mixed = tmp_path / "meeting.wav"
|
|
tracks_dir = tmp_path / "tracks"
|
|
sink = recording.MeetingSink(str(mixed), str(tracks_dir))
|
|
|
|
# Silence with no user, then silence with a user: neither creates a track.
|
|
sink.write(None, FakeData(SILENCE))
|
|
sink.write(FakeUser(999, display_name="Quiet"), FakeData(SILENCE))
|
|
sink.cleanup()
|
|
|
|
manifest = json.loads((tracks_dir / "tracks.json").read_text())
|
|
assert manifest["tracks"] == []
|
|
# Mixed archive still finalized and valid.
|
|
assert _read_wav_frames(mixed) == SILENCE * 2
|
|
|
|
|
|
def test_meeting_sink_ignores_writes_after_cleanup(tmp_path):
|
|
"""Verify that writes after cleanup() are silently dropped."""
|
|
mixed = tmp_path / "meeting.wav"
|
|
tracks_dir = tmp_path / "tracks"
|
|
sink = recording.MeetingSink(str(mixed), str(tracks_dir))
|
|
|
|
alice = FakeUser(111, display_name="Alice")
|
|
sink.write(alice, FakeData(SPEECH))
|
|
sink.cleanup()
|
|
|
|
# These writes should be dropped, not cause crashes or corrupt the files.
|
|
sink.write(alice, FakeData(SPEECH))
|
|
sink.write(None, FakeData(SILENCE))
|
|
|
|
# Mixed file should contain only the pre-cleanup frame.
|
|
assert _read_wav_frames(mixed) == SPEECH
|
|
assert _read_wav_frames(tracks_dir / "track-111.wav") == SPEECH
|
|
# Manifest should still reference Alice.
|
|
manifest = json.loads((tracks_dir / "tracks.json").read_text())
|
|
assert len(manifest["tracks"]) == 1 |