Download server.py from niobures/Fast-Hebrew-ASR: direct link, hf CLI and curl.
- Browser
- Download file 5.02 kB
-
https://huggingface.co/niobures/Fast-Hebrew-ASR/resolve/main/server.py
- Command line
-
hf download hf://niobures/Fast-Hebrew-ASR/server.py
-
curl -L -o server.py https://huggingface.co/niobures/Fast-Hebrew-ASR/resolve/main/server.py
5.02 kB
| """OpenAI-compatible transcription server for GigaAM-He. | |
| pip install fastapi uvicorn soundfile torch torchaudio numpy | |
| pip install git+https://github.com/salute-developers/GigaAM.git | |
| python server.py # :6002 | |
| curl -s http://localhost:6002/v1/audio/transcriptions \ | |
| -F file=@clip.wav | jq -r .text | |
| The shape of the response matches OpenAI's /v1/audio/transcriptions, so any | |
| client that speaks to Whisper's API speaks to this: point its base URL here. | |
| """ | |
| import io | |
| import logging | |
| import os | |
| import sys | |
| import time | |
| import numpy as np | |
| import soundfile as sf | |
| import torch | |
| import uvicorn | |
| from fastapi import FastAPI, File, Form, HTTPException, UploadFile | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from longform import SAMPLE_RATE, segment_audio # noqa: E402 | |
| CHECKPOINT = os.getenv("HE_CHECKPOINT", "gigaam-he-twostage.ckpt") | |
| PORT = int(os.getenv("HE_PORT", "6002")) | |
| DEVICE = os.getenv("HE_DEVICE", "cuda" if torch.cuda.is_available() else "cpu") | |
| MAX_LEN = float(os.getenv("HE_MAX_LEN", "18.0")) # the model is trained on <=20s | |
| BATCH = int(os.getenv("HE_BATCH", "8")) | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| app = FastAPI(title="GigaAM-He") | |
| _model = None | |
| def _load(): | |
| global _model | |
| import gigaam | |
| logging.info(f"loading {CHECKPOINT} on {DEVICE}") | |
| _model = gigaam.load_model(CHECKPOINT, device=DEVICE) | |
| logging.info("model ready") | |
| def _warmup(): | |
| """Touch every length bucket once so no caller pays algorithm selection. | |
| Inputs are bucketed to whole seconds, so there are only ~20 possible | |
| shapes. Compiling them all at startup removes a ~190 ms first-touch spike | |
| from the serving path -- otherwise the first request of each new length | |
| pays it, which is exactly the wrong place for it. | |
| """ | |
| t0 = time.time() | |
| for secs in range(1, int(MAX_LEN) + 2): | |
| buf = io.BytesIO() | |
| sf.write(buf, np.zeros(int(secs * SAMPLE_RATE), dtype="float32"), | |
| SAMPLE_RATE, format="WAV") | |
| _transcribe(buf.getvalue()) | |
| logging.info(f"warmup done ({int(MAX_LEN) + 1} buckets, {time.time() - t0:.1f}s)") | |
| def _transcribe(audio_bytes: bytes) -> str: | |
| import torchaudio.functional as taF | |
| x, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32") | |
| if x.ndim > 1: | |
| x = x.mean(axis=1) | |
| if sr != SAMPLE_RATE: | |
| # torchaudio's resampler, never np.interp: linear interpolation is not | |
| # an anti-aliasing filter and folds everything above 8 kHz back into | |
| # the band. | |
| x = taF.resample(torch.from_numpy(x), sr, SAMPLE_RATE).numpy() | |
| if len(x) < int(0.1 * SAMPLE_RATE): | |
| return "" | |
| spans = segment_audio(x, max_len_s=MAX_LEN) | |
| chunks = [torch.from_numpy(x[int(s * SAMPLE_RATE):int(e * SAMPLE_RATE)]) for s, e in spans] | |
| chunks = [c for c in chunks if len(c) >= int(0.1 * SAMPLE_RATE)] | |
| if not chunks: | |
| return "" | |
| out = [] | |
| with torch.inference_mode(): | |
| for i in range(0, len(chunks), BATCH): | |
| b = chunks[i:i + BATCH] | |
| lens = torch.tensor([len(c) for c in b], dtype=torch.long) | |
| # Round the padded width up to a whole second. Every distinct input | |
| # length is a distinct tensor shape and cuDNN re-runs algorithm | |
| # selection per shape -- measured, that costs 6x (p50 181 ms across | |
| # varied lengths vs 28 ms when one shape repeats). Bucketing | |
| # collapses thousands of widths to ~20. True lengths still go to the | |
| # model, so the padding changes nothing about the output. | |
| width = int(lens.max()) | |
| width = -(-width // SAMPLE_RATE) * SAMPLE_RATE | |
| pad = torch.zeros(len(b), width, dtype=torch.float32) | |
| for j, c in enumerate(b): | |
| pad[j, :len(c)] = c | |
| enc, enc_len = _model.forward( | |
| pad.to(_model._device).to(_model._dtype), lens.to(_model._device)) | |
| for text, _ in _model._decode(enc, enc_len, lens.to(_model._device), False): | |
| if text.strip(): | |
| out.append(text.strip()) | |
| return " ".join(out) | |
| async def transcriptions(file: UploadFile = File(...), | |
| model: str = Form(None), | |
| language: str = Form(None)): | |
| """`model` and `language` are accepted and ignored: this server hosts one | |
| Hebrew model, and rejecting the fields would break OpenAI clients that | |
| always send them.""" | |
| try: | |
| return {"text": _transcribe(await file.read())} | |
| except Exception as e: # noqa: BLE001 | |
| logging.exception("transcription failed") | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| def health(): | |
| return {"status": "ok", "model": os.path.basename(CHECKPOINT), "device": DEVICE} | |
| if __name__ == "__main__": | |
| _load() | |
| _warmup() | |
| uvicorn.run(app, host=os.getenv("HE_HOST", "0.0.0.0"), port=PORT) | |