2121
2222import faster_whisper
2323import torch
24- from PyQt6 .QtMultimedia import QMediaPlayer , QAudioOutput
25- from PyQt6 .QtCore import Qt , QThread , QObject , pyqtSignal , QUrl , QTimer
24+ from PyQt6 .QtCore import Qt , QThread , QObject , pyqtSignal , QTimer
2625from PyQt6 .QtGui import QFont
2726from PyQt6 .QtWidgets import (
2827 QWidget ,
4645from buzz .db .service .transcription_service import TranscriptionService
4746from buzz .paths import file_path_as_title
4847from buzz .settings .settings import Settings
48+ from buzz .sounddevice_player import AudioFilePlayer
4949from buzz .widgets .line_edit import LineEdit
5050from buzz .transcriber .transcriber import Segment
5151from buzz .transcriber .speaker_identifier import build_speaker_segments
5252
5353
5454
55+ # Longest speaker sample played by the "Play sample" button
56+ MAX_PREVIEW_DURATION_MS = 10 * 1000
57+
58+
5559def process_in_batches (
5660 items ,
5761 process_func ,
@@ -695,13 +699,35 @@ def _create_save_section(self, layout: QFormLayout) -> None:
695699 layout .addRow (self .save_button )
696700
697701 def _setup_audio_player (self ) -> None :
698- url = QUrl .fromLocalFile (self .transcription .file )
699- self .player = QMediaPlayer ()
700- self .audio_output = QAudioOutput ()
701- self .player .setAudioOutput (self .audio_output )
702- self .player .setSource (url )
702+ # Created on first playback: decoding the file takes a moment and the
703+ # dialog is often closed without any sample being played.
704+ self .player : Optional [AudioFilePlayer ] = None
703705 self .player_timer = None
704706
707+ def _get_audio_player (self ) -> Optional ["AudioFilePlayer" ]:
708+ """Returns the sample player, creating it on first use, or None if the file cannot be played."""
709+ if self .player is not None :
710+ return self .player
711+
712+ if not os .path .isfile (self .transcription .file ):
713+ logging .warning (
714+ "Speaker identification: cannot play sample, file not found %s" ,
715+ self .transcription .file ,
716+ )
717+ return None
718+
719+ player = AudioFilePlayer (self .transcription .file )
720+ if not player .ready :
721+ player .close ()
722+ logging .error (
723+ "Speaker identification: could not open %s for playback" ,
724+ self .transcription .file ,
725+ )
726+ return None
727+
728+ self .player = player
729+ return self .player
730+
705731 def on_identify_button_clicked (self ):
706732 self .step_1_button .setEnabled (False )
707733 self .step_1_button .setVisible (False )
@@ -841,24 +867,40 @@ def on_identification_finished(self, result):
841867 self .needs_layout_update = True
842868
843869 def on_speaker_preview (self , speaker_id ):
844- if self .player_timer :
845- self .player_timer .stop ()
870+ self ._stop_playback ()
846871
847872 speaker_records = [record for record in self .identification_result if record ['speaker' ] == speaker_id ]
848873
849- if speaker_records :
850- random_record = random . choice ( speaker_records )
874+ if not speaker_records :
875+ return
851876
852- start_time = random_record ['start_time' ]
853- end_time = random_record ['end_time' ]
877+ player = self ._get_audio_player ()
878+ if player is None :
879+ return
854880
855- self .player .setPosition (int (start_time ))
856- self .player .play ()
881+ random_record = random .choice (speaker_records )
857882
858- self .player_timer = QTimer (self )
859- self .player_timer .setSingleShot (True )
860- self .player_timer .timeout .connect (self .player .stop )
861- self .player_timer .start (min (end_time , 10 * 1000 )) # 10 seconds
883+ start_time = int (random_record ['start_time' ])
884+ end_time = int (random_record ['end_time' ])
885+ # Stop at the end of the sampled segment, capped so a long monologue
886+ # does not keep playing after the button was clicked.
887+ duration = min (max (end_time - start_time , 0 ), MAX_PREVIEW_DURATION_MS )
888+ if duration <= 0 :
889+ return
890+
891+ player .seek (start_time )
892+ player .play ()
893+
894+ self .player_timer = QTimer (self )
895+ self .player_timer .setSingleShot (True )
896+ self .player_timer .timeout .connect (self ._stop_playback )
897+ self .player_timer .start (duration )
898+
899+ def _stop_playback (self ) -> None :
900+ if self .player_timer :
901+ self .player_timer .stop ()
902+ if self .player is not None :
903+ self .player .stop ()
862904
863905 def on_save_button_clicked (self ):
864906 speaker_names = []
@@ -892,10 +934,7 @@ def on_save_button_clicked(self):
892934 if self .transcriptions_updated_signal :
893935 self .transcriptions_updated_signal .emit (new_transcript_id )
894936
895- self .player .stop ()
896-
897- if self .player_timer :
898- self .player_timer .stop ()
937+ self ._stop_playback ()
899938
900939 self .close ()
901940
@@ -911,10 +950,11 @@ def changeEvent(self, event):
911950 def closeEvent (self , event ):
912951 self .hide ()
913952
914- # Stop media player
915- self .player .stop ()
916- if self .player_timer :
917- self .player_timer .stop ()
953+ # Stop playback and release the decoded audio
954+ self ._stop_playback ()
955+ if self .player is not None :
956+ self .player .close ()
957+ self .player = None
918958
919959 # Clean up thread if running
920960 self ._cleanup_thread ()
0 commit comments