LingkodAI Audio LID Head

Identifies whether a spoken clip is Cebuano, Filipino, or English, so the LingkodAI pipeline can route it to the appropriate Whisper STT model.

How it works

A small classifier head on top of the frozen openai/whisper-medium encoder. The encoder's output is averaged into 50 chunks (0.6 s each), and the head uses attention pooling plus a 3-layer MLP to predict the language.

Classes: 0 = ceb, 1 = fil, 2 = eng

Files

  • deeper_50chunks.pt: weights for the classifier head only. The whisper-medium encoder is loaded separately.

Usage

import numpy as np, torch, torch.nn as nn, librosa
from transformers import WhisperFeatureExtractor, WhisperModel
from huggingface_hub import hf_hub_download

LABELS = ("ceb", "fil", "eng")
device = "cuda" if torch.cuda.is_available() else "cpu"

def build_head():
    return nn.ModuleDict({
        "attn_fc1": nn.Linear(1024, 128),
        "attn_fc2": nn.Linear(128, 1),
        "clf_fc1":  nn.Linear(2048, 256),
        "clf_fc2":  nn.Linear(256, 128),
        "clf_fc3":  nn.Linear(128, 3),
    })

def head_forward(head, chunks, num_valid):
    scores = head["attn_fc2"](torch.tanh(head["attn_fc1"](chunks))).squeeze(-1)
    idx = torch.arange(chunks.shape[1], device=chunks.device).unsqueeze(0)
    scores = scores.masked_fill(idx >= num_valid.unsqueeze(1), float("-inf"))
    w = torch.softmax(scores, dim=1).unsqueeze(-1)
    mean = (chunks * w).sum(1)
    std = torch.sqrt((((chunks - mean.unsqueeze(1)) ** 2) * w).sum(1).clamp(min=1e-8))
    h = torch.relu(head["clf_fc1"](torch.cat([mean, std], 1)))
    h = torch.relu(head["clf_fc2"](h))
    return head["clf_fc3"](h)

# Load the frozen encoder (fp32) and the head
fe = WhisperFeatureExtractor.from_pretrained("openai/whisper-medium")
encoder = WhisperModel.from_pretrained("openai/whisper-medium", torch_dtype=torch.float32).encoder.to(device).eval()
head = build_head()
path = hf_hub_download("lingkodai/lingkodai-audio-lid-head", "deeper_50chunks.pt")
head.load_state_dict(torch.load(path, map_location=device, weights_only=True))
head = head.to(device).eval()

# Predict on a 16 kHz mono clip
audio, _ = librosa.load("clip.wav", sr=16000)
feats = fe(audio, sampling_rate=16000, return_tensors="pt").input_features.to(device)
with torch.no_grad():
    hidden = encoder(feats).last_hidden_state
    chunks = hidden.view(1, 50, 30, 1024).mean(dim=2)         # 50 chunks of 30 frames
    real_frames = min(int(np.round(len(audio) / 16000 * 50)), 1500)
    num_valid = torch.tensor([int(np.clip(np.ceil(real_frames / 30), 1, 50))], device=device)
    probs = torch.softmax(head_forward(head, chunks, num_valid), dim=1)[0]

print(LABELS[int(probs.argmax())], probs.tolist())

The chunking (50 ร— 30 frames), valid-chunk masking, and fp32 encoder must match these settings, since the head was trained under them.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support

Model tree for beabucayan/lingkodai-audio-lid-head

Finetuned
(969)
this model