Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions bots/paused_caption_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class PausedCaptionFilter:
"""Drops caption updates that would leak speech from a recording pause.

Platform captions (Google Meet/Teams/Zoom web) arrive as *growing* segment texts keyed
by ``deviceId:captionId``. Ignoring updates only while paused is not enough:
the first update after resume carries the whole segment, including the words
spoken during the pause. Any segment that sends an update during a pause is
therefore frozen for good; segments that start after resume pass normally.
"""

def __init__(self):
self._frozen_keys = set()

@staticmethod
def key(caption):
return f"{caption.get('deviceId')}:{caption.get('captionId')}"

def should_drop(self, caption, recording_paused):
key = self.key(caption)
if recording_paused:
self._frozen_keys.add(key)
return True
return key in self._frozen_keys
31 changes: 31 additions & 0 deletions bots/tests/test_paused_caption_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import unittest

from bots.paused_caption_filter import PausedCaptionFilter


class PausedCaptionFilterTest(unittest.TestCase):
def test_segment_spanning_a_pause_stays_dropped_after_resume(self):
f = PausedCaptionFilter()
seg = {"captionId": "c1", "deviceId": "u1"}
self.assertFalse(f.should_drop(seg, recording_paused=False)) # before pause: recorded
self.assertTrue(f.should_drop(seg, recording_paused=True)) # during pause: dropped
# After resume the same segment carries the paused words -> still dropped
self.assertTrue(f.should_drop(seg, recording_paused=False))

def test_new_segment_after_resume_passes(self):
f = PausedCaptionFilter()
f.should_drop({"captionId": "c1", "deviceId": "u1"}, recording_paused=True)
self.assertFalse(f.should_drop({"captionId": "c2", "deviceId": "u1"}, recording_paused=False))

def test_segments_are_keyed_per_device(self):
f = PausedCaptionFilter()
f.should_drop({"captionId": "c1", "deviceId": "u1"}, recording_paused=True)
self.assertFalse(f.should_drop({"captionId": "c1", "deviceId": "u2"}, recording_paused=False))

def test_untouched_segments_pass_when_not_paused(self):
f = PausedCaptionFilter()
self.assertFalse(f.should_drop({"captionId": "c1", "deviceId": "u1"}, recording_paused=False))


if __name__ == "__main__":
unittest.main()
5 changes: 4 additions & 1 deletion bots/web_bot_adapter/web_bot_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from bots.automatic_leave_utils import participant_is_another_bot
from bots.bot_adapter import BotAdapter
from bots.models import ParticipantEventTypes, RecordingViews
from bots.paused_caption_filter import PausedCaptionFilter
from bots.per_participant_realtime_video_configuration import PerParticipantRealtimeVideoConfiguration
from bots.room_sync_source_participant_configuration import RoomSyncSourceParticipantConfiguration
from bots.room_sync_utils import add_bot_indicator_to_display_name
Expand Down Expand Up @@ -132,6 +133,7 @@ def __init__(
self.ready_to_send_chat_messages = False

self.recording_paused = False
self.paused_caption_filter = PausedCaptionFilter()

def pause_recording(self):
self.recording_paused = True
Expand Down Expand Up @@ -378,7 +380,8 @@ def handle_failed_to_join(self, reason):
self.subclass_specific_handle_failed_to_join(reason)

def handle_caption_update(self, json_data):
if self.recording_paused:
# Segments that carry speech from a pause stay dropped after resume, too.
if self.paused_caption_filter.should_drop(json_data["caption"], self.recording_paused):
return

# Count a caption as audio activity
Expand Down
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "1.82.6"
"version": "1.82.7"
}