Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# RunPod Serverless worker: Speech-to-Text (faster-whisper).
|
||||
# Lightweight — no torch/NeMo; ctranslate2 uses the CUDA + cuDNN in the base image.
|
||||
# Paths are relative to the REPO ROOT (RunPod's build context).
|
||||
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip ffmpeg && \
|
||||
ln -sf /usr/bin/python3 /usr/bin/python && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY workers/voice-to-text/requirements.txt .
|
||||
RUN pip3 install --no-cache-dir -r requirements.txt
|
||||
COPY workers/voice-to-text/handler.py .
|
||||
|
||||
CMD ["python3", "-u", "handler.py"]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Speech-to-Text worker — Whisper (RunPod Serverless)
|
||||
|
||||
Multilingual voice-to-text via **faster-whisper** — lets a worker **speak**
|
||||
instead of typing annotation text, in both the image and video editors. 99
|
||||
languages incl. Hindi/regional + English. Chosen over Parakeet/NeMo because it
|
||||
has no heavy dependencies (no NeMo, no torch), so it builds cleanly.
|
||||
|
||||
## Contract
|
||||
|
||||
Request → `POST /runsync`:
|
||||
```json
|
||||
{ "input": { "audio": "<base64 audio>", "language": "en", "timestamps": false } }
|
||||
```
|
||||
- `audio`: base64 wav/mp3/m4a (any sample rate — resampled internally)
|
||||
- `language`: optional (e.g. `"hi"`, `"en"`); omit to auto-detect
|
||||
- `timestamps`: optional — per-segment start/end
|
||||
|
||||
Response → `{ "output": { "transcript": "...", "language": "en" } }`
|
||||
(with `segments: [{text, start_sec, end_sec}]` if `timestamps: true`), or `{ "output": { "error": "..." } }`.
|
||||
|
||||
## Deploy (new endpoint — one per model)
|
||||
|
||||
1. **RunPod → Serverless → New Endpoint → Deploy from a GitHub repository** → `Sumit-Pluto/photo_gallery`.
|
||||
2. **Dockerfile path:** `/workers/voice-to-text/Dockerfile`
|
||||
3. Config:
|
||||
- **GPU:** light — **16 GB (T4 / A4000)** is plenty (`large-v3` ≈ 5 GB VRAM).
|
||||
- **Network volume:** attach your weights volume (caches the model).
|
||||
- **Workers:** Max 1, Active 0, FlashBoot on. **Execution timeout** ~300s (first-run download).
|
||||
4. **Warm up once** (downloads the model, ~1.5 GB):
|
||||
```bash
|
||||
curl -X POST https://api.runpod.ai/v2/<NEW_ID>/runsync \
|
||||
-H "Content-Type: application/json" -H "Authorization: Bearer <API_KEY>" \
|
||||
-d '{"input":{"audio":"<base64-wav>","language":"en"}}'
|
||||
```
|
||||
5. In **Vercel**, set `RUNPOD_STT_URL` = `https://api.runpod.ai/v2/<NEW_ID>/runsync` (server-only).
|
||||
|
||||
## Tuning (env vars)
|
||||
|
||||
| Var | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `WHISPER_MODEL` | `large-v3` | accuracy vs speed — `medium` / `small` are faster/lighter |
|
||||
| `WHISPER_COMPUTE` | `float16` | `int8_float16` uses less VRAM |
|
||||
|
||||
## App integration (next step, not yet built)
|
||||
|
||||
Mic-record button in the editor's text/annotation tool → `/api/ai/transcribe`
|
||||
(proxies here) → inserts the transcript. Client `rpTranscribe()` already exists in
|
||||
`apps/web/src/lib/runpod/endpoints.ts`.
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
RunPod Serverless worker: Speech-to-Text (Whisper via faster-whisper).
|
||||
|
||||
Chosen over Parakeet/NeMo because it has NO heavy dependencies (no NeMo, no
|
||||
torch — just ctranslate2), so it builds cleanly, AND it's multilingual (99
|
||||
languages incl. Hindi/regional + English) — a better fit for a mixed crew.
|
||||
|
||||
Lets a worker speak instead of typing annotation text (image + video editors).
|
||||
|
||||
Matches the app's transcribe contract:
|
||||
{"input": {
|
||||
"audio": "<base64 audio>", # required (wav/mp3/m4a; any sample rate — resampled internally)
|
||||
"language": "en", # optional (e.g. "hi", "en"); omit = auto-detect
|
||||
"timestamps": false # optional — return per-segment start/end
|
||||
}}
|
||||
Returns: {"transcript": "...", "language": "en", "segments"?: [{text,start_sec,end_sec}]}
|
||||
(or {"error": "..."}).
|
||||
|
||||
Tuning (env): WHISPER_MODEL (default large-v3; "medium"/"small" are faster),
|
||||
WHISPER_COMPUTE (default float16; "int8_float16" for less VRAM).
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
_VOLUME = "/runpod-volume"
|
||||
if os.path.isdir(_VOLUME):
|
||||
os.environ.setdefault("HF_HOME", os.path.join(_VOLUME, "huggingface"))
|
||||
|
||||
import runpod # noqa: E402
|
||||
from faster_whisper import WhisperModel # noqa: E402
|
||||
|
||||
MODEL_SIZE = os.environ.get("WHISPER_MODEL", "large-v3")
|
||||
DEVICE = os.environ.get("WHISPER_DEVICE", "cuda")
|
||||
COMPUTE = os.environ.get("WHISPER_COMPUTE", "float16")
|
||||
|
||||
_model = None
|
||||
|
||||
|
||||
def _get_model():
|
||||
global _model
|
||||
if _model is None:
|
||||
_model = WhisperModel(
|
||||
MODEL_SIZE,
|
||||
device=DEVICE,
|
||||
compute_type=COMPUTE,
|
||||
download_root=os.environ.get("HF_HOME"),
|
||||
)
|
||||
return _model
|
||||
|
||||
|
||||
def handler(event):
|
||||
inp = (event or {}).get("input") or {}
|
||||
audio_b64 = inp.get("audio")
|
||||
if not audio_b64:
|
||||
return {"error": "Missing 'audio' (base64)."}
|
||||
|
||||
language = inp.get("language") or None
|
||||
want_ts = bool(inp.get("timestamps", False))
|
||||
|
||||
path = None
|
||||
try:
|
||||
data = audio_b64.split(",", 1)[1] if "," in audio_b64 else audio_b64
|
||||
with tempfile.NamedTemporaryFile(suffix=".audio", delete=False) as f:
|
||||
f.write(base64.b64decode(data))
|
||||
path = f.name
|
||||
|
||||
segments, info = _get_model().transcribe(path, language=language, vad_filter=True)
|
||||
|
||||
parts = []
|
||||
segs = []
|
||||
for s in segments:
|
||||
parts.append(s.text)
|
||||
if want_ts:
|
||||
segs.append(
|
||||
{"text": s.text.strip(), "start_sec": round(s.start, 2), "end_sec": round(s.end, 2)}
|
||||
)
|
||||
|
||||
out = {"transcript": "".join(parts).strip(), "language": info.language}
|
||||
if want_ts:
|
||||
out["segments"] = segs
|
||||
return out
|
||||
except Exception as exc:
|
||||
return {"error": f"{type(exc).__name__}: {exc}"}
|
||||
finally:
|
||||
if path and os.path.exists(path):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
runpod.serverless.start({"handler": handler})
|
||||
@@ -0,0 +1,2 @@
|
||||
runpod==1.7.9
|
||||
faster-whisper==1.0.3
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user