Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- README.md +16 -7
- app.py +158 -0
- requirements.txt +9 -0
README.md
CHANGED
|
@@ -1,15 +1,24 @@
|
|
| 1 |
---
|
| 2 |
title: Phonetic Reduction
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.14.0
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
-
license:
|
| 12 |
-
short_description:
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Phonetic Reduction
|
| 3 |
+
emoji: 🎙️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.14.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: Per-word phonetic reduction in Czech speech.
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Phonetic Reduction Analyzer (Czech)
|
| 15 |
+
|
| 16 |
+
Per-word reduction scores via **STT → per-word TTS canonical reference → duration ratio + spectral DTW**.
|
| 17 |
+
|
| 18 |
+
- Skóre `< 1` = slovo redukováno oproti průměru nahrávky
|
| 19 |
+
- Skóre `> 1` = slovo plně artikulováno
|
| 20 |
+
- Červené sloupce = nízká STT confidence (možný přeslech / halucinace)
|
| 21 |
+
|
| 22 |
+
Pipeline: faster-whisper (large-v3) → silero-vad → piper-tts (cs_CZ-jirka-medium) → MFCC + DTW.
|
| 23 |
+
|
| 24 |
+
Free CPU tier: ~30–60 s per ~10 s recording.
|
app.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio web UI for the phonetic-reduction analyzer.
|
| 2 |
+
|
| 3 |
+
Run with:
|
| 4 |
+
.venv/bin/python app.py
|
| 5 |
+
Then open the URL Gradio prints (defaults to http://127.0.0.1:7860).
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import tempfile
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import matplotlib
|
| 14 |
+
|
| 15 |
+
matplotlib.use("Agg")
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
import pandas as pd
|
| 18 |
+
|
| 19 |
+
from reduction import pipeline, stt, tts
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# --- model cache ---------------------------------------------------------
|
| 23 |
+
_models: dict[str, object] = {}
|
| 24 |
+
_voice = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _get_model(size: str):
|
| 28 |
+
if size not in _models:
|
| 29 |
+
_models[size] = stt.load_model(size=size)
|
| 30 |
+
return _models[size]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _get_voice():
|
| 34 |
+
global _voice
|
| 35 |
+
if _voice is None:
|
| 36 |
+
_voice = tts.load_voice()
|
| 37 |
+
return _voice
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# --- plot ----------------------------------------------------------------
|
| 41 |
+
def _make_plot(df: pd.DataFrame) -> plt.Figure:
|
| 42 |
+
n = len(df)
|
| 43 |
+
fig, ax = plt.subplots(figsize=(max(8, n * 0.55), 4.5))
|
| 44 |
+
x = list(range(n))
|
| 45 |
+
labels = df["word"].tolist()
|
| 46 |
+
|
| 47 |
+
# Colour bars by STT confidence: low-confidence → red, normal → steelblue
|
| 48 |
+
probs = df["stt_probability"].fillna(1.0).tolist()
|
| 49 |
+
bar_colors = ["#cc3333" if p < 0.6 else "#4a90c4" for p in probs]
|
| 50 |
+
ax.bar(x, df["combined_score"], color=bar_colors, alpha=0.55, label="combined")
|
| 51 |
+
ax.plot(x, df["duration_score"], "o-", color="#2c7a2c", lw=1.4, label="duration", alpha=0.85)
|
| 52 |
+
ax.plot(x, df["spectral_score"], "s-", color="#d97726", lw=1.4, label="spectral", alpha=0.85)
|
| 53 |
+
ax.axhline(1.0, color="gray", linestyle="--", linewidth=0.8)
|
| 54 |
+
|
| 55 |
+
ax.set_xticks(x)
|
| 56 |
+
ax.set_xticklabels(labels, rotation=40, ha="right", fontsize=10)
|
| 57 |
+
ax.set_ylabel("skóre (1 = průměr, <1 redukováno)")
|
| 58 |
+
ax.set_xlabel("token")
|
| 59 |
+
ax.set_title("Per-word reduction scores")
|
| 60 |
+
ax.legend(loc="upper right", framealpha=0.9)
|
| 61 |
+
ax.grid(True, axis="y", alpha=0.25)
|
| 62 |
+
fig.tight_layout()
|
| 63 |
+
return fig
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# --- main analyze callback ----------------------------------------------
|
| 67 |
+
def analyze_audio(audio_path: str | None, whisper_size: str, progress=gr.Progress()):
|
| 68 |
+
if not audio_path:
|
| 69 |
+
return "❌ Nahraj nebo nahrej zvuk.", None, None, None
|
| 70 |
+
|
| 71 |
+
progress(0.1, desc=f"Načítám Whisper {whisper_size}…")
|
| 72 |
+
model = _get_model(whisper_size)
|
| 73 |
+
progress(0.3, desc="Načítám Piper hlas…")
|
| 74 |
+
voice = _get_voice()
|
| 75 |
+
|
| 76 |
+
progress(0.5, desc="Analyzuji…")
|
| 77 |
+
rows, whisper_text, _ = pipeline.analyze(
|
| 78 |
+
audio_path, model, voice, language="cs", groundtruth=None, trim=True
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
progress(0.9, desc="Renderuji graf a CSV…")
|
| 82 |
+
df = pd.DataFrame(rows)
|
| 83 |
+
|
| 84 |
+
# Pretty display dataframe (round numerics, drop internal cols)
|
| 85 |
+
display_cols = [
|
| 86 |
+
"word",
|
| 87 |
+
"stt_probability",
|
| 88 |
+
"orig_duration",
|
| 89 |
+
"tts_duration",
|
| 90 |
+
"duration_ratio",
|
| 91 |
+
"spectral_distance",
|
| 92 |
+
"duration_score",
|
| 93 |
+
"spectral_score",
|
| 94 |
+
"combined_score",
|
| 95 |
+
]
|
| 96 |
+
display_df = df[display_cols].copy()
|
| 97 |
+
for c in display_cols[1:]:
|
| 98 |
+
display_df[c] = display_df[c].round(3)
|
| 99 |
+
|
| 100 |
+
fig = _make_plot(df)
|
| 101 |
+
|
| 102 |
+
# CSV download (full row, not abbreviated)
|
| 103 |
+
csv_path = os.path.join(tempfile.gettempdir(), "reduction_result.csv")
|
| 104 |
+
df.to_csv(csv_path, index=False)
|
| 105 |
+
|
| 106 |
+
return whisper_text or "(žádný transkript)", display_df, fig, csv_path
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# --- UI -----------------------------------------------------------------
|
| 110 |
+
HEADER_MD = """
|
| 111 |
+
# Měření fonetické redukce — prototyp
|
| 112 |
+
Per-word reduction scores via STT → per-word TTS canonical → duration + spectral DTW.
|
| 113 |
+
Skóre **< 1** = redukováno oproti průměru nahrávky, **> 1** = plně artikulováno.
|
| 114 |
+
Červené sloupce = nízká STT confidence (pravděpodobně přeslech / halucinace).
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
with gr.Blocks(title="Phonetic reduction analyzer") as demo:
|
| 118 |
+
gr.Markdown(HEADER_MD)
|
| 119 |
+
with gr.Row():
|
| 120 |
+
with gr.Column(scale=2):
|
| 121 |
+
audio_in = gr.Audio(
|
| 122 |
+
sources=["upload", "microphone"],
|
| 123 |
+
type="filepath",
|
| 124 |
+
label="Nahrávka (drag & drop wav, nebo klikni na mikrofon a namluv)",
|
| 125 |
+
)
|
| 126 |
+
with gr.Column(scale=1):
|
| 127 |
+
whisper_dd = gr.Dropdown(
|
| 128 |
+
choices=["small", "medium", "large-v3"],
|
| 129 |
+
value="large-v3",
|
| 130 |
+
label="Whisper model",
|
| 131 |
+
info="large-v3 = nejlepší kvalita, ~6s/věta na CPU",
|
| 132 |
+
)
|
| 133 |
+
run_btn = gr.Button("Analyzuj", variant="primary", size="lg")
|
| 134 |
+
|
| 135 |
+
transcript_out = gr.Textbox(label="Whisper transkript", interactive=False, lines=2)
|
| 136 |
+
plot_out = gr.Plot(label="Skóre redukce")
|
| 137 |
+
table_out = gr.Dataframe(label="Per-word detaily", interactive=False)
|
| 138 |
+
csv_out = gr.File(label="Stáhnout CSV", interactive=False)
|
| 139 |
+
|
| 140 |
+
run_btn.click(
|
| 141 |
+
analyze_audio,
|
| 142 |
+
inputs=[audio_in, whisper_dd],
|
| 143 |
+
outputs=[transcript_out, table_out, plot_out, csv_out],
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
if __name__ == "__main__":
|
| 148 |
+
# On HuggingFace Spaces, $SPACE_ID is set; on local dev it isn't.
|
| 149 |
+
on_hf = bool(os.environ.get("SPACE_ID"))
|
| 150 |
+
if on_hf:
|
| 151 |
+
# HF provides PORT and expects 0.0.0.0; let HF's launcher own model preloading
|
| 152 |
+
demo.launch(server_name="0.0.0.0")
|
| 153 |
+
else:
|
| 154 |
+
print("[startup] preloading large-v3 + Piper voice (one-time, ~10s)…")
|
| 155 |
+
_get_model("large-v3")
|
| 156 |
+
_get_voice()
|
| 157 |
+
print("[startup] ready, launching Gradio…")
|
| 158 |
+
demo.launch(server_name="127.0.0.1", server_port=7860, inbrowser=False)
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
faster-whisper>=1.2
|
| 2 |
+
piper-tts
|
| 3 |
+
librosa>=0.11
|
| 4 |
+
soundfile
|
| 5 |
+
numpy
|
| 6 |
+
silero-vad
|
| 7 |
+
gradio>=6.0
|
| 8 |
+
matplotlib
|
| 9 |
+
pandas
|