Jahmori commited on
Commit
05fdc41
·
1 Parent(s): ca15b4f

- Formatted Comments

Browse files

- Added Custome Gradio Theme
- Replaced Storage Buckets with Model download

Files changed (4) hide show
  1. .gitignore +5 -4
  2. README.md +2 -2
  3. app.py +140 -111
  4. requirements.txt +1 -0
.gitignore CHANGED
@@ -2,16 +2,17 @@
2
 
3
 
4
  # Virtual Environments
5
- .env
6
- .venv
7
- env/
8
- venv/
9
  ENV/
10
 
11
 
12
  # Ignore specific output directories
13
  speechbrain
14
  uploads
 
15
 
16
  # Audio formats from processing
17
  *.wav
 
2
 
3
 
4
  # Virtual Environments
5
+ .env
6
+ .venv
7
+ env/
8
+ venv/
9
  ENV/
10
 
11
 
12
  # Ignore specific output directories
13
  speechbrain
14
  uploads
15
+ pretrained_models
16
 
17
  # Audio formats from processing
18
  *.wav
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 💻
4
  colorFrom: red
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.24.0
8
  python_version: 3.12.12
9
  app_file: app.py
10
  pinned: false
@@ -16,7 +16,7 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
16
 
17
  # PureVoiceAI
18
 
19
- An advanced, end-to-end audio processing pipeline designed to improve the quality of transcription/note-taking. PureVoiceAI integrates machine learning models to perform audio source separation, speaker diarization, and speech transcription, turning noisy, overlapping audio into clean, attributed text.
20
 
21
  ## Key Features
22
 
 
4
  colorFrom: red
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.26.0
8
  python_version: 3.12.12
9
  app_file: app.py
10
  pinned: false
 
16
 
17
  # PureVoiceAI
18
 
19
+ An advanced, end-to-end audio processing pipeline designed to improve the quality of transcription/note-taking. PureVoiceAI integrates machine learning models to perform audio source separation, voice activity detection, and speech transcription, turning noisy, overlapping audio into clean, attributed text.
20
 
21
  ## Key Features
22
 
app.py CHANGED
@@ -1,37 +1,24 @@
 
1
  import os
2
  import tempfile
 
 
 
3
  import torch
4
  import torchaudio
5
- import glob
6
  import transformers
7
- import gradio as gr
8
- import spaces
9
- from pathlib import Path
10
- from speechbrain.inference.separation import SepformerSeparation
11
- from pyannote.audio import Pipeline
12
- from pyannote.core import Segment, Annotation
13
- from transformers import pipeline, AutoModelForSpeechSeq2Seq, AutoProcessor
14
  from huggingface_hub import sync_bucket
15
-
16
-
17
- # Process Outline
18
- # 1. Initialize ReSepformer model
19
- # 2. Initialize PyAnnote Huggingface model
20
- # 3. Initialize Whisper Huggingface model
21
- # 4. Recieve POST file from front end
22
- # 5. Save file to temporary location
23
- # 6. Loading file into RE-SepFormer and process
24
- # 7. Output files to temporary location
25
- # 8. Load each audio file into PyAnnote model
26
- # 9. Export each audio timings.
27
- # 10. Load separated audio into Whisper model
28
- # 11. Export
29
 
30
  # Disable TF32 for maximum precision (fixes the Pyannote warning)
31
  torch.backends.cuda.matmul.allow_tf32 = False
32
  torch.backends.cudnn.allow_tf32 = False
33
 
34
- # device = "cuda:0" if torch.cuda.is_available() else "cpu" # GPU availability check
35
 
36
 
37
  token = os.environ.get("HF_TOKEN")
@@ -40,19 +27,16 @@ temp_dir = tempfile.gettempdir()
40
 
41
  @spaces.GPU
42
  def process_audio(mixture):
43
-
44
  print(f"temp directory created: {temp_dir}")
45
 
46
  # Name of separated file folder
