mariesig commited on
Commit
ba729b8
·
1 Parent(s): f5f5219

fix offline pipeline

Browse files
aic_dataset.py DELETED
@@ -1,57 +0,0 @@
1
- import os
2
- from huggingface_hub import hf_hub_download
3
- from constants import DATASET_REPO, MIX_DIR, TRANS_DIR, DATASET_METADATA, HF_TOKEN
4
-
5
-
6
- def _get_base_filenames_from_metadata(metadata_path):
7
- local_path = hf_hub_download(
8
- repo_id=DATASET_REPO,
9
- repo_type="dataset",
10
- filename=metadata_path,
11
- token=HF_TOKEN
12
- )
13
- with open(local_path, "r", encoding="utf-8") as f:
14
- lines = f.read().splitlines()
15
- base_names = []
16
- for line in lines[1:]: # skip header
17
- parts = line.split(",")
18
- if parts and parts[0]:
19
- # Remove directory and extension
20
- filename = os.path.splitext(os.path.basename(parts[0]))[0]
21
- base_names.append(filename)
22
- return base_names
23
-
24
- ALL_FILES = _get_base_filenames_from_metadata(DATASET_METADATA)
25
-
26
- def get_local_mix_path(file_stem: str) -> str:
27
- if not file_stem:
28
- return ""
29
-
30
- mix_path = f"{MIX_DIR}/{file_stem}.wav"
31
-
32
- # Download selected files into local cache; returns local filesystem paths
33
- mix_local = hf_hub_download(
34
- repo_id=DATASET_REPO, repo_type="dataset",
35
- filename=mix_path, token=HF_TOKEN
36
- )
37
-
38
- return mix_local
39
-
40
- def download_transcript(file_stem: str) -> str:
41
- """
42
- file_stem is the base filename to be downloaded
43
- """
44
- if not file_stem:
45
- return ""
46
-
47
- transcript_path = f"{TRANS_DIR}/{file_stem}.txt"
48
-
49
-
50
- transcript_text = ""
51
- transcript_local = hf_hub_download(
52
- repo_id=DATASET_REPO, repo_type="dataset",
53
- filename=transcript_path, token=HF_TOKEN
54
- )
55
- with open(transcript_local, "r", encoding="utf-8", errors="replace") as f:
56
- transcript_text = f.read()
57
- return transcript_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,45 +1,20 @@
1
- import os
2
- import time
3
 
4
  import gradio as gr
