1.0
Browse files- README.md +4 -3
- app.py +73 -0
- apt.txt +3 -0
- kenlm_asr_pipeline.py +130 -0
- requirements.txt +21 -0
README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
---
|
| 2 |
title: Wav2vec2 Cypriot ASR
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.45.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
title: Wav2vec2 Cypriot ASR
|
| 3 |
+
emoji: 🐠
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 5.45.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
import os
|
| 5 |
+
import numpy as np
|
| 6 |
+
import librosa
|
| 7 |
+
import gradio as gr
|
| 8 |
+
from transformers import pipeline
|
| 9 |
+
import kenlm_asr_pipeline
|
| 10 |
+
|
| 11 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 12 |
+
|
| 13 |
+
MODEL_ID = "Elormiden/wav2vec2-cypriot-dialect"
|
| 14 |
+
KENLM_FILE = "cypriot.klm"
|
| 15 |
+
|
| 16 |
+
ASR = pipeline(
|
| 17 |
+
"automatic-speech-recognition-kenlm",
|
| 18 |
+
model=MODEL_ID,
|
| 19 |
+
kenlm_filename=KENLM_FILE,
|
| 20 |
+
alpha=0.4,
|
| 21 |
+
beta=0.9,
|
| 22 |
+
token=HF_TOKEN,
|
| 23 |
+
device=0,
|
| 24 |
+
model_id_or_path=MODEL_ID,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
def transcribe(audio: tuple[int, np.ndarray] | None):
|
| 28 |
+
def wrap(msg: str) -> str:
|
| 29 |
+
msg = (msg or "").strip()
|
| 30 |
+
return f"Output: {msg if msg else '(empty)'}"
|
| 31 |
+
|
| 32 |
+
if audio is None:
|
| 33 |
+
return wrap("No audio.")
|
| 34 |
+
sr, data = audio
|
| 35 |
+
|
| 36 |
+
if isinstance(data, np.ndarray) and data.ndim == 2:
|
| 37 |
+
data = data.mean(axis=1)
|
| 38 |
+
|
| 39 |
+
target_sr = 16000
|
| 40 |
+
if sr != target_sr:
|
| 41 |
+
data = librosa.resample(data.astype(np.float32), orig_sr=sr, target_sr=target_sr)
|
| 42 |
+
sr = target_sr
|
| 43 |
+
|
| 44 |
+
if data.size == 0 or not np.isfinite(data).all():
|
| 45 |
+
return wrap("No valid audio.")
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
out = ASR(data, sampling_rate=sr)
|
| 49 |
+
if isinstance(out, dict):
|
| 50 |
+
text = out.get("text", "")
|
| 51 |
+
elif isinstance(out, list) and out and isinstance(out[0], dict):
|
| 52 |
+
text = out[0].get("text", "")
|
| 53 |
+
elif isinstance(out, str):
|
| 54 |
+
text = out
|
| 55 |
+
else:
|
| 56 |
+
text = str(out)
|
| 57 |
+
|
| 58 |
+
return wrap(text)
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return wrap(f"Error: {e}")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
with gr.Blocks(title="KenLM Wav2Vec2 ASR") as demo:
|
| 64 |
+
gr.Markdown("# KenLM Wav2Vec2 ASR\nUpload or record audio; decoding uses KenLM for better accuracy.")
|
| 65 |
+
audio = gr.Audio(sources=["microphone", "upload"], type="numpy", label="Audio (16kHz preferred)")
|
| 66 |
+
btn = gr.Button("Transcribe")
|
| 67 |
+
txt = gr.Textbox(label="Transcription")
|
| 68 |
+
|
| 69 |
+
btn.click(fn=transcribe, inputs=audio, outputs=txt)
|
| 70 |
+
audio.change(fn=transcribe, inputs=audio, outputs=txt)
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
demo.launch()
|
apt.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
build-essential
|
| 2 |
+
cmake
|
| 3 |
+
libeigen3-dev
|
kenlm_asr_pipeline.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# kenlm_asr_pipeline.py
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
from huggingface_hub import hf_hub_download
|
| 12 |
+
from pyctcdecode import build_ctcdecoder
|
| 13 |
+
from transformers import (
|
| 14 |
+
Pipeline,
|
| 15 |
+
Wav2Vec2ForCTC,
|
| 16 |
+
Wav2Vec2Processor,
|
| 17 |
+
)
|
| 18 |
+
from transformers.pipelines import PIPELINE_REGISTRY
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class KenLMWav2Vec2Pipeline(Pipeline):
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
model: Wav2Vec2ForCTC,
|
| 25 |
+
tokenizer=None,
|
| 26 |
+
feature_extractor=None,
|
| 27 |
+
*,
|
| 28 |
+
alpha: float = 0.4,
|
| 29 |
+
beta: float = 0.9,
|
| 30 |
+
kenlm_filename: str = "cypriot.klm",
|
| 31 |
+
suppress_token_ids: Optional[Sequence[int]] = None,
|
| 32 |
+
quiet: bool = True,
|
| 33 |
+
token: Optional[str] = None,
|
| 34 |
+
model_id_or_path: Optional[str] = None, # for .klm
|
| 35 |
+
**kwargs,
|
| 36 |
+
) -> None:
|
| 37 |
+
if quiet:
|
| 38 |
+
import warnings, logging
|
| 39 |
+
warnings.filterwarnings("ignore")
|
| 40 |
+
logging.getLogger("pyctcdecode").setLevel(logging.CRITICAL)
|
| 41 |
+
logging.getLogger("pyctcdecode.decoder").setLevel(logging.CRITICAL)
|
| 42 |
+
|
| 43 |
+
# HF Pipeline contract
|
| 44 |
+
super().__init__(
|
| 45 |
+
model=model,
|
| 46 |
+
tokenizer=tokenizer,
|
| 47 |
+
feature_extractor=feature_extractor,
|
| 48 |
+
**kwargs,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Собираем processor из имеющихся частей
|
| 52 |
+
if self.tokenizer is None or self.feature_extractor is None:
|
| 53 |
+
raise ValueError("Tokenizer and feature_extractor are required.")
|
| 54 |
+
self.processor = Wav2Vec2Processor(
|
| 55 |
+
feature_extractor=self.feature_extractor, tokenizer=self.tokenizer
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# bottleneck
|
| 59 |
+
vocab = self.processor.tokenizer.get_vocab()
|
| 60 |
+
kenlm_vocab_list = []
|
| 61 |
+
for tok, idx in sorted(vocab.items(), key=lambda x: x[1]):
|
| 62 |
+
if idx not in [55, 56]: # excluding <s>, </s> tokens
|
| 63 |
+
kenlm_vocab_list.append(tok)
|
| 64 |
+
|
| 65 |
+
if os.path.isfile(kenlm_filename):
|
| 66 |
+
kenlm_path = kenlm_filename
|
| 67 |
+
elif model_id_or_path and os.path.isdir(model_id_or_path) and os.path.isfile(os.path.join(model_id_or_path, kenlm_filename)):
|
| 68 |
+
kenlm_path = os.path.join(model_id_or_path, kenlm_filename)
|
| 69 |
+
elif model_id_or_path and "/" in model_id_or_path:
|
| 70 |
+
kenlm_path = hf_hub_download(model_id_or_path, kenlm_filename, token=token)
|
| 71 |
+
else:
|
| 72 |
+
name_or_path = getattr(self.model.config, "_name_or_path", None)
|
| 73 |
+
if name_or_path and os.path.isdir(name_or_path) and os.path.isfile(os.path.join(name_or_path, kenlm_filename)):
|
| 74 |
+
kenlm_path = os.path.join(name_or_path, kenlm_filename)
|
| 75 |
+
elif name_or_path and "/" in name_or_path:
|
| 76 |
+
kenlm_path = hf_hub_download(name_or_path, kenlm_filename, token=token)
|
| 77 |
+
else:
|
| 78 |
+
raise FileNotFoundError(f"KenLM file '{kenlm_filename}' not found")
|
| 79 |
+
|
| 80 |
+
self.ctc_decoder = build_ctcdecoder(labels=kenlm_vocab_list, kenlm_model_path=kenlm_path, alpha=alpha, beta=beta)
|
| 81 |
+
self.suppress_token_ids: Sequence[int] = tuple(suppress_token_ids or ())
|
| 82 |
+
self.model.eval()
|
| 83 |
+
|
| 84 |
+
# hugging face api stuff
|
| 85 |
+
def _sanitize_parameters(self, **kwargs):
|
| 86 |
+
pre, fwd, post = {}, {}, {}
|
| 87 |
+
if "sampling_rate" in kwargs:
|
| 88 |
+
pre["sampling_rate"] = kwargs["sampling_rate"]
|
| 89 |
+
if "return_timestamps" in kwargs:
|
| 90 |
+
post["return_timestamps"] = kwargs["return_timestamps"]
|
| 91 |
+
return pre, fwd, post
|
| 92 |
+
|
| 93 |
+
def preprocess(
|
| 94 |
+
self,
|
| 95 |
+
inputs: Union[np.ndarray, List[np.ndarray], Dict[str, Any], List[Dict[str, Any]]],
|
| 96 |
+
sampling_rate: int = 16000,
|
| 97 |
+
) -> Dict[str, torch.Tensor]:
|
| 98 |
+
if isinstance(inputs, dict) and "array" in inputs:
|
| 99 |
+
arrays = [np.asarray(inputs["array"])]
|
| 100 |
+
elif isinstance(inputs, list):
|
| 101 |
+
if len(inputs) == 0:
|
| 102 |
+
raise ValueError("Empty input list.")
|
| 103 |
+
arrays = [np.asarray(x["array"] if isinstance(x, dict) and "array" in x else x) for x in inputs]
|
| 104 |
+
else:
|
| 105 |
+
arrays = [np.asarray(inputs)]
|
| 106 |
+
|
| 107 |
+
proc = self.processor(arrays, sampling_rate=sampling_rate, return_tensors="pt", padding=True)
|
| 108 |
+
return {k: (v.to(self.device) if torch.is_tensor(v) else v) for k, v in proc.items()}
|
| 109 |
+
|
| 110 |
+
@torch.inference_mode()
|
| 111 |
+
def _forward(self, model_inputs: Dict[str, torch.Tensor]):
|
| 112 |
+
outputs = self.model(**model_inputs)
|
| 113 |
+
logits = outputs.logits # (B, T, V)
|
| 114 |
+
for tid in self.suppress_token_ids:
|
| 115 |
+
logits[..., tid] = -float("inf")
|
| 116 |
+
log_probs = F.log_softmax(logits, dim=-1)
|
| 117 |
+
return {"log_probs": log_probs}
|
| 118 |
+
|
| 119 |
+
def postprocess(self, model_outputs, return_timestamps: bool = False):
|
| 120 |
+
log_probs = model_outputs["log_probs"].detach().cpu().numpy() # (B, T, V)
|
| 121 |
+
texts = [{"text": self.ctc_decoder.decode(lp)} for lp in log_probs]
|
| 122 |
+
return texts[0] if len(texts) == 1 else texts
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
PIPELINE_REGISTRY.register_pipeline(
|
| 126 |
+
task="automatic-speech-recognition-kenlm",
|
| 127 |
+
pipeline_class=KenLMWav2Vec2Pipeline,
|
| 128 |
+
pt_model=Wav2Vec2ForCTC,
|
| 129 |
+
default={"pt": (Wav2Vec2ForCTC, Wav2Vec2Processor)},
|
| 130 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
|
| 3 |
+
transformers[torch]>=4.40.0
|
| 4 |
+
torch # можно убрать, если берёшь предустановленный образ с PyTorch
|
| 5 |
+
huggingface_hub>=0.23.0
|
| 6 |
+
|
| 7 |
+
pyctcdecode>=0.5.0
|
| 8 |
+
kenlm>=0.2.0
|
| 9 |
+
|
| 10 |
+
librosa>=0.10.1
|
| 11 |
+
|
| 12 |
+
tf-keras
|
| 13 |
+
|
| 14 |
+
datasets==3.6.0
|
| 15 |
+
transformers
|
| 16 |
+
|
| 17 |
+
evaluate
|
| 18 |
+
librosa
|
| 19 |
+
jiwer
|
| 20 |
+
numpy==1.26.4
|
| 21 |
+
pyctcdecode
|