token speakers are only reattributed for token coming after last_validated_token

This commit is contained in:
Quentin Fuxa 2025-10-04 09:52:00 +02:00
parent 543972ef38
commit 374618e050
4 changed files with 97 additions and 108 deletions

View file

@ -62,13 +62,13 @@ class AudioProcessor:
self.bytes_per_sec = self.samples_per_sec * self.bytes_per_sample self.bytes_per_sec = self.samples_per_sec * self.bytes_per_sample
self.max_bytes_per_sec = 32000 * 5 # 5 seconds of audio at 32 kHz self.max_bytes_per_sec = 32000 * 5 # 5 seconds of audio at 32 kHz
self.is_pcm_input = self.args.pcm_input self.is_pcm_input = self.args.pcm_input
self.debug = False
# State management # State management
self.is_stopping = False self.is_stopping = False
self.silence = False self.silence = False
self.silence_duration = 0.0 self.silence_duration = 0.0
self.tokens = [] self.tokens = []
self.last_validated_token = 0
self.translated_segments = [] self.translated_segments = []
self.buffer_transcription = Transcript() self.buffer_transcription = Transcript()
self.end_buffer = 0 self.end_buffer = 0
@ -138,7 +138,7 @@ class AudioProcessor:
async def add_dummy_token(self): async def add_dummy_token(self):
"""Placeholder token when no transcription is available.""" """Placeholder token when no transcription is available."""
async with self.lock: async with self.lock:
current_time = time() - self.beg_loop if self.beg_loop else 0 current_time = time() - self.beg_loop
self.tokens.append(ASRToken( self.tokens.append(ASRToken(
start=current_time, end=current_time + 1, start=current_time, end=current_time + 1,
text=".", speaker=-1, is_dummy=True text=".", speaker=-1, is_dummy=True
@ -161,6 +161,7 @@ class AudioProcessor:
return State( return State(
tokens=self.tokens.copy(), tokens=self.tokens.copy(),
last_validated_token=self.last_validated_token,
translated_segments=self.translated_segments.copy(), translated_segments=self.translated_segments.copy(),
buffer_transcription=self.buffer_transcription, buffer_transcription=self.buffer_transcription,
end_buffer=self.end_buffer, end_buffer=self.end_buffer,
@ -428,35 +429,23 @@ class AudioProcessor:
"""Format processing results for output.""" """Format processing results for output."""
while True: while True:
try: try:
# If FFmpeg error occurred, notify front-end
if self._ffmpeg_error: if self._ffmpeg_error:
yield FrontData( yield FrontData(status="error", error=f"FFmpeg error: {self._ffmpeg_error}")
status="error",
error=f"FFmpeg error: {self._ffmpeg_error}"
)
self._ffmpeg_error = None self._ffmpeg_error = None
await asyncio.sleep(1) await asyncio.sleep(1)
continue continue
# Get current state
state = await self.get_current_state() state = await self.get_current_state()
# Add dummy tokens if needed
if (not state.tokens or state.tokens[-1].is_dummy) and not self.args.transcription and self.args.diarization:
await self.add_dummy_token()
sleep(0.5)
state = await self.get_current_state()
# Format output lines, undiarized_text = format_output(
lines, undiarized_text, end_w_silence = format_output(
state, state,
self.silence, self.silence,
current_time = time() - self.beg_loop if self.beg_loop else None, current_time = time() - self.beg_loop,
args = self.args, args = self.args,
debug = self.debug,
sep=self.sep sep=self.sep
) )
if end_w_silence: if lines and lines[-1].speaker == -2:
buffer_transcription = Transcript() buffer_transcription = Transcript()
else: else:
buffer_transcription = state.buffer_transcription buffer_transcription = state.buffer_transcription

View file

@ -78,16 +78,8 @@ def no_token_to_silence(tokens):
return new_tokens return new_tokens
def ends_with_silence(tokens, current_time, vac_detected_silence): def ends_with_silence(tokens, current_time, vac_detected_silence):
end_w_silence = False
if not tokens:
return [], end_w_silence
last_token = tokens[-1] last_token = tokens[-1]
if tokens and current_time and ( if vac_detected_silence or (current_time - last_token.end >= END_SILENCE_DURATION):
current_time - last_token.end >= END_SILENCE_DURATION
or
(current_time - last_token.end >= 3 and vac_detected_silence)
):
end_w_silence = True
if last_token.speaker == -2: if last_token.speaker == -2:
last_token.end = current_time last_token.end = current_time
else: else:
@ -99,12 +91,14 @@ def ends_with_silence(tokens, current_time, vac_detected_silence):
probability=0.95 probability=0.95
) )
) )
return tokens, end_w_silence return tokens
def handle_silences(tokens, current_time, vac_detected_silence): def handle_silences(tokens, current_time, vac_detected_silence):
if not tokens:
return []
tokens = blank_to_silence(tokens) #useful for simulstreaming backend which tends to generate [BLANK_AUDIO] text tokens = blank_to_silence(tokens) #useful for simulstreaming backend which tends to generate [BLANK_AUDIO] text
tokens = no_token_to_silence(tokens) tokens = no_token_to_silence(tokens)
tokens, end_w_silence = ends_with_silence(tokens, current_time, vac_detected_silence) tokens = ends_with_silence(tokens, current_time, vac_detected_silence)
return tokens, end_w_silence return tokens

View file

@ -7,6 +7,8 @@ logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
CHECK_AROUND = 4 CHECK_AROUND = 4
DEBUG = False
def is_punctuation(token): def is_punctuation(token):
if token.is_punctuation(): if token.is_punctuation():
@ -30,99 +32,96 @@ def next_speaker_change(i, tokens, speaker):
def new_line( def new_line(
token, token,
speaker,
debug_info = ""
): ):
return Line( return Line(
speaker = speaker, speaker = token.corrected_speaker,
text = token.text + debug_info, text = token.text + (f"[{format_time(token.start)} : {format_time(token.end)}]" if DEBUG else ""),
start = token.start, start = token.start,
end = token.end, end = token.end,
detected_language=token.detected_language detected_language=token.detected_language
) )
def append_token_to_last_line(lines, sep, token, debug_info): def append_token_to_last_line(lines, sep, token):
if token.text: if not lines:
lines[-1].text += sep + token.text + debug_info lines.append(new_line(token))
lines[-1].end = token.end else:
if not lines[-1].detected_language and token.detected_language: if token.text:
lines[-1].detected_language = token.detected_language lines[-1].text += sep + token.text + (f"[{format_time(token.start)} : {format_time(token.end)}]" if DEBUG else "")
lines[-1].end = token.end
if not lines[-1].detected_language and token.detected_language:
lines[-1].detected_language = token.detected_language
def format_output(state, silence, current_time, args, debug, sep): def format_output(state, silence, current_time, args, sep):
diarization = args.diarization diarization = args.diarization
disable_punctuation_split = args.disable_punctuation_split disable_punctuation_split = args.disable_punctuation_split
tokens = state.tokens tokens = state.tokens
translated_segments = state.translated_segments # Here we will attribute the speakers only based on the timestamps of the segments translated_segments = state.translated_segments # Here we will attribute the speakers only based on the timestamps of the segments
end_attributed_speaker = state.end_attributed_speaker last_validated_token = state.last_validated_token
previous_speaker = -1 previous_speaker = 1
lines = []
undiarized_text = [] undiarized_text = []
tokens, end_w_silence = handle_silences(tokens, current_time, silence) tokens = handle_silences(tokens, current_time, silence)
last_punctuation = None last_punctuation = None
for i, token in enumerate(tokens): for i, token in enumerate(tokens[last_validated_token:]):
speaker = token.speaker speaker = int(token.speaker)
if not diarization and speaker == -1: #Speaker -1 means no attributed by diarization. In the frontend, it should appear under 'Speaker 1' token.corrected_speaker = speaker
speaker = 1 if not diarization:
if diarization and not tokens[-1].speaker == -2: if speaker == -1: #Speaker -1 means no attributed by diarization. In the frontend, it should appear under 'Speaker 1'
if (speaker in [-1, 0]) and token.end >= end_attributed_speaker: token.corrected_speaker = 1
undiarized_text.append(token.text) token.validated_speaker = True
continue
elif (speaker in [-1, 0]) and token.end < end_attributed_speaker:
speaker = previous_speaker
debug_info = ""
if debug:
debug_info = f"[{format_time(token.start)} : {format_time(token.end)}]"
if not lines:
lines.append(new_line(token, speaker, debug_info = ""))
continue
else: else:
previous_speaker = lines[-1].speaker # if token.end > end_attributed_speaker and token.speaker != -2:
# if tokens[-1].speaker == -2: #if it finishes by a silence, we want to append the undiarized text to the last speaker.
# token.corrected_speaker = previous_speaker
# else:
# undiarized_text.append(token.text)
# continue
# else:
if is_punctuation(token):
last_punctuation = i
if is_punctuation(token): if last_punctuation == i-1:
last_punctuation = i if token.speaker != previous_speaker:
token.validated_speaker = True
# perfect, diarization perfectly aligned
last_punctuation = None
else:
speaker_change_pos, new_speaker = next_speaker_change(i, tokens, speaker)
if speaker_change_pos:
# Corrects delay:
# That was the idea. <Okay> haha |SPLIT SPEAKER| that's a good one
# should become:
# That was the idea. |SPLIT SPEAKER| <Okay> haha that's a good one
token.corrected_speaker = new_speaker
token.validated_speaker = True
elif speaker != previous_speaker:
if not (speaker == -2 or previous_speaker == -2):
if next_punctuation_change(i, tokens):
# Corrects advance:
# Are you |SPLIT SPEAKER| <okay>? yeah, sure. Absolutely
# should become:
# Are you <okay>? |SPLIT SPEAKER| yeah, sure. Absolutely
token.corrected_speaker = previous_speaker
token.validated_speaker = True
else: #Problematic, except if the language has no punctuation. We append to previous line, except if disable_punctuation_split is set to True.
if not disable_punctuation_split:
token.corrected_speaker = previous_speaker
token.validated_speaker = False
if token.validated_speaker:
state.last_validated_token = i
previous_speaker = token.corrected_speaker
previous_speaker = 1
if last_punctuation == i-1: lines = []
if speaker != previous_speaker: for token in tokens:
# perfect, diarization perfectly aligned if int(token.corrected_speaker) != int(previous_speaker):
lines.append(new_line(token, speaker, debug_info = "")) lines.append(new_line(token))
last_punctuation, next_punctuation = None, None else:
continue append_token_to_last_line(lines, sep, token)
speaker_change_pos, new_speaker = next_speaker_change(i, tokens, speaker) previous_speaker = token.corrected_speaker
if speaker_change_pos:
# Corrects delay:
# That was the idea. Okay haha |SPLIT SPEAKER| that's a good one
# should become:
# That was the idea. |SPLIT SPEAKER| Okay haha that's a good one
lines.append(new_line(token, new_speaker, debug_info = ""))
else:
# No speaker change to come
append_token_to_last_line(lines, sep, token, debug_info)
continue
if speaker != previous_speaker:
if speaker == -2 or previous_speaker == -2: #silences can happen anytime
lines.append(new_line(token, speaker, debug_info = ""))
continue
elif next_punctuation_change(i, tokens):
# Corrects advance:
# Are you |SPLIT SPEAKER| okay? yeah, sure. Absolutely
# should become:
# Are you okay? |SPLIT SPEAKER| yeah, sure. Absolutely
append_token_to_last_line(lines, sep, token, debug_info)
continue
else: #we create a new speaker, but that's no ideal. We are not sure about the split. We prefer to append to previous line
if disable_punctuation_split:
lines.append(new_line(token, speaker, debug_info = ""))
continue
pass
append_token_to_last_line(lines, sep, token, debug_info)
if lines and translated_segments: if lines and translated_segments:
unassigned_translated_segments = [] unassigned_translated_segments = []
@ -158,4 +157,4 @@ def format_output(state, silence, current_time, args, debug, sep):
if state.buffer_transcription and lines: if state.buffer_transcription and lines:
lines[-1].end = max(state.buffer_transcription.end, lines[-1].end) lines[-1].end = max(state.buffer_transcription.end, lines[-1].end)
return lines, undiarized_text, end_w_silence return lines, undiarized_text

View file

@ -43,6 +43,12 @@ class TimedText:
@dataclass() @dataclass()
class ASRToken(TimedText): class ASRToken(TimedText):
corrected_speaker: Optional[int] = -1
validated_speaker: bool = False
validated_text: bool = False
validated_language: bool = False
def with_offset(self, offset: float) -> "ASRToken": def with_offset(self, offset: float) -> "ASRToken":
"""Return a new token with the time offset added.""" """Return a new token with the time offset added."""
return ASRToken(self.start + offset, self.end + offset, self.text, self.speaker, self.probability, detected_language=self.detected_language) return ASRToken(self.start + offset, self.end + offset, self.text, self.speaker, self.probability, detected_language=self.detected_language)
@ -169,6 +175,7 @@ class ChangeSpeaker:
@dataclass @dataclass
class State(): class State():
tokens: list tokens: list
last_validated_token: int
translated_segments: list translated_segments: list
buffer_transcription: str buffer_transcription: str
end_buffer: float end_buffer: float