Views
No views yet
1# ==========================================================
2# STEP 0: INSTALL LIBRARIES
3# # ==========================================================
4# print("Installing necessary libraries...")
5# !pip install -q --upgrade transformers datasets accelerate torch torchaudio soundfile librosa
6
7import torch
8import librosa
9import numpy as np
10import os
11from transformers import WhisperProcessor, WhisperForConditionalGeneration
12
13# # --- (Optional) Mount Google Drive ---
14# try:
15# from google.colab import drive
16# drive.mount('/content/drive')
17# print("Google Drive mounted successfully.")
18# except ImportError:
19# print("Not in a Google Colab environment. Skipping Google Drive mount.")
20
21
22# ==========================================================
23# STEP 1: DEFINE THE FINAL, ROBUST, PRODUCTION-READY MODEL CLASS
24# ==========================================================
25class WhisperForSpeakerASR(WhisperForConditionalGeneration):
26 def __init__(self, config):
27 super().__init__(config)
28 self.diarization_head = torch.nn.Linear(config.d_model, 2)
29 # ### THE FINAL, DEFINITIVE FIX - PART 1 ###
30 # Create a temporary storage to capture the logits during generation
31 self.latest_diarization_logits = None
32
33 def forward(
34 self,
35 input_features=None,
36 labels=None,
37 diarization_labels=None,
38 **kwargs,
39 ):
40 # We need to get the encoder_hidden_states.
41 if "encoder_outputs" in kwargs and kwargs["encoder_outputs"] is not None:
42 encoder_hidden_states = kwargs["encoder_outputs"][0]
43 else:
44 if input_features is None:
45 raise ValueError("input_features must be provided when encoder_outputs is not.")
46 encoder_outputs = self.model.encoder(
47 input_features=input_features, output_hidden_states=True
48 )
49 encoder_hidden_states = encoder_outputs.last_hidden_state
50
51 # Run the diarization head on the encoder states
52 diarization_logits = self.diarization_head(encoder_hidden_states)
53
54 # ### THE FINAL, DEFINITIVE FIX - PART 2 ###
55 # Store the logits so we can access them after calling .generate()
56 self.latest_diarization_logits = diarization_logits
57
58 # We now call the ORIGINAL forward pass of the base Whisper model.
59 kwargs["encoder_outputs"] = (encoder_hidden_states,)
60 outputs = super().forward(
61 input_features=input_features,
62 labels=labels,
63 **kwargs,
64 )
65
66 # Attach our custom outputs to the result for consistency (optional but good practice)
67 outputs.diarization_logits = diarization_logits
68
69 # Handle custom loss during training
70 if diarization_labels is not None:
71 loss_fct = torch.nn.CrossEntropyLoss()
72 reshaped_logits = diarization_logits.view(-1, 2)
73 reshaped_labels = diarization_labels.view(-1)
74 diarization_loss = loss_fct(reshaped_logits, reshaped_labels)
75 outputs.loss = diarization_loss if outputs.loss is None else outputs.loss + diarization_loss
76
77 return outputs
78
79# ==========================================================
80# STEP 2: SETUP MODEL AND INFERENCE FUNCTION
81# ==========================================================
82MODEL_HUB_PATH = "Ahmed107/whisper-small-speaker-asr-final"
83
84def test_and_format_from_path(audio_path):
85 print("="*60)
86 print("🚀 Starting Final Inference Test...")
87 print("="*60)
88
89 if not os.path.exists(audio_path):
90 print(f"❌ ERROR: Audio file not found: {audio_path}")
91 return
92
93 print(f"Loading model from: {MODEL_HUB_PATH}")
94 device = "cuda" if torch.cuda.is_available() else "cpu"
95 processor = WhisperProcessor.from_pretrained(MODEL_HUB_PATH)
96 model = WhisperForSpeakerASR.from_pretrained(MODEL_HUB_PATH).to(device).eval()
97
98 print(f"Loading audio file: {audio_path}")
99 audio_array, _ = librosa.load(audio_path, sr=16000)
100 print("Audio loaded successfully.")
101
102 chunk_duration_s = 30
103 samples_per_chunk = chunk_duration_s * 16000
104 time_per_token = chunk_duration_s / 1500
105 speaker_map = {0: "SPEAKER_A", 1: "SPEAKER_B"}
106
107 print("Running model for transcription and diarization...")
108 full_transcription = ""
109 diarization_timeline = []
110
111 with torch.no_grad():
112 for i in range(0, len(audio_array), samples_per_chunk):
113 chunk_start_time = i / 16000
114 chunk_audio = audio_array[i : i + samples_per_chunk]
115 if len(chunk_audio) < samples_per_chunk:
116 chunk_audio = np.pad(chunk_audio, (0, samples_per_chunk - len(chunk_audio)))
117
118 inputs = processor(chunk_audio, return_tensors="pt", sampling_rate=16000)
119 input_features = inputs.input_features.to(device)
120
121 # --- Transcription will now work correctly ---
122 generated_ids = model.generate(input_features, language="en")
123 transcription = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
124 full_transcription += transcription + " "
125
126 # ### THE FINAL, DEFINITIVE FIX - PART 3 ###
127 # Instead of calling the model again, retrieve the stored logits
128 predicted_ids = torch.argmax(model.latest_diarization_logits, dim=-1).squeeze(0).cpu()
129
130 # Process timeline logic (remains the same)
131 current_speaker_id, segment_start_token = -1, 0
132 for token_idx, speaker_id in enumerate(predicted_ids):
133 speaker_id = speaker_id.item()
134 if speaker_id != current_speaker_id:
135 if current_speaker_id != -1:
136 end_time = chunk_start_time + (token_idx * time_per_token)
137 start_time = chunk_start_time + (segment_start_token * time_per_token)
138 diarization_timeline.append({"speaker": speaker_map.get(current_speaker_id, "UNKNOWN"), "start": start_time, "end": end_time})
139 current_speaker_id, segment_start_token = speaker_id, token_idx
140 if current_speaker_id != -1:
141 end_time = chunk_start_time + (len(predicted_ids) * time_per_token)
142 start_time = chunk_start_time + (segment_start_token * time_per_token)
143 diarization_timeline.append({"speaker": speaker_map.get(current_speaker_id, "UNKNOWN"), "start": start_time, "end": end_time})
144
145 # Merge adjacent segments logic (remains the same)
146 merged_timeline = []
147 if diarization_timeline:
148 merged_timeline.append(diarization_timeline[0])
149 for segment in diarization_timeline[1:]:
150 if segment["speaker"] == merged_timeline[-1]["speaker"] and segment["start"] - merged_timeline[-1]["end"] < 0.1:
151 merged_timeline[-1]["end"] = segment["end"]
152 else:
153 merged_timeline.append(segment)
154
155 # Final output for README
156 print("\n\n" + "="*25 + " COPY BELOW THIS LINE " + "="*25)
157 print("\n### Example Output\n")
158 print("```text")
159 print("Full Transcription:")
160 print(full_transcription.strip())
161 print("\nDiarization Timeline:")
162 for segment in merged_timeline:
163 if segment['start'] < segment['end']:
164 print(f"[{segment['start']:0>6.2f}s - {segment['end']:0>6.2f}s] {segment['speaker']}")
165 print("```")
166 print("\n" + "="*26 + " COPY ABOVE THIS LINE " + "="*26 + "\n")
167
168# ==========================================================
169# STEP 3: SPECIFY YOUR FILE PATH AND RUN THE TEST
170# ==========================================================
171# --- ⬇️⬇️⬇️ EDIT THIS LINE ⬇️⬇️⬇️ ---
172AUDIO_FILE_PATH = "/content/audio.wav"
173# --- ⬆️⬆️⬆️ EDIT THIS LINE ⬆️⬆️⬆️ ---
174
175# Run the function
176test_and_format_from_path(AUDIO_FILE_PATH)
177