multimodalart's picture
multimodalart HF Staff
Upload app.py with huggingface_hub
3bd5d19 verified
Raw History Blame Contribute Delete
6.64 kB
import os
os.environ.setdefault("NUMBA_DISABLE_CUDA", "1")
import spaces # noqa: E402 (must precede torch)
import json
import subprocess
import sys
import sysconfig
import time
from pathlib import Path
import gradio as gr
import numpy as np
import torch
from huggingface_hub import snapshot_download
MODEL_ID = "LocalDoc/speakwrite-azerbaijani"
HERE = Path(__file__).resolve().parent
BALBAL = HERE / "balbal" # balbal @ 1be9f88b77c63fbafa7713c6e46e79847f6c390e (Apache-2.0), refiner.json from the model repo
NATIVE = BALBAL / "inference" / "native"
MAX_SECONDS = 300
CHUNK_SECONDS = 30.0
# ---- build balbal's native C++/pybind11 KenLM decoders once (flashlight-text 0.0.7 ABI) ----
if not (NATIVE / ("ctc_fast_decoder" + sysconfig.get_config_var("EXT_SUFFIX"))).exists():
print("Building balbal native decoders ...", flush=True)
subprocess.run([sys.executable, str(BALBAL / "inference" / "build_native.py")], check=True,
env=dict(os.environ, CXXFLAGS="-O3"))
sys.path[:0] = [str(BALBAL / "inference"), str(BALBAL)]
from balbal import models as M # noqa: E402
from balbal.audio import read_audio # noqa: E402
from decoder import KenLMDecoder # noqa: E402
# ---- weights ----
model_dir = Path(snapshot_download(MODEL_ID))
settings = dict(M.decoder_config("balbal-v2-manas"), **json.loads((model_dir / "decoder.json").read_text()))
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
net = M.build_model("balbal-v2-manas", model_dir / "tokenizer", model_dir / "model.pt", device="cpu")
net = net.to(DEVICE).eval()
torch.set_float32_matmul_precision("highest")
lm_decoder = KenLMDecoder(model_dir / "lm", settings, fast=True)
sp = lm_decoder.sp
VOCAB = lm_decoder.vocab_size
print(f"Model + KenLM loaded (device={DEVICE}).", flush=True)
def _forward(wave: np.ndarray) -> np.ndarray:
"""Mono float32 16 kHz waveform (<= 30 s) -> [frames, V + 1] natural-log CTC probabilities (blank last)."""
wave = np.ascontiguousarray(wave, dtype=np.float32)
if len(wave) < 4000: # the conv subsampler needs >= 0.25 s
wave = np.pad(wave, (0, 4000 - len(wave)))
with torch.inference_mode():
lp, n, _ = net(input_signal=torch.from_numpy(wave)[None].to(DEVICE),
input_signal_length=torch.tensor([len(wave)], device=DEVICE))
return lp[0, : int(n[0])].float().cpu().numpy()
def _greedy(lp: np.ndarray) -> str:
ids = lp.argmax(axis=1)
out, prev = [], -1
for t in ids:
t = int(t)
if t != prev and t < VOCAB:
out.append(t)
prev = t
return sp.decode(out)
@spaces.GPU(duration=20)
def transcribe(audio_path: str, use_lm: bool = True, hotwords: str = ""):
"""Transcribe Azerbaijani speech to text with speakwrite-azerbaijani (FastConformer-CTC + 6-gram KenLM).
Args:
audio_path: path to an audio file (any format ffmpeg can read); resampled to 16 kHz mono. Max 5 minutes.
use_lm: decode with the 6-gram token KenLM beam search (lower WER). If False, greedy CTC argmax.
hotwords: optional comma-separated names/words expected in the audio; they get a small bonus during LM decoding.
Returns:
The transcript (cased, punctuated, numbers as words), word timestamps, and timing info.
"""
if not audio_path:
raise gr.Error("Please upload or record an audio clip.")
wave = read_audio(audio_path)
duration = len(wave) / 16000
if duration > MAX_SECONDS:
raise gr.Error(f"Audio is {duration:.0f} s long; please keep it under {MAX_SECONDS // 60} minutes.")
hot = [w.strip() for w in (hotwords or "").split(",") if w.strip()] or None
t0 = time.perf_counter()
step = int(CHUNK_SECONDS * 16000)
texts, words = [], []
for i in range(0, len(wave), step):
lp = _forward(wave[i : i + step])
if use_lm:
out = lm_decoder.decode_words(lp, hot, i / 16000)
words += [[w["word"], round(w["start"], 2), round(w["end"], 2)] for w in out["words"]]
text = out["text"]
else:
text = _greedy(lp)
if text:
texts.append(text)
elapsed = time.perf_counter() - t0
info = (f"Audio: {duration:.1f} s · inference: {elapsed:.2f} s "
f"({duration / elapsed if elapsed else 0:.0f}× real time) · decoder: {'KenLM beam search' if use_lm else 'greedy CTC'}")
return " ".join(texts), words, info
CSS = """
#col-container { max-width: 1000px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks() as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(
"# 🎙️ speakwrite-azerbaijani\n"
"Azerbaijani speech recognition: FastConformer-CTC with a 3-pass loop refiner, product-key acoustic memory "
"and a 6-gram token KenLM. Output is cased and punctuated, with numbers written as words. \n"
"[Model card](https://huggingface.co/LocalDoc/speakwrite-azerbaijani) · "
"[balbal](https://github.com/erayyap/balbal) · "
"*speakwrite-azerbaijani by LocalDoc* — CC BY-NC 4.0 (non-commercial use only)."
)
with gr.Row():
with gr.Column():
audio = gr.Audio(sources=["upload", "microphone"], type="filepath", label="Azerbaijani speech")
run = gr.Button("Transcribe", variant="primary")
with gr.Accordion("Advanced settings", open=False):
use_lm = gr.Checkbox(value=True, label="Use KenLM beam search (uncheck for greedy CTC)")
hotwords = gr.Textbox(value="", label="Hotwords (comma-separated names/words to favour; LM only)",
placeholder="e.g. Bakı, Nizami")
with gr.Column():
text = gr.Textbox(label="Transcript", lines=6, buttons=["copy"])
info = gr.Markdown()
words = gr.Dataframe(headers=["word", "start (s)", "end (s)"], label="Word timestamps", interactive=False)
run.click(transcribe, inputs=[audio, use_lm, hotwords], outputs=[text, words, info])
gr.Examples(
examples=[["examples/suni_zeka.wav"], ["examples/honkonq.wav"],
["examples/medeni_sok.wav"], ["examples/quslar.wav"]],
inputs=[audio],
outputs=[text, words, info],
fn=transcribe,
cache_examples=True,
cache_mode="lazy",
label="Examples (FLEURS Azerbaijani test set, CC BY 4.0)",
)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)