Spaces:
Running
Running
File size: 4,017 Bytes
beff3ce 6ff26b5 beff3ce 6ff26b5 4cdd2cf ae8ded8 6ff26b5 ae8ded8 6ff26b5 ae8ded8 beff3ce ae8ded8 beff3ce 6ff26b5 ae8ded8 056f219 beff3ce ae8ded8 6ff26b5 ae8ded8 6ff26b5 ae8ded8 6ff26b5 ae8ded8 6ff26b5 ae8ded8 6ff26b5 ae8ded8 6ff26b5 ae8ded8 6ff26b5 | 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 | """Nawah-ASR-118M-v5 demo.
The browser does all audio handling: decodeAudioData accepts wav/mp3/m4a/webm, resamples to
16 kHz mono and posts raw float32 samples. So this server needs no ffmpeg, no soundfile, and no
format guessing -- it only ever sees the array the model wants.
The audio token count is per-clip, not fixed: `<audio>` is repeated exactly
`_get_feat_extract_output_lengths(...)` times, which is 50 per second in v5 (v1/v2 were 25). A 3-second clip costs 150
tokens rather than the 375 a fixed 30-second window would spend on silence.
Loading goes through load_nawah, NOT from_pretrained: this model's projector is an MLP and its
LM nests differently, so from_pretrained silently random-initialises ~a third of the tensors and
serves fluent nonsense. See load_model.py.
"""
import os, time
import numpy as np
import torch
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from load_v5 import load_v5
REPO = os.environ.get("ASR_MODEL", "oddadmix/Nawah-ASR-118M-v5")
TOKEN = os.environ.get("MODEL_HF_TOKEN") or os.environ.get("HF_TOKEN")
SR, MAX_SEC = 16000, 30
torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", 4)))
_env_seen = {k: (len(v) if v else 0) for k, v in os.environ.items()
if "TOKEN" in k.upper() or k.upper().startswith("HF")}
print(f"[*] token env vars visible: {_env_seen}", flush=True)
print(f"[*] loading {REPO} (token {'present' if TOKEN else 'MISSING'})", flush=True)
model, tok, fe = load_v5(REPO, token=TOKEN) # sets the 50 Hz rate and asserts it
CV = tok.convert_tokens_to_ids
AUDIO, A_START, A_END = CV("<audio>"), CV("<|audio_start|>"), CV("<|audio_end|>")
PARAMS = sum(p.numel() for p in model.parameters())
# Greedy always -- this IS temperature 0. transformers rejects temperature=0.0 (must be > 0) and
# ignores it when do_sample is False, so determinism is pinned on the config, not the call site.
gc = model.generation_config
gc.do_sample, gc.num_beams = False, 1
gc.temperature = gc.top_p = gc.top_k = None
print(f"[+] {PARAMS/1e6:.1f}M params ready", flush=True)
app = FastAPI()
class Req(BaseModel):
samples: list[float] = []
max_new: int = 96
@torch.no_grad()
def run(wav: np.ndarray, max_new: int):
wav = wav[: SR * MAX_SEC]
feats = fe([wav], sampling_rate=SR, return_attention_mask=True, return_tensors="pt")
_, n = model.model.audio_tower._get_feat_extract_output_lengths(feats.attention_mask.sum(-1))
n = int(n[0])
ids = torch.tensor([[tok.bos_token_id, A_START] + [AUDIO] * n + [A_END]])
t0 = time.perf_counter()
out = model.generate(input_ids=ids, attention_mask=torch.ones_like(ids),
input_features=feats.input_features,
feature_attention_mask=feats.attention_mask,
max_new_tokens=max_new, do_sample=False, num_beams=1,
pad_token_id=tok.pad_token_id, eos_token_id=tok.eos_token_id)
ms = (time.perf_counter() - t0) * 1000
text = tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip()
dur = len(wav) / SR
return {"text": text, "audio_tokens": n, "duration": round(dur, 2), "ms": round(ms),
"rtf": round(dur * 1000 / max(ms, 1), 1),
"new_tokens": int(out.shape[1] - ids.shape[1]), "model": REPO, "params": PARAMS}
@app.get("/")
def index():
return FileResponse("static/index.html")
@app.get("/api/ready")
def ready():
return {"ready": True, "model": REPO, "params": PARAMS, "max_sec": MAX_SEC}
@app.post("/api/asr")
def asr(req: Req):
if not req.samples:
return JSONResponse({"error": "no audio"}, status_code=400)
wav = np.asarray(req.samples, dtype=np.float32)
if wav.size < SR // 4:
return JSONResponse({"error": "clip too short"}, status_code=400)
return JSONResponse(run(wav, max(8, min(req.max_new, 200))))
app.mount("/static", StaticFiles(directory="static"), name="static")
|