Elormiden commited on
Commit
f1687db
·
1 Parent(s): 52fbe7a
Files changed (5) hide show
  1. README.md +4 -3
  2. app.py +73 -0
  3. apt.txt +3 -0
  4. kenlm_asr_pipeline.py +130 -0
  5. requirements.txt +21 -0
README.md CHANGED
@@ -1,12 +1,13 @@
1
  ---
2
  title: Wav2vec2 Cypriot ASR
3
- emoji: 🐨
4
- colorFrom: pink
5
- colorTo: green
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