47
  sources = Path(temp_dir) / "sources"
48
 
49
-
50
  # Creates separated directory
51
- Path.mkdir(sources,exist_ok=True)
52
-
53
 
54
  # Downloads RE-SepFormer model from huggingface buckets to space local directory
55
- sync_bucket("hf://buckets/Jahmori-R/PureVoiceAISpace-storage","./local" )
56
 
57
  # ---------------------------------------------------------
58
  # INITIALIZE MODELS
@@ -60,19 +44,20 @@ def process_audio(mixture):
60
 
61
  print("Intializing models...")
62
 
63
- #Loading Resepformer Model
64
  try:
65
- separator = SepformerSeparation.from_hparams(source=f"./local/resepformer-librimix2spk",hparams_file="hyperparams.yaml",run_opts={"device": "cuda"})
66
-
67
  # separator = SepformerSeparation.from_hparams(
68
- # source="Jahmori-R/resepformer-librimix2spk",
69
- # savedir=f"./pretrained_models/resepformer-librimix2spk",
70
  # run_opts={"device": "cuda"},
71
- # # fetch_config=FetchConfig(token=True)
72
- # token=token,
73
- #
74
  # )
75
- #
 
 
 
 
 
 
76
 
77
  print("RE-SepFormer model successfully loaded")
78
  except Exception as e:
@@ -81,14 +66,16 @@ def process_audio(mixture):
81
 
82
  # #Loading Pyannote Model
83
  try:
84
- diarize = Pipeline.from_pretrained("pyannote/speaker-diarization-community-1",token=token)
 
 
85
  diarize.to(torch.device("cuda"))
86
  print("Pyannote model successfully loaded")
87
  except Exception as e:
88
  print(f"Failed to load Pyannote model. Error: {e}")
89
  raise SystemExit()
90
 
91
- #Loading Whisper Model
92
  # Prevents WhisperAI logging in cmd
93
  transformers.logging.set_verbosity_error()
94
  try:
@@ -98,14 +85,15 @@ def process_audio(mixture):
98
  transcription_model_id,
99
  dtype=torch_dtype,
100
  low_cpu_mem_usage=True,
101
- use_safetensors=True
102
  )
103
 
104
  processor = AutoProcessor.from_pretrained(transcription_model_id)
105
 
