-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathtranscribe.py
More file actions
executable file
·3221 lines (2767 loc) · 148 KB
/
Copy pathtranscribe.py
File metadata and controls
executable file
·3221 lines (2767 loc) · 148 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
__author__ = "Jérôme Louradour"
__credits__ = ["Jérôme Louradour"]
__license__ = "GPLv3"
__version__ = "1.15.8"
# Set some environment variables
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '1' # Remove warning "This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN)..."
os.environ['CUDA_DEVICE_ORDER'] = 'PCI_BUS_ID' # GPU in the right order
# openai-whisper and pytorch
import whisper
import torch
import torch.nn.functional as F
from importlib.util import find_spec
if find_spec("intel_extension_for_pytorch") is not None:
try:
import intel_extension_for_pytorch
except ImportError:
pass
# For alignment
import numpy as np
import dtw
# from scipy.signal import medfilt as median_filter
from scipy.ndimage import median_filter # faster owing to https://github.com/openai/whisper/commit/f0083e7eb20d032390e42f6f6039947fa8669c93
from scipy.signal import find_peaks
# Additional
import string
import csv
import sys
import gzip, base64
import copy
import re
import shutil
import json
# Constant variables
from whisper.utils import format_timestamp
from whisper.audio import N_FRAMES, HOP_LENGTH, SAMPLE_RATE # 3000, 160, 16000
AUDIO_SAMPLES_PER_TOKEN = HOP_LENGTH * 2 # 320
AUDIO_TIME_PER_TOKEN = AUDIO_SAMPLES_PER_TOKEN / SAMPLE_RATE # 0.02 (sec)
SEGMENT_DURATION = N_FRAMES * HOP_LENGTH / SAMPLE_RATE # 30.0 (sec)
# Access attention in latest versions...
if whisper.__version__ >= "20240930":
from whisper.model import disable_sdpa
else:
from contextlib import contextmanager
# Dummy context manager that does nothing
@contextmanager
def disable_sdpa():
try:
yield
finally:
pass
# Logs
import logging
logger = logging.getLogger("whisper_timestamped")
DEFAULT_BACKEND = "openai-whisper" # "transformers"
USE_EFFICIENT_BY_DEFAULT = True
TRUST_WHISPER_TIMESTAMP_BY_DEFAULT = True
DISFLUENCY_MARK = "[*]"
try:
whisper_version = whisper.__version__
except NameError:
whisper_version = ""
WHIPSER_GE_20230306 = whisper_version >= "20230306"
WHIPSER_GE_20230308 = whisper_version >= "20230308"
def transcribe_timestamped(
# Main Whisper options
model,
audio,
language=None,
task="transcribe",
# Additional options for word alignment
remove_punctuation_from_words=False,
compute_word_confidence=True,
include_punctuation_in_confidence=False,
refine_whisper_precision=0.5,
min_word_duration=0.02, # Was 0.04 before 1.11
plot_word_alignment=False,
word_alignment_most_top_layers=None, # Was 6 before 1.9
remove_empty_words=False,
use_backend_timestamps=False,
# Reproducibility
seed=1234,
vad=False,
detect_disfluencies=False,
trust_whisper_timestamps=TRUST_WHISPER_TIMESTAMP_BY_DEFAULT,
naive_approach=False,
# Other Whisper options
temperature=0.0 if USE_EFFICIENT_BY_DEFAULT else (0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
best_of=None,
beam_size=None,
patience=None,
length_penalty=None,
compression_ratio_threshold=2.4,
logprob_threshold=-1.0,
no_speech_threshold=0.6,
fp16=None,
condition_on_previous_text=True,
initial_prompt=None,
suppress_tokens="-1",
sample_len=None,
verbose=False,
avoid_empty_speech=True,
vad_min_speech_duration=0.1,
vad_min_silence_duration=1,
vad_dilatation=0.5,
):
"""
Transcribe an audio file using Whisper
Parameters
----------
model: Whisper
The Whisper model instance.
audio: Union[str, np.ndarray, torch.Tensor]
The path to the audio file to open, or the audio waveform in 16kHz.
language: str
The language to use for the transcription. If None, the language is detected automatically.
task: str
The task to perform: either "transcribe" or "translate".
remove_punctuation_from_words: bool
If False, words will be glued with the next punctuation mark (if any).
If True, there will be no punctuation mark in the `words[:]["text"]` list.
It only affects these strings; This has no influence on the computation of the word confidence, whatever the value of `include_punctuation_in_confidence` is.
include_punctuation_in_confidence: bool
Whether to include proba of punctuation in the computation of the (previous) word confidence.
compute_word_confidence: bool
Whether to compute word confidence.
If True, a finer confidence for each segment will be computed as well.
vad: bool or str in ["silero", "silero:3.1", "auditok"] or list of start/end timestamps pairs corresponding to speech (ex: [(0.0, 3.50), (32.43, 36.43)])
Whether to perform voice activity detection (VAD) on the audio file, to remove silent parts before transcribing with Whisper model.
This should decrease hallucinations from the Whisper model.
When set to True, the default VAD algorithm is used (silero).
When set to a string, the corresponding VAD algorithm is used (silero, silero:3.1 or auditok).
Note that the library for the corresponding VAD algorithm must be installed.
detect_disfluencies: bool
Whether to detect disfluencies (i.e. hesitations, filler words, repetitions, corrections, etc.) that Whisper model might have omitted in the transcription.
This should make the word timestamp prediction more accurate.
And probable disfluencies will be marked as special words "[*]".
trust_whisper_timestamps: bool
Whether to rely on Whisper's timestamps to get approximative first estimate of segment positions (up to refine_whisper_precision).
refine_whisper_precision: float
How much can we refine Whisper segment positions, in seconds. Must be a multiple of 0.02.
min_word_duration: float
Minimum duration of a word, in seconds. If a word is shorter than this, timestamps will be adjusted.
plot_word_alignment: bool
Whether to plot the word alignment for each segment. matplotlib must be installed to use this option.
remove_empty_words: bool
Whether to remove words with no duration occuring at the end of segments (probable Whisper hallucinations).
use_backend_timestamps: bool
Whether to use word timestamps provided by the backend (openai-whisper or transformers), instead of the ones computed by more complex heuristics of whisper-timestamped.
seed: int
Random seed to use for temperature sampling, for the sake of reproducibility.
Choose None for unpredictable randomness.
naive_approach: bool
Force the naive approach that consists in decoding twice the audio file, once to get the transcritpion and once with the decoded tokens to get the alignment.
Note that this approach is used anyway when beam_size is not None and/or when the temperature is a list with more than one element.
temperature: float
Temperature for sampling.
compression_ratio_threshold: float
If the gzip compression ratio is above this value, treat as failed.
logprob_threshold: float
If the average log probability over sampled tokens is below this value, treat as failed.
no_speech_threshold: float
If the no_speech probability is higher than this value AND the average log probability
over sampled tokens is below `logprob_threshold`, consider the segment as silent.
condition_on_previous_text: bool
if True, the previous output of the model is provided as a prompt for the next window;
disabling may make the text inconsistent across windows, but the model becomes less prone to
getting stuck in a failure loop, such as repetition looping or timestamps going out of sync.
initial_prompt: str
Optional text to provide as a prompt for the first window.
suppress_tokens: str
Comma-separated list of token ids to suppress during sampling;
'-1' will suppress most special characters except common punctuations.
verbose: bool
Whether to display the text being decoded to the console. If True, displays all the details,
If False, displays minimal details. If None, does not display anything
avoid_empty_speech: bool
Whether to avoid empty speech segments (i.e. segments with no speech detected).
vad_min_speech_duration: float
Minimum duration of a speech segment, in seconds. If a speech segment is shorter than this, it will be removed.
vad_min_silence_duration: float
Minimum duration of a silence segment, in seconds. If a silence segment is shorter than this, it will be removed.
vad_dilatation: float
Dilatation factor for the speech segments. If a speech segment is shorter than this, it will be removed.
Returns
-------
A dictionary containing the resulting text ("text") and segment-level details ("segments"), and
the spoken language ("language"), which is detected when `decode_options["language"]` is None.
"""
if seed is not None:
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
# Check input options
assert refine_whisper_precision >= 0 and refine_whisper_precision / AUDIO_TIME_PER_TOKEN == round(refine_whisper_precision / AUDIO_TIME_PER_TOKEN), f"refine_whisper_precision must be a positive multiple of {AUDIO_TIME_PER_TOKEN}"
refine_whisper_precision_nframes = round(refine_whisper_precision / AUDIO_TIME_PER_TOKEN)
assert min_word_duration >= 0, f"min_word_duration must be a positive number"
assert word_alignment_most_top_layers is None or word_alignment_most_top_layers > 0, f"word_alignment_most_top_layers must be a strictly positive number"
if isinstance(temperature, (list, tuple)) and len(temperature) == 1:
temperature = temperature[0]
if isinstance(temperature, (list, tuple)): # temperature fallback
naive_approach = True
elif temperature > 0 and best_of is not None and best_of > 1: # random sampling
naive_approach = True
if beam_size is not None: # beam-search
naive_approach = True
# TODO: check if efficient approach is possible with transformers backend
# (careful: decoding heuristics are completely different from the ones used in openai-whisper)
if is_transformer_model(model) or use_backend_timestamps:
naive_approach = True
# Input options
vad = check_vad_method(vad)
if isinstance(model, str):
model = load_model(model)
if fp16 is None:
fp16 = model.device != torch.device("cpu")
# Safety check
input_stride = N_FRAMES // model.dims.n_audio_ctx
time_precision = input_stride * HOP_LENGTH / SAMPLE_RATE
assert time_precision == AUDIO_TIME_PER_TOKEN
alignment_heads = get_alignment_heads(model) if word_alignment_most_top_layers is None else None
if alignment_heads is None and word_alignment_most_top_layers is None:
word_alignment_most_top_layers = 6
alignment_options = dict(
remove_punctuation_from_words=remove_punctuation_from_words,
compute_word_confidence=compute_word_confidence,
include_punctuation_in_confidence=include_punctuation_in_confidence,
detect_disfluencies=detect_disfluencies,
refine_whisper_precision_nframes=refine_whisper_precision_nframes,
plot_word_alignment=plot_word_alignment,
word_alignment_most_top_layers=word_alignment_most_top_layers,
alignment_heads=alignment_heads,
)
whisper_options = dict(
language=language,
task=task,
fp16=fp16,
temperature=temperature,
best_of=best_of,
beam_size=beam_size,
patience=patience,
length_penalty=length_penalty,
condition_on_previous_text=condition_on_previous_text,
initial_prompt=initial_prompt,
suppress_tokens=suppress_tokens,
sample_len=sample_len,
verbose=verbose if (not vad or verbose is not True) else False,
)
other_options = dict(
no_speech_threshold=no_speech_threshold,
logprob_threshold=logprob_threshold,
compression_ratio_threshold=compression_ratio_threshold,
)
if vad is not None:
audio = get_audio_tensor(audio)
audio, vad_segments, convert_timestamps = remove_non_speech(audio,
method=vad,
sample_rate=SAMPLE_RATE,
plot=plot_word_alignment,
avoid_empty_speech=avoid_empty_speech,
min_speech_duration=vad_min_speech_duration,
min_silence_duration=vad_min_silence_duration,
dilatation=vad_dilatation,
)
else:
vad_segments = None
global num_alignment_for_plot
num_alignment_for_plot = 0
if naive_approach:
(transcription, words) = _transcribe_timestamped_naive(model, audio,
min_word_duration=0.0, # Was 0.04 before 1.11
trust_whisper_timestamps=trust_whisper_timestamps,
use_backend_timestamps=use_backend_timestamps,
**alignment_options, **whisper_options, **other_options)
else:
(transcription, words) = _transcribe_timestamped_efficient(model, audio,
trust_whisper_timestamps=trust_whisper_timestamps,
**alignment_options, **whisper_options, **other_options)
if remove_empty_words:
# Remove words with empty duration happening at the end of segments, to remove some hallucinations
transcription, words = remove_last_null_duration_words(transcription, words, recompute_text=True)
# Refine word positions
ensure_increasing_positions(words, min_duration=min_word_duration if trust_whisper_timestamps else 0)
# Combine words and segments
whisper_segments = transcription["segments"]
for word in words:
if verbose and not naive_approach and not vad:
print_timestamped(word)
word.pop("tokens", None)
word.pop("tokens_indices", None)
if "avg_logprob_reliable" in word:
word.pop("avg_logprob_reliable")
idx_segment = word.pop("idx_segment")
assert idx_segment < len(whisper_segments), f"Fatal error: Got unexpected segment index {idx_segment} >= {len(whisper_segments)}"
segment = whisper_segments[idx_segment]
if "words" in segment:
segment["words"].append(word)
else:
segment["words"] = [word]
if refine_whisper_precision:
segment["start"] = word["start"]
if refine_whisper_precision:
segment["end"] = word["end"]
if vad:
# Recompute timestamps to match the original audio
for segment in whisper_segments:
for word in segment.get("words", []):
word["start"], word["end"] = convert_timestamps(word["start"], word["end"])
if verbose:
print_timestamped(word)
if refine_whisper_precision and len(segment.get("words", [])):
segment["start"] = segment["words"][0]["start"]
segment["end"] = segment["words"][-1]["end"]
else:
segment["start"], segment["end"] = convert_timestamps(segment["start"], segment["end"])
if vad_segments is not None:
transcription["speech_activity"] = [{"start":s, "end":e} for (s,e) in vad_segments]
return transcription
def _transcribe_timestamped_efficient(
model,
audio,
remove_punctuation_from_words,
compute_word_confidence,
include_punctuation_in_confidence,
refine_whisper_precision_nframes,
alignment_heads,
plot_word_alignment,
word_alignment_most_top_layers,
detect_disfluencies,
trust_whisper_timestamps,
use_timestamps_for_alignment = True,
# Whisper specific options
**whisper_options,
):
# Get options
sample_len = whisper_options["sample_len"]
temperature = whisper_options["temperature"]
no_speech_threshold = whisper_options["no_speech_threshold"]
logprob_threshold = whisper_options["logprob_threshold"]
verbose = whisper_options["verbose"]
# Note: "on-the-fly" verbose is not implementable in the current state (we don't know the absolute position of the current chunk). See issue #18
verbose_bugged = False
whisper_options["verbose"] = None if whisper_options["verbose"] is True else whisper_options["verbose"] # We will print intermediate results ourselves
logit_filters = get_logit_filters(model, whisper_options)
language = whisper_options["language"]
tokenizer = get_tokenizer(model, task=whisper_options["task"], language=language)
max_sample_len = sample_len or model.dims.n_text_ctx // 2
n_ctx = model.dims.n_text_ctx
debug = logger.getEffectiveLevel() >= logging.DEBUG
word_alignment_most_top_layers = float("inf") if word_alignment_most_top_layers is None else word_alignment_most_top_layers
# The main outcome
timestamped_word_segments = [] # list of timestamped word segments that have been collected so far
# Main variables to be accumulated
segment_tokens = [[]] # list of lists of token indices that have been collected so far (one list per segment)
segment_attweights = [[] for _ in range(min(word_alignment_most_top_layers, len(model.decoder.blocks)))]
# attention weights on the last segments
segment_avglogprobs = [] # average log probability for each segment (actually of the corresponding chunk, as computed by whisper)
segment_logprobs = [] # token log probabilities for each segment
# Variables related to options that can skip some segments
sot_index = None # index of the SOT token in the current set of processed tokens
no_speech_prob = None # no speech probability for the current 30 sec chunk
chunk_logprobs = [] # log probabilities for the current 30 sec chunk
chunk_tokens = [] # tokens for the current 30 sec chunk (list of Torch tensors)
chunk_tokens_nosot = [] # tokens for the current 30 sec chunk, without the SOT tokens (list of indices)
last_chunk_token = None # last token of the current chunk, that may be needed for corner cases
last_token_fallback = None # last token to use as a fallback if the model gets stuck
has_started = False # whether we have started decoding
mfcc = None # MFCC features for the current 30 sec chunk
new_mfcc = None #
num_inference_steps = 0 # number of inference steps performed so far (for debugging only)
language_probs = None # language detection probabilities
def is_sot(curr_tokens):
return curr_tokens is None or len(curr_tokens) > 1 or curr_tokens[0] == tokenizer.sot
def has_reached_decoding_limit():
n = len(chunk_tokens_nosot) + 1
m = n + (len(chunk_tokens[0]) if len(chunk_tokens) > 0 else 0)
return n + 1 >= max_sample_len or m > n_ctx
def reset(add_segment, keep_last_token=True):
""" Reset the list of tokens for the current speech segment, and corresponding cross-attention weights """
nonlocal segment_tokens, segment_attweights
if add_segment:
if keep_last_token:
segment_tokens.append([segment_tokens[-1][-1]])
segment_attweights = [w[-1:] for w in segment_attweights]
else:
segment_tokens.append([])
segment_attweights = [[] for w in segment_attweights]
segment_tokens[-2].pop(0)
elif len(segment_tokens[-1]) > 0:
if debug:
logger.debug(f"Reset last segment: {tokenizer.decode_with_timestamps(segment_tokens[-1])}")
segment_tokens[-1] = []
segment_attweights = [[] for w in segment_attweights]
saw_consecutive_timestamps = False
def must_flush_segment(curr_tokens):
""" Return whether or not the previously collected tokens must be used to add a new speech segment """
nonlocal segment_tokens, saw_consecutive_timestamps, chunk_tokens_nosot
if not is_sot(curr_tokens):
is_timestamp = curr_tokens[0] >= tokenizer.timestamp_begin
is_previous_timestamp = segment_tokens[-1][-1] >= tokenizer.timestamp_begin if len(segment_tokens[-1]) > 0 else False
consecutive_timestamps = is_timestamp and is_previous_timestamp
if consecutive_timestamps:
saw_consecutive_timestamps = True
return consecutive_timestamps
else: # Several tokens as a prompt or must flush last segments
must_flush = len(segment_tokens[-1]) > 1 and not saw_consecutive_timestamps
if not must_flush and WHIPSER_GE_20230306: # If the last token is a timestamp, the last segment is used
if last_chunk_token is None:
must_flush = (len(segment_tokens[-1]) > 2 and segment_tokens[-1][-1] >= tokenizer.timestamp_begin)
else:
must_flush = (last_chunk_token >= tokenizer.timestamp_begin)
if not must_flush and trust_whisper_timestamps:
# Discard the end of the last transcription
reset(False)
saw_consecutive_timestamps = False
return must_flush
index_begin_30sec_chunck = 0
def get_index_begin_30sec_chunck(curr_tokens):
nonlocal index_begin_30sec_chunck, has_started
if is_sot(curr_tokens) and has_started:
if trust_whisper_timestamps:
res = index_begin_30sec_chunck
index_begin_30sec_chunck = len(segment_tokens)-1
else:
res = len(segment_tokens)-1
return res
def align_last_segment(curr_tokens=None):
nonlocal segment_tokens, segment_attweights, timestamped_word_segments, has_started, no_speech_prob, chunk_tokens, chunk_tokens_nosot, chunk_logprobs, mfcc, new_mfcc, logit_filters, index_begin_30sec_chunck, last_token_fallback, num_inference_steps
if debug and trust_whisper_timestamps:
logger.debug(f"Add segment {len(timestamped_word_segments)+1} at step {num_inference_steps}:\n\t{tokenizer.decode_with_timestamps(segment_tokens[-1])}")
tokens = segment_tokens[-1][1:]
# When the decoding hit the max limit (number of tokens) -- usually when the language model gets stuck --
# then we have to recover the last token from what is send to the decoder
unfinished_decoding = has_reached_decoding_limit()
last_is_not_timestamp = len(tokens) and tokens[-1] < tokenizer.timestamp_begin
last_token_reliable = True
if unfinished_decoding:
logger.debug(f"WARNING: decoding hit the max limit for segment {segment_tokens[-1]} (It usually happens when the language model gets stuck)")
# The last token chosen is in the prompt for the new chunk
if curr_tokens is not None and curr_tokens[0] == tokenizer.sot_prev:
index_sot = (curr_tokens == tokenizer.sot).nonzero(as_tuple=True)
assert len(index_sot) == 1
index_sot = index_sot[0].item()
assert index_sot > 0
last_token_fallback = curr_tokens[index_sot-1].item()
logger.debug(f" Guessed last token from the prompt for the new chunk: {last_token_fallback}")
# Fallback for the last segment, or without prompt: Assume greedy decoding
else:
last_token_fallback = torch.argmax(chunk_logprobs[-1]).item() if last_chunk_token is None else last_chunk_token
last_token_reliable = (temperature == 0)
logger.debug(f" Guess last token using probas (assuming greedy decoding): {last_token_fallback}")
if debug:
logger.debug(f"WARNING: also add last token: {tokenizer.decode_with_timestamps([last_token_fallback])}")
tokens.append(last_token_fallback)
segment_tokens[-1].append(last_token_fallback)
attention_weights = [torch.cat(w, dim=-2) for w in segment_attweights]
last_logprobs = chunk_logprobs[-1]
elif last_is_not_timestamp: # <eot> was emitted early, without a timestamp before
logger.debug(f"WARNING: end timestamp not produced. Adding <|endoftext|>")
tokens.append(tokenizer.eot)
segment_tokens[-1].append(tokenizer.eot)
attention_weights = [torch.cat(w, dim=-2) for w in segment_attweights]
last_logprobs = chunk_logprobs[-1]
else:
attention_weights = [torch.cat(w[:-1], dim=-2) for w in segment_attweights]
last_logprobs = chunk_logprobs[-2]
# Check prediction of last token
end_token = tokens[-1]
if end_token >= tokenizer.timestamp_begin:
start_token = tokens[0]
assert start_token >= tokenizer.timestamp_begin
# If Whisper prediction of the end is obviously wrong, we predict it again (constrained)
if end_token <= start_token:
new_end_token = last_logprobs[start_token+1:].argmax() + start_token + 1
tokens[-1] = new_end_token.item()
if debug:
logger.debug(f"Re-estimated end token {tokenizer.decode_with_timestamps([new_end_token])} (was {tokenizer.decode_with_timestamps([end_token])}) to be after start token {tokenizer.decode_with_timestamps([start_token])}")
if len(tokens) <= 1:
# Corner case: nothing in between timestamps
ws = []
else:
ws = perform_word_alignment(
tokens,
attention_weights,
tokenizer,
use_space=should_use_space(language),
alignment_heads=alignment_heads,
remove_punctuation_from_words=remove_punctuation_from_words,
refine_whisper_precision_nframes=refine_whisper_precision_nframes,
detect_disfluencies=detect_disfluencies,
unfinished_decoding=unfinished_decoding,
mfcc=mfcc,
plot=plot_word_alignment,
debug=debug,
)
add_segment = len(ws) > 0
if add_segment:
timestamped_word_segments.append(ws)
else:
logger.debug(f"Not added!")
reset(add_segment, not is_sot(curr_tokens))
return add_segment, unfinished_decoding, last_token_reliable
def may_flush_segment(curr_tokens = None):
""" Add a speech segment with the new tokens if necessary.
May also remove the last collected segments if filtered out by Whisper (no_speech_prob <= no_speech_threshold)
"""
nonlocal segment_tokens, segment_attweights, timestamped_word_segments, segment_logprobs, has_started, no_speech_prob, chunk_tokens, chunk_tokens_nosot, chunk_logprobs, mfcc, new_mfcc, logit_filters, index_begin_30sec_chunck, last_token_fallback, num_inference_steps, last_chunk_token
# Check if a new segment should be added
unfinished_decoding = False
last_token_reliable = True
if must_flush_segment(curr_tokens) and trust_whisper_timestamps:
_, unfinished_decoding, last_token_reliable = align_last_segment(curr_tokens)
i_start = get_index_begin_30sec_chunck(curr_tokens)
# All segments from previous 30sec chunck have been collected
if i_start is not None:
if not trust_whisper_timestamps:
tokens = torch.Tensor(segment_tokens[-1]).int()
idx_task = torch.where(tokens==tokenizer.sot_sequence[-1])[0][0].item() # index of <|transcribe|>
is_special = tokens.ge(tokenizer.eot)
# Remove prompt
is_special[:idx_task] = True
# Keep begin timestamp
is_special[idx_task:idx_task+2] = False
is_timestamp = tokens.ge(tokenizer.timestamp_begin)
consecutive = torch.where(is_timestamp[1:] & is_timestamp[:-1])[0]
if (WHIPSER_GE_20230306 or has_reached_decoding_limit()) and (
(is_timestamp[-1] and not is_timestamp[-2]) if last_chunk_token is None else
last_chunk_token >= tokenizer.timestamp_begin and not is_timestamp[-2]
):
consecutive = torch.cat([consecutive, torch.Tensor([len(tokens)-1]).int()])
last_is_timestamp = True
if len(consecutive):
# Remove last tokens
is_special[consecutive[-1]+1:] = True
# Keep end timestamp
is_special[consecutive[-1]] = False
elif is_timestamp[-1]:
# Keep end timestamp
is_special[-1] = False
else:
last_is_timestamp = False
if use_timestamps_for_alignment and len(consecutive):
# Keep all timestamps
is_special[idx_task+2:consecutive[-1]] = False
# Do remove what has to be removed
is_next_achar = ~torch.cat([is_special[1:], torch.Tensor([False]).bool()])
for i, weights in enumerate(segment_attweights):
assert len(weights) == len(tokens), f"{len(weights)} attention weights != {len(tokens)}"
# We must remove attention weights used to predict timestamp tokens
segment_attweights[i] = [w for s, w in zip(is_next_achar, weights) if s]
tokens_filtered = tokens[~is_special]
assert len(segment_attweights[0]) == len(tokens_filtered), f"{len(segment_attweights[0])} attention weights != {len(tokens_filtered)} "
# Replace first and last timestamp
orig_start, orig_end = tokens_filtered[1].item(), tokens_filtered[-1].item()
tokens_filtered[1] = tokenizer.timestamp_begin # <|0.00|>
if last_is_timestamp:
tokens_filtered[-1] = tokenizer.timestamp_begin + N_FRAMES // 2 # <|30.00|>
segment_tokens[-1] = tokens_filtered.tolist()
# Do alignment
added, unfinished_decoding, last_token_reliable = align_last_segment()
# Re-split into segments (if necessary)
if added:
if len(consecutive) > 1:
segments_timestamped_concat = timestamped_word_segments[-1]
new_segments_timestamped = []
new_segment_tokens = []
start = idx_task+1
i_word = 0
for i, end in enumerate(consecutive):
end = end.item()
new_segment_tokens.append(tokens[start:end+1].tolist())
if debug:
logger.debug(f"Add segment {len(timestamped_word_segments)+i}:\n\t{tokenizer.decode_with_timestamps(new_segment_tokens[-1])}")
total_length = end - start - 1
start = end+1
length = 0
new_segments_timestamped.append([])
while length < total_length:
if not use_timestamps_for_alignment and i_word == len(segments_timestamped_concat):
# This can happen in the case of "..."
assert total_length == 1 and i == len(consecutive)-1, "Unexpected situation!"
break
assert i_word < len(segments_timestamped_concat), f"i_word={i_word} < len(segments_timestamped_concat)={len(segments_timestamped_concat)}"
word = segments_timestamped_concat[i_word]
new_segments_timestamped[-1].append(word)
length += len(word["tokens_indices"])
i_word += 1
# This can be non zero, when a punctuation (alone in a segment) is glued to the previous segment
if use_timestamps_for_alignment:
assert length == total_length, f"length={length} != total_length={total_length}"
elif length > total_length:
delta = length - total_length
word = new_segments_timestamped[-1][-1]
word_tokindices = word["tokens_indices"]
word_tokens = word["tokens"]
word["tokens_indices"] = word_tokindices[:-delta]
word["tokens"] = word_tokens[:-delta]
word["word"] = "".join(word_tokens[:-delta])
i_word -= 1
t = segments_timestamped_concat[i_word]["end"]
segments_timestamped_concat[i_word] = dict(
text="".join(word_tokens[-delta:]),
start=t, end=t, # Word without timestamp
tokens=word_tokens[-delta:],
tokens_indices=word_tokindices[-delta:],
)
assert i_word == len(segments_timestamped_concat)
segment_tokens = segment_tokens[:-2] + new_segment_tokens + [segment_tokens[-1]]
timestamped_word_segments = timestamped_word_segments[:-1] + new_segments_timestamped
else:
# Recover start and end token
segment = segment_tokens[-2]
tokenizer.decode_with_timestamps([orig_start,orig_end])
segment[0] = orig_start
if last_is_timestamp:
segment[-1] = orig_end
if debug:
logger.debug(f"Add segment {len(timestamped_word_segments)}:\n\t{tokenizer.decode_with_timestamps(segment)}")
if unfinished_decoding:
timestamped_word_segments[-1][-1]["avg_logprob_reliable"] = last_token_reliable
reset(False)
mfcc = new_mfcc
n_segments = len(segment_tokens)-1
# Get word confidence and/or check if previous segments shoud have been skipped
should_skip = False
if compute_word_confidence or no_speech_threshold is not None:
# no voice activity check
should_skip = (no_speech_prob > no_speech_threshold) if (no_speech_threshold is not None) else False
if compute_word_confidence or (should_skip and logprob_threshold is not None):
n = len(chunk_logprobs)
if n == len(chunk_tokens_nosot):
chunk_tokens_nosot = chunk_tokens_nosot[1:]
if unfinished_decoding:
assert last_token_fallback is not None
last_tokens = [last_token_fallback]
timestamped_word_segments[-1][-1]["avg_logprob_reliable"] = last_token_reliable
n += 1
elif has_reached_decoding_limit():
# there were segments in the 30sec chunck, and then the LM got stuck
last_tokens = [torch.argmax(chunk_logprobs[-1]).item()]
timestamped_word_segments[-1][-1]["avg_logprob_reliable"] = (temperature == 0)
else:
last_tokens = [tokenizer.eot]
chunck_indices = chunk_tokens_nosot + last_tokens
assert len(chunk_logprobs) == len(chunck_indices), f"{len(chunk_logprobs)} != {len(chunck_indices)}"
logprobs = torch.cat([logprob[i].unsqueeze(0) for (logprob, i) in zip(chunk_logprobs, chunck_indices)])
assert min([p.isfinite().item() for p in logprobs]), \
f"Got infinite logprob among ({len(logprobs)}) {[(i, tokenizer.decode_with_timestamps([i]), v.item()) for (i,v) in zip(chunck_indices, logprobs)]}"
sum_logprob = sum(logprobs)
avg_logprob = sum_logprob/n
# don't skip if the logprob is high enough, whatever the no_speech_prob is
if logprob_threshold is not None and avg_logprob > logprob_threshold:
should_skip = False
if should_skip:
logger.debug(f"Skipping last {n_segments-i_start} segments (no_speech_prob {no_speech_prob} > {no_speech_threshold} and avg_logprob {avg_logprob} < {logprob_threshold})")
index_begin_30sec_chunck -= n_segments-i_start
segment_tokens = segment_tokens[:i_start] + [segment_tokens[-1]]
timestamped_word_segments = timestamped_word_segments[:i_start]
elif compute_word_confidence:
avg_logprob = avg_logprob.item()
i_token_end = -1
for i in range(i_start, n_segments):
tokens = segment_tokens[i]
i_token_start = i_token_end + 1
i_token_end = i_token_start + len(tokens)
assert chunck_indices[i_token_start:i_token_end] == tokens, f"Inconsistent token list {tokenizer.decode_with_timestamps(chunck_indices[i_token_start:i_token_end])} != {tokenizer.decode_with_timestamps(tokens)}"
i_token_start += 1 # skip sos (start time)
if not unfinished_decoding or i != n_segments-1:
i_token_end -= 1 # skip eos (end time)
segment_logprobs.append(logprobs[i_token_start:i_token_end])
segment_avglogprobs.append(avg_logprob)
else:
for i in range(i_start, n_segments):
segment_logprobs.append(None)
segment_avglogprobs.append(None)
else:
for i in range(i_start, n_segments):
segment_logprobs.append(None)
segment_avglogprobs.append(None)
if verbose_bugged and not should_skip:
for segment in timestamped_word_segments[i_start:]:
for word in segment:
print_timestamped(word)
# Reset counters
chunk_tokens = []
chunk_tokens_nosot = []
chunk_logprobs = []
no_speech_prob = None
def hook_attention_weights(layer, ins, outs, index):
nonlocal segment_attweights
# In old version of whisper, output is a single tensor
assert isinstance(outs, tuple) and len(outs) == 2, "whisper seems to be outdated, please update it (pip install --upgrade --no-deps --force-reinstall git+https://github.com/openai/whisper.git)"
if not has_started:
return
w = outs[-1]
# Only the last attention weights is useful
if w.shape[-2] > 1:
w = w[:, :, -1:, :]
segment_attweights[index].append(w.cpu())
def hook_mfcc(layer, ins, outs):
nonlocal new_mfcc, mfcc
new_mfcc = ins[0]
if mfcc is None:
mfcc = new_mfcc
def hook_input_tokens(layer, ins, outs):
nonlocal segment_tokens, sot_index, chunk_tokens, chunk_tokens_nosot, logit_filters, has_started, language, num_inference_steps
num_inference_steps += 1
curr_tokens = ins[0]
assert curr_tokens.shape[0] == 1, "Batch decoding is not supported"
curr_tokens = curr_tokens.squeeze(0)
if is_sot(curr_tokens):
chunk_prompt = curr_tokens.tolist()
if language is None:
if len(curr_tokens) > 1:
language = tokenizer.decode(curr_tokens[-2:-1])
language = language[2:-2] # remove trailing "<|" and "|>"
whisper_options["language"] = language
if verbose and not whisper_options["verbose"] and len(curr_tokens) > 1:
# Reproduce whisper verbose (2/2)
print(f"Detected language: {whisper.tokenizer.LANGUAGES[language].title()}")
sys.stdout.flush()
logit_filters = get_logit_filters(model, whisper_options, prompt = chunk_prompt[1:-len(tokenizer.sot_sequence)])
may_flush_segment(curr_tokens)
# Get the index of the <|startoftranscript|> tokens (to get proba of silence later)
if is_sot(curr_tokens):
has_started = len(curr_tokens) > 1 or not model.is_multilingual
if no_speech_threshold is not None:
sot_index = curr_tokens.tolist().index(tokenizer.sot)
else:
sot_index = None
# Keep the last token only
if has_started:
segment_tokens[-1].append(curr_tokens[-1].item())
# Accumulate tokens
if has_started:
chunk_tokens.append(curr_tokens)
if not is_sot(curr_tokens):
chunk_tokens_nosot.append(curr_tokens[-1].item())
else:
if verbose and not whisper_options["verbose"]:
# Reproduce whisper verbose (1/2)
print("Detecting language using up to the first 30 seconds. Use `--language` to specify the language")
embedding_weights = None
def hook_output_logits(layer, ins, outs):
nonlocal no_speech_prob, chunk_logprobs, segment_tokens, chunk_tokens, chunk_tokens_nosot, last_chunk_token, embedding_weights, has_started, language, language_probs
if embedding_weights is None:
embedding_weights = torch.transpose(model.decoder.token_embedding.weight, 0, 1).to(outs[0].dtype)
# Get the probability of silence
if sot_index is not None and no_speech_prob is None:
logits = (outs[0][sot_index,:] @ embedding_weights).float()
logits = logits.softmax(dim=-1)
no_speech_prob = logits[tokenizer.no_speech].item()
# Get language probabilities
if language is None and sot_index is not None and model.is_multilingual:
index_start = tokenizer.sot + 1
index_end = index_start + len(tokenizer.all_language_tokens)
logits = (outs[0][sot_index,:] @ embedding_weights).float()
language_probs = logits[index_start:index_end].softmax(dim=-1)
language_probs = dict(zip(whisper.tokenizer.LANGUAGES, language_probs.tolist()))
# Get the log-probabilities of tokens (we don't know yet which one will be chosen)
if has_started:
logits = (outs[0][-1:,:] @ embedding_weights).float()
tokens = torch.cat(chunk_tokens).unsqueeze(0)
for logit_filter in logit_filters:
logit_filter.apply(logits, tokens)
logits = F.log_softmax(logits.squeeze(0), dim=-1)
chunk_logprobs.append(logits)
if WHIPSER_GE_20230306 and has_reached_decoding_limit():
last_chunk_token = torch.argmax(logits).item()
else:
last_chunk_token = None
try:
# Add hooks to the model, to get tokens and attention weights on the fly
all_hooks = []
all_hooks.append(model.encoder.conv1.register_forward_hook(hook_mfcc))
all_hooks.append(model.decoder.token_embedding.register_forward_hook(hook_input_tokens))
nblocks = len(model.decoder.blocks)
j = 0
for i, block in enumerate(model.decoder.blocks):
if i < nblocks - word_alignment_most_top_layers:
continue
all_hooks.append(
block.cross_attn.register_forward_hook(
lambda layer, ins, outs, index=j: hook_attention_weights(layer, ins, outs, index))
)
j += 1
if compute_word_confidence or no_speech_threshold is not None:
all_hooks.append(model.decoder.ln.register_forward_hook(hook_output_logits))
with torch.no_grad():
with disable_sdpa():
transcription = model.transcribe(audio, **whisper_options)
finally:
# Remove hooks
for hook in all_hooks:
hook.remove()
# Finalize (collect last segment)
may_flush_segment()
segment_tokens.pop(-1)
token_special_idx = min(tokenizer.sot, tokenizer.eot)
def filter_tokens(tokens):
while len(tokens) and tokens[0] >= token_special_idx:
tokens = tokens[1:]
while len(tokens) and tokens[-1] >= token_special_idx:
tokens = tokens[:-1]
return tokens
assert len(segment_tokens) == len(timestamped_word_segments), f"Inconsistent number of segments: tokens ({len(segment_tokens)}) != timestamped_word_segments ({len(timestamped_word_segments)})"
assert len(segment_avglogprobs) == len(segment_tokens), f"Inconsistent number of segments: avg logprobs ({len(segment_avglogprobs)}) != tokens ({len(segment_tokens)})"
assert len(segment_logprobs) == len(segment_tokens), f"Inconsistent number of segments: logprobs ({len(segment_logprobs)}) != tokens ({len(segment_tokens)})"
whisper_segments = transcription["segments"]
# See issue 64: some segments may have empty text
if any(not s["text"] for s in whisper_segments):
whisper_segments = [s for s in whisper_segments if s["text"]]
l1 = len(whisper_segments)
l2 = len(timestamped_word_segments)
if l1 != l2 and l1 != 0:
logger.warning(f"Inconsistent number of segments: whisper_segments ({l1}) != timestamped_word_segments ({l2})")
assert l1 == l2 or l1 == 0, f"Inconsistent number of segments: whisper_segments ({l1}) != timestamped_word_segments ({l2})"
logger.debug("Compile results")
words = []
for i, (segment, timestamped_words, token, avglogprob, logprobs) in enumerate(zip(whisper_segments, timestamped_word_segments, segment_tokens, segment_avglogprobs, segment_logprobs)):
timestamped_tokens = filter_tokens(token)
whisper_tokens = filter_tokens(segment["tokens"])
if timestamped_tokens != whisper_tokens:
if len(timestamped_tokens) == len(whisper_tokens) + 1:
logger.warning(f"An additional token was added on segment {i}")
elif WHIPSER_GE_20230306 and len(whisper_tokens) == 0:
logger.warning(f"Whisper has empty segment {i}")
assert segment["end"] == segment["start"], f"Fatal Error: Got empty segment {i} with non-zero duration"
segment["tokens"] = timestamped_tokens
segment["text"] = tokenizer.decode(timestamped_tokens)
else:
assert len(timestamped_tokens) < len(whisper_tokens) and timestamped_tokens == whisper_tokens[:len(timestamped_tokens)], \
f"Fatal Error: Got inconsistent text for segment {i}:\n({len(timestamped_tokens)})\n{tokenizer.decode_with_timestamps(timestamped_tokens)}\n{timestamped_tokens}\n!=\n({len(whisper_tokens)})\n{tokenizer.decode_with_timestamps(whisper_tokens)}\n{whisper_tokens[:len(timestamped_tokens)]}"
segment["tokens"] = token if WHIPSER_GE_20230306 else timestamped_tokens # tokens include special timestamp tokens since 20230306
segment["text"] = tokenizer.decode(segment["tokens"])
logger.warning(f"Text had to be shortned on segment {i}:\n{tokenizer.decode(timestamped_tokens)}\n!=\n{tokenizer.decode(whisper_tokens)}")
timestamped_words[-1]["avg_logprob_reliable"] = False
offset = segment["seek"] * HOP_LENGTH / SAMPLE_RATE
for timestamped_word in timestamped_words:
timestamped_word["start"] += offset
timestamped_word["end"] += offset
timestamped_word["idx_segment"] = i
if compute_word_confidence:
if "avg_logprob_reliable" not in timestamped_words[-1] or timestamped_words[-1]["avg_logprob_reliable"]:
# assert abs(segment["avg_logprob"] - avglogprob) < 1e-2, f"Fatal Error: Got inconsistent logprob for segment {i}: {segment['avg_logprob']} != {avglogprob}"
if abs(segment["avg_logprob"] - avglogprob) >= 1e-2:
logger.warning(f"Recomputed different logprob for segment {i}: {avglogprob} != {segment['avg_logprob']}")
if include_punctuation_in_confidence:
segment["confidence"] = round_confidence(logprobs.mean().exp().item())
else:
logprobs_nopunc = []
i_end = 0
for timestamped_word in timestamped_words:
i_start = i_end