ubaliiringim's picture
Upload app.py with huggingface_hub
45aad5d verified
Raw
History Blame Contribute Delete
6.16 kB
import spaces # Must precede torch: ZeroGPU installs its CUDA hooks here.
import math
import tempfile
import time
from pathlib import Path
import gradio as gr
import numpy as np
import torch
from scipy.signal import resample_poly
from transformers import WhisperForConditionalGeneration, WhisperProcessor
MODEL_ID = "BuzzASR/hausa"
MAX_SECONDS = 30
SAMPLE_RATE = 16000
processor = WhisperProcessor.from_pretrained(MODEL_ID)
model = WhisperForConditionalGeneration.from_pretrained(
MODEL_ID, dtype=torch.float16, attn_implementation="sdpa",
).to("cuda").eval()
def prepare_audio(audio: tuple[int, np.ndarray] | None) -> tuple[np.ndarray, float]:
"""Validate a short recording and resample it to mono 16 kHz."""
if audio is None:
raise gr.Error("Record your voice or upload an audio file first.")
sample_rate, raw = audio
samples = np.asarray(raw)
if sample_rate <= 0 or samples.ndim not in (1, 2) or samples.size == 0:
raise gr.Error("This recording is empty or invalid. Please record again.")
duration = samples.shape[0] / sample_rate
if duration > MAX_SECONDS:
raise gr.Error("Please use a clip of 30 seconds or less. Longer recordings are not truncated.")
if np.issubdtype(samples.dtype, np.integer):
limits = np.iinfo(samples.dtype)
if limits.min == 0:
midpoint = (limits.max + 1) / 2
samples = (samples.astype(np.float32) - midpoint) / midpoint
else:
samples = samples.astype(np.float32) / max(abs(limits.min), limits.max)
else:
samples = samples.astype(np.float32)
if samples.ndim == 2:
samples = samples.mean(axis=1)
if not np.isfinite(samples).all():
raise gr.Error("The recording contains invalid audio samples.")
if sample_rate != SAMPLE_RATE:
divisor = math.gcd(sample_rate, SAMPLE_RATE)
samples = resample_poly(samples, SAMPLE_RATE // divisor, sample_rate // divisor)
return np.ascontiguousarray(samples, dtype=np.float32), duration
@spaces.GPU(duration=60)
def transcribe(audio: tuple[int, np.ndarray] | None) -> tuple[str, str | None, str]:
"""Transcribe a microphone recording or uploaded Hausa clip of up to 30 seconds.
Returns the unedited transcript, a UTF-8 download, and processing statistics.
"""
samples, duration = prepare_audio(audio)
if float(np.sqrt(np.mean(samples ** 2))) < 1e-5:
return "", None, "No audible speech detected. Check your microphone and try again."
started = time.perf_counter()
inputs = processor(
samples, sampling_rate=SAMPLE_RATE, return_tensors="pt", return_attention_mask=True,
)
with torch.inference_mode():
ids = model.generate(
inputs.input_features.to(device="cuda", dtype=torch.float16),
attention_mask=inputs.attention_mask.to("cuda"),
num_beams=1, no_repeat_ngram_size=3, repetition_penalty=1.2,
)
transcript = processor.batch_decode(ids, skip_special_tokens=True)[0].strip()
elapsed = time.perf_counter() - started
if not transcript:
return "", None, "No transcript returned. Try a clearer recording."
# Gradio copies this output into its managed cache before serving it.
Path(demo.GRADIO_CACHE).mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", suffix=".txt", dir=demo.GRADIO_CACHE, delete=False) as output:
output.write(transcript + "\n")
transcript_path = output.name
stats = f"**Audio:** {duration:.1f}s · **Processing:** {elapsed:.1f}s · **Words:** {len(transcript.split())}"
return transcript, transcript_path, stats
CSS = """
.gradio-container {max-width: 1060px !important; margin: auto;}
#hero {padding: 24px 0 12px;}
#hero h1 {font-size: 2.6rem; letter-spacing: -0.04em; margin-bottom: 8px;}
#hero p {font-size: 1.05rem; max-width: 720px;}
#transcript textarea {font-size: 1.15rem; line-height: 1.8;}
"""
with gr.Blocks(title="Hausa Voice Lab", delete_cache=(300, 3600)) as demo:
gr.Markdown(
"# Hausa Voice Lab\n"
"### Ka yi magana. Ka ga rubutun Hausa.\n"
"Record a short Hausa clip and see what BuzzASR hears. "
"Listen back, compare names and spelling, and keep the transcript.",
elem_id="hero",
)
with gr.Row():
with gr.Column(scale=1):
audio = gr.Audio(
sources=["microphone", "upload"], type="numpy", label="Your recording",
)
gr.Markdown("**1.** Record or upload up to 30 seconds.\n\n**2.** Finish recording, then transcribe.")
run = gr.Button("Transcribe Hausa", variant="primary", size="lg")
with gr.Column(scale=1):
transcript = gr.Textbox(
label="Hausa transcript", placeholder="Your words will appear here…",
lines=8, interactive=False, buttons=["copy"], elem_id="transcript",
)
stats = gr.Markdown("Ready when you are.")
download = gr.File(label="Download transcript (.txt)", interactive=False)
gr.ClearButton([audio, transcript, download, stats], value="Clear recording and transcript")
with gr.Accordion("Try a short accuracy check", open=False):
gr.Markdown(
"Say a sentence containing your name and hometown, then check every word. "
"For example: **Suna na Auwal. Na zo ne daga garin Ringim.**\n\n"
"This is a suggested phrase to record, not a prerecorded or verified model result."
)
gr.Markdown(
"Powered by [BuzzASR/hausa](https://huggingface.co/BuzzASR/hausa). "
"Hausa-focused; English and names may be inaccurate. No automatic name corrections.\n\n"
"Audio is uploaded to this Space and temporarily cached for processing. "
"Free GPU access has queues and usage limits."
)
run.click(transcribe, inputs=audio, outputs=[transcript, download, stats], api_name="transcribe", concurrency_limit=1)
demo.queue(max_size=12).launch(
theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="blue"),
css=CSS, mcp_server=True, max_file_size="20mb",
)