5
- from loguru import logger
6
-
7
- from constants import MINUTES_KEEP
8
- from aic_dataset import ALL_FILES, get_local_mix_path
9
-
10
- from online import transcribe as online_transcribe, clear_ui as online_clear_ui, change_stt_model
11
- from offline import transcribe_with_original, transcribe_no_original, denoise_audio, cleanup, start_processing
12
-
13
- # ===============================
14
- # Temporary File & Cache Management
15
- # ===============================
16
- def cleanup_tmp(minutes_keep: int = MINUTES_KEEP, filter: list[str] = []):
17
- skipped = 0
18
- removed = 0
19
- if os.path.exists("/tmp"):
20
- for root, _, files in os.walk("/tmp"):
21
- for name in files:
22
- f = os.path.join(root, name)
23
- is_old = (time.time() - os.path.getmtime(f)) / 60 > minutes_keep
24
- filtered = any(filt in f for filt in filter)
25
- if filtered or not is_old:
26
- skipped += 1
27
- continue
28
- try:
29
- os.remove(f)
30
- removed += 1
31
- except Exception as e:
32
- logger.warning(f"Failed to remove file {f}: {e}")
33
- logger.info(f"Cleanup tmp complete. Removed {removed} files, skipped {skipped} files.")
34
-
35
-
36
- # ===============================
37
- # Gradio UI Layout
38
- # ===============================
39
- with gr.Blocks() as demo:
40
- input_enhancement = gr.State()
41
- last_audio_file = gr.State()
42
 
 
 
 
 
 
 
 
43
  gr.HTML(
44
  '<a href="https://ai-coustics.com/" target="_blank">'
45
  '<img src="https://mintcdn.com/ai-coustics/Sxcrv8jVSE2qWMR1/logo/dark.svg?fit=max&auto=format&n=Sxcrv8jVSE2qWMR1&q=85&s=7f26caaf21e963912961cbd8541e6d84" '
@@ -66,20 +41,20 @@ with gr.Blocks() as demo:
66
  # =========================
67
  # OFFLINE TAB
68
  # =========================
69
- with gr.Tab("Offline", elem_classes="tab-offline"):
70
  with gr.Group(elem_classes="panel"):
71
- with gr.Tab("Upload", elem_classes="upload-tab"):
72
- audio_file_upload = gr.Audio(type="filepath", sources=["upload", "microphone"])
73
  enhance_btn_for_upload = gr.Button("Enhance", scale=2)
74
 
75
- with gr.Tab("AIC Dataset", elem_classes="dataset-tab"):
76
  dataset_dropdown = gr.Dropdown(choices=ALL_FILES, label="Choose sample", value=None)
77
  audio_file_from_dataset = gr.Audio(type="filepath", interactive=False)
78
  enhance_btn_for_dataset = gr.Button("Enhance", scale=2)
79
 
80
  with gr.Group(elem_classes="panel results-card", visible=False) as results_card:
81
  result_title = gr.Markdown("")
82
- enhanced_audio = gr.Audio(type="filepath", interactive=False)
83
 
84
  with gr.Row(equal_height=True, elem_classes="results-row"):
85
  with gr.Column(scale=5, min_width=320, elem_classes="results-left"):
@@ -91,48 +66,47 @@ with gr.Blocks() as demo:
91
  enhanced_transcript = gr.Textbox(label="Enhanced transcript", lines=3, interactive=False)
92
  wer_box = gr.Number(label="Word Error Rate (WER)", interactive=False)
93
 
94
- # Wiring (offline)
95
- dataset_dropdown.change(get_local_mix_path, inputs=dataset_dropdown, outputs=[audio_file_from_dataset])
96
-
 
 
 
97
  enhance_btn_for_dataset.click(
98
- cleanup,
99
- inputs=[input_enhancement, last_audio_file, audio_file_from_dataset],
100
- outputs=None,
101
- ).then(
102
- start_processing,
103
- inputs=audio_file_from_dataset,
104
- outputs=[input_enhancement, last_audio_file, results_card, result_title],
105
  ).success(
106
  denoise_audio,
107
- inputs=[input_enhancement, enhancement_level],
108
- outputs=[enhanced_audio, enhanced_image, noisy_image],
109
- ).then(
110
- transcribe_with_original,
111
- inputs=[enhanced_audio, dataset_dropdown, stt_model],
112
- outputs=[enhanced_transcript, wer_box, original_transcript],
113
- ).then(
114
  lambda: gr.update(visible=True),
115
  inputs=None,
116
  outputs=results_card,
117
  )
118
 
119
  enhance_btn_for_upload.click(
120
- cleanup,
121
- inputs=[input_enhancement, last_audio_file, audio_file_upload],
122
- outputs=None,
123
  ).then(
124
- start_processing,
125
- inputs=audio_file_upload,
126
- outputs=[input_enhancement, last_audio_file, results_card, result_title],
127
  ).success(
128
  denoise_audio,
129
- inputs=[input_enhancement, enhancement_level],
130
- outputs=[enhanced_audio, enhanced_image, noisy_image],
131
- ).then(
132
- transcribe_no_original,
133
- inputs=[enhanced_audio, stt_model],
134
- outputs=[enhanced_transcript, wer_box, original_transcript],
135
- ).then(
136
  lambda: gr.update(visible=True),
137
  inputs=None,
138
  outputs=results_card,
@@ -154,17 +128,23 @@ with gr.Blocks() as demo:
154
 
155
 
156
  audio_stream.stream(
157
- fn=online_transcribe,
158
  inputs=[stream_state, audio_stream, enhancement_level],
159
  outputs=[stream_state, enhanced_text, raw_text],
160
  stream_every=0.05,
161
  )
162
 
163
  clear_btn.click(
164
- fn=online_clear_ui,
165
  outputs=[stream_state, enhanced_text, raw_text],
166
  )
167
-
168
-
169
- cleanup_tmp(minutes_keep=0, filter=[])
 
 
 
 
 
 
170
  demo.launch(allowed_paths=["/tmp", "/"])
 
1
+ from random import sample
 
2
 
3
  import gradio as gr
4
+ from hf_dataset_utils import ALL_FILES
5
+
6
+ from online_pipeline import transcribe_stream, reset_streamers, stop_streaming, change_stt_model
7
+ from offline_pipeline import load_file_from_dataset, load_local_file, denoise_audio, retrieve_audio_information
8
+ from clean_up import purge_tmp_directory, cleanup_previous_run
9
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+
12
+ with gr.Blocks() as demo:
13
+ sample_stem_dropdown= gr.State("")
14
+ sample_stem_local = gr.State("")
15
+ input_array = gr.State()
16
+ enhanced_array = gr.State()
17
+
18
  gr.HTML(
19
  '<a href="https://ai-coustics.com/" target="_blank">'
20
  '<img src="https://mintcdn.com/ai-coustics/Sxcrv8jVSE2qWMR1/logo/dark.svg?fit=max&auto=format&n=Sxcrv8jVSE2qWMR1&q=85&s=7f26caaf21e963912961cbd8541e6d84" '
 
41
  # =========================
42
  # OFFLINE TAB
43
  # =========================
44
+ with gr.Tab("Offline", elem_classes="tab-offline") as offline_tab:
45
  with gr.Group(elem_classes="panel"):
46
+ with gr.Tab("Upload", elem_classes="upload-tab") as upload_tab:
47
+ audio_file_upload = gr.Audio(type="filepath", sources=["upload"])
48
  enhance_btn_for_upload = gr.Button("Enhance", scale=2)
49
 
50
+ with gr.Tab("Dawn Chorus", elem_classes="dataset-tab") as dataset_tab:
51
  dataset_dropdown = gr.Dropdown(choices=ALL_FILES, label="Choose sample", value=None)
52
  audio_file_from_dataset = gr.Audio(type="filepath", interactive=False)
53
  enhance_btn_for_dataset = gr.Button("Enhance", scale=2)
54
 
55
  with gr.Group(elem_classes="panel results-card", visible=False) as results_card:
56
  result_title = gr.Markdown("")
57
+ enhanced_audio = gr.Audio(type="numpy", interactive=False)
58
 
59
  with gr.Row(equal_height=True, elem_classes="results-row"):
60
  with gr.Column(scale=5, min_width=320, elem_classes="results-left"):
 
66
  enhanced_transcript = gr.Textbox(label="Enhanced transcript", lines=3, interactive=False)
67
  wer_box = gr.Number(label="Word Error Rate (WER)", interactive=False)
68
 
69
+ dataset_dropdown.change(
70
+ load_file_from_dataset,
71
+ inputs=dataset_dropdown,
72
+ outputs=[audio_file_from_dataset, input_array, sample_stem_dropdown]
73
+ )
74
+
75
  enhance_btn_for_dataset.click(
76
+ cleanup_previous_run,
77
+ inputs=[sample_stem_dropdown],
78
+ outputs=[enhanced_image, noisy_image, enhanced_transcript, original_transcript, wer_box, results_card],
 
 
 
 
79
  ).success(
80
  denoise_audio,
81
+ inputs=[input_array, enhancement_level],
82
+ outputs=[enhanced_array, enhanced_audio],
83
+ ).success(
84
+ retrieve_audio_information,
85
+ inputs=[input_array, enhanced_array, sample_stem_dropdown, stt_model],
86
+ outputs=[enhanced_image, noisy_image, enhanced_transcript, original_transcript, wer_box],
87
+ ).success(
88
  lambda: gr.update(visible=True),
89
  inputs=None,
90
  outputs=results_card,
91
  )
92
 
93
  enhance_btn_for_upload.click(
94
+ cleanup_previous_run,
95
+ inputs=[sample_stem_local],
96
+ outputs=[enhanced_image, noisy_image, enhanced_transcript, original_transcript, wer_box, results_card],
97
  ).then(
98
+ load_local_file,
99
+ inputs=[audio_file_upload],
100
+ outputs=[input_array, sample_stem_local],
101
  ).success(
102
  denoise_audio,
103
+ inputs=[input_array, enhancement_level],
104
+ outputs=[enhanced_array, enhanced_audio],
105
+ ).success(
106
+ retrieve_audio_information,
107
+ inputs=[input_array, enhanced_array, sample_stem_local, stt_model],
108
+ outputs=[enhanced_image, noisy_image, enhanced_transcript, original_transcript, wer_box],
109
+ ).success(
110
  lambda: gr.update(visible=True),
111
  inputs=None,
112
  outputs=results_card,
 
128
 
129
 
130
  audio_stream.stream(
131
+ fn=transcribe_stream,
132
  inputs=[stream_state, audio_stream, enhancement_level],
133
  outputs=[stream_state, enhanced_text, raw_text],
134
  stream_every=0.05,
135
  )
136
 
137
  clear_btn.click(
138
+ fn=reset_streamers,
139
  outputs=[stream_state, enhanced_text, raw_text],
140
  )
141
+
142
+ offline_tab.select(
143
+ fn=stop_streaming,
144
+ inputs=None,
145
+ outputs=[audio_stream, stream_state, enhanced_text, raw_text],
146
+ )
147
+
148
+
149
+ purge_tmp_directory(max_age_minutes=0, skip_substrings=[])
150
  demo.launch(allowed_paths=["/tmp", "/"])
clean_up.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ from loguru import logger
4
+ import gradio as gr
5
+ from typing import Any
6
+ from constants import MINUTES_KEEP
7
+
8
+
9
+ def purge_tmp_directory(
10
+ max_age_minutes: int = MINUTES_KEEP,
11
+ skip_substrings: list[str] | None = None,
12
+ tmp_dir: str = "/tmp",
13
+ ) -> tuple[int, int]:
14
+ """
15
+ Delete files in `tmp_dir` older than `max_age_minutes`, optionally skipping paths
16
+ that contain any substring in `skip_substrings`.
17
+
18
+ Args:
19
+ max_age_minutes: Files older than this many minutes will be deleted.
20
+ skip_substrings: If provided, any file path containing one of these substrings
21
+ will NOT be deleted.
22
+ tmp_dir: Directory to scan (default: /tmp).
23
+
24
+ Returns:
25
+ (removed, skipped): Number of files removed and skipped.
26
+ """
27
+ skip_substrings = skip_substrings or []
28
+ skipped = 0
29
+ removed = 0
30
+
31
+ if not os.path.exists(tmp_dir):
32
+ logger.info(f"Temp directory does not exist: {tmp_dir}")
33
+ return removed, skipped
34
+
35
+ now = time.time()
36
+
37
+ for root, _, files in os.walk(tmp_dir):
38
+ for name in files:
39
+ path = os.path.join(root, name)
40
+
41
+ # Skip explicitly excluded paths
42
+ if any(s in path for s in skip_substrings):
43
+ skipped += 1
44
+ continue
45
+
46
+ try:
47
+ age_minutes = (now - os.path.getmtime(path)) / 60.0
48
+ except Exception as e:
49
+ logger.warning(f"Could not read mtime for {path}: {e}")
50
+ skipped += 1
51
+ continue
52
+
53
+ if age_minutes <= max_age_minutes:
54
+ skipped += 1
55
+ continue
56
+
57
+ try:
58
+ os.remove(path)
59
+ removed += 1
60
+ except Exception as e:
61
+ logger.warning(f"Failed to remove file {path}: {e}")
62
+ skipped += 1
63
+
64
+ logger.info(f"Temp cleanup complete in {tmp_dir}. Removed {removed} files, skipped {skipped} files.")
65
+ return removed, skipped
66
+
67
+
68
+ def remove_files_related_to(stem: str, tmp_dir: str = "/tmp") -> int:
69
+ deleted = 0
70
+
71
+ if not stem:
72
+ return 0
73
+ if not os.path.exists(tmp_dir):
74
+ return 0
75
+
76
+ for root, _, files in os.walk(tmp_dir):
77
+ for filename in files:
78
+ if stem not in filename:
79
+ continue
80
+ full_path = os.path.join(root, filename)
81
+ try:
82
+ os.remove(full_path)
83
+ deleted += 1
84
+ except Exception as e:
85
+ logger.warning(f"Failed to delete file {full_path}: {e}")
86
+
87
+ logger.info(f"Deleted {deleted} files in {tmp_dir} related to '{stem}'.")
88
+ return deleted
89
+
90
+
91
+ def cleanup_previous_run(
92
+ sample_stem: str,
93
+ tmp_dir: str = "/tmp",
94
+ max_age_minutes: int = MINUTES_KEEP,
95
+ ) -> tuple[None, None, str, str, None, Any]:
96
+ try:
97
+ remove_files_related_to(sample_stem, tmp_dir=tmp_dir)
98
+ except Exception as e:
99
+ print(f"Failed to delete last run with id {sample_stem}: {e}")
100
+ purge_tmp_directory(max_age_minutes=max_age_minutes, tmp_dir=tmp_dir)
101
+ return None, None, "", "", None, gr.update(visible=False)
constants.py CHANGED
@@ -1,6 +1,8 @@
1
  from typing import Final
2
  import os
3
 
 
 
4
  CHUNK_SIZE: Final = 1024
5
  TIMEOUT_FACTOR_MB: Final = 60
6
  BASE_TIMEOUT_SECONDS: Final = 120
@@ -9,13 +11,12 @@ MODEL_ID: Final = "quail-vf-2.0-l-16khz"
9
 
10
  MINUTES_KEEP: Final = 60
11
 
12
- DATASET_REPO: Final = "ai-coustics/leo_butch_voice_focus_open_source_en_vad"
 
13
  MIX_DIR: Final = "mix"
14
  SPEECH_DIR: Final = "speech"
15
  TRANS_DIR: Final = "transcripts"
16
- DATASET_METADATA: Final = "metadata.csv"
17
 
18
  # Private access token from Space secrets:
19
- HF_TOKEN: Final = os.getenv("HF_TOKEN") # set in HF Space Secrets
20
 
21
- DEFAULT_SR: Final = 16000
 
1
  from typing import Final
2
  import os
3
 
4
+ from stt_streamers.soniox_streamer import SONIOX_WEBSOCKET_URL
5
+
6
  CHUNK_SIZE: Final = 1024
7
  TIMEOUT_FACTOR_MB: Final = 60
8
  BASE_TIMEOUT_SECONDS: Final = 120
 
11
 
12
  MINUTES_KEEP: Final = 60
13
 
14
+ DATASET_NAME: Final = "ai-coustics/dawn_chorus_en"
15
+ DEFAULT_SPLIT: Final = "eval"
16
  MIX_DIR: Final = "mix"
17
  SPEECH_DIR: Final = "speech"
18
  TRANS_DIR: Final = "transcripts"
 
19
 
20
  # Private access token from Space secrets:
21
+ DEFAULT_SR: Final = 16000
22
 
 
docs/intro.md CHANGED
@@ -1,2 +1,9 @@
1
- This demo showcases **Voice Focus**, our real-time speech enhancement model, combined with **speech-to-text (STT)** transcription.
2
- Enhance speech, view transcripts instantly, and evaluate recognition quality — either offline or live.
 
 
 
 
 
 
 
 
1
+ Welcome! This Space lets you try **ai‑coustics VoiceFocus (Quail Voice Focus)** — a real‑time, STT‑oriented enhancement model that **isolates the foreground speaker** and **suppresses competing voices + background noise**. It’s tuned to keep the phonetic cues speech‑to‑text systems need, so the output isn’t always “prettier” — just cleaner for transcription.
2
+
3
+ **Offline:** upload an audio file or pick a sample from the dataset, then listen to the enhanced result and compare raw vs enhanced transcripts.
4
+
5
+ **Online:** stream from your microphone and watch raw vs enhanced text update live.
6
+
7
+ Use **Enhancement level (0–100)** to dial in the strength, and switch the **STT backend (Deepgram / Soniox)** to see how different engines react to cleaner input.
8
+
9
+ Tip: speak close to your mic (near‑field) and keep a steady level for best results. Please don’t upload sensitive or private audio—use test material only.
hf_dataset_utils.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from datasets import load_dataset, Audio
4
+ from constants import DATASET_NAME, DEFAULT_SPLIT
5
+
6
+ # Load once (HF datasets handles caching; HF_TOKEN / login is used automatically if needed)
7
+ ds = load_dataset(DATASET_NAME, split=DEFAULT_SPLIT)
8
+ ds = ds.cast_column("mix", Audio(sampling_rate=16000, decode=True))
9
+ ds = ds.cast_column("speech", Audio(sampling_rate=16000, decode=True))
10
+
11
+ ALL_FILES = ds["id"]
12
+
13
+
14
+
15
+ def get_audio(sample_id: str, prefix: str) -> tuple[np.ndarray, int]:
16
+ row = ds.filter(lambda x: x["id"] == sample_id)[0]
17
+ array = row[f"{prefix}"]["array"]
18
+ sr = row[f"{prefix}"]["sampling_rate"]
19
+ np_array = np.array(array, dtype=np.float32)
20
+ return np_array, sr
21
+
22
+
23
+ def get_transcript(sample_id: str) -> str:
24
+ row = ds.filter(lambda x: x["id"] == sample_id)[0]
25
+ return row.get("transcript", "")
offline.py DELETED
@@ -1,112 +0,0 @@
1
- import os
2
- from typing import Optional, Any
3
-
4
- import gradio as gr
5
- from loguru import logger
6
-
7
- from sdk import SDKWrapper
8
- from audio_tools import spec_image
9
- import shutil
10
- import tempfile
11
- from aic_dataset import download_transcript
12
- from transcribe import transcribe_and_evaluate, transcribe_file
13
-
14
-
15
- def transcribe_with_original(
16
- audio_file_path: str,
17
- file_stem: str,
18
- streamer_type: str = "deepgram",
19
- ) -> tuple[str, Any, Any]:
20
- original_transcript = download_transcript(file_stem)
21
- transcript, wer = transcribe_and_evaluate(audio_file_path, original_transcript, streamer_type)
22
- wer_box = gr.update(value=wer, visible=True)
23
- original_transcript_update = gr.update(value=original_transcript, visible=True)
24
- return transcript, wer_box, original_transcript_update
25
-
26
-
27
- def transcribe_no_original(audio_file_path: str, stt_model: str = "deepgram") -> tuple[str, Any, Any]:
28
- transcript = transcribe_file(audio_file_path, stt_model)
29
- hidden = gr.update(visible=False)
30
- return transcript, hidden, hidden
31
-
32
-
33
- def create_results_title(path: str) -> str:
34
- file_name = os.path.basename(path)
35
- return f"## Results for {file_name}" if file_name else "## Results"
36
-
37
-
38
- # ===============================
39
- # Enhancement (offline)
40
- # ===============================
41
- def denoise_audio(
42
- sample_path: str,
43
- enhancement_level: float = 50.0,
44
- ) -> tuple[Optional[str], Optional[str], Optional[str]]:
45
- gr.Info("Processing started. This may take a moment. Please do not refresh or close the window.")
46
-
47
- base, ext = os.path.splitext(sample_path)
48
- enhanced_path = f"{base}_enhanced{ext}"
49
- noisy_spec_path = f"{base}_noisy_spectrogram.png"
50
- enhanced_spec_path = f"{base}_enhanced_spectrogram.png"
51
-
52
- try:
53
- sdk = SDKWrapper(os.getenv("SECRET_SDK_KEY"))
54
- sdk.init_processor(sample_rate=16000, enhancement_level=float(enhancement_level) / 100.0)
55
- sdk.process_file(sample_path, enhanced_path)
56
- except Exception as e:
57
- gr.Warning(f"{e}")
58
- delete_related_files(sample_path)
59
- return None, None, None
60
-
61
- spec_image(sample_path).save(noisy_spec_path)
62
- spec_image(enhanced_path).save(enhanced_spec_path)
63
-
64
- return enhanced_path, enhanced_spec_path, noisy_spec_path
65
-
66
-
67
- def delete_related_files(path: str):
68
- filename_no_ext = os.path.splitext(os.path.basename(path))[0]
69
- base_dir = "/tmp"
70
- deleted = 0
71
- for root, _, files in os.walk(base_dir):
72
- for f in files:
73
- if filename_no_ext in f:
74
- full_path = os.path.join(root, f)
75
- try:
76
- os.remove(full_path)
77
- deleted += 1
78
- except Exception as e:
79
- logger.warning(f"Failed to delete file {full_path}: {e}")
80
- logger.info(f"Deleted {deleted} files related to '{filename_no_ext}'.")
81
-
82
-
83
- def cleanup(last_enhancement: str, last_audio_file: str = "", new_audio_file: str = ""):
84
- # delete last uploaded audio if a new one is uploaded
85
- if last_audio_file and last_audio_file != new_audio_file and os.path.exists(last_audio_file):
86
- try:
87
- os.remove(last_audio_file)
88
- logger.info(f"Deleted last uploaded audio file: {last_audio_file}")
89
- except Exception as e:
90
- logger.warning(f"Failed to delete last uploaded audio file {last_audio_file}: {e}")
91
-
92
- if last_enhancement:
93
- delete_related_files(last_enhancement)
94
-
95
- cleanup_tmp(minutes_keep=120)
96
-
97
-
98
- def start_processing(sample_path: str) -> tuple[str, str, Any, str]:
99
- if not sample_path or not os.path.exists(sample_path):
100
- raise ValueError("Missing audio sample. Please upload an audio sample or use the microphone input.")
101
-
102
- if not os.getenv("SECRET_SDK_KEY"):
103
- raise ValueError("No SDK key provided. Please contact us at https://ai-coustics.com/contact/.")
104
-
105
- result_title = create_results_title(sample_path)
106
-
107
- ext = os.path.splitext(sample_path)[1]
108
- with tempfile.NamedTemporaryFile(delete=False, suffix=ext, dir="/tmp") as tmp_file:
109
- shutil.copy(sample_path, tmp_file.name)
110
- input_enhancement_path = tmp_file.name
111
-
112
- return input_enhancement_path, sample_path, gr.update(visible=False), result_title
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
offline_pipeline.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional, Any
3
+
4
+ import gradio as gr
5
+ import librosa
6
+ from sdk import SDKWrapper
7
+ from utils import spec_image, compute_wer, transcribe_audio, to_gradio_audio
8
+ import soundfile as sf
9
+ from hf_dataset_utils import get_audio, get_transcript
10
+ from constants import DEFAULT_SR
11
+ import numpy as np
12
+
13
+ def retrieve_audio_information(
14
+ original_array: np.ndarray,
15
+ enhanced_array: np.ndarray,
16
+ sample_id: str,
17
+ stt_model: str,
18
+ ) -> tuple[Optional[str], Optional[str], Optional[str], Any, Any]:
19
+ noisy_spec_path = f"/tmp/{sample_id}_noisy_spectrogram.png"
20
+ enhanced_spec_path = f"/tmp/{sample_id}_enhanced_spectrogram.png"
21
+ spec_image(original_array).save(noisy_spec_path)
22
+ spec_image(enhanced_array).save(enhanced_spec_path)
23
+ enhanced_transcript = transcribe_audio(enhanced_array, DEFAULT_SR, stt_model, mode="ENHANCED")
24
+ try:
25
+ original_transcript = get_transcript(sample_id)
26
+ wer = compute_wer(original_transcript, enhanced_transcript)
27
+ except Exception:
28
+ original_transcript = "N/A"
29
+ wer = "N/A"
30
+ return enhanced_spec_path, noisy_spec_path, enhanced_transcript, original_transcript, wer
31
+
32
+
33
+ def denoise_audio(
34
+ sample_16k: np.ndarray,
35
+ enhancement_level: float = 50.0,
36
+ ) -> tuple[np.ndarray | None , tuple[int, np.ndarray]| None]:
37
+ gr.Info("Processing started. This may take a moment. Please do not refresh or close the window.")
38
+ try:
39
+ sdk = SDKWrapper()
40
+ sdk.init_processor(sample_rate=DEFAULT_SR, enhancement_level=float(enhancement_level) / 100.0)
41
+ enhanced_array = sdk.process_sync(sample_16k)
42
+ except Exception as e:
43
+ gr.Warning(f"{e}")
44
+ raise e
45
+ gradio_enhanced_audio = to_gradio_audio(enhanced_array, DEFAULT_SR)
46
+ return enhanced_array, gradio_enhanced_audio
47
+
48
+
49
+ def load_local_file(
50
+ sample_path: str
51
+ ) -> tuple[np.ndarray, str]:
52
+ if not sample_path or not os.path.exists(sample_path):
53
+ gr.Warning("Please upload a valid audio file.")
54
+ raise ValueError("Missing audio sample. Please upload an audio sample or use the microphone input.")
55
+ new_sample_stem = os.path.splitext(os.path.basename(sample_path))[0]
56
+ y_16k, _ = librosa.load(sample_path, sr=DEFAULT_SR, dtype="float32")
57
+ return y_16k, new_sample_stem
58
+
59
+ def load_file_from_dataset(sample_id: str) -> tuple[tuple, np.ndarray, str]:
60
+ if not sample_id:
61
+ gr.Warning("Please select a sample from the dropdown.")
62
+ raise ValueError("No sample selected. Please select a sample from the dropdown.")
63
+ new_sample_stem = sample_id
64
+ y_16k, sr = get_audio(sample_id, prefix="mix")
65
+ y_16k_for_gradio = to_gradio_audio(y_16k, sr)# Convert to 16-bit PCM for gradio audio component
66
+ return y_16k_for_gradio, y_16k, new_sample_stem
requirements.txt CHANGED
@@ -8,4 +8,6 @@ aic-sdk>=2.0.0
8
  dotenv
9
  resampy
10
  whisper-normalizer
11
- soxr
 
 
 
8
  dotenv
9
  resampy
10
  whisper-normalizer
11
+ soxr
12
+ datasets
13
+ torchcodec
sdk.py CHANGED
@@ -7,6 +7,7 @@ import librosa
7
  import soundfile as sf
8
  from dotenv import load_dotenv
9
  import aic_sdk as aic
 
10
 
11
  from constants import MODEL_ID
12
 
@@ -14,8 +15,10 @@ load_dotenv()
14
 
15
 
16
  class SDKWrapper:
17
- def __init__(self, sdk_key: str, model_id: str = MODEL_ID, models_dir: str = "./models"):
18
- self.sdk_key = sdk_key
 
 
19
  model_path = aic.Model.download(model_id, models_dir)
20
  self.model = aic.Model.from_file(model_path)
21
 
@@ -40,41 +43,33 @@ class SDKWrapper:
40
  self.processor.get_processor_context().set_parameter(
41
  aic.ProcessorParameter.EnhancementLevel, float(enhancement_level)
42
  )
43
-
44
- def process_file(
45
  self,
46
- input_path: str,
47
- output_path: str,
48
- ) -> None:
49
  """
50
- Offline processing: load file, run fixed-frame processing, save output.
51
  """
52
- audio, _ = librosa.load(input_path, sr=self.processor_sample_rate, mono=True)
53
- # audio shape: (n_samples,)
54
- audio = audio.astype(np.float32)
55
- if not hasattr(self, "processor"):
56
- raise ValueError("Processor not initialized")
57
- # Planar format expected by SDK: (channels, frames)
58
- audio_planar = audio.reshape(1, -1)
59
- out = np.zeros_like(audio_planar)
60
-
61
  chunk_size = self.processor_optimal_frames
62
- n = audio_planar.shape[1]
63
-
64
  for i in range(0, n, chunk_size):
65
- chunk = audio_planar[:, i : i + chunk_size]
66
  if chunk.shape[1] < chunk_size:
67
  last = chunk.shape[1]
68
- padded = np.zeros((1, chunk_size), dtype=audio_planar.dtype)
69
  padded[:, :last] = chunk
70
  enhanced = self.processor.process(padded)
71
  out[:, i : i + last] = enhanced[:, :last]
72
  break
73
-
74
  enhanced = self.processor.process(chunk)
75
  out[:, i : i + chunk_size] = enhanced[:, :chunk_size]
76
-
77
- sf.write(output_path, out.T, self.processor_sample_rate)
78
 
79
  def process_chunk(
80
  self,
 
7
  import soundfile as sf
8
  from dotenv import load_dotenv
9
  import aic_sdk as aic
10
+ import os
11
 
12
  from constants import MODEL_ID
13
 
 
15
 
16
 
17
  class SDKWrapper:
18
+ def __init__(self, model_id: str = MODEL_ID, models_dir: str = "./models"):
19
+ if os.getenv("AIC_SDK_KEY") is None:
20
+ raise RuntimeError("Missing AIC_SDK_KEY.")
21
+ self.sdk_key = os.getenv("AIC_SDK_KEY")
22
  model_path = aic.Model.download(model_id, models_dir)
23
  self.model = aic.Model.from_file(model_path)
24
 
 
43
  self.processor.get_processor_context().set_parameter(
44
  aic.ProcessorParameter.EnhancementLevel, float(enhancement_level)
45
  )
46
+
47
+ def process_sync(
48
  self,
49
+ audio: np.ndarray,
50
+ ) -> np.ndarray:
 
51
  """
52
+ audio_array: 2D NumPy array with shape (num_channels, samples) containing audio data to be enhanced
53
  """
54
+ if len(audio.shape) == 1:
55
+ audio = audio.reshape(1, -1)
56
+ if audio.shape[0] > 2 or len(audio.shape) != 2:
57
+ raise ValueError("Expected audio with shape (n, frames)")
58
+ out = np.zeros_like(audio)
 
 
 
 
59
  chunk_size = self.processor_optimal_frames
60
+ n = audio.shape[1]
 
61
  for i in range(0, n, chunk_size):
62
+ chunk = audio[:, i : i + chunk_size]
63
  if chunk.shape[1] < chunk_size:
64
  last = chunk.shape[1]
65
+ padded = np.zeros((1, chunk_size), dtype=audio.dtype)
66
  padded[:, :last] = chunk
67
  enhanced = self.processor.process(padded)
68
  out[:, i : i + last] = enhanced[:, :last]
69
  break
 
70
  enhanced = self.processor.process(chunk)
71
  out[:, i : i + chunk_size] = enhanced[:, :chunk_size]
72
+ return out
 
73
 
74
  def process_chunk(
75
  self,
stt_streamers/soniox_streamer.py CHANGED
@@ -44,6 +44,7 @@ class SonioxStreamer:
44
  # 3. Wait for the 'finished' message from the receive loop
45
  self.finished_event.wait()
46
  self.close()
 
47
  with self.lock:
48
  return self.render_tokens(self.final_tokens, [])
49
 
 
44
  # 3. Wait for the 'finished' message from the receive loop
45
  self.finished_event.wait()
46
  self.close()
47
+ print(f"{self.stream_name} streaming complete.")
48
  with self.lock:
49
  return self.render_tokens(self.final_tokens, [])
50
 
transcribe.py DELETED
@@ -1,69 +0,0 @@
1
- from pathlib import Path
2
- import resampy
3
- import soundfile as sf
4
- from stt_streamers import DeepgramStreamer, SonioxStreamer
5
- from constants import DEFAULT_SR
6
- from word_error_rate import compute_wer
7
-
8
- def transcribe_file(audio_file_path: str, streamer_type: str = "deepgram", mode: str = "RAW"):
9
- """
10
- Transcribe an audio file using the specified STT streamer.
11
-
12
- Args:
13
- streamer_type (str): "soniox" or "deepgram"
14
- audio_file_path (str): Path to WAV file
15
- mode (str): Optional label for streamer instance ("RAW", "ENHANCED", etc.)
16
-
17
- Returns:
18
- str: Transcript text
19
- """
20
-
21
- # Validate file
22
- audio_path = Path(audio_file_path)
23
- if not audio_path.exists():
24
- raise FileNotFoundError(f"Audio file not found: {audio_file_path}")
25
-
26
- # Load audio
27
- pcm, fs_hz = sf.read(audio_file_path, dtype="float32")
28
- if fs_hz != DEFAULT_SR:
29
- pcm = resampy.resample(pcm, fs_hz, DEFAULT_SR)
30
- fs_hz = DEFAULT_SR
31
-
32
- # Select streamer
33
- api_map = {
34
- "soniox": SonioxStreamer,
35
- "deepgram": DeepgramStreamer,
36
- }
37
-
38
- streamer_key = streamer_type.lower()
39
- if streamer_key not in api_map:
40
- raise ValueError(
41
- f"Invalid streamer_type '{streamer_type}'. "
42
- f"Choose from: {', '.join(api_map.keys())}"
43
- )
44
-
45
- StreamerClass = api_map[streamer_key]
46
- streamer = StreamerClass(fs_hz, mode)
47
-
48
- # Transcribe
49
- transcript = streamer.stream_array(pcm)
50
-
51
- return transcript
52
-
53
-
54
- def transcribe_and_evaluate(
55
- audio_file_path: str, reference_transcript: str, streamer_type: str = "deepgram"
56
- ) -> tuple[str, float | tuple[float, float, float, float]]:
57
- """
58
- Transcribe an audio file and compute WER against a reference transcript.
59
-
60
- Args:
61
- audio_file_path (str): Path to WAV file
62
- reference_transcript (str): Reference transcript for WER calculation
63
- streamer_type (str): "soniox" or "deepgram"
64
-
65
- Returns:
66
- tuple[str, float]: Transcript text and Word Error Rate (WER)
67
- """
68
- transcript = transcribe_file(audio_file_path, streamer_type)
69
- return transcript, compute_wer(reference_transcript, transcript)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
audio_tools.py → utils.py RENAMED
@@ -1,21 +1,47 @@
1
  from typing import Optional
 
2
  import numpy as np
3
  import librosa
4
  from PIL import Image
5
  import io
6
  import matplotlib.pyplot as plt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  def spec_image(
9
- audio_wav: str,
 
10
  n_fft: int = 2048,
11
  hop_length: int = 512,
12
  n_mels: int = 128,
13
  fmax: Optional[float] = None,
14
  ) -> Image.Image:
15
  """
16
- Generate a mel-spectrogram image from an audio file.
17
  """
18
- y, sr = librosa.load(audio_wav, mono=True, sr=None)
19
  S = librosa.feature.melspectrogram(
20
  y=y,
21
  sr=sr,
@@ -65,4 +91,43 @@ def compute_wer(reference: str, hypothesis: str) -> float:
65
  d[i - 1][j - 1] + cost, # Substitution
66
  )
67
  wer = d[len(ref_words)][len(hyp_words)] / max(len(ref_words), 1)
68
- return wer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from typing import Optional
2
+
3
  import numpy as np
4
  import librosa
5
  from PIL import Image
6
  import io
7
  import matplotlib.pyplot as plt
8
+ import resampy
9
+ from stt_streamers import DeepgramStreamer, SonioxStreamer
10
+ from constants import DEFAULT_SR
11
+
12
+ def to_gradio_audio(x: np.ndarray, sr: int) -> tuple[int, np.ndarray]:
13
+ x = np.asarray(x)
14
+
15
+ # Remove extra dims like (1, n, 1) etc.
16
+ x = np.squeeze(x)
17
+
18
+ # If it's (channels, samples), transpose to (samples, channels)
19
+ if x.ndim == 2 and x.shape[0] in (1, 2) and x.shape[1] > x.shape[0]:
20
+ x = x.T
21
+
22
+ # Ensure mono is (n_samples,)
23
+ if x.ndim == 2 and x.shape[1] == 1:
24
+ x = x[:, 0]
25
+
26
+ # Return float32 in [-1, 1] (simplest for Gradio)
27
+ x = x.astype(np.float32)
28
+ x = np.clip(x, -1.0, 1.0)
29
+
30
+ return (sr, x)
31
+
32
 
33
  def spec_image(
34
+ audio_array: np.ndarray,
35
+ sr: int = DEFAULT_SR,
36
  n_fft: int = 2048,
37
  hop_length: int = 512,
38
  n_mels: int = 128,
39
  fmax: Optional[float] = None,
40
  ) -> Image.Image:
41
  """
42
+ Generate a mel-spectrogram image from an audio array.
43
  """
44
+ y = audio_array.flatten() # Ensure it's 1D
45
  S = librosa.feature.melspectrogram(
46
  y=y,
47
  sr=sr,
 
91
  d[i - 1][j - 1] + cost, # Substitution
92
  )
93
  wer = d[len(ref_words)][len(hyp_words)] / max(len(ref_words), 1)
94
+ return wer
95
+
96
+
97
+
98
+ def transcribe_audio(audio_array: np.ndarray, sr: int, streamer_type: str = "Deepgram", mode: str = "RAW"):
99
+ """
100
+ Transcribe an audio array using the specified STT streamer.
101
+
102
+ Args:
103
+ audio_array (np.ndarray): Audio data array
104
+ sr (int): Sample rate of the audio array
105
+ streamer_type (str): "Soniox" or "Deepgram"
106
+ mode (str): Optional label for streamer instance ("RAW", "ENHANCED", etc.)
107
+
108
+ Returns:
109
+ str: Transcript text
110
+ """
111
+
112
+ if sr != DEFAULT_SR:
113
+ audio_array = resampy.resample(audio_array, sr, DEFAULT_SR)
114
+ sr = DEFAULT_SR
115
+
116
+ api_map = {
117
+ "Soniox": SonioxStreamer,
118
+ "Deepgram": DeepgramStreamer,
119
+ }
120
+
121
+ streamer_key = streamer_type
122
+ if streamer_key not in api_map:
123
+ raise ValueError(
124
+ f"Invalid streamer_type '{streamer_type}'. "
125
+ f"Choose from: {', '.join(api_map.keys())}"
126
+ )
127
+
128
+ StreamerClass = api_map[streamer_key]
129
+ streamer = StreamerClass(sr, mode)
130
+
131
+ transcript = streamer.stream_array(audio_array)
132
+
133
+ return transcript
word_error_rate.py DELETED
@@ -1,78 +0,0 @@
1
- import numpy as np
2
- from whisper_normalizer.english import EnglishTextNormalizer
3
-
4
- def tokenize_text(text: str) -> list[str]:
5
- """Normalize + tokenize into words."""
6
- normalizer = EnglishTextNormalizer()
7
- normalized = normalizer(text)
8
- # simple whitespace word tokens; adjust if you need punctuation handling
9
- return normalized.split()
10
-
11
- def compute_wer(y_true: str, y_pred: str, detailed: bool = False)-> float | tuple[float, float, float, float]:
12
- """
13
- Returns:
14
- - if detailed=False: wer
15
- - if detailed=True: (wer, ins_rate, del_rate, sub_rate)
16
- """
17
- ref = tokenize_text(y_true)
18
- hyp = tokenize_text(y_pred)
19
-
20
- total_err, ins, dels, subs = levenshtein_word_errors(ref, hyp)
21
- ref_len = max(1, len(ref))
22
-
23
- if detailed:
24
- return (
25
- total_err / ref_len,
26
- ins / ref_len,
27
- dels / ref_len,
28
- subs / ref_len,
29
- )
30
- return total_err / ref_len
31
-
32
-
33
- def levenshtein_word_errors(reference: list[str], prediction: list[str]):
34
- """Edit distance with insertion/deletion/substitution breakdown."""
35
- n, m = len(reference), len(prediction)
36
-
37
- # distance + counts
38
- d = np.zeros((n + 1, m + 1), dtype=np.int32)
39
- ins = np.zeros((n + 1, m + 1), dtype=np.int32)
40
- dels = np.zeros((n + 1, m + 1), dtype=np.int32)
41
- subs = np.zeros((n + 1, m + 1), dtype=np.int32)
42
-
43
- # init borders
44
- for j in range(1, m + 1):
45
- d[0, j] = j
46
- ins[0, j] = j
47
- for i in range(1, n + 1):
48
- d[i, 0] = i
49
- dels[i, 0] = i
50
-
51
- # DP
52
- for i in range(1, n + 1):
53
- for j in range(1, m + 1):
54
- is_sub = 0 if reference[i - 1] == prediction[j - 1] else 1
55
-
56
- # candidates: delete, insert, substitute/match
57
- del_cost = d[i - 1, j] + 1
58
- ins_cost = d[i, j - 1] + 1
59
- sub_cost = d[i - 1, j - 1] + is_sub
60
-
61
- # choose best (tie-breaking: prefer sub/match, then del, then ins)
62
- if sub_cost <= del_cost and sub_cost <= ins_cost:
63
- d[i, j] = sub_cost
64
- ins[i, j] = ins[i - 1, j - 1]
65
- dels[i, j] = dels[i - 1, j - 1]
66
- subs[i, j] = subs[i - 1, j - 1] + is_sub
67
- elif del_cost <= ins_cost:
68
- d[i, j] = del_cost
69
- ins[i, j] = ins[i - 1, j]
70
- dels[i, j] = dels[i - 1, j] + 1
71
- subs[i, j] = subs[i - 1, j]
72
- else:
73
- d[i, j] = ins_cost
74
- ins[i, j] = ins[i, j - 1] + 1
75
- dels[i, j] = dels[i, j - 1]
76
- subs[i, j] = subs[i, j - 1]
77
-
78
- return int(d[n, m]), int(ins[n, m]), int(dels[n, m]), int(subs[n, m])