Module: Cohere::Transcribe::VAD::Timestamps

Defined in:
lib/cohere/transcribe/vad/timestamps.rb

Overview

Silero 6.2.1's sample-domain timestamp state machine.

Keeping this separate from the inference backend makes every Silero engine share exactly the same threshold, hysteresis, duration, and padding behavior.

Constant Summary collapse

WINDOW_SAMPLES =
512
CANCELLATION_CHECK_FRAMES =
4_096

Class Method Summary collapse

Class Method Details

.from_probabilities(audio_length_samples, speech_probabilities, sampling_rate: 16_000, threshold: 0.5, min_speech_duration_ms: 250, max_speech_duration_s: Float::INFINITY, min_silence_duration_ms: 100, speech_pad_ms: 30, neg_threshold: nil, min_silence_at_max_speech: 98, use_max_poss_sil_at_max_speech: true, cancel_check: nil) ⇒ Object

Raises:

  • (ArgumentError)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/cohere/transcribe/vad/timestamps.rb', line 17

def from_probabilities(
  audio_length_samples,
  speech_probabilities,
  sampling_rate: 16_000,
  threshold: 0.5,
  min_speech_duration_ms: 250,
  max_speech_duration_s: Float::INFINITY,
  min_silence_duration_ms: 100,
  speech_pad_ms: 30,
  neg_threshold: nil,
  min_silence_at_max_speech: 98,
  use_max_poss_sil_at_max_speech: true,
  cancel_check: nil
)
  check_cancellation(cancel_check)
  raise ArgumentError, "The vectorized Silero v6 export requires 16 kHz audio" unless sampling_rate == 16_000
  unless audio_length_samples.is_a?(Integer) && audio_length_samples >= 0
    raise ArgumentError, "audio_length_samples must be a non-negative integer"
  end

  speech_probs = one_dimensional_float32(speech_probabilities)
  expected_frames = (audio_length_samples + WINDOW_SAMPLES - 1) / WINDOW_SAMPLES
  unless speech_probs.length == expected_frames
    raise ArgumentError,
          "Silero probability count does not match the audio length: " \
          "expected #{expected_frames}, got #{speech_probs.length}"
  end
  raise ArgumentError, "Silero probabilities contain non-finite values" unless speech_probs.all?(&:finite?)
  unless speech_probs.all? { |probability| probability.between?(0.0, 1.0) }
    raise ArgumentError, "Silero probabilities must be between zero and one"
  end

  check_cancellation(cancel_check)

  min_speech_samples = sampling_rate * min_speech_duration_ms / 1000.0
  speech_pad_samples = sampling_rate * speech_pad_ms / 1000.0
  max_speech_samples = (
    (sampling_rate * max_speech_duration_s) - WINDOW_SAMPLES - (2 * speech_pad_samples)
  )
  min_silence_samples = sampling_rate * min_silence_duration_ms / 1000.0
  min_silence_samples_at_max_speech = sampling_rate * min_silence_at_max_speech / 1000.0
  # NumPy keeps scalar comparisons in the probability array's float32
  # dtype. Coerce the comparison thresholds likewise so decimal values
  # such as 0.01 and 0.35 do not move a frame across a boundary merely
  # because Ruby Float is binary64.
  positive_threshold = float32(threshold)
  negative_threshold = float32(neg_threshold || [threshold - 0.15, 0.01].max)

  triggered = false
  speeches = []
  current_speech = {}
  temp_end = 0
  prev_end = 0
  next_start = 0
  possible_ends = []
  next_cancellation_check = CANCELLATION_CHECK_FRAMES

  speech_probs.each_with_index do |speech_prob, index|
    if index == next_cancellation_check
      check_cancellation(cancel_check)
      next_cancellation_check += CANCELLATION_CHECK_FRAMES
    end
    current_sample = WINDOW_SAMPLES * index

    if speech_prob >= positive_threshold && temp_end.positive?
      silence_duration = current_sample - temp_end
      possible_ends << [temp_end, silence_duration] if silence_duration > min_silence_samples_at_max_speech
      temp_end = 0
      next_start = current_sample if next_start < prev_end
    end

    if speech_prob >= positive_threshold && !triggered
      triggered = true
      current_speech[:start] = current_sample
      next
    end

    if triggered && current_sample - current_speech.fetch(:start) > max_speech_samples
      if use_max_poss_sil_at_max_speech && !possible_ends.empty?
        prev_end, silence_duration = possible_ends.max_by { |_end_sample, duration| duration }
        current_speech[:end] = prev_end
        speeches << current_speech
        current_speech = {}
        next_start = prev_end + silence_duration
        if next_start < prev_end + current_sample
          current_speech[:start] = next_start
        else
          triggered = false
        end
        prev_end = next_start = temp_end = 0
        possible_ends = []
      elsif prev_end.positive?
        current_speech[:end] = prev_end
        speeches << current_speech
        current_speech = {}
        if next_start < prev_end
          triggered = false
        else
          current_speech[:start] = next_start
        end
        prev_end = next_start = temp_end = 0
        possible_ends = []
      else
        current_speech[:end] = current_sample
        speeches << current_speech
        current_speech = {}
        prev_end = next_start = temp_end = 0
        triggered = false
        possible_ends = []
        next
      end
    end

    next unless speech_prob < negative_threshold && triggered

    temp_end = current_sample unless temp_end.positive?
    current_silence_duration = current_sample - temp_end
    if !use_max_poss_sil_at_max_speech &&
       current_silence_duration > min_silence_samples_at_max_speech
      prev_end = temp_end
    end
    next if current_silence_duration < min_silence_samples

    current_speech[:end] = temp_end
    speeches << current_speech if current_speech.fetch(:end) - current_speech.fetch(:start) > min_speech_samples
    current_speech = {}
    prev_end = next_start = temp_end = 0
    triggered = false
    possible_ends = []
  end

  check_cancellation(cancel_check)
  if !current_speech.empty? &&
     audio_length_samples - current_speech.fetch(:start) > min_speech_samples
    current_speech[:end] = audio_length_samples
    speeches << current_speech
  end

  pad_speeches!(speeches, audio_length_samples, speech_pad_samples, cancel_check)
  check_cancellation(cancel_check)
  speeches
end