106
  transcribe = pipeline(
107
  "automatic-speech-recognition",
108
- model=model,tokenizer=processor.tokenizer,
 
109
  feature_extractor=processor.feature_extractor,
110
  dtype=torch_dtype,
111
  device="cuda",
@@ -116,101 +104,142 @@ def process_audio(mixture):
116
  print(f"Failed to load Whisper model. Error: {e}")
117
  raise SystemExit()
118
 
119
-
120
  # ---------------------------------------------------------
121
  # LOAD & SEPARATE AUDIO
122
  # ---------------------------------------------------------
123
 
124
- #Importing mixed audio source
125
 
126
- #Calling ReSepformer model onto audio source to separate
127
  assert separator is not None, "Failed to load the Sepformer model."
128
 
129
  print("Separating Audio Mixture")
130
- est_sources = separator.separate_file(mixture)
131
  print("Mixture separated")
132
 
133
- torchaudio.save(f"{sources}/speaker_1.wav", est_sources[:,:,0].detach().cpu(), 16000)
134
- torchaudio.save(f"{sources}/speaker_2.wav", est_sources[:,:,1].detach().cpu(), 16000)
135
-
136
- # Saving each speaker as individual file
137
- # for i in range(num_speakers):
138
- # speaker_audio = est_sources[:, :, i]
139
- # temp_pth = os.path.join(sources, f"speaker_{i}.flac")
140
- # torchaudio.save(temp_pth, speaker_audio.cpu(), 16000)
141
- # print(f"Saved: {temp_pth}")
142
 
143
  # ---------------------------------------------------------
144
  # DIARIZATION ON FILES
145
  # ---------------------------------------------------------
146
 
147
  # Dictionary to store speaker information
148
- log = {}
149
- full_timestamps = []
150
-
151
 
152
  # Storing both speakers
153
- file_paths = sorted(glob.glob(f"{sources}/speaker_*.wav")) # Glob finds all similar files
154
-
155
-
156
 
157
  spk_1 = file_paths[0]
158
  spk_2 = file_paths[1]
159
 
160
-
161
  for i, path in enumerate(file_paths):
162
- speaker_key = f"speaker_{i}"
163
- log[speaker_key] = {}
164
-
165
- # waveform, sample_rate = torchaudio.load(path)
166
-
167
- try:
168
- # diarization = diarize({"waveform": waveform, "sample_rate": sample_rate, "speakers": 1})
169
- diarization = diarize(path, num_speakers=1)
170
- timestamps = []
171
-
172
- for turn,_ in diarization.speaker_diarization:
173
- timestamps.append({
174
- 'start': float(round(turn.start,2)),
175
- "end": float(round(turn.end,2)),
176
- })
177
- log[speaker_key]["timestamps"] = timestamps
178
- except Exception as e:
179
- print(f"Pyannote error: {e}")
180
- raise SystemExit()
181
-
182
-
183
-
184
- # ---------------------------------------------------------
185
- # TRANSCRIPTION
186
- # ---------------------------------------------------------
187
-
188
- print("Transcribing...")
189
-
190
-
191
- transcriptions =(transcribe(path,return_timestamps=True))
192
-
193
-
194
- log[speaker_key]["transcriptions"] = transcriptions
195
-
196
- full_timestamps.append(list(transcriptions["chunks"]))
197
-
198
-
199
-
200
-
201
-
202
-
203
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
 
 
 
205
 
206
- return spk_1, spk_2, log, full_timestamps
 
207
 
208
- demo = gr.Interface(
209
- fn=process_audio,
210
- inputs = gr.Audio(type="filepath", label = "Upload Mixture Audio"),
211
- outputs = [gr.Audio(label="Speaker 1"),gr.Audio(label="Speaker 2"),gr.JSON(label="Logs"),gr.JSON(label="Whisper Logs")],
212
- title="PureVoiceAI Project 2026", description = "Source separated end-to-end transcription processing pipeline"
213
- )
214
 
215
  if __name__ == "__main__":
216
- demo.launch(allowed_paths=[temp_dir])
 
1
+ import glob
2
  import os
3
  import tempfile
4
+ from pathlib import Path
5
+ import gradio as gr
6
+ import spaces
7
  import torch
8
  import torchaudio
 
9
  import transformers
10
+ import numpy as np
 
 
 
 
 
 
11
  from huggingface_hub import sync_bucket
12
+ from pyannote.audio import Pipeline
13
+ from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline
14
+ from speechbrain.inference.separation import SepformerSeparation
15
+ from pydub import AudioSegment
 
 
 
 
 
 
 
 
 
 
16
 
17
  # Disable TF32 for maximum precision (fixes the Pyannote warning)
18
  torch.backends.cuda.matmul.allow_tf32 = False
19
  torch.backends.cudnn.allow_tf32 = False
20
 
21
+ device = "cuda:0" if torch.cuda.is_available() else "cpu" # GPU availability check
22
 
23
 
24
  token = os.environ.get("HF_TOKEN")
 
27
 
28
  @spaces.GPU
29
  def process_audio(mixture):
 
30
  print(f"temp directory created: {temp_dir}")
31
 
32
  # Name of separated file folder
33
  sources = Path(temp_dir) / "sources"
34
 
 
35
  # Creates separated directory
36
+ Path.mkdir(sources, exist_ok=True)
 
37
 
38
  # Downloads RE-SepFormer model from huggingface buckets to space local directory
39
+ # sync_bucket("hf://buckets/Jahmori-R/PureVoiceAISpace-storage", "./local")
40
 
41
  # ---------------------------------------------------------
42
  # INITIALIZE MODELS
 
44
 
45
  print("Intializing models...")
46
 
47
+ # Loading Resepformer Model
48
  try:
 
 
49
  # separator = SepformerSeparation.from_hparams(
50
+ # source=f"./local/resepformer-librimix2spk",
51
+ # hparams_file="hyperparams.yaml",
52
  # run_opts={"device": "cuda"},
 
 
 
53
  # )
54
+
55
+ separator = SepformerSeparation.from_hparams(
56
+ source="Jahmori-R/resepformer-librimix2spk",
57
+ savedir=f"./pretrained_models/resepformer-librimix2spk",
58
+ # run_opts={"device": "cuda"},
59
+ # fetch_config=FetchConfig(token=True)
60
+ )
61
 
62
  print("RE-SepFormer model successfully loaded")
63
  except Exception as e:
 
66
 
67
  # #Loading Pyannote Model
68
  try:
69
+ diarize = Pipeline.from_pretrained(
70
+ "pyannote/speaker-diarization-community-1", token=token
71
+ )
72
  diarize.to(torch.device("cuda"))
73
  print("Pyannote model successfully loaded")
74
  except Exception as e:
75
  print(f"Failed to load Pyannote model. Error: {e}")
76
  raise SystemExit()
77
 
78
+ # Loading Whisper Model
79
  # Prevents WhisperAI logging in cmd
80
  transformers.logging.set_verbosity_error()
81
  try:
 
85
  transcription_model_id,
86
  dtype=torch_dtype,
87
  low_cpu_mem_usage=True,
88
+ use_safetensors=True,
89
  )
