internal rework 4
This commit is contained in:
parent
270faf2069
commit
8e7aea4fcf
3 changed files with 169 additions and 61 deletions
|
|
@ -1,6 +1,10 @@
|
||||||
from whisperlivekit.timed_objects import Line, SilentLine, format_time, SpeakerSegment, Silence
|
|
||||||
from whisperlivekit.timed_objects import PunctuationSegment
|
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from whisperlivekit.timed_objects import Line, SilentLine, ASRToken, SpeakerSegment, Silence
|
||||||
|
from whisperlivekit.timed_objects import PunctuationSegment
|
||||||
|
|
||||||
|
ALIGNMENT_TIME_TOLERANCE = 0.2 # seconds
|
||||||
|
|
||||||
|
|
||||||
class TokensAlignment:
|
class TokensAlignment:
|
||||||
|
|
@ -12,15 +16,16 @@ class TokensAlignment:
|
||||||
self._diarization_index = 0
|
self._diarization_index = 0
|
||||||
self._translation_index = 0
|
self._translation_index = 0
|
||||||
|
|
||||||
self.all_tokens = []
|
self.all_tokens : list[ASRToken] = []
|
||||||
self.all_diarization_segments = []
|
self.all_diarization_segments: list[SpeakerSegment] = []
|
||||||
self.all_translation_segments = []
|
self.all_translation_segments = []
|
||||||
|
|
||||||
self.new_tokens = []
|
self.new_tokens : list[ASRToken] = []
|
||||||
|
self.new_diarization: list[SpeakerSegment] = []
|
||||||
self.new_translation = []
|
self.new_translation = []
|
||||||
self.new_diarization = []
|
|
||||||
self.new_tokens_buffer = []
|
self.new_tokens_buffer = []
|
||||||
self.sep = ' '
|
self.sep = sep if sep is not None else ' '
|
||||||
|
self.beg_loop = None
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.new_tokens, self.state.new_tokens = self.state.new_tokens, []
|
self.new_tokens, self.state.new_tokens = self.state.new_tokens, []
|
||||||
|
|
@ -32,7 +37,10 @@ class TokensAlignment:
|
||||||
self.all_diarization_segments.extend(self.new_diarization)
|
self.all_diarization_segments.extend(self.new_diarization)
|
||||||
self.all_translation_segments.extend(self.new_translation)
|
self.all_translation_segments.extend(self.new_translation)
|
||||||
|
|
||||||
def create_lines_from_tokens(self, current_silence, beg_loop):
|
def get_lines(self, current_silence):
|
||||||
|
"""
|
||||||
|
In the case without diarization
|
||||||
|
"""
|
||||||
lines = []
|
lines = []
|
||||||
current_line_tokens = []
|
current_line_tokens = []
|
||||||
for token in self.all_tokens:
|
for token in self.all_tokens:
|
||||||
|
|
@ -40,7 +48,7 @@ class TokensAlignment:
|
||||||
if current_line_tokens:
|
if current_line_tokens:
|
||||||
lines.append(Line().build_from_tokens(current_line_tokens))
|
lines.append(Line().build_from_tokens(current_line_tokens))
|
||||||
current_line_tokens = []
|
current_line_tokens = []
|
||||||
end_silence = token.end if token.has_ended else time() - beg_loop
|
end_silence = token.end if token.has_ended else time() - self.beg_loop
|
||||||
if lines and lines[-1].is_silent():
|
if lines and lines[-1].is_silent():
|
||||||
lines[-1].end = end_silence
|
lines[-1].end = end_silence
|
||||||
else:
|
else:
|
||||||
|
|
@ -53,7 +61,7 @@ class TokensAlignment:
|
||||||
if current_line_tokens:
|
if current_line_tokens:
|
||||||
lines.append(Line().build_from_tokens(current_line_tokens))
|
lines.append(Line().build_from_tokens(current_line_tokens))
|
||||||
if current_silence:
|
if current_silence:
|
||||||
end_silence = current_silence.end if current_silence.has_ended else time() - beg_loop
|
end_silence = current_silence.end if current_silence.has_ended else time() - self.beg_loop
|
||||||
if lines and lines[-1].is_silent():
|
if lines and lines[-1].is_silent():
|
||||||
lines[-1].end = end_silence
|
lines[-1].end = end_silence
|
||||||
else:
|
else:
|
||||||
|
|
@ -64,22 +72,104 @@ class TokensAlignment:
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def align_tokens(self):
|
|
||||||
if not self.diarization:
|
|
||||||
pass
|
|
||||||
# return self.all_tokens
|
|
||||||
|
|
||||||
def compute_punctuations_segments(self):
|
def _get_asr_tokens(self) -> list[ASRToken]:
|
||||||
|
return [token for token in self.all_tokens if isinstance(token, ASRToken)]
|
||||||
|
|
||||||
|
def _tokens_to_text(self, tokens: list[ASRToken]) -> str:
|
||||||
|
return ''.join(token.text for token in tokens)
|
||||||
|
|
||||||
|
def _extract_detected_language(self, tokens: list[ASRToken]):
|
||||||
|
for token in tokens:
|
||||||
|
if getattr(token, 'detected_language', None):
|
||||||
|
return token.detected_language
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _speaker_display_id(self, raw_speaker) -> int:
|
||||||
|
if isinstance(raw_speaker, int):
|
||||||
|
speaker_index = raw_speaker
|
||||||
|
else:
|
||||||
|
digits = ''.join(ch for ch in str(raw_speaker) if ch.isdigit())
|
||||||
|
speaker_index = int(digits) if digits else 0
|
||||||
|
return speaker_index + 1 if speaker_index >= 0 else 0
|
||||||
|
|
||||||
|
def _line_from_tokens(self, tokens: list[ASRToken], speaker: int) -> Line:
|
||||||
|
line = Line().build_from_tokens(tokens)
|
||||||
|
line.speaker = speaker
|
||||||
|
detected_language = self._extract_detected_language(tokens)
|
||||||
|
if detected_language:
|
||||||
|
line.detected_language = detected_language
|
||||||
|
return line
|
||||||
|
|
||||||
|
def _find_initial_diar_index(self, diar_segments: list[SpeakerSegment], start_time: float) -> int:
|
||||||
|
for idx, segment in enumerate(diar_segments):
|
||||||
|
if segment.end + ALIGNMENT_TIME_TOLERANCE >= start_time:
|
||||||
|
return idx
|
||||||
|
return len(diar_segments)
|
||||||
|
|
||||||
|
def _find_speaker_for_token(self, token: ASRToken, diar_segments: list[SpeakerSegment], diar_idx: int):
|
||||||
|
if not diar_segments:
|
||||||
|
return None, diar_idx
|
||||||
|
idx = min(diar_idx, len(diar_segments) - 1)
|
||||||
|
midpoint = (token.start + token.end) / 2 if token.end is not None else token.start
|
||||||
|
|
||||||
|
while idx < len(diar_segments) and diar_segments[idx].end + ALIGNMENT_TIME_TOLERANCE < midpoint:
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
candidate_indices = []
|
||||||
|
if idx < len(diar_segments):
|
||||||
|
candidate_indices.append(idx)
|
||||||
|
if idx > 0:
|
||||||
|
candidate_indices.append(idx - 1)
|
||||||
|
|
||||||
|
for candidate_idx in candidate_indices:
|
||||||
|
segment = diar_segments[candidate_idx]
|
||||||
|
seg_start = (segment.start or 0) - ALIGNMENT_TIME_TOLERANCE
|
||||||
|
seg_end = (segment.end or 0) + ALIGNMENT_TIME_TOLERANCE
|
||||||
|
if seg_start <= midpoint <= seg_end:
|
||||||
|
return segment.speaker, candidate_idx
|
||||||
|
|
||||||
|
return None, idx
|
||||||
|
|
||||||
|
def _build_lines_for_tokens(self, tokens: list[ASRToken], diar_segments: list[SpeakerSegment], diar_idx: int):
|
||||||
|
if not tokens:
|
||||||
|
return [], diar_idx
|
||||||
|
|
||||||
|
segment_lines: list[Line] = []
|
||||||
|
current_tokens: list[ASRToken] = []
|
||||||
|
current_speaker = None
|
||||||
|
pointer = diar_idx
|
||||||
|
|
||||||
|
for token in tokens:
|
||||||
|
speaker_raw, pointer = self._find_speaker_for_token(token, diar_segments, pointer)
|
||||||
|
if speaker_raw is None:
|
||||||
|
return [], diar_idx
|
||||||
|
speaker = self._speaker_display_id(speaker_raw)
|
||||||
|
if current_speaker is None or current_speaker != speaker:
|
||||||
|
if current_tokens:
|
||||||
|
segment_lines.append(self._line_from_tokens(current_tokens, current_speaker))
|
||||||
|
current_tokens = [token]
|
||||||
|
current_speaker = speaker
|
||||||
|
else:
|
||||||
|
current_tokens.append(token)
|
||||||
|
|
||||||
|
if current_tokens:
|
||||||
|
segment_lines.append(self._line_from_tokens(current_tokens, current_speaker))
|
||||||
|
|
||||||
|
return segment_lines, pointer
|
||||||
|
|
||||||
|
def compute_punctuations_segments(self, tokens: Optional[list[ASRToken]] = None):
|
||||||
"""Compute segments of text between punctuation marks.
|
"""Compute segments of text between punctuation marks.
|
||||||
|
|
||||||
Returns a list of PunctuationSegment objects, each representing
|
Returns a list of PunctuationSegment objects, each representing
|
||||||
the text from the start (or previous punctuation) to the current punctuation mark.
|
the text from the start (or previous punctuation) to the current punctuation mark.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.all_tokens:
|
tokens = tokens if tokens is not None else self._get_asr_tokens()
|
||||||
|
if not tokens:
|
||||||
return []
|
return []
|
||||||
punctuation_indices = [
|
punctuation_indices = [
|
||||||
i for i, token in enumerate(self.all_tokens)
|
i for i, token in enumerate[ASRToken](tokens)
|
||||||
if token.is_punctuation()
|
if token.is_punctuation()
|
||||||
]
|
]
|
||||||
if not punctuation_indices:
|
if not punctuation_indices:
|
||||||
|
|
@ -91,7 +181,7 @@ class TokensAlignment:
|
||||||
end_idx = punct_idx
|
end_idx = punct_idx
|
||||||
if start_idx <= end_idx:
|
if start_idx <= end_idx:
|
||||||
segment = PunctuationSegment.from_token_range(
|
segment = PunctuationSegment.from_token_range(
|
||||||
tokens=self.all_tokens,
|
tokens=tokens,
|
||||||
token_index_start=start_idx,
|
token_index_start=start_idx,
|
||||||
token_index_end=end_idx,
|
token_index_end=end_idx,
|
||||||
punctuation_token_index=punct_idx
|
punctuation_token_index=punct_idx
|
||||||
|
|
@ -109,4 +199,42 @@ class TokensAlignment:
|
||||||
merged[-1].end = segment.end
|
merged[-1].end = segment.end
|
||||||
else:
|
else:
|
||||||
merged.append(segment)
|
merged.append(segment)
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
def get_lines(self, diarization=False, translation=False):
|
||||||
|
"""
|
||||||
|
Align diarization speaker segments with punctuation-delimited transcription
|
||||||
|
segments (see docs/alignement_principles.md).
|
||||||
|
"""
|
||||||
|
tokens = self._get_asr_tokens()
|
||||||
|
if not tokens:
|
||||||
|
return [], ''
|
||||||
|
|
||||||
|
punctuation_segments = self.compute_punctuations_segments(tokens=tokens)
|
||||||
|
diar_segments = self.concatenate_diar_segments()
|
||||||
|
|
||||||
|
if not punctuation_segments or not diar_segments:
|
||||||
|
return [], self._tokens_to_text(tokens)
|
||||||
|
|
||||||
|
max_diar_end = diar_segments[-1].end
|
||||||
|
if max_diar_end is None:
|
||||||
|
return [], self._tokens_to_text(tokens)
|
||||||
|
|
||||||
|
lines: list[Line] = []
|
||||||
|
last_consumed_index = -1
|
||||||
|
diar_idx = self._find_initial_diar_index(diar_segments, tokens[0].start or 0)
|
||||||
|
|
||||||
|
for segment in punctuation_segments:
|
||||||
|
if segment.end is None or segment.end > max_diar_end:
|
||||||
|
break
|
||||||
|
slice_tokens = tokens[segment.token_index_start:segment.token_index_end + 1]
|
||||||
|
segment_lines, diar_idx = self._build_lines_for_tokens(slice_tokens, diar_segments, diar_idx)
|
||||||
|
if not segment_lines:
|
||||||
|
break
|
||||||
|
lines.extend(segment_lines)
|
||||||
|
last_consumed_index = segment.token_index_end
|
||||||
|
|
||||||
|
buffer_tokens = tokens[last_consumed_index + 1:] if last_consumed_index + 1 < len(tokens) else []
|
||||||
|
buffer_diarization = self._tokens_to_text(buffer_tokens)
|
||||||
|
|
||||||
|
return lines, buffer_diarization
|
||||||
|
|
@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
SENTINEL = object() # unique sentinel object for end of stream marker
|
SENTINEL = object() # unique sentinel object for end of stream marker
|
||||||
MILENCE_DURATION = 3
|
MIN_DURATION_REAL_SILENCE = 5
|
||||||
|
|
||||||
def cut_at(cumulative_pcm, cut_sec):
|
def cut_at(cumulative_pcm, cut_sec):
|
||||||
cumulative_len = 0
|
cumulative_len = 0
|
||||||
|
|
@ -165,7 +165,7 @@ class AudioProcessor:
|
||||||
self.current_silence.is_starting=False
|
self.current_silence.is_starting=False
|
||||||
self.current_silence.has_ended=True
|
self.current_silence.has_ended=True
|
||||||
self.current_silence.compute_duration()
|
self.current_silence.compute_duration()
|
||||||
if self.current_silence.duration > MILENCE_DURATION:
|
if self.current_silence.duration > MIN_DURATION_REAL_SILENCE:
|
||||||
self.state_light.new_tokens.append(self.current_silence)
|
self.state_light.new_tokens.append(self.current_silence)
|
||||||
await self._push_silence_event()
|
await self._push_silence_event()
|
||||||
self.current_silence = None
|
self.current_silence = None
|
||||||
|
|
@ -410,57 +410,32 @@ class AudioProcessor:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.tokens_alignment.update()
|
self.tokens_alignment.update()
|
||||||
lines = self.tokens_alignment.create_lines_from_tokens(self.current_silence, self.beg_loop)
|
lines, buffer_diarization_text, buffer_translation_text = self.tokens_alignment.get_lines(
|
||||||
undiarized_text = ''
|
diarization=self.args.diarization,
|
||||||
|
translation=self.args.translation
|
||||||
|
)
|
||||||
state = await self.get_current_state()
|
state = await self.get_current_state()
|
||||||
# self.tokens_alignment.compute_punctuations_segments()
|
|
||||||
# lines, undiarized_text = format_output(
|
|
||||||
# state,
|
|
||||||
# self.current_silence,
|
|
||||||
# args = self.args,
|
|
||||||
# sep=self.sep
|
|
||||||
# )
|
|
||||||
if lines and lines[-1].speaker == -2:
|
|
||||||
buffer_transcription = Transcript()
|
|
||||||
else:
|
|
||||||
buffer_transcription = state.buffer_transcription
|
|
||||||
|
|
||||||
buffer_diarization = ''
|
|
||||||
if undiarized_text:
|
|
||||||
buffer_diarization = self.sep.join(undiarized_text)
|
|
||||||
|
|
||||||
async with self.lock:
|
|
||||||
self.state.end_attributed_speaker = state.end_attributed_speaker
|
|
||||||
|
|
||||||
buffer_translation_text = ''
|
buffer_translation_text = ''
|
||||||
if state.buffer_translation:
|
buffer_transcription_text = ''
|
||||||
raw_buffer_translation = getattr(state.buffer_translation, 'text', state.buffer_translation)
|
buffer_diarization_text = ''
|
||||||
if raw_buffer_translation:
|
|
||||||
buffer_translation_text = raw_buffer_translation.strip()
|
|
||||||
|
|
||||||
response_status = "active_transcription"
|
response_status = "active_transcription"
|
||||||
if not state.tokens and not buffer_transcription and not buffer_diarization:
|
if not lines and not buffer_transcription_text and not buffer_diarization_text:
|
||||||
response_status = "no_audio_detected"
|
response_status = "no_audio_detected"
|
||||||
lines = []
|
|
||||||
elif not lines:
|
|
||||||
lines = [Line(
|
|
||||||
speaker=1,
|
|
||||||
start=state.end_buffer,
|
|
||||||
end=state.end_buffer
|
|
||||||
)]
|
|
||||||
|
|
||||||
response = FrontData(
|
response = FrontData(
|
||||||
status=response_status,
|
status=response_status,
|
||||||
lines=lines,
|
lines=lines,
|
||||||
buffer_transcription=buffer_transcription.text.strip(),
|
buffer_transcription=buffer_transcription_text,
|
||||||
buffer_diarization=buffer_diarization,
|
buffer_diarization=buffer_diarization_text,
|
||||||
buffer_translation=buffer_translation_text,
|
buffer_translation=buffer_translation_text,
|
||||||
remaining_time_transcription=state.remaining_time_transcription,
|
remaining_time_transcription=state.remaining_time_transcription,
|
||||||
remaining_time_diarization=state.remaining_time_diarization if self.args.diarization else 0
|
remaining_time_diarization=state.remaining_time_diarization if self.args.diarization else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
should_push = (response != self.last_response_content)
|
should_push = (response != self.last_response_content)
|
||||||
if should_push and (lines or buffer_transcription or buffer_diarization or response_status == "no_audio_detected"):
|
if should_push:
|
||||||
yield response
|
yield response
|
||||||
self.last_response_content = response
|
self.last_response_content = response
|
||||||
|
|
||||||
|
|
@ -582,6 +557,7 @@ class AudioProcessor:
|
||||||
if not self.beg_loop:
|
if not self.beg_loop:
|
||||||
self.beg_loop = time()
|
self.beg_loop = time()
|
||||||
self.current_silence = Silence(start=0.0, is_starting=True)
|
self.current_silence = Silence(start=0.0, is_starting=True)
|
||||||
|
self.tokens_alignment.beg_loop = self.beg_loop
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
logger.info("Empty audio message received, initiating stop sequence.")
|
logger.info("Empty audio message received, initiating stop sequence.")
|
||||||
|
|
|
||||||
|
|
@ -162,8 +162,10 @@ class Line(TimedText):
|
||||||
return self.speaker == -2
|
return self.speaker == -2
|
||||||
|
|
||||||
class SilentLine(Line):
|
class SilentLine(Line):
|
||||||
speaker = -2
|
def __init__(self, *args, **kwargs):
|
||||||
text = ''
|
super().__init__(*args, **kwargs)
|
||||||
|
self.speaker = -2
|
||||||
|
self.text = ''
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -192,8 +194,10 @@ class FrontData():
|
||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PunctuationSegment(TimedText):
|
class PunctuationSegment():
|
||||||
"""Represents a segment of text between punctuation marks."""
|
"""Represents a segment of text between punctuation marks."""
|
||||||
|
start: Optional[float]
|
||||||
|
end: Optional[float]
|
||||||
token_index_start: int
|
token_index_start: int
|
||||||
token_index_end: int
|
token_index_end: int
|
||||||
punctuation_token_index: int
|
punctuation_token_index: int
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue