Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
mariesig commited on
Commit ·
ba729b8
1
Parent(s): f5f5219
fix offline pipeline
Browse files- aic_dataset.py +0 -57
- app.py +59 -79
- clean_up.py +101 -0
- constants.py +5 -4
- docs/intro.md +9 -2
- hf_dataset_utils.py +25 -0
- offline.py +0 -112
- offline_pipeline.py +66 -0
- requirements.txt +3 -1
- sdk.py +19 -24
- stt_streamers/soniox_streamer.py +1 -0
- transcribe.py +0 -69
- audio_tools.py → utils.py +69 -4
- word_error_rate.py +0 -78
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
|
| 2 |
-
import time
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
-
from
|
| 6 |
-
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
-
|
| 10 |
-
|
| 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"
|
| 73 |
enhance_btn_for_upload = gr.Button("Enhance", scale=2)
|
| 74 |
|
| 75 |
-
with gr.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="
|
| 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 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
| 97 |
enhance_btn_for_dataset.click(
|
| 98 |
-
|
| 99 |
-
inputs=[
|
| 100 |
-
outputs=
|
| 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=[
|
| 108 |
-
outputs=[
|
| 109 |
-
).
|
| 110 |
-
|
| 111 |
-
inputs=[
|
| 112 |
-
outputs=[
|
| 113 |
-
).
|
| 114 |
lambda: gr.update(visible=True),
|
| 115 |
inputs=None,
|
| 116 |
outputs=results_card,
|
| 117 |
)
|
| 118 |
|
| 119 |
enhance_btn_for_upload.click(
|
| 120 |
-
|
| 121 |
-
inputs=[
|
| 122 |
-
outputs=
|
| 123 |
).then(
|
| 124 |
-
|
| 125 |
-
inputs=audio_file_upload,
|
| 126 |
-
outputs=[
|
| 127 |
).success(
|
| 128 |
denoise_audio,
|
| 129 |
-
inputs=[
|
| 130 |
-
outputs=[
|
| 131 |
-
).
|
| 132 |
-
|
| 133 |
-
inputs=[
|
| 134 |
-
outputs=[
|
| 135 |
-
).
|
| 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=
|
| 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=
|
| 165 |
outputs=[stream_state, enhanced_text, raw_text],
|
| 166 |
)
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 18 |
-
|
|
|
|
|
|
|
| 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
|
| 45 |
self,
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
) -> None:
|
| 49 |
"""
|
| 50 |
-
|
| 51 |
"""
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
audio
|
| 55 |
-
|
| 56 |
-
|
| 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 =
|
| 63 |
-
|
| 64 |
for i in range(0, n, chunk_size):
|
| 65 |
-
chunk =
|
| 66 |
if chunk.shape[1] < chunk_size:
|
| 67 |
last = chunk.shape[1]
|
| 68 |
-
padded = np.zeros((1, chunk_size), 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 |
-
|
|
|
|
| 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
|
| 17 |
"""
|
| 18 |
-
y
|
| 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])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|