Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Luis Küffner commited on
Commit ·
e368e99
1
Parent(s): be23752
Push latest updates
Browse files- .gitignore +20 -6
- README.md +1 -1
- app.py +229 -47
- assets/active_light.css +68 -0
- clean_up.py +9 -7
- constants.py +4 -0
- docs/dawn_chorus.md +1 -1
- offline_pipeline.py +278 -9
- requirements.txt +1 -0
- stream_pipeline.py +78 -19
- stt_streamers/soniox_streamer.py +3 -1
- utils.py +15 -7
.gitignore
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
-
|
| 3 |
__pycache__/
|
| 4 |
-
.gradio/
|
| 5 |
-
app.py.lprof
|
| 6 |
-
.DS_Store
|
| 7 |
.venv/
|
|
|
|
| 8 |
.ruff_cache/
|
| 9 |
pyrightconfig.json
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
.env
|
| 11 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
*.aicmodel
|
| 13 |
-
uv.lock
|
|
|
|
| 1 |
+
# Local / editor
|
| 2 |
+
.vscode/
|
| 3 |
+
.idea/
|
| 4 |
+
*.swp
|
| 5 |
+
*.swo
|
| 6 |
+
.DS_Store
|
| 7 |
|
| 8 |
+
# Python
|
| 9 |
__pycache__/
|
|
|
|
|
|
|
|
|
|
| 10 |
.venv/
|
| 11 |
+
.python-version
|
| 12 |
.ruff_cache/
|
| 13 |
pyrightconfig.json
|
| 14 |
+
pyproject.toml
|
| 15 |
+
uv.lock
|
| 16 |
+
|
| 17 |
+
# App / env
|
| 18 |
.env
|
| 19 |
+
.env.*
|
| 20 |
+
!.env.example
|
| 21 |
+
|
| 22 |
+
# Gradio / profiling
|
| 23 |
+
.gradio/
|
| 24 |
+
app.py.lprof
|
| 25 |
+
|
| 26 |
+
# Models / assets (large or local-only)
|
| 27 |
*.aicmodel
|
|
|
README.md
CHANGED
|
@@ -4,7 +4,7 @@ emoji: 🌍
|
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
short_description: Try out ai-coustics speech enhancement models.
|
|
|
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "6.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
short_description: Try out ai-coustics speech enhancement models.
|
app.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from stream_pipeline import (
|
| 6 |
clear_ui,
|
|
|
|
|
|
|
| 7 |
set_stt_streamer,
|
| 8 |
stop_online_backend,
|
| 9 |
transcribe_stream,
|
|
@@ -12,17 +19,140 @@ from stream_pipeline import (
|
|
| 12 |
from offline_pipeline import (
|
| 13 |
load_file_from_dataset,
|
| 14 |
load_local_file,
|
| 15 |
-
|
| 16 |
-
retrieve_audio_information,
|
| 17 |
)
|
|
|
|
| 18 |
from clean_up import purge_tmp_directory, cleanup_previous_run
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
with gr.Blocks() as demo:
|
| 22 |
sample_stem = gr.State("")
|
| 23 |
last_sample_stem = gr.State("")
|
| 24 |
input_array = gr.State()
|
| 25 |
enhanced_array = gr.State()
|
|
|
|
| 26 |
|
| 27 |
gr.HTML(
|
| 28 |
'<a href="https://ai-coustics.com/" target="_blank">'
|
|
@@ -39,12 +169,12 @@ with gr.Blocks() as demo:
|
|
| 39 |
value="Soniox STT-RT v3",
|
| 40 |
interactive=True,
|
| 41 |
)
|
| 42 |
-
|
| 43 |
enhancement_level = gr.Slider(
|
| 44 |
minimum=0,
|
| 45 |
maximum=100,
|
| 46 |
step=1,
|
| 47 |
-
value=
|
| 48 |
label="Enhancement level (%)",
|
| 49 |
scale=2,
|
| 50 |
)
|
|
@@ -54,38 +184,73 @@ with gr.Blocks() as demo:
|
|
| 54 |
gr.Markdown(open("docs/online.md", "r", encoding="utf-8").read())
|
| 55 |
with gr.Group(elem_classes="panel"):
|
| 56 |
stream_state = gr.State(None)
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
with gr.Group(elem_classes="panel"):
|
| 61 |
with gr.Column(scale=5, min_width=320):
|
| 62 |
enhanced_text = gr.Textbox(
|
| 63 |
-
label="Enhanced Transcribed Text", lines=6
|
| 64 |
)
|
| 65 |
with gr.Column(scale=5, min_width=320):
|
| 66 |
-
raw_text = gr.Textbox(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
with gr.Tab("Dataset: Dawn Chorus") as dataset_tab:
|
| 69 |
with gr.Row():
|
| 70 |
gr.Markdown(open("docs/dawn_chorus.md", "r", encoding="utf-8").read())
|
| 71 |
-
dataset_dropdown = gr.Dropdown(
|
|
|
|
|
|
|
| 72 |
audio_file_from_dataset = gr.Audio(
|
| 73 |
-
type="filepath", interactive=False,
|
| 74 |
)
|
| 75 |
|
| 76 |
with gr.Tab("Upload local file") as upload_tab:
|
| 77 |
with gr.Row():
|
| 78 |
gr.Markdown(open("docs/local_file.md", "r", encoding="utf-8").read())
|
| 79 |
audio_file_upload = gr.Audio(
|
| 80 |
-
type="filepath", sources=["upload"],
|
| 81 |
)
|
| 82 |
|
| 83 |
-
enhance_btn = gr.Button("Enhance", scale=2, visible=False)
|
| 84 |
|
| 85 |
with gr.Group(elem_classes="panel results-card", visible=False) as results_card:
|
| 86 |
result_title = gr.Markdown("")
|
| 87 |
enhanced_audio = gr.Audio(
|
| 88 |
-
type="numpy",
|
|
|
|
|
|
|
|
|
|
| 89 |
)
|
| 90 |
|
| 91 |
with gr.Row(equal_height=True, elem_classes="results-row"):
|
|
@@ -111,32 +276,56 @@ with gr.Blocks() as demo:
|
|
| 111 |
# ------------------------------------------------------
|
| 112 |
# GLOBAL EVENTS (TAB CHANGES, MODEL CHANGES)
|
| 113 |
# ------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
stream_tab.select(
|
| 115 |
lambda: (
|
| 116 |
gr.update(visible=False),
|
| 117 |
gr.update(visible=False),
|
| 118 |
-
gr.update(visible=True, interactive=True, streaming=True),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
),
|
| 120 |
inputs=None,
|
| 121 |
-
outputs=[results_card, enhance_btn, audio_stream],
|
| 122 |
)
|
| 123 |
|
| 124 |
upload_tab.select(
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
inputs=None,
|
| 127 |
-
outputs=[stream_state, enhanced_text, raw_text, audio_stream],
|
| 128 |
).then(
|
| 129 |
lambda: gr.update(visible=True),
|
| 130 |
outputs=enhance_btn,
|
| 131 |
)
|
| 132 |
|
| 133 |
dataset_tab.select(
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
inputs=None,
|
| 136 |
-
outputs=[stream_state, enhanced_text, raw_text, audio_stream],
|
| 137 |
).then(
|
| 138 |
lambda: gr.update(visible=True),
|
| 139 |
outputs=enhance_btn,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
)
|
| 141 |
|
| 142 |
stt_model.change(
|
|
@@ -157,7 +346,7 @@ with gr.Blocks() as demo:
|
|
| 157 |
|
| 158 |
audio_stream.stream(
|
| 159 |
fn=transcribe_stream,
|
| 160 |
-
inputs=[stream_state, audio_stream, enhancement_level],
|
| 161 |
outputs=[stream_state, enhanced_text, raw_text],
|
| 162 |
stream_every=STREAM_EVERY,
|
| 163 |
time_limit=60 * 2,
|
|
@@ -165,7 +354,7 @@ with gr.Blocks() as demo:
|
|
| 165 |
)
|
| 166 |
|
| 167 |
audio_stream.stop_recording(
|
| 168 |
-
|
| 169 |
)
|
| 170 |
|
| 171 |
audio_stream.start_recording(
|
|
@@ -199,40 +388,33 @@ with gr.Blocks() as demo:
|
|
| 199 |
inputs=None,
|
| 200 |
outputs=results_card,
|
| 201 |
).then(
|
| 202 |
-
load_local_file,
|
|
|
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
-
# Enhancement button
|
| 206 |
enhance_btn.click(
|
| 207 |
-
|
| 208 |
-
inputs=[last_sample_stem],
|
| 209 |
outputs=[
|
|
|
|
|
|
|
| 210 |
enhanced_image,
|
| 211 |
noisy_image,
|
| 212 |
-
enhanced_transcript,
|
| 213 |
original_transcript,
|
| 214 |
noisy_transcript,
|
| 215 |
-
],
|
| 216 |
-
).success(
|
| 217 |
-
denoise_audio,
|
| 218 |
-
inputs=[input_array, enhancement_level],
|
| 219 |
-
outputs=[enhanced_array, enhanced_audio],
|
| 220 |
-
).success(
|
| 221 |
-
lambda: gr.update(visible=True),
|
| 222 |
-
inputs=None,
|
| 223 |
-
outputs=results_card,
|
| 224 |
-
).success(
|
| 225 |
-
retrieve_audio_information,
|
| 226 |
-
inputs=[input_array, enhanced_array, sample_stem, stt_model],
|
| 227 |
-
outputs=[
|
| 228 |
-
enhanced_image,
|
| 229 |
-
noisy_image,
|
| 230 |
enhanced_transcript,
|
| 231 |
-
original_transcript,
|
| 232 |
-
noisy_transcript,
|
| 233 |
last_sample_stem,
|
|
|
|
|
|
|
| 234 |
],
|
| 235 |
)
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import threading
|
| 3 |
+
import time
|
| 4 |
import gradio as gr
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from constants import STREAM_EVERY, WARMUP_SECONDS, APP_TMP_DIR
|
| 8 |
+
from hf_dataset_utils import ALL_FILES, get_transcript
|
| 9 |
|
| 10 |
from stream_pipeline import (
|
| 11 |
clear_ui,
|
| 12 |
+
get_live_transcripts,
|
| 13 |
+
on_stop_recording,
|
| 14 |
set_stt_streamer,
|
| 15 |
stop_online_backend,
|
| 16 |
transcribe_stream,
|
|
|
|
| 19 |
from offline_pipeline import (
|
| 20 |
load_file_from_dataset,
|
| 21 |
load_local_file,
|
| 22 |
+
run_offline_pipeline_streaming,
|
|
|
|
| 23 |
)
|
| 24 |
+
from utils import spec_image
|
| 25 |
from clean_up import purge_tmp_directory, cleanup_previous_run
|
| 26 |
|
| 27 |
+
# Active light HTML: whole container is the light (gray = warming up, red = ready)
|
| 28 |
+
ACTIVE_LIGHT_GRAY = (
|
| 29 |
+
'<div class="active-light active-light--off" title="Warming up" style="display:flex;justify-content:center;align-items:center;width:100%;height:100%;min-height:3rem;">'
|
| 30 |
+
'<span class="active-light__label">Warming up...</span></div>'
|
| 31 |
+
)
|
| 32 |
+
ACTIVE_LIGHT_RED = (
|
| 33 |
+
'<div class="active-light active-light--on" title="Ready" style="display:flex;justify-content:center;align-items:center;width:100%;height:100%;min-height:3rem;">'
|
| 34 |
+
'<span class="active-light__label">Ready!</span></div>'
|
| 35 |
+
)
|
| 36 |
+
WARMUP_TICKS = max(1, int(WARMUP_SECONDS * 2)) # timer ticks every 0.5s
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def warmup_tick(on_stream_tab, warmup_elapsed, _current_html):
|
| 40 |
+
if not on_stream_tab:
|
| 41 |
+
return 0, ACTIVE_LIGHT_GRAY, gr.update(active=True)
|
| 42 |
+
if warmup_elapsed >= WARMUP_TICKS:
|
| 43 |
+
return warmup_elapsed, ACTIVE_LIGHT_RED, gr.update(active=False)
|
| 44 |
+
return warmup_elapsed + 1, ACTIVE_LIGHT_GRAY, gr.update(active=True)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def process_with_live_transcript(
|
| 48 |
+
input_array,
|
| 49 |
+
enhancement_level,
|
| 50 |
+
sample_stem,
|
| 51 |
+
stt_model,
|
| 52 |
+
last_sample_stem,
|
| 53 |
+
):
|
| 54 |
+
"""Generator that runs the offline pipeline in real time (chunked): enhanced audio and
|
| 55 |
+
both transcripts stream from the first chunk so playback and transcription start immediately."""
|
| 56 |
+
progress_state = {}
|
| 57 |
+
result_holder = {}
|
| 58 |
+
|
| 59 |
+
def worker():
|
| 60 |
+
try:
|
| 61 |
+
result_holder["result"] = run_offline_pipeline_streaming(
|
| 62 |
+
input_array,
|
| 63 |
+
enhancement_level,
|
| 64 |
+
sample_stem,
|
| 65 |
+
stt_model,
|
| 66 |
+
progress_state,
|
| 67 |
+
)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
result_holder["error"] = e
|
| 70 |
+
|
| 71 |
+
# 1) First yield: ground truth + input spectrogram only (no audio, no enhanced spec, no transcripts yet)
|
| 72 |
+
cleanup_out = cleanup_previous_run(last_sample_stem)
|
| 73 |
+
noisy_spec_path = f"{APP_TMP_DIR}/{sample_stem}_noisy_spectrogram.png"
|
| 74 |
+
if input_array is not None:
|
| 75 |
+
try:
|
| 76 |
+
spec_image(input_array).save(noisy_spec_path)
|
| 77 |
+
except Exception:
|
| 78 |
+
noisy_spec_path = None
|
| 79 |
+
else:
|
| 80 |
+
noisy_spec_path = None
|
| 81 |
+
try:
|
| 82 |
+
original_transcript = get_transcript(sample_stem)
|
| 83 |
+
except Exception:
|
| 84 |
+
original_transcript = "Unavailable"
|
| 85 |
+
|
| 86 |
+
yield (
|
| 87 |
+
gr.update(visible=True),
|
| 88 |
+
None, # enhanced_audio: set only in final yield (smooth playback)
|
| 89 |
+
gr.update(value=None), # enhanced_image: clear until step 3 (last)
|
| 90 |
+
gr.update(value=noisy_spec_path), # noisy_image: input spectrogram (step 1)
|
| 91 |
+
original_transcript,
|
| 92 |
+
"",
|
| 93 |
+
"",
|
| 94 |
+
sample_stem,
|
| 95 |
+
None,
|
| 96 |
+
"",
|
| 97 |
+
)
|
| 98 |
+
# Let the UI render step 1 before we flood with polling updates
|
| 99 |
+
time.sleep(0.2)
|
| 100 |
+
|
| 101 |
+
thread = threading.Thread(target=worker, daemon=True)
|
| 102 |
+
thread.start()
|
| 103 |
+
|
| 104 |
+
poll_interval = 0.05
|
| 105 |
+
while "result" not in result_holder and "error" not in result_holder:
|
| 106 |
+
time.sleep(poll_interval)
|
| 107 |
+
# 2) Realtime: stream transcripts only; audio set in final yield for smooth playback
|
| 108 |
+
yield (
|
| 109 |
+
gr.update(visible=True),
|
| 110 |
+
gr.update(), # enhanced_audio: set only in final yield, then autoplay
|
| 111 |
+
gr.update(), # enhanced_image: reveal only in step 3 (final yield)
|
| 112 |
+
gr.update(), # noisy_image already set in step 1
|
| 113 |
+
gr.update(), # original_transcript unchanged
|
| 114 |
+
gr.update(value=progress_state.get("noisy", "")),
|
| 115 |
+
gr.update(value=progress_state.get("enhanced", "")),
|
| 116 |
+
gr.update(),
|
| 117 |
+
gr.update(),
|
| 118 |
+
gr.update(),
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
if "error" in result_holder:
|
| 122 |
+
raise result_holder["error"]
|
| 123 |
+
|
| 124 |
+
(
|
| 125 |
+
enhanced_spec_path,
|
| 126 |
+
enhanced_transcript,
|
| 127 |
+
noisy_transcript_with_wer,
|
| 128 |
+
enhanced_audio,
|
| 129 |
+
last_stem,
|
| 130 |
+
enhanced_array,
|
| 131 |
+
precomputed_noisy,
|
| 132 |
+
) = result_holder["result"]
|
| 133 |
|
| 134 |
+
# 3) Last: reveal enhanced spectrogram (and final audio/transcripts)
|
| 135 |
+
yield (
|
| 136 |
+
gr.update(visible=True),
|
| 137 |
+
enhanced_audio,
|
| 138 |
+
enhanced_spec_path, # enhanced_image: show only now
|
| 139 |
+
noisy_spec_path,
|
| 140 |
+
original_transcript,
|
| 141 |
+
noisy_transcript_with_wer,
|
| 142 |
+
enhanced_transcript,
|
| 143 |
+
last_stem,
|
| 144 |
+
enhanced_array,
|
| 145 |
+
precomputed_noisy,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
_CSS_DIR = Path(__file__).resolve().parent / "assets"
|
| 150 |
with gr.Blocks() as demo:
|
| 151 |
sample_stem = gr.State("")
|
| 152 |
last_sample_stem = gr.State("")
|
| 153 |
input_array = gr.State()
|
| 154 |
enhanced_array = gr.State()
|
| 155 |
+
precomputed_noisy_transcript = gr.State("")
|
| 156 |
|
| 157 |
gr.HTML(
|
| 158 |
'<a href="https://ai-coustics.com/" target="_blank">'
|
|
|
|
| 169 |
value="Soniox STT-RT v3",
|
| 170 |
interactive=True,
|
| 171 |
)
|
| 172 |
+
# Streamers are created on first start_recording to avoid Soniox "No audio received" timeout
|
| 173 |
enhancement_level = gr.Slider(
|
| 174 |
minimum=0,
|
| 175 |
maximum=100,
|
| 176 |
step=1,
|
| 177 |
+
value=80,
|
| 178 |
label="Enhancement level (%)",
|
| 179 |
scale=2,
|
| 180 |
)
|
|
|
|
| 184 |
gr.Markdown(open("docs/online.md", "r", encoding="utf-8").read())
|
| 185 |
with gr.Group(elem_classes="panel"):
|
| 186 |
stream_state = gr.State(None)
|
| 187 |
+
on_stream_tab = gr.State(True) # True on load: stream tab is the default first tab
|
| 188 |
+
warmup_elapsed = gr.State(0)
|
| 189 |
+
input_gain_db = gr.Slider(
|
| 190 |
+
minimum=0,
|
| 191 |
+
maximum=20,
|
| 192 |
+
step=0.5,
|
| 193 |
+
value=0,
|
| 194 |
+
label="Input gain (dB)",
|
| 195 |
)
|
| 196 |
+
with gr.Row(elem_classes="stream-row"):
|
| 197 |
+
with gr.Column(scale=4, min_width=200):
|
| 198 |
+
audio_stream = gr.Audio(
|
| 199 |
+
sources=["microphone"], streaming=True, elem_id="audio_stream"
|
| 200 |
+
)
|
| 201 |
+
with gr.Column(scale=1, min_width=120, elem_classes="active-light-column"):
|
| 202 |
+
active_light = gr.HTML(value=ACTIVE_LIGHT_GRAY)
|
| 203 |
with gr.Group(elem_classes="panel"):
|
| 204 |
with gr.Column(scale=5, min_width=320):
|
| 205 |
enhanced_text = gr.Textbox(
|
| 206 |
+
label="Enhanced Transcribed Text", lines=6, autoscroll=False
|
| 207 |
)
|
| 208 |
with gr.Column(scale=5, min_width=320):
|
| 209 |
+
raw_text = gr.Textbox(
|
| 210 |
+
label="Raw Transcribed Text", lines=6, autoscroll=False
|
| 211 |
+
)
|
| 212 |
+
# Poll transcript globals so interim results show live (streamers update in background)
|
| 213 |
+
transcript_timer = gr.Timer(0.1, active=True)
|
| 214 |
+
transcript_timer.tick(
|
| 215 |
+
get_live_transcripts,
|
| 216 |
+
inputs=None,
|
| 217 |
+
outputs=[enhanced_text, raw_text],
|
| 218 |
+
show_progress="hidden",
|
| 219 |
+
)
|
| 220 |
+
warmup_timer = gr.Timer(0.5, active=True)
|
| 221 |
+
warmup_timer.tick(
|
| 222 |
+
warmup_tick,
|
| 223 |
+
inputs=[on_stream_tab, warmup_elapsed, active_light],
|
| 224 |
+
outputs=[warmup_elapsed, active_light, warmup_timer],
|
| 225 |
+
show_progress="hidden",
|
| 226 |
+
)
|
| 227 |
|
| 228 |
with gr.Tab("Dataset: Dawn Chorus") as dataset_tab:
|
| 229 |
with gr.Row():
|
| 230 |
gr.Markdown(open("docs/dawn_chorus.md", "r", encoding="utf-8").read())
|
| 231 |
+
dataset_dropdown = gr.Dropdown(
|
| 232 |
+
choices=ALL_FILES, value="en_00412_i_h_36", label="Sample"
|
| 233 |
+
)
|
| 234 |
audio_file_from_dataset = gr.Audio(
|
| 235 |
+
type="filepath", interactive=False, buttons=["download"], autoplay=True
|
| 236 |
)
|
| 237 |
|
| 238 |
with gr.Tab("Upload local file") as upload_tab:
|
| 239 |
with gr.Row():
|
| 240 |
gr.Markdown(open("docs/local_file.md", "r", encoding="utf-8").read())
|
| 241 |
audio_file_upload = gr.Audio(
|
| 242 |
+
type="filepath", sources=["upload"], buttons=["download"], autoplay=True
|
| 243 |
)
|
| 244 |
|
| 245 |
+
enhance_btn = gr.Button("Enhance with Quail Voice Focus 2.0", scale=2, visible=False)
|
| 246 |
|
| 247 |
with gr.Group(elem_classes="panel results-card", visible=False) as results_card:
|
| 248 |
result_title = gr.Markdown("")
|
| 249 |
enhanced_audio = gr.Audio(
|
| 250 |
+
type="numpy",
|
| 251 |
+
interactive=False,
|
| 252 |
+
buttons=["download"],
|
| 253 |
+
autoplay=True,
|
| 254 |
)
|
| 255 |
|
| 256 |
with gr.Row(equal_height=True, elem_classes="results-row"):
|
|
|
|
| 276 |
# ------------------------------------------------------
|
| 277 |
# GLOBAL EVENTS (TAB CHANGES, MODEL CHANGES)
|
| 278 |
# ------------------------------------------------------
|
| 279 |
+
DEFAULT_DATASET_SAMPLE = "en_00412_i_h_36"
|
| 280 |
+
|
| 281 |
+
def load_dataset_sample_on_tab_visit(dropdown_value):
|
| 282 |
+
"""Load the selected sample when visiting the Dataset tab; use default if dropdown is empty."""
|
| 283 |
+
sample_id = dropdown_value or DEFAULT_DATASET_SAMPLE
|
| 284 |
+
audio_path, arr, stem = load_file_from_dataset(sample_id)
|
| 285 |
+
return sample_id, audio_path, arr, stem
|
| 286 |
+
|
| 287 |
stream_tab.select(
|
| 288 |
lambda: (
|
| 289 |
gr.update(visible=False),
|
| 290 |
gr.update(visible=False),
|
| 291 |
+
gr.update(visible=True, interactive=True, streaming=True, sources=["microphone"]),
|
| 292 |
+
True,
|
| 293 |
+
0,
|
| 294 |
+
ACTIVE_LIGHT_GRAY,
|
| 295 |
+
gr.update(active=True),
|
| 296 |
),
|
| 297 |
inputs=None,
|
| 298 |
+
outputs=[results_card, enhance_btn, audio_stream, on_stream_tab, warmup_elapsed, active_light, warmup_timer],
|
| 299 |
)
|
| 300 |
|
| 301 |
upload_tab.select(
|
| 302 |
+
lambda: (
|
| 303 |
+
*stop_online_backend(),
|
| 304 |
+
False,
|
| 305 |
+
ACTIVE_LIGHT_GRAY,
|
| 306 |
+
),
|
| 307 |
inputs=None,
|
| 308 |
+
outputs=[stream_state, enhanced_text, raw_text, audio_stream, on_stream_tab, active_light],
|
| 309 |
).then(
|
| 310 |
lambda: gr.update(visible=True),
|
| 311 |
outputs=enhance_btn,
|
| 312 |
)
|
| 313 |
|
| 314 |
dataset_tab.select(
|
| 315 |
+
lambda: (
|
| 316 |
+
*stop_online_backend(),
|
| 317 |
+
False,
|
| 318 |
+
ACTIVE_LIGHT_GRAY,
|
| 319 |
+
),
|
| 320 |
inputs=None,
|
| 321 |
+
outputs=[stream_state, enhanced_text, raw_text, audio_stream, on_stream_tab, active_light],
|
| 322 |
).then(
|
| 323 |
lambda: gr.update(visible=True),
|
| 324 |
outputs=enhance_btn,
|
| 325 |
+
).then(
|
| 326 |
+
load_dataset_sample_on_tab_visit,
|
| 327 |
+
inputs=[dataset_dropdown],
|
| 328 |
+
outputs=[dataset_dropdown, audio_file_from_dataset, input_array, sample_stem],
|
| 329 |
)
|
| 330 |
|
| 331 |
stt_model.change(
|
|
|
|
| 346 |
|
| 347 |
audio_stream.stream(
|
| 348 |
fn=transcribe_stream,
|
| 349 |
+
inputs=[stream_state, audio_stream, enhancement_level, input_gain_db],
|
| 350 |
outputs=[stream_state, enhanced_text, raw_text],
|
| 351 |
stream_every=STREAM_EVERY,
|
| 352 |
time_limit=60 * 2,
|
|
|
|
| 354 |
)
|
| 355 |
|
| 356 |
audio_stream.stop_recording(
|
| 357 |
+
on_stop_recording,
|
| 358 |
)
|
| 359 |
|
| 360 |
audio_stream.start_recording(
|
|
|
|
| 388 |
inputs=None,
|
| 389 |
outputs=results_card,
|
| 390 |
).then(
|
| 391 |
+
load_local_file,
|
| 392 |
+
inputs=[audio_file_upload],
|
| 393 |
+
outputs=[input_array, sample_stem]
|
| 394 |
)
|
| 395 |
|
| 396 |
+
# Enhancement button: run pipeline with live transcript progress (dataset + local file modes).
|
| 397 |
enhance_btn.click(
|
| 398 |
+
process_with_live_transcript,
|
| 399 |
+
inputs=[input_array, enhancement_level, sample_stem, stt_model, last_sample_stem],
|
| 400 |
outputs=[
|
| 401 |
+
results_card,
|
| 402 |
+
enhanced_audio,
|
| 403 |
enhanced_image,
|
| 404 |
noisy_image,
|
|
|
|
| 405 |
original_transcript,
|
| 406 |
noisy_transcript,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
enhanced_transcript,
|
|
|
|
|
|
|
| 408 |
last_sample_stem,
|
| 409 |
+
enhanced_array,
|
| 410 |
+
precomputed_noisy_transcript,
|
| 411 |
],
|
| 412 |
)
|
| 413 |
|
| 414 |
+
os.makedirs(APP_TMP_DIR, exist_ok=True)
|
| 415 |
+
purge_tmp_directory(max_age_minutes=0, tmp_dir=APP_TMP_DIR)
|
| 416 |
+
demo.queue()
|
| 417 |
+
demo.launch(
|
| 418 |
+
css=(_CSS_DIR / "active_light.css").read_text(encoding="utf-8"),
|
| 419 |
+
allowed_paths=[APP_TMP_DIR, "/tmp", "/"],
|
| 420 |
+
)
|
assets/active_light.css
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Stream row: stretch columns to match audio height */
|
| 2 |
+
.stream-row {
|
| 3 |
+
align-items: stretch !important;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
/* Active light column: flex container, fill height */
|
| 7 |
+
.stream-row > div:last-child {
|
| 8 |
+
display: flex !important;
|
| 9 |
+
align-items: stretch !important;
|
| 10 |
+
min-height: 100%;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
/* Gradio block wrapper: fill and flex so child can stretch */
|
| 14 |
+
.stream-row > div:last-child > div {
|
| 15 |
+
display: flex !important;
|
| 16 |
+
width: 100% !important;
|
| 17 |
+
min-height: 100% !important;
|
| 18 |
+
height: 100% !important;
|
| 19 |
+
box-sizing: border-box;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/* All divs in light column: fill so panel stretches */
|
| 23 |
+
.stream-row > div:last-child div {
|
| 24 |
+
width: 100% !important;
|
| 25 |
+
min-height: 100% !important;
|
| 26 |
+
height: 100% !important;
|
| 27 |
+
box-sizing: border-box;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* Active light panel: fill container, base styles */
|
| 31 |
+
.active-light {
|
| 32 |
+
width: 100% !important;
|
| 33 |
+
min-height: 100% !important;
|
| 34 |
+
height: 100% !important;
|
| 35 |
+
box-sizing: border-box;
|
| 36 |
+
border-radius: 8px;
|
| 37 |
+
border: 1px solid var(--border-color-primary);
|
| 38 |
+
transition: background 0.2s, box-shadow 0.2s;
|
| 39 |
+
padding: 0.5rem;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Warming up state */
|
| 43 |
+
.active-light--off {
|
| 44 |
+
background: #555;
|
| 45 |
+
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.4);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Ready state */
|
| 49 |
+
.active-light--on {
|
| 50 |
+
background: #c00;
|
| 51 |
+
box-shadow:
|
| 52 |
+
0 0 16px rgba(220, 0, 0, 0.5),
|
| 53 |
+
inset 0 0 12px rgba(255, 80, 80, 0.3);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Label text */
|
| 57 |
+
.active-light__label {
|
| 58 |
+
font-size: 12px;
|
| 59 |
+
font-weight: 500;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.active-light--off .active-light__label {
|
| 63 |
+
color: rgba(255, 255, 255, 0.85);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.active-light--on .active-light__label {
|
| 67 |
+
color: #fff;
|
| 68 |
+
}
|
clean_up.py
CHANGED
|
@@ -1,24 +1,25 @@
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
from loguru import logger
|
| 4 |
-
from constants import MINUTES_KEEP
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
|
| 8 |
def purge_tmp_directory(
|
| 9 |
max_age_minutes: int = MINUTES_KEEP,
|
| 10 |
skip_substrings: list[str] | None = None,
|
| 11 |
-
tmp_dir: str =
|
| 12 |
) -> tuple[int, int]:
|
| 13 |
"""
|
| 14 |
Delete files in `tmp_dir` older than `max_age_minutes`, optionally skipping paths
|
| 15 |
-
that contain any substring in `skip_substrings`.
|
|
|
|
| 16 |
|
| 17 |
Args:
|
| 18 |
max_age_minutes: Files older than this many minutes will be deleted.
|
| 19 |
skip_substrings: If provided, any file path containing one of these substrings
|
| 20 |
will NOT be deleted.
|
| 21 |
-
tmp_dir: Directory to scan (default:
|
| 22 |
|
| 23 |
Returns:
|
| 24 |
(removed, skipped): Number of files removed and skipped.
|
|
@@ -28,7 +29,8 @@ def purge_tmp_directory(
|
|
| 28 |
removed = 0
|
| 29 |
|
| 30 |
if not os.path.exists(tmp_dir):
|
| 31 |
-
|
|
|
|
| 32 |
return removed, skipped
|
| 33 |
|
| 34 |
now = time.time()
|
|
@@ -64,7 +66,7 @@ def purge_tmp_directory(
|
|
| 64 |
return removed, skipped
|
| 65 |
|
| 66 |
|
| 67 |
-
def remove_files_related_to(stem: str, tmp_dir: str =
|
| 68 |
deleted = 0
|
| 69 |
|
| 70 |
if not stem:
|
|
@@ -89,7 +91,7 @@ def remove_files_related_to(stem: str, tmp_dir: str = "/tmp") -> int:
|
|
| 89 |
|
| 90 |
def cleanup_previous_run(
|
| 91 |
sample_stem: str,
|
| 92 |
-
tmp_dir: str =
|
| 93 |
max_age_minutes: int = MINUTES_KEEP,
|
| 94 |
) -> tuple[None, None, str, str, str]:
|
| 95 |
gr.Info("Processing started. This may take a moment. Please do not refresh or close the window.")
|
|
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
from loguru import logger
|
| 4 |
+
from constants import MINUTES_KEEP, APP_TMP_DIR
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
|
| 8 |
def purge_tmp_directory(
|
| 9 |
max_age_minutes: int = MINUTES_KEEP,
|
| 10 |
skip_substrings: list[str] | None = None,
|
| 11 |
+
tmp_dir: str = APP_TMP_DIR,
|
| 12 |
) -> tuple[int, int]:
|
| 13 |
"""
|
| 14 |
Delete files in `tmp_dir` older than `max_age_minutes`, optionally skipping paths
|
| 15 |
+
that contain any substring in `skip_substrings`. Use APP_TMP_DIR so we only
|
| 16 |
+
touch our own temp files (spectrograms, audio, etc.), not other apps' files in /tmp.
|
| 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: APP_TMP_DIR).
|
| 23 |
|
| 24 |
Returns:
|
| 25 |
(removed, skipped): Number of files removed and skipped.
|
|
|
|
| 29 |
removed = 0
|
| 30 |
|
| 31 |
if not os.path.exists(tmp_dir):
|
| 32 |
+
os.makedirs(tmp_dir, exist_ok=True)
|
| 33 |
+
logger.info(f"Temp directory created: {tmp_dir}")
|
| 34 |
return removed, skipped
|
| 35 |
|
| 36 |
now = time.time()
|
|
|
|
| 66 |
return removed, skipped
|
| 67 |
|
| 68 |
|
| 69 |
+
def remove_files_related_to(stem: str, tmp_dir: str = APP_TMP_DIR) -> int:
|
| 70 |
deleted = 0
|
| 71 |
|
| 72 |
if not stem:
|
|
|
|
| 91 |
|
| 92 |
def cleanup_previous_run(
|
| 93 |
sample_stem: str,
|
| 94 |
+
tmp_dir: str = APP_TMP_DIR,
|
| 95 |
max_age_minutes: int = MINUTES_KEEP,
|
| 96 |
) -> tuple[None, None, str, str, str]:
|
| 97 |
gr.Info("Processing started. This may take a moment. Please do not refresh or close the window.")
|
constants.py
CHANGED
|
@@ -10,6 +10,9 @@ MODEL_ID: Final = "quail-vf-2.0-l-16khz"
|
|
| 10 |
|
| 11 |
MINUTES_KEEP: Final = 60
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
DATASET_NAME: Final = "ai-coustics/dawn_chorus_en"
|
| 14 |
DEFAULT_SPLIT: Final = "eval"
|
| 15 |
MIX_DIR: Final = "mix"
|
|
@@ -18,6 +21,7 @@ TRANS_DIR: Final = "transcripts"
|
|
| 18 |
|
| 19 |
DEFAULT_SR: Final = 16000
|
| 20 |
STREAM_EVERY: Final = 0.2
|
|
|
|
| 21 |
|
| 22 |
STREAMER_CLASSES: Final = {
|
| 23 |
"Deepgram Nova-3 RT": DeepgramStreamer,
|
|
|
|
| 10 |
|
| 11 |
MINUTES_KEEP: Final = 60
|
| 12 |
|
| 13 |
+
# All app temp files (spectrograms, audio, etc.) go here so we only purge our own files.
|
| 14 |
+
APP_TMP_DIR: Final = "/tmp/voicefocus"
|
| 15 |
+
|
| 16 |
DATASET_NAME: Final = "ai-coustics/dawn_chorus_en"
|
| 17 |
DEFAULT_SPLIT: Final = "eval"
|
| 18 |
MIX_DIR: Final = "mix"
|
|
|
|
| 21 |
|
| 22 |
DEFAULT_SR: Final = 16000
|
| 23 |
STREAM_EVERY: Final = 0.2
|
| 24 |
+
WARMUP_SECONDS: Final = 2 # seconds before "recording ready" light turns on
|
| 25 |
|
| 26 |
STREAMER_CLASSES: Final = {
|
| 27 |
"Deepgram Nova-3 RT": DeepgramStreamer,
|
docs/dawn_chorus.md
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
Choose a sample from our open-source [Dawn Chorus evaluation dataset](https://huggingface.co/datasets/ai-coustics/dawn_chorus_en). This dataset includes audio with varying levels of background voice activity, ideal for testing the model performance.
|
|
|
|
| 1 |
+
Choose a sample from our open-source [Dawn Chorus evaluation dataset](https://huggingface.co/datasets/ai-coustics/dawn_chorus_en). This dataset includes audio with varying levels of background voice activity, ideal for testing the model performance.
|
offline_pipeline.py
CHANGED
|
@@ -1,28 +1,42 @@
|
|
| 1 |
import os
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
import librosa
|
| 5 |
from sdk import SDKWrapper
|
| 6 |
from utils import spec_image, compute_wer, transcribe_audio, to_gradio_audio
|
| 7 |
from hf_dataset_utils import get_audio, get_transcript
|
| 8 |
-
from constants import DEFAULT_SR
|
| 9 |
-
import numpy as np
|
|
|
|
| 10 |
|
| 11 |
def retrieve_audio_information(
|
| 12 |
original_array: np.ndarray,
|
| 13 |
enhanced_array: np.ndarray,
|
| 14 |
sample_id: str,
|
| 15 |
stt_model: str,
|
|
|
|
|
|
|
| 16 |
) -> tuple[str, str, str, str, str, str]:
|
|
|
|
|
|
|
|
|
|
| 17 |
if original_array is None or enhanced_array is None:
|
| 18 |
raise ValueError("Audio arrays are not available.")
|
| 19 |
-
noisy_spec_path = f"/
|
| 20 |
-
enhanced_spec_path = f"/
|
| 21 |
spec_image(original_array).save(noisy_spec_path)
|
| 22 |
spec_image(enhanced_array).save(enhanced_spec_path)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
original_transcript = get_transcript(sample_id)
|
| 27 |
wer_enhanced = compute_wer(original_transcript, enhanced_transcript)
|
| 28 |
wer_noisy = compute_wer(original_transcript, noisy_transcript)
|
|
@@ -33,10 +47,64 @@ def retrieve_audio_information(
|
|
| 33 |
return enhanced_spec_path, noisy_spec_path, enhanced_transcript, original_transcript, noisy_transcript, sample_id
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def denoise_audio(
|
| 37 |
sample_16k: np.ndarray,
|
| 38 |
enhancement_level: float = 50.0,
|
| 39 |
) -> tuple[np.ndarray | None , tuple[int, np.ndarray]| None]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
if sample_16k is None:
|
| 41 |
raise ValueError("No audio to enhance. Please upload a file first.")
|
| 42 |
try:
|
|
@@ -50,6 +118,207 @@ def denoise_audio(
|
|
| 50 |
return enhanced_array, gradio_enhanced_audio
|
| 51 |
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
def load_local_file(
|
| 54 |
sample_path: str
|
| 55 |
) -> tuple[np.ndarray, str]:
|
|
@@ -63,10 +332,10 @@ def load_local_file(
|
|
| 63 |
y_16k, _ = librosa.load(sample_path, sr=DEFAULT_SR, dtype="float32", mono=True)
|
| 64 |
return y_16k, new_sample_stem
|
| 65 |
|
| 66 |
-
def load_file_from_dataset(sample_id: str) -> tuple[tuple, np.ndarray, str]:
|
| 67 |
if not sample_id:
|
| 68 |
gr.Warning("Please select a sample from the dropdown.")
|
| 69 |
-
|
| 70 |
new_sample_stem = sample_id
|
| 71 |
try:
|
| 72 |
y_16k, sr = get_audio(sample_id, prefix="mix")
|
|
|
|
| 1 |
import os
|
| 2 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 3 |
+
from typing import Optional
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
import librosa
|
| 7 |
from sdk import SDKWrapper
|
| 8 |
from utils import spec_image, compute_wer, transcribe_audio, to_gradio_audio
|
| 9 |
from hf_dataset_utils import get_audio, get_transcript
|
| 10 |
+
from constants import DEFAULT_SR, APP_TMP_DIR, STREAMER_CLASSES
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
|
| 14 |
def retrieve_audio_information(
|
| 15 |
original_array: np.ndarray,
|
| 16 |
enhanced_array: np.ndarray,
|
| 17 |
sample_id: str,
|
| 18 |
stt_model: str,
|
| 19 |
+
noisy_transcript: str,
|
| 20 |
+
progress_state: Optional[dict] = None,
|
| 21 |
) -> tuple[str, str, str, str, str, str]:
|
| 22 |
+
"""Build spectrograms, transcribe enhanced audio, and compute WER. Caller must supply
|
| 23 |
+
noisy_transcript (transcription of original_array) so STT on the original is never run here.
|
| 24 |
+
If progress_state is provided, progress_state['enhanced'] is updated with partial transcript as enhanced STT streams."""
|
| 25 |
if original_array is None or enhanced_array is None:
|
| 26 |
raise ValueError("Audio arrays are not available.")
|
| 27 |
+
noisy_spec_path = f"{APP_TMP_DIR}/{sample_id}_noisy_spectrogram.png"
|
| 28 |
+
enhanced_spec_path = f"{APP_TMP_DIR}/{sample_id}_enhanced_spectrogram.png"
|
| 29 |
spec_image(original_array).save(noisy_spec_path)
|
| 30 |
spec_image(enhanced_array).save(enhanced_spec_path)
|
| 31 |
+
on_enhanced = (lambda t: progress_state.__setitem__("enhanced", t)) if progress_state is not None else None
|
| 32 |
+
enhanced_transcript = transcribe_audio(
|
| 33 |
+
enhanced_array,
|
| 34 |
+
DEFAULT_SR,
|
| 35 |
+
stt_model,
|
| 36 |
+
stream_name=f"{sample_id}_enhanced",
|
| 37 |
+
on_update=on_enhanced,
|
| 38 |
+
)
|
| 39 |
+
try:
|
| 40 |
original_transcript = get_transcript(sample_id)
|
| 41 |
wer_enhanced = compute_wer(original_transcript, enhanced_transcript)
|
| 42 |
wer_noisy = compute_wer(original_transcript, noisy_transcript)
|
|
|
|
| 47 |
return enhanced_spec_path, noisy_spec_path, enhanced_transcript, original_transcript, noisy_transcript, sample_id
|
| 48 |
|
| 49 |
|
| 50 |
+
def denoise_and_transcribe_noisy_parallel(
|
| 51 |
+
sample_16k: np.ndarray,
|
| 52 |
+
enhancement_level: float,
|
| 53 |
+
sample_id: str,
|
| 54 |
+
stt_model: str,
|
| 55 |
+
progress_state: Optional[dict] = None,
|
| 56 |
+
) -> tuple[np.ndarray | None, tuple[int, np.ndarray] | None, str]:
|
| 57 |
+
"""Run denoising and noisy transcription in parallel. Returns (enhanced_array, gradio_audio, noisy_transcript).
|
| 58 |
+
If progress_state is provided, progress_state['noisy'] is updated with partial transcript as noisy STT streams."""
|
| 59 |
+
if sample_16k is None:
|
| 60 |
+
raise ValueError("No audio to enhance. Please upload a file first.")
|
| 61 |
+
on_noisy = (lambda t: progress_state.__setitem__("noisy", t)) if progress_state is not None else None
|
| 62 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
| 63 |
+
future_denoise = executor.submit(
|
| 64 |
+
_denoise_audio_impl, sample_16k, float(enhancement_level) / 100.0
|
| 65 |
+
)
|
| 66 |
+
future_noisy = executor.submit(
|
| 67 |
+
transcribe_audio,
|
| 68 |
+
sample_16k,
|
| 69 |
+
DEFAULT_SR,
|
| 70 |
+
stt_model,
|
| 71 |
+
stream_name=f"{sample_id}_noisy",
|
| 72 |
+
on_update=on_noisy,
|
| 73 |
+
)
|
| 74 |
+
enhanced_array, gradio_enhanced_audio = future_denoise.result()
|
| 75 |
+
noisy_transcript = future_noisy.result()
|
| 76 |
+
return enhanced_array, gradio_enhanced_audio, noisy_transcript
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _denoise_audio_impl(
|
| 80 |
+
sample_16k: np.ndarray, enhancement_level_frac: float
|
| 81 |
+
) -> tuple[np.ndarray, tuple[int, np.ndarray]]:
|
| 82 |
+
"""Run the enhancer on a single array and return both the array and Gradio-ready audio.
|
| 83 |
+
|
| 84 |
+
Used by denoise_and_transcribe_noisy_parallel so the denoise step can run in a
|
| 85 |
+
thread without Gradio UI calls. Expects enhancement_level_frac in [0, 1] (not percent).
|
| 86 |
+
Returns (enhanced_array, (sample_rate, numpy_array)) for Gradio Audio.
|
| 87 |
+
"""
|
| 88 |
+
sdk = SDKWrapper()
|
| 89 |
+
sdk.init_processor(
|
| 90 |
+
sample_rate=DEFAULT_SR,
|
| 91 |
+
enhancement_level=enhancement_level_frac,
|
| 92 |
+
)
|
| 93 |
+
enhanced_array = sdk.process_sync(sample_16k)
|
| 94 |
+
gradio_enhanced_audio = to_gradio_audio(enhanced_array, DEFAULT_SR)
|
| 95 |
+
return enhanced_array, gradio_enhanced_audio
|
| 96 |
+
|
| 97 |
+
|
| 98 |
def denoise_audio(
|
| 99 |
sample_16k: np.ndarray,
|
| 100 |
enhancement_level: float = 50.0,
|
| 101 |
) -> tuple[np.ndarray | None , tuple[int, np.ndarray]| None]:
|
| 102 |
+
"""Enhance-only entry point: run the SDK on the given audio and return enhanced array + Gradio audio.
|
| 103 |
+
|
| 104 |
+
The main app uses denoise_and_transcribe_noisy_parallel (denoise + noisy STT in parallel)
|
| 105 |
+
instead. This function remains for backward compatibility, scripts, or any caller that
|
| 106 |
+
needs only enhancement without transcription (e.g. backup flows, tests).
|
| 107 |
+
"""
|
| 108 |
if sample_16k is None:
|
| 109 |
raise ValueError("No audio to enhance. Please upload a file first.")
|
| 110 |
try:
|
|
|
|
| 118 |
return enhanced_array, gradio_enhanced_audio
|
| 119 |
|
| 120 |
|
| 121 |
+
def run_offline_pipeline_ordered(
|
| 122 |
+
sample_16k: np.ndarray,
|
| 123 |
+
enhancement_level: float,
|
| 124 |
+
sample_id: str,
|
| 125 |
+
stt_model: str,
|
| 126 |
+
progress_state: dict,
|
| 127 |
+
) -> tuple[str, str, str, tuple[int, np.ndarray], str, np.ndarray, str]:
|
| 128 |
+
"""Run pipeline in UI order: 1) Denoise only. 2) When done, set progress_state['enhanced_spec_path']
|
| 129 |
+
and progress_state['enhanced_audio']. 3) Run noisy STT and enhanced STT in parallel (both stream via
|
| 130 |
+
progress_state['noisy'] and progress_state['enhanced']). 4) Return final transcripts with WER.
|
| 131 |
+
|
| 132 |
+
Returns: (enhanced_spec_path, enhanced_transcript, noisy_transcript_with_wer, enhanced_audio,
|
| 133 |
+
last_stem, enhanced_array, precomputed_noisy).
|
| 134 |
+
"""
|
| 135 |
+
if sample_16k is None:
|
| 136 |
+
raise ValueError("No audio to enhance. Please upload a file first.")
|
| 137 |
+
# 1) Denoise only
|
| 138 |
+
enhanced_array, gradio_enhanced_audio = _denoise_audio_impl(
|
| 139 |
+
sample_16k, float(enhancement_level) / 100.0
|
| 140 |
+
)
|
| 141 |
+
# 2) As soon as enhanced audio is ready: build enhanced spectrogram and expose to UI
|
| 142 |
+
enhanced_spec_path = f"{APP_TMP_DIR}/{sample_id}_enhanced_spectrogram.png"
|
| 143 |
+
spec_image(enhanced_array).save(enhanced_spec_path)
|
| 144 |
+
progress_state["enhanced_spec_path"] = enhanced_spec_path
|
| 145 |
+
progress_state["enhanced_audio"] = gradio_enhanced_audio
|
| 146 |
+
# 3) Noisy and enhanced transcription starting both at the same time (parallel, both with on_update).
|
| 147 |
+
# Sync so the UI shows both boxes updating together: only write to noisy/enhanced once both have sent at least one update.
|
| 148 |
+
progress_state["noisy_pending"] = ""
|
| 149 |
+
progress_state["enhanced_pending"] = ""
|
| 150 |
+
progress_state["noisy_has_sent"] = False
|
| 151 |
+
progress_state["enhanced_has_sent"] = False
|
| 152 |
+
|
| 153 |
+
def _flush_both():
|
| 154 |
+
if progress_state.get("noisy_has_sent") and progress_state.get("enhanced_has_sent"):
|
| 155 |
+
progress_state["noisy"] = progress_state.get("noisy_pending", "")
|
| 156 |
+
progress_state["enhanced"] = progress_state.get("enhanced_pending", "")
|
| 157 |
+
|
| 158 |
+
def on_noisy(t: str):
|
| 159 |
+
progress_state["noisy_pending"] = t
|
| 160 |
+
progress_state["noisy_has_sent"] = True
|
| 161 |
+
_flush_both()
|
| 162 |
+
|
| 163 |
+
def on_enhanced(t: str):
|
| 164 |
+
progress_state["enhanced_pending"] = t
|
| 165 |
+
progress_state["enhanced_has_sent"] = True
|
| 166 |
+
_flush_both()
|
| 167 |
+
|
| 168 |
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
| 169 |
+
future_noisy = executor.submit(
|
| 170 |
+
transcribe_audio,
|
| 171 |
+
sample_16k,
|
| 172 |
+
DEFAULT_SR,
|
| 173 |
+
stt_model,
|
| 174 |
+
stream_name=f"{sample_id}_noisy",
|
| 175 |
+
on_update=on_noisy,
|
| 176 |
+
)
|
| 177 |
+
future_enhanced = executor.submit(
|
| 178 |
+
transcribe_audio,
|
| 179 |
+
enhanced_array,
|
| 180 |
+
DEFAULT_SR,
|
| 181 |
+
stt_model,
|
| 182 |
+
stream_name=f"{sample_id}_enhanced",
|
| 183 |
+
on_update=on_enhanced,
|
| 184 |
+
)
|
| 185 |
+
noisy_transcript = future_noisy.result()
|
| 186 |
+
enhanced_transcript = future_enhanced.result()
|
| 187 |
+
# 4) WER and final strings
|
| 188 |
+
precomputed_noisy = noisy_transcript
|
| 189 |
+
try:
|
| 190 |
+
original_transcript = get_transcript(sample_id)
|
| 191 |
+
wer_enhanced = compute_wer(original_transcript, enhanced_transcript)
|
| 192 |
+
wer_noisy = compute_wer(original_transcript, noisy_transcript)
|
| 193 |
+
enhanced_transcript += f" (WER: {wer_enhanced:.2f})"
|
| 194 |
+
noisy_transcript += f" (WER: {wer_noisy:.2f})"
|
| 195 |
+
except Exception:
|
| 196 |
+
original_transcript = "Unavailable"
|
| 197 |
+
return (
|
| 198 |
+
enhanced_spec_path,
|
| 199 |
+
enhanced_transcript,
|
| 200 |
+
noisy_transcript,
|
| 201 |
+
gradio_enhanced_audio,
|
| 202 |
+
sample_id,
|
| 203 |
+
enhanced_array,
|
| 204 |
+
precomputed_noisy,
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _close_stt_stream(streamer) -> None:
|
| 209 |
+
"""Signal end-of-stream; streamer type may be Soniox (close_stream) or Deepgram (close)."""
|
| 210 |
+
if hasattr(streamer, "close_stream"):
|
| 211 |
+
streamer.close_stream()
|
| 212 |
+
else:
|
| 213 |
+
streamer.close()
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def run_offline_pipeline_streaming(
|
| 217 |
+
sample_16k: np.ndarray,
|
| 218 |
+
enhancement_level: float,
|
| 219 |
+
sample_id: str,
|
| 220 |
+
stt_model: str,
|
| 221 |
+
progress_state: dict,
|
| 222 |
+
) -> tuple[str, str, str, tuple[int, np.ndarray], str, np.ndarray, str]:
|
| 223 |
+
"""Run enhancement and both STTs in real time by processing in chunks. Transcripts stream
|
| 224 |
+
via progress_state['noisy'] and progress_state['enhanced']. Enhanced audio is returned
|
| 225 |
+
only at the end; the app plays it automatically when processing is complete.
|
| 226 |
+
Returns same tuple as run_offline_pipeline_ordered."""
|
| 227 |
+
if sample_16k is None:
|
| 228 |
+
raise ValueError("No audio to enhance. Please upload a file first.")
|
| 229 |
+
sample_16k = np.asarray(sample_16k, dtype=np.float32).flatten()
|
| 230 |
+
|
| 231 |
+
sdk = SDKWrapper()
|
| 232 |
+
sdk.init_processor(
|
| 233 |
+
sample_rate=DEFAULT_SR,
|
| 234 |
+
enhancement_level=float(enhancement_level) / 100.0,
|
| 235 |
+
)
|
| 236 |
+
chunk_size = sdk.num_frames
|
| 237 |
+
|
| 238 |
+
# Sync transcript callbacks so both boxes update together
|
| 239 |
+
progress_state["noisy_pending"] = ""
|
| 240 |
+
progress_state["enhanced_pending"] = ""
|
| 241 |
+
progress_state["noisy_has_sent"] = False
|
| 242 |
+
progress_state["enhanced_has_sent"] = False
|
| 243 |
+
|
| 244 |
+
def _flush_both():
|
| 245 |
+
if progress_state.get("noisy_has_sent") and progress_state.get("enhanced_has_sent"):
|
| 246 |
+
progress_state["noisy"] = progress_state.get("noisy_pending", "")
|
| 247 |
+
progress_state["enhanced"] = progress_state.get("enhanced_pending", "")
|
| 248 |
+
|
| 249 |
+
def on_noisy(t: str):
|
| 250 |
+
progress_state["noisy_pending"] = t
|
| 251 |
+
progress_state["noisy_has_sent"] = True
|
| 252 |
+
_flush_both()
|
| 253 |
+
|
| 254 |
+
def on_enhanced(t: str):
|
| 255 |
+
progress_state["enhanced_pending"] = t
|
| 256 |
+
progress_state["enhanced_has_sent"] = True
|
| 257 |
+
_flush_both()
|
| 258 |
+
|
| 259 |
+
if stt_model not in STREAMER_CLASSES:
|
| 260 |
+
raise ValueError(f"Unknown STT model: {stt_model}")
|
| 261 |
+
StreamerClass = STREAMER_CLASSES[stt_model]
|
| 262 |
+
streamer_noisy = StreamerClass(DEFAULT_SR, f"{sample_id}_noisy", on_update=on_noisy)
|
| 263 |
+
streamer_enhanced = StreamerClass(DEFAULT_SR, f"{sample_id}_enhanced", on_update=on_enhanced)
|
| 264 |
+
|
| 265 |
+
accumulated_enhanced: list[np.ndarray] = []
|
| 266 |
+
n = len(sample_16k)
|
| 267 |
+
|
| 268 |
+
for i in range(0, n, chunk_size):
|
| 269 |
+
raw_chunk = sample_16k[i : i + chunk_size]
|
| 270 |
+
if raw_chunk.size < chunk_size:
|
| 271 |
+
raw_chunk = np.pad(
|
| 272 |
+
raw_chunk,
|
| 273 |
+
(0, chunk_size - raw_chunk.size),
|
| 274 |
+
mode="constant",
|
| 275 |
+
constant_values=0.0,
|
| 276 |
+
)
|
| 277 |
+
raw_2d = raw_chunk.reshape(1, -1)
|
| 278 |
+
enhanced_chunk = sdk.process_chunk(raw_2d)
|
| 279 |
+
enhanced_1d = np.asarray(enhanced_chunk).flatten()
|
| 280 |
+
streamer_noisy.process_chunk(raw_chunk)
|
| 281 |
+
streamer_enhanced.process_chunk(enhanced_1d)
|
| 282 |
+
accumulated_enhanced.append(enhanced_1d)
|
| 283 |
+
|
| 284 |
+
_close_stt_stream(streamer_noisy)
|
| 285 |
+
_close_stt_stream(streamer_enhanced)
|
| 286 |
+
streamer_noisy.finished_event.wait()
|
| 287 |
+
streamer_enhanced.finished_event.wait()
|
| 288 |
+
|
| 289 |
+
with streamer_noisy.lock:
|
| 290 |
+
noisy_transcript = streamer_noisy.render_tokens(streamer_noisy.final_tokens, [])
|
| 291 |
+
with streamer_enhanced.lock:
|
| 292 |
+
enhanced_transcript = streamer_enhanced.render_tokens(streamer_enhanced.final_tokens, [])
|
| 293 |
+
|
| 294 |
+
enhanced_array = np.concatenate(accumulated_enhanced).astype(np.float32)
|
| 295 |
+
gradio_enhanced_audio = to_gradio_audio(enhanced_array, DEFAULT_SR)
|
| 296 |
+
|
| 297 |
+
enhanced_spec_path = f"{APP_TMP_DIR}/{sample_id}_enhanced_spectrogram.png"
|
| 298 |
+
spec_image(enhanced_array).save(enhanced_spec_path)
|
| 299 |
+
progress_state["enhanced_spec_path"] = enhanced_spec_path
|
| 300 |
+
|
| 301 |
+
precomputed_noisy = noisy_transcript
|
| 302 |
+
try:
|
| 303 |
+
original_transcript = get_transcript(sample_id)
|
| 304 |
+
wer_enhanced = compute_wer(original_transcript, enhanced_transcript)
|
| 305 |
+
wer_noisy = compute_wer(original_transcript, noisy_transcript)
|
| 306 |
+
enhanced_transcript += f" (WER: {wer_enhanced:.2f})"
|
| 307 |
+
noisy_transcript += f" (WER: {wer_noisy:.2f})"
|
| 308 |
+
except Exception:
|
| 309 |
+
pass
|
| 310 |
+
|
| 311 |
+
return (
|
| 312 |
+
enhanced_spec_path,
|
| 313 |
+
enhanced_transcript,
|
| 314 |
+
noisy_transcript,
|
| 315 |
+
gradio_enhanced_audio,
|
| 316 |
+
sample_id,
|
| 317 |
+
enhanced_array,
|
| 318 |
+
precomputed_noisy,
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
def load_local_file(
|
| 323 |
sample_path: str
|
| 324 |
) -> tuple[np.ndarray, str]:
|
|
|
|
| 332 |
y_16k, _ = librosa.load(sample_path, sr=DEFAULT_SR, dtype="float32", mono=True)
|
| 333 |
return y_16k, new_sample_stem
|
| 334 |
|
| 335 |
+
def load_file_from_dataset(sample_id: str) -> tuple[tuple | None, np.ndarray | None, str]:
|
| 336 |
if not sample_id:
|
| 337 |
gr.Warning("Please select a sample from the dropdown.")
|
| 338 |
+
return None, None, ""
|
| 339 |
new_sample_stem = sample_id
|
| 340 |
try:
|
| 341 |
y_16k, sr = get_audio(sample_id, prefix="mix")
|
requirements.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
# Requires Python 3.11
|
|
|
|
| 2 |
matplotlib>=3.8,<3.10
|
| 3 |
soundfile>=0.12.1
|
| 4 |
aiohttp>=3.9,<4
|
|
|
|
| 1 |
# Requires Python 3.11
|
| 2 |
+
gradio>=6.0
|
| 3 |
matplotlib>=3.8,<3.10
|
| 4 |
soundfile>=0.12.1
|
| 5 |
aiohttp>=3.9,<4
|
stream_pipeline.py
CHANGED
|
@@ -12,7 +12,6 @@ from dataclasses import dataclass
|
|
| 12 |
_ENHANCED_TRANSCRIPT: str = ""
|
| 13 |
_RAW_TRANSCRIPT: str = ""
|
| 14 |
|
| 15 |
-
|
| 16 |
def _set_transcript_enhanced(text: str) -> None:
|
| 17 |
"""Deepgram callback: update latest transcript text (no printing)."""
|
| 18 |
global _ENHANCED_TRANSCRIPT
|
|
@@ -24,14 +23,24 @@ def _set_transcript_raw(text: str) -> None:
|
|
| 24 |
_RAW_TRANSCRIPT = text
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
SDK = SDKWrapper()
|
| 28 |
SDK.init_processor(
|
| 29 |
-
sample_rate=DEFAULT_SR,
|
| 30 |
-
enhancement_level=1.0,
|
| 31 |
-
allow_variable_frames=
|
| 32 |
num_channels=1,
|
| 33 |
-
)
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
@dataclass
|
|
@@ -66,15 +75,25 @@ def _to_float32_mono(y: np.ndarray) -> np.ndarray:
|
|
| 66 |
return y
|
| 67 |
|
| 68 |
|
| 69 |
-
def transcribe_stream(session: StreamSession | None, new_chunk, enhancement_level):
|
| 70 |
-
if
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 73 |
if new_chunk is None or new_chunk[1] is None:
|
| 74 |
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 75 |
|
| 76 |
sr, y = new_chunk
|
| 77 |
y = _to_float32_mono(y)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
session = _get_or_init_session(session, sr)
|
| 79 |
SDK.change_enhancement_level(float(enhancement_level) / 100.0)
|
| 80 |
if session.resampler is not None:
|
|
@@ -82,28 +101,58 @@ def transcribe_stream(session: StreamSession | None, new_chunk, enhancement_leve
|
|
| 82 |
else:
|
| 83 |
y_16k = y
|
| 84 |
|
|
|
|
|
|
|
|
|
|
| 85 |
# Ringbuffer (nicht unendlich konkatenieren)
|
| 86 |
if y_16k.size > 0:
|
| 87 |
tail = np.concatenate([session.tail_16k, y_16k])
|
| 88 |
if tail.size > session.tail_max:
|
| 89 |
tail = tail[-session.tail_max:]
|
| 90 |
session.tail_16k = tail
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
enhanced_chunk_16k = SDK.process_sync(y_16k)
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 95 |
|
| 96 |
|
| 97 |
-
def shutdown_streamers():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
try:
|
| 99 |
-
if Streamer_enhanced.ws is not None:
|
| 100 |
Streamer_enhanced.shutdown()
|
| 101 |
-
if Streamer_raw.ws is not None:
|
| 102 |
Streamer_raw.shutdown()
|
| 103 |
except Exception:
|
| 104 |
-
print("Failed to shutdown
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
def clear_ui():
|
| 108 |
global _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 109 |
_ENHANCED_TRANSCRIPT = ""
|
|
@@ -111,22 +160,32 @@ def clear_ui():
|
|
| 111 |
return None, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 112 |
|
| 113 |
def stop_online_backend():
|
|
|
|
|
|
|
|
|
|
| 114 |
shutdown_streamers()
|
| 115 |
session, enhanced_transcript, raw_transcript = clear_ui()
|
| 116 |
-
return
|
| 117 |
|
| 118 |
|
| 119 |
def set_stt_streamer(model_name):
|
| 120 |
StreamerCls = STREAMER_CLASSES.get(model_name, DeepgramStreamer)
|
| 121 |
-
global Streamer_enhanced, Streamer_raw
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
fs_hz=DEFAULT_SR,
|
| 124 |
stream_name="enhanced",
|
| 125 |
on_update=_set_transcript_enhanced,
|
| 126 |
)
|
| 127 |
-
|
| 128 |
fs_hz=DEFAULT_SR,
|
| 129 |
stream_name="raw",
|
| 130 |
on_update=_set_transcript_raw,
|
| 131 |
)
|
|
|
|
|
|
|
|
|
|
| 132 |
|
|
|
|
| 12 |
_ENHANCED_TRANSCRIPT: str = ""
|
| 13 |
_RAW_TRANSCRIPT: str = ""
|
| 14 |
|
|
|
|
| 15 |
def _set_transcript_enhanced(text: str) -> None:
|
| 16 |
"""Deepgram callback: update latest transcript text (no printing)."""
|
| 17 |
global _ENHANCED_TRANSCRIPT
|
|
|
|
| 23 |
_RAW_TRANSCRIPT = text
|
| 24 |
|
| 25 |
|
| 26 |
+
def get_live_transcripts() -> tuple[str, str]:
|
| 27 |
+
"""Return current enhanced and raw transcript for live UI updates."""
|
| 28 |
+
return _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 29 |
+
|
| 30 |
+
|
| 31 |
SDK = SDKWrapper()
|
| 32 |
SDK.init_processor(
|
| 33 |
+
sample_rate=DEFAULT_SR,
|
| 34 |
+
enhancement_level=1.0,
|
| 35 |
+
allow_variable_frames=True, # streaming chunks are variable-sized
|
| 36 |
num_channels=1,
|
| 37 |
+
)
|
| 38 |
|
| 39 |
+
# Created on first start_recording (lazy) to avoid Soniox "No audio received" timeout at app load
|
| 40 |
+
Streamer_enhanced = None
|
| 41 |
+
Streamer_raw = None
|
| 42 |
+
_streamer_generation = 0
|
| 43 |
+
_last_stop_generation = 1 # so first stop doesn't skip (1 > 1 is False)
|
| 44 |
|
| 45 |
|
| 46 |
@dataclass
|
|
|
|
| 75 |
return y
|
| 76 |
|
| 77 |
|
| 78 |
+
def transcribe_stream(session: StreamSession | None, new_chunk, enhancement_level, input_gain_db: float = 0.0):
|
| 79 |
+
if (
|
| 80 |
+
Streamer_enhanced is None
|
| 81 |
+
or Streamer_raw is None
|
| 82 |
+
or Streamer_enhanced.ws is None
|
| 83 |
+
or Streamer_raw.ws is None
|
| 84 |
+
):
|
| 85 |
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 86 |
if new_chunk is None or new_chunk[1] is None:
|
| 87 |
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 88 |
|
| 89 |
sr, y = new_chunk
|
| 90 |
y = _to_float32_mono(y)
|
| 91 |
+
# Apply input gain: linear = 10^(dB/20), clip to avoid overflow
|
| 92 |
+
if input_gain_db is not None and input_gain_db > 0:
|
| 93 |
+
gain_linear = np.float32(10.0 ** (float(input_gain_db) / 20.0))
|
| 94 |
+
y = (y * gain_linear).astype(np.float32)
|
| 95 |
+
y = np.clip(y, -1.0, 1.0)
|
| 96 |
+
|
| 97 |
session = _get_or_init_session(session, sr)
|
| 98 |
SDK.change_enhancement_level(float(enhancement_level) / 100.0)
|
| 99 |
if session.resampler is not None:
|
|
|
|
| 101 |
else:
|
| 102 |
y_16k = y
|
| 103 |
|
| 104 |
+
# Ensure 1D float32 for SDK and streamers (resample_chunk can return 0 samples or 2D)
|
| 105 |
+
y_16k = np.asarray(y_16k, dtype=np.float32).flatten()
|
| 106 |
+
|
| 107 |
# Ringbuffer (nicht unendlich konkatenieren)
|
| 108 |
if y_16k.size > 0:
|
| 109 |
tail = np.concatenate([session.tail_16k, y_16k])
|
| 110 |
if tail.size > session.tail_max:
|
| 111 |
tail = tail[-session.tail_max:]
|
| 112 |
session.tail_16k = tail
|
| 113 |
+
|
| 114 |
+
# Only send when we have samples (resample_chunk can return empty; SDK needs valid input)
|
| 115 |
+
if y_16k.size == 0:
|
| 116 |
+
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 117 |
+
|
| 118 |
+
# Parallel path: send raw to STT immediately, then enhance and send enhanced.
|
| 119 |
+
# SDK requires fixed num_frames (AudioConfigMismatchError if we use process_chunk with variable size).
|
| 120 |
+
Streamer_raw.process_chunk(y_16k)
|
| 121 |
enhanced_chunk_16k = SDK.process_sync(y_16k)
|
| 122 |
+
out_1d = np.asarray(enhanced_chunk_16k, dtype=np.float32).flatten()
|
| 123 |
+
# Always send something to enhanced so Soniox doesn't close with "No audio received"
|
| 124 |
+
if out_1d.size > 0:
|
| 125 |
+
Streamer_enhanced.process_chunk(out_1d)
|
| 126 |
+
else:
|
| 127 |
+
Streamer_enhanced.process_chunk(np.zeros(160, dtype=np.float32))
|
| 128 |
return session, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 129 |
|
| 130 |
|
| 131 |
+
def shutdown_streamers(from_stop_recording: bool = False):
|
| 132 |
+
"""Shut down STT streamers. If from_stop_recording, skip when streamers were
|
| 133 |
+
created after the last stop (avoids delayed stop killing new streamers)."""
|
| 134 |
+
global Streamer_enhanced, Streamer_raw, _streamer_generation, _last_stop_generation
|
| 135 |
+
if from_stop_recording and _streamer_generation > _last_stop_generation:
|
| 136 |
+
return
|
| 137 |
+
gen = _streamer_generation
|
| 138 |
try:
|
| 139 |
+
if Streamer_enhanced is not None and Streamer_enhanced.ws is not None:
|
| 140 |
Streamer_enhanced.shutdown()
|
| 141 |
+
if Streamer_raw is not None and Streamer_raw.ws is not None:
|
| 142 |
Streamer_raw.shutdown()
|
| 143 |
except Exception:
|
| 144 |
+
print("Failed to shutdown streamers.")
|
| 145 |
+
finally:
|
| 146 |
+
Streamer_enhanced = None
|
| 147 |
+
Streamer_raw = None
|
| 148 |
+
if from_stop_recording:
|
| 149 |
+
_last_stop_generation = gen
|
| 150 |
|
| 151 |
+
def on_stop_recording():
|
| 152 |
+
"""Call from Gradio stop_recording so streamers shut down when user clicks Stop."""
|
| 153 |
+
shutdown_streamers(from_stop_recording=True)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
def clear_ui():
|
| 157 |
global _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 158 |
_ENHANCED_TRANSCRIPT = ""
|
|
|
|
| 160 |
return None, _ENHANCED_TRANSCRIPT, _RAW_TRANSCRIPT
|
| 161 |
|
| 162 |
def stop_online_backend():
|
| 163 |
+
"""Stop streamers and clear transcripts. Do not update the Audio component:
|
| 164 |
+
toggling streaming=False then back to True can make the frontend lose the
|
| 165 |
+
microphone (getUserMedia not re-called), so we leave it unchanged."""
|
| 166 |
shutdown_streamers()
|
| 167 |
session, enhanced_transcript, raw_transcript = clear_ui()
|
| 168 |
+
return session, enhanced_transcript, raw_transcript, gr.update()
|
| 169 |
|
| 170 |
|
| 171 |
def set_stt_streamer(model_name):
|
| 172 |
StreamerCls = STREAMER_CLASSES.get(model_name, DeepgramStreamer)
|
| 173 |
+
global Streamer_enhanced, Streamer_raw, _streamer_generation
|
| 174 |
+
# Shut down current streamers first so we don't leak
|
| 175 |
+
if Streamer_enhanced is not None or Streamer_raw is not None:
|
| 176 |
+
shutdown_streamers()
|
| 177 |
+
# Create both before assigning so transcribe_stream never sees one new and one old
|
| 178 |
+
new_enhanced = StreamerCls(
|
| 179 |
fs_hz=DEFAULT_SR,
|
| 180 |
stream_name="enhanced",
|
| 181 |
on_update=_set_transcript_enhanced,
|
| 182 |
)
|
| 183 |
+
new_raw = StreamerCls(
|
| 184 |
fs_hz=DEFAULT_SR,
|
| 185 |
stream_name="raw",
|
| 186 |
on_update=_set_transcript_raw,
|
| 187 |
)
|
| 188 |
+
_streamer_generation += 1
|
| 189 |
+
Streamer_enhanced = new_enhanced
|
| 190 |
+
Streamer_raw = new_raw
|
| 191 |
|
stt_streamers/soniox_streamer.py
CHANGED
|
@@ -172,7 +172,9 @@ class SonioxStreamer:
|
|
| 172 |
self.ws.send(json.dumps({"type": "keepalive"})) # Soniox-format citeturn2view0
|
| 173 |
self._last_send_ts = time.monotonic()
|
| 174 |
except Exception as e:
|
| 175 |
-
|
|
|
|
|
|
|
| 176 |
self.ws = None
|
| 177 |
return
|
| 178 |
|
|
|
|
| 172 |
self.ws.send(json.dumps({"type": "keepalive"})) # Soniox-format citeturn2view0
|
| 173 |
self._last_send_ts = time.monotonic()
|
| 174 |
except Exception as e:
|
| 175 |
+
# Normal closure (1000 OK) when stream ends; don't log as failure
|
| 176 |
+
if "1000" not in str(e):
|
| 177 |
+
print(f"[Soniox:{self.stream_name}] keepalive failed: {e}")
|
| 178 |
self.ws = None
|
| 179 |
return
|
| 180 |
|
utils.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from typing import Optional
|
| 2 |
|
| 3 |
import numpy as np
|
| 4 |
import librosa
|
|
@@ -9,6 +9,8 @@ import resampy
|
|
| 9 |
from constants import DEFAULT_SR, STREAMER_CLASSES
|
| 10 |
|
| 11 |
def to_gradio_audio(x: np.ndarray, sr: int) -> tuple[int, np.ndarray]:
|
|
|
|
|
|
|
| 12 |
x = np.asarray(x)
|
| 13 |
|
| 14 |
# Remove extra dims like (1, n, 1) etc.
|
|
@@ -22,9 +24,10 @@ def to_gradio_audio(x: np.ndarray, sr: int) -> tuple[int, np.ndarray]:
|
|
| 22 |
if x.ndim == 2 and x.shape[1] == 1:
|
| 23 |
x = x[:, 0]
|
| 24 |
|
| 25 |
-
# Return float32 in [-1, 1] (simplest for Gradio)
|
| 26 |
x = x.astype(np.float32)
|
| 27 |
x = np.clip(x, -1.0, 1.0)
|
|
|
|
|
|
|
| 28 |
|
| 29 |
return (sr, x)
|
| 30 |
|
|
@@ -94,7 +97,13 @@ def compute_wer(reference: str, hypothesis: str) -> float:
|
|
| 94 |
|
| 95 |
|
| 96 |
|
| 97 |
-
def transcribe_audio(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
"""
|
| 99 |
Transcribe an audio array using the specified STT streamer.
|
| 100 |
|
|
@@ -103,15 +112,14 @@ def transcribe_audio(audio_array: np.ndarray, sr: int, streamer_type: str = "Dee
|
|
| 103 |
sr (int): Sample rate of the audio array
|
| 104 |
streamer_type (str): "Soniox" or "Deepgram"
|
| 105 |
stream_name (str): Optional label for streamer instance ("RAW", "ENHANCED", etc.)
|
|
|
|
| 106 |
|
| 107 |
Returns:
|
| 108 |
-
str:
|
| 109 |
"""
|
| 110 |
-
|
| 111 |
if sr != DEFAULT_SR:
|
| 112 |
audio_array = resampy.resample(audio_array, sr, DEFAULT_SR)
|
| 113 |
sr = DEFAULT_SR
|
| 114 |
-
|
| 115 |
|
| 116 |
if streamer_type not in STREAMER_CLASSES:
|
| 117 |
raise ValueError(
|
|
@@ -120,7 +128,7 @@ def transcribe_audio(audio_array: np.ndarray, sr: int, streamer_type: str = "Dee
|
|
| 120 |
)
|
| 121 |
|
| 122 |
StreamerClass = STREAMER_CLASSES[streamer_type]
|
| 123 |
-
streamer = StreamerClass(sr, stream_name)
|
| 124 |
|
| 125 |
transcript = streamer.stream_array(audio_array)
|
| 126 |
|
|
|
|
| 1 |
+
from typing import Callable, Optional
|
| 2 |
|
| 3 |
import numpy as np
|
| 4 |
import librosa
|
|
|
|
| 9 |
from constants import DEFAULT_SR, STREAMER_CLASSES
|
| 10 |
|
| 11 |
def to_gradio_audio(x: np.ndarray, sr: int) -> tuple[int, np.ndarray]:
|
| 12 |
+
"""Return (sample_rate, int16 mono array) for Gradio Audio. Gradio expects int16;
|
| 13 |
+
passing float32 triggers an internal conversion and a warning."""
|
| 14 |
x = np.asarray(x)
|
| 15 |
|
| 16 |
# Remove extra dims like (1, n, 1) etc.
|
|
|
|
| 24 |
if x.ndim == 2 and x.shape[1] == 1:
|
| 25 |
x = x[:, 0]
|
| 26 |
|
|
|
|
| 27 |
x = x.astype(np.float32)
|
| 28 |
x = np.clip(x, -1.0, 1.0)
|
| 29 |
+
# Gradio Audio expects int16; convert here so Gradio doesn't convert and warn
|
| 30 |
+
x = (x * 32767).astype(np.int16)
|
| 31 |
|
| 32 |
return (sr, x)
|
| 33 |
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
+
def transcribe_audio(
|
| 101 |
+
audio_array: np.ndarray,
|
| 102 |
+
sr: int,
|
| 103 |
+
streamer_type: str = "Deepgram",
|
| 104 |
+
stream_name: str = "RAW",
|
| 105 |
+
on_update: Optional[Callable[[str], None]] = None,
|
| 106 |
+
):
|
| 107 |
"""
|
| 108 |
Transcribe an audio array using the specified STT streamer.
|
| 109 |
|
|
|
|
| 112 |
sr (int): Sample rate of the audio array
|
| 113 |
streamer_type (str): "Soniox" or "Deepgram"
|
| 114 |
stream_name (str): Optional label for streamer instance ("RAW", "ENHANCED", etc.)
|
| 115 |
+
on_update: Optional callback(text: str) called with partial transcript as results stream in.
|
| 116 |
|
| 117 |
Returns:
|
| 118 |
+
str: Final transcript text
|
| 119 |
"""
|
|
|
|
| 120 |
if sr != DEFAULT_SR:
|
| 121 |
audio_array = resampy.resample(audio_array, sr, DEFAULT_SR)
|
| 122 |
sr = DEFAULT_SR
|
|
|
|
| 123 |
|
| 124 |
if streamer_type not in STREAMER_CLASSES:
|
| 125 |
raise ValueError(
|
|
|
|
| 128 |
)
|
| 129 |
|
| 130 |
StreamerClass = STREAMER_CLASSES[streamer_type]
|
| 131 |
+
streamer = StreamerClass(sr, stream_name, on_update=on_update)
|
| 132 |
|
| 133 |
transcript = streamer.stream_array(audio_array)
|
| 134 |
|