90
 
91
  processor = AutoProcessor.from_pretrained(transcription_model_id)
92
 
93
  transcribe = pipeline(
94
  "automatic-speech-recognition",
95
+ model=model,
96
+ tokenizer=processor.tokenizer,
97
  feature_extractor=processor.feature_extractor,
98
  dtype=torch_dtype,
99
  device="cuda",
 
104
  print(f"Failed to load Whisper model. Error: {e}")
105
  raise SystemExit()
106
 
 
107
  # ---------------------------------------------------------
108
  # LOAD & SEPARATE AUDIO
109
  # ---------------------------------------------------------
110
 
111
+ # Importing mixed audio source
112
 
113
+ # Calling ReSepformer model onto audio source to separate
114
  assert separator is not None, "Failed to load the Sepformer model."
115
 
116
  print("Separating Audio Mixture")
117
+ est_sources = separator.separate_file(mixture)
118
  print("Mixture separated")
119
 
120
+ torchaudio.save(
121
+ f"{sources}/speaker_1.wav", est_sources[:, :, 0].detach().cpu(), 16000
122
+ )
123
+ torchaudio.save(
124
+ f"{sources}/speaker_2.wav", est_sources[:, :, 1].detach().cpu(), 16000
125
+ )
 
 
 
126
 
127
  # ---------------------------------------------------------
128
  # DIARIZATION ON FILES
129
  # ---------------------------------------------------------
130
 
131
  # Dictionary to store speaker information
132
+ unfiltered_log = [[], []]
133
+ overlap_durations = []
 
134
 
135
  # Storing both speakers
136
+ file_paths = sorted(
137
+ glob.glob(f"{sources}/speaker_*.wav")
138
+ ) # Glob finds all similar files
139
 
140
  spk_1 = file_paths[0]
141
  spk_2 = file_paths[1]
142
 
143
+ # Individual Speaker Processing
144
  for i, path in enumerate(file_paths):
145
+ speaker_key = f"speaker_{i + 1}"
146
+
147
+ # Diarize speaker audio for time segments
148
+ print(f"Diarizing {speaker_key}")
149
+ diarization = diarize(path, num_speakers=1)
150
+
151
+ # Storing speaker voice activity timestamp to dictionary
152
+ for turn, _ in diarization.speaker_diarization:
153
+ unfiltered_log[i].append(
154
+ {
155
+ "start": float(round(turn.start, 2)),
156
+ "end": float(round(turn.end, 2)),
157
+ "speaker": speaker_key,
158
+ }
159
+ )
160
+
161
+ audio_seg = AudioSegment.from_file(path)
162
+
163
+ print("Transcribing Chunks...")
164
+ # Transcribing each voice activity detected by Pyannote
165
+ for segment in unfiltered_log[i]:
166
+ # Converting segment timestamps to milliseconds for pydub
167
+ start_ms = segment["start"] * 1000
168
+ end_ms = segment["end"] * 1000
169
+ chunk = audio_seg[start_ms:end_ms]
170
+
171
+ # Converting pydub AudioSegment to numpy array
172
+ samples = np.array(chunk.get_array_of_samples())
173
+ samples = samples.astype(np.float32) / 32768.0
174
+
175
+ transcription = transcribe(
176
+ samples,
177
+ generate_kwargs={
178
+ "language": "english",
179
+ "condition_on_prev_tokens": True,
180
+ },
181
+ )
182
+ segment["transcription"] = transcription
183
+
184
+ # Combined both speaker logs
185
+ combined_log = unfiltered_log[0] + unfiltered_log[1]
186
+
187
+ # Sorting by start time of timestamps
188
+ filtered_log = sorted(combined_log, key=lambda x: x["start"])
189
+
190
+ # Calculating the overlap between each segment to assign appropriate speaker to segment
191
+ # for log, log_next in zip(filtered_log, filtered_log[1:]):
192
+ # current_start = log["start"]
193
+ # current_end = log["end"]
194
+
195
+ # next_start = log_next["start"]
196
+ # next_end = log_next["end"]
197
+
198
+ # overlap_start = max(current_start, next_start)
199
+ # overlap_end = min(current_end, next_end)
200
+ # overlap = max(0.0, overlap_end - overlap_start)
201
+ # overlap_durations.append(overlap)
202
+
203
+ # Overlkap formula = Min(end 1, end 2) - Max(start 1, start 2)
204
+
205
+ print("Task Finished")
206
+ return (spk_1, spk_2, filtered_log)
207
+
208
+
209
+ # css for interface modification
210
+ css = """
211
+
212
+ #centered-row {
213
+ justify-content: center;
214
+ display: flex;
215
+ }
216
+ """
217
+
218
+ with gr.Blocks(css=css) as demo:
219
+ gr.Markdown("PureVoiceAI")
220
+ with gr.Row(variant="panel"):
221
+ audio_input = gr.Audio(
222
+ type="filepath", label="Input Audio Mixture | 2 Speakers"
223
+ )
224
+ with gr.Row(elem_id="centered-row"):
225
+ button = gr.Button(
226
+ "Process",
227
+ variant="primary",
228
+ scale=0,
229
+ )
230
 
231
+ with gr.Row(variant="panel"):
232
+ speaker_1_output = gr.Audio(label="Speaker 1", interactive=False)
233
+ speaker_2_output = gr.Audio(label="Speaker 2", interactive=False)
234
 
235
+ with gr.Row(variant="panel"):
236
+ logs = gr.JSON(label="Conversation Log")
237
 
238
+ button.click(
239
+ fn=process_audio,
240
+ inputs=audio_input,
241
+ outputs=[speaker_1_output, speaker_2_output, logs],
242
+ )
 
243
 
244
  if __name__ == "__main__":
245
+ demo.launch(allowed_paths=[temp_dir], theme=gr.themes.Soft())
requirements.txt CHANGED
@@ -41,3 +41,4 @@ pytest==7.4.0
41
  ruff==v0.12.4
42
  yamllint==1.35.1
43
  spacy
 
 
41
  ruff==v0.12.4
42
  yamllint==1.35.1
43
  spacy
44
+ pydub