OppaAI commited on
Commit
56f8550
·
1 Parent(s): fee7b1d

refactor: decouple ASR processing from HTTP endpoint and update Modal environment to CUDA 12.3 registry image

Browse files
Files changed (1) hide show
  1. backend/asr.py +27 -72
backend/asr.py CHANGED
@@ -6,27 +6,21 @@ import tempfile
6
  from pathlib import Path
7
 
8
  import modal
 
 
9
 
10
- # ---------------------------------------------------------------------------
11
- # Constants
12
- # ---------------------------------------------------------------------------
13
  MINUTES = 60
14
  MODEL_NAME = "large-v3-turbo"
15
  COMPUTE_TYPE = "float16"
16
- ASR_PORT = 8000
17
  MODELS_DIR = Path("/models")
18
 
19
- # ---------------------------------------------------------------------------
20
- # Volume — persists downloaded model weights across deploys
21
- # ---------------------------------------------------------------------------
22
  volume = modal.Volume.from_name("aiko-asr-models", create_if_missing=True)
23
 
24
- # ---------------------------------------------------------------------------
25
- # Image — debian slim + CUDA cublas runtime + Python deps
26
- # ---------------------------------------------------------------------------
27
  image = (
28
- modal.Image.debian_slim(python_version="3.12")
29
- .apt_install("libcublas12")
 
 
30
  .pip_install(
31
  "faster-whisper",
32
  "fastapi",
@@ -35,15 +29,10 @@ image = (
35
  )
36
  )
37
 
38
- # ---------------------------------------------------------------------------
39
- # App
40
- # ---------------------------------------------------------------------------
41
  app = modal.App("aiko-asr", image=image)
 
42
 
43
 
44
- # ---------------------------------------------------------------------------
45
- # ASR Server class
46
- # ---------------------------------------------------------------------------
47
  @app.cls(
48
  gpu="T4",
49
  timeout=10 * MINUTES,
@@ -56,9 +45,7 @@ class ASRServer:
56
 
57
  @modal.enter()
58
  def startup(self):
59
- """Load the Whisper model once — reused across all requests."""
60
  from faster_whisper import WhisperModel
61
-
62
  print(f"Loading faster-whisper {MODEL_NAME} ...")
63
  self.model = WhisperModel(
64
  MODEL_NAME,
@@ -67,23 +54,14 @@ class ASRServer:
67
  download_root=str(MODELS_DIR),
68
  )
69
  print("Model ready.")
70
- volume.commit() # persist downloaded weights
71
-
72
- @modal.web_endpoint(method="GET")
73
- def health(self):
74
- return {"status": "ok", "model": MODEL_NAME}
75
-
76
- @modal.web_endpoint(method="POST")
77
- async def transcribe(self, audio: "UploadFile"): # type: ignore[name-defined]
78
- from fastapi import File, UploadFile
79
- from fastapi.responses import JSONResponse
80
-
81
- suffix = Path(audio.filename or "audio.wav").suffix or ".wav"
82
 
 
 
 
83
  with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
84
- tmp.write(await audio.read())
85
  tmp_path = tmp.name
86
-
87
  try:
88
  segments, info = self.model.transcribe(
89
  tmp_path,
@@ -92,53 +70,30 @@ class ASRServer:
92
  condition_on_previous_text=False,
93
  )
94
  text = " ".join(s.text.strip() for s in segments).strip()
95
- return JSONResponse({
96
  "text": text,
97
  "language": info.language,
98
  "language_probability": round(info.language_probability, 3),
99
- })
100
  finally:
101
  os.unlink(tmp_path)
102
 
103
 
104
- # ---------------------------------------------------------------------------
105
- # Local test entrypoint: modal run asr.py [path/to/audio.wav]
106
- # ---------------------------------------------------------------------------
107
- @app.local_entrypoint()
108
- def main():
109
- import sys
110
- import wave
111
- import struct
112
- import math
113
- import httpx
114
-
115
- test_audio = sys.argv[1] if len(sys.argv) > 1 else None
116
 
117
- # Generate a 1-second 440 Hz sine-wave WAV if no file provided
118
- if test_audio is None:
119
- test_audio = "/tmp/asr_test_tone.wav"
120
- with wave.open(test_audio, "w") as wf:
121
- wf.setnchannels(1)
122
- wf.setsampwidth(2)
123
- wf.setframerate(16000)
124
- frames = [
125
- struct.pack("<h", int(32767 * math.sin(2 * math.pi * 440 * i / 16000)))
126
- for i in range(16000)
127
- ]
128
- wf.writeframes(b"".join(frames))
129
- print(f"No audio file given — generated test tone at {test_audio}")
130
-
131
- print(f"Testing with {test_audio} ...")
132
 
 
 
 
 
133
  server = ASRServer()
134
- url = server.transcribe.web_url
 
135
 
136
- with open(test_audio, "rb") as f:
137
- resp = httpx.post(
138
- url,
139
- files={"audio": (Path(test_audio).name, f, "audio/wav")},
140
- timeout=60,
141
- )
142
 
143
- resp.raise_for_status()
144
- print("✓", resp.json())
 
 
 
6
  from pathlib import Path
7
 
8
  import modal
9
+ from fastapi import FastAPI, File, UploadFile
10
+ from fastapi.responses import JSONResponse
11
 
 
 
 
12
  MINUTES = 60
13
  MODEL_NAME = "large-v3-turbo"
14
  COMPUTE_TYPE = "float16"
 
15
  MODELS_DIR = Path("/models")
16
 
 
 
 
17
  volume = modal.Volume.from_name("aiko-asr-models", create_if_missing=True)
18
 
 
 
 
19
  image = (
20
+ modal.Image.from_registry(
21
+ "nvidia/cuda:12.3.2-runtime-ubuntu22.04",
22
+ add_python="3.12",
23
+ )
24
  .pip_install(
25
  "faster-whisper",
26
  "fastapi",
 
29
  )
30
  )
31
 
 
 
 
32
  app = modal.App("aiko-asr", image=image)
33
+ web_app = FastAPI()
34
 
35
 
 
 
 
36
  @app.cls(
37
  gpu="T4",
38
  timeout=10 * MINUTES,
 
45
 
46
  @modal.enter()
47
  def startup(self):
 
48
  from faster_whisper import WhisperModel
 
49
  print(f"Loading faster-whisper {MODEL_NAME} ...")
50
  self.model = WhisperModel(
51
  MODEL_NAME,
 
54
  download_root=str(MODELS_DIR),
55
  )
56
  print("Model ready.")
57
+ volume.commit()
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ @modal.method()
60
+ def _transcribe(self, audio_bytes: bytes, suffix: str) -> dict:
61
+ import tempfile
62
  with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
63
+ tmp.write(audio_bytes)
64
  tmp_path = tmp.name
 
65
  try:
66
  segments, info = self.model.transcribe(
67
  tmp_path,
 
70
  condition_on_previous_text=False,
71
  )
72
  text = " ".join(s.text.strip() for s in segments).strip()
73
+ return {
74
  "text": text,
75
  "language": info.language,
76
  "language_probability": round(info.language_probability, 3),
77
+ }
78
  finally:
79
  os.unlink(tmp_path)
80
 
81
 
82
+ @web_app.get("/health")
83
+ def health():
84
+ return {"status": "ok", "model": MODEL_NAME}
 
 
 
 
 
 
 
 
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ @web_app.post("/transcribe")
88
+ async def transcribe(audio: UploadFile = File(...)):
89
+ suffix = Path(audio.filename or "audio.wav").suffix or ".wav"
90
+ audio_bytes = await audio.read()
91
  server = ASRServer()
92
+ result = server._transcribe.remote(audio_bytes, suffix)
93
+ return JSONResponse(result)
94
 
 
 
 
 
 
 
95
 
96
+ @app.function()
97
+ @modal.asgi_app()
98
+ def fastapi_app():
99
+ return web_app