File size: 4,827 Bytes
783c42e
 
 
 
 
 
b86a6d6
 
 
 
 
 
 
 
 
 
783c42e
b8d8ebe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783c42e
 
 
 
 
145c9e7
 
 
 
 
 
 
 
 
 
4d51cdd
145c9e7
4d51cdd
145c9e7
 
 
4d51cdd
145c9e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d51cdd
145c9e7
4d51cdd
 
783c42e
 
b8d8ebe
 
 
145c9e7
 
4d51cdd
 
 
 
 
783c42e
 
b8d8ebe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee038c5
b8d8ebe
 
783c42e
 
c12a0a7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""NepaliConformer demo — transcribe Nepali speech, tuned for real telephone audio."""
import os

import gradio as gr
import torch

# Preload at startup: the checkpoint is 485 MB — downloading it lazily made the first
# user's request hang for minutes and look broken.
from huggingface_hub import hf_hub_download
from nemo.collections.asr.models import EncDecHybridRNNTCTCBPEModel

_path = hf_hub_download("ampixa/nepali-conformer-offline", "nepali_conformer_offline.nemo")
torch.set_num_threads(2)
MODEL = EncDecHybridRNNTCTCBPEModel.restore_from(_path, map_location="cpu")
MODEL.eval()
print("model preloaded", flush=True)

DESCRIPTION = (
    "121M Conformer trained on ~1,655 h of conversational Nepali. "
    "**33.8% WER on real call-center audio** (NepTel benchmark) where Whisper-large-v3 "
    "zero-shot scores ~99%. Honest limitations and the full benchmark: "
    "[github.com/Ampixa/nepaliconformer](https://github.com/Ampixa/nepaliconformer). "
    "CPU demo — a 30 s clip takes roughly 10-20 s. Example clips are real call-center "
    "audio (CC-BY-4.0, © InfoBayAI)."
)

ARTICLE = (
    "Recording transcribes itself as soon as you press ⏹ stop. "
    "Mic blocked? [Open the demo full-screen](https://voidash-nepaliconformer.hf.space) · "
    "Model downloads and usage: "
    "[github.com/Ampixa/nepaliconformer](https://github.com/Ampixa/nepaliconformer#download--run)"
)


def get_model():
    return MODEL


def _prepare(path):
    """Normalize any input to mono 16 kHz PCM and return (path, duration_seconds).

    Browser microphone recordings arrive as stereo (and sometimes as webm/mp4 that
    libsndfile cannot open at all). The model takes a mono signal only: feeding it a
    two-channel file raises "Output shape expected = (batch, time)" inside NeMo.
    """
    import subprocess
    import tempfile

    import soundfile as sf

    try:
        info = sf.info(path)
        needs_convert = info.channels != 1 or info.samplerate != 16000
        duration = info.duration
    except Exception:
        needs_convert = True
        duration = None

    if not needs_convert:
        return path, duration

    out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
    proc = subprocess.run(
        ["ffmpeg", "-y", "-loglevel", "error", "-i", path,
         "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", out],
        capture_output=True, text=True,
    )
    if proc.returncode != 0:
        raise gr.Error(
            "Could not read that audio (ffmpeg: %s). Try a WAV or MP3 file."
            % (proc.stderr.strip().splitlines() or ["unknown error"])[-1]
        )
    return out, sf.info(out).duration


def transcribe(audio_path):
    if not audio_path:
        # Most often: the user pressed Transcribe while the mic was still recording,
        # so no file exists yet.
        return "Press ⏹ stop to finish the recording — it transcribes automatically."
    audio_path, duration = _prepare(audio_path)
    if duration is not None and duration > 60:
        return "Please keep clips under 60 seconds for this CPU demo."
    model = get_model()
    out = model.transcribe([audio_path], batch_size=1, verbose=False)[0]
    text = out.text if hasattr(out, "text") else str(out)
    return " ".join(t for t in text.split() if t != "<breath>") or "(no speech detected)"


example_files = [[f"examples/{f}"] for f in sorted(os.listdir("examples"))] \
    if os.path.isdir("examples") else []

with gr.Blocks(title="NepaliConformer — Nepali ASR for real telephone calls") as demo:
    gr.Markdown("# NepaliConformer — Nepali ASR for real telephone calls")
    gr.Markdown(DESCRIPTION)
    with gr.Row():
        with gr.Column():
            audio_in = gr.Audio(
                sources=["microphone", "upload"], type="filepath",
                label="Nepali speech (mic or file, ≤60 s)",
            )
            with gr.Row():
                clear_btn = gr.Button("Clear")
                submit_btn = gr.Button("Transcribe", variant="primary")
        with gr.Column():
            text_out = gr.Textbox(label="Transcript (Devanagari)", lines=6)

    # Transcribe as soon as the recording stops or a file lands: waiting for an explicit
    # Transcribe click made the demo look broken for anyone who never pressed stop.
    audio_in.stop_recording(transcribe, audio_in, text_out)
    audio_in.upload(transcribe, audio_in, text_out)
    submit_btn.click(transcribe, audio_in, text_out)
    clear_btn.click(lambda: (None, ""), None, [audio_in, text_out])

    if example_files:
        gr.Examples(
            examples=example_files, inputs=audio_in, outputs=text_out,
            fn=transcribe, cache_examples=False, run_on_click=True,
        )
    gr.Markdown(ARTICLE)

if __name__ == "__main__":
    demo.launch(ssr_mode=False, show_error=True)