Spaces:
Running on Zero
Running on Zero
Download app.py from LocalDoc/speakwrite-azerbaijani: direct link, hf CLI and curl.
- Browser
- Download file 6.64 kB
-
https://huggingface.co/spaces/LocalDoc/speakwrite-azerbaijani/resolve/main/app.py
- Command line
-
hf download hf://spaces/LocalDoc/speakwrite-azerbaijani/app.py
-
curl -L -o app.py https://huggingface.co/spaces/LocalDoc/speakwrite-azerbaijani/resolve/main/app.py
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) | |
| 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) | |