Manar312 commited on
Commit
6dce2be
·
verified ·
1 Parent(s): 726d870

Upload 6 files

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ entire_model_vol_8_5_5_40.pth filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # System dependencies for librosa / audio processing
4
+ RUN apt-get update && apt-get install -y \
5
+ ffmpeg \
6
+ libsndfile1 \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+
11
+ # Install Python dependencies
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY app.py .
17
+
18
+ # HuggingFace Spaces runs on port 7860
19
+ EXPOSE 7860
20
+
21
+ # MODEL_PATH env var – override if you name your .pth differently
22
+ ENV MODEL_PATH=entire_model_vol_8_5_5_40.pth
23
+
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: RAFIQ Arabic ASR
3
+ emoji: 🎙️
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # RAFIQ – Egyptian Arabic Speech-to-Text API
11
+
12
+ Custom character-level Transformer trained on Egyptian Arabic audio.
13
+ Built as part of the **RAFIQ** autism-support platform – FCAI, Beni-Suef University 2026.
14
+
15
+ ## Usage
16
+
17
+ 1. Upload your model file as `entire_model.pth` to the Space files.
18
+ 2. Open `/docs` for the interactive Swagger UI.
19
+ 3. `POST /transcribe` with a WAV/MP3/OGG file (≤ 15 s).
20
+
21
+ ## Endpoints
22
+
23
+ | Method | Path | Description |
24
+ |--------|------|-------------|
25
+ | GET | `/` | API info |
26
+ | GET | `/health` | Model status + config |
27
+ | POST | `/transcribe` | Upload audio → Arabic text |
28
+
29
+ ## Model config
30
+ - Character-level vocab (Arabic + English) – 74 tokens
31
+ - Encoder: 2 layers, Decoder: 1 layer, d_model=512, 8 heads
32
+ - Input: 128-mel spectrogram, 15-second chunks
33
+ - Sample rate: 16 000 Hz
app.py ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ RAFIQ - Arabic Speech-to-Text API
4
+ Egyptian Arabic ASR using custom character-level Transformer
5
+ """
6
+
7
+ import os
8
+ import io
9
+ import math
10
+ import gc
11
+ import re
12
+ import string
13
+ import tempfile
14
+ from contextlib import asynccontextmanager
15
+ from typing import Optional
16
+
17
+ import numpy as np
18
+ import librosa
19
+ import noisereduce as nr
20
+ import torch
21
+ import torch.nn as nn
22
+ import torch.nn.functional as F
23
+ from num2words import num2words
24
+ from fastapi import FastAPI, File, UploadFile, HTTPException
25
+ from fastapi.responses import JSONResponse
26
+ from pydantic import BaseModel
27
+
28
+ # ─────────────────────────────────────────────
29
+ # Constants (must match training config)
30
+ # ─────────────────────────────────────────────
31
+ MAX_TEXT_LEN = 70
32
+ MAX_SEQ_LEN = 70
33
+ N_MELS = 128
34
+ SAMPLE_RATE = 16000
35
+ HOP_LENGTH = 512
36
+ N_FFT = 2048
37
+ CHUNK_LENGTH = 15
38
+ N_SAMPLES = SAMPLE_RATE * CHUNK_LENGTH
39
+ N_FRAMES = math.ceil(N_SAMPLES / HOP_LENGTH)
40
+ NEG_INFTY = -1e9
41
+
42
+ # ─────────────────────────────────────────────
43
+ # Vocabulary (identical to training)
44
+ # ─────────────────────────────────────────────
45
+ special_tokens = ['<PAD>', '<UNK>', '<SOS>', '<EOS>']
46
+ english_characters = list(string.ascii_lowercase + ' ')
47
+ arabic_characters = list("ابتثجحخدذرزسشصضطظعغفقكلمنهويئءىةؤ")
48
+ characters = english_characters + arabic_characters
49
+ vocab = special_tokens + characters
50
+ char2idx = {char: idx for idx, char in enumerate(vocab)}
51
+ idx2char = {idx: char for idx, char in enumerate(vocab)}
52
+ vocab_size = len(vocab)
53
+
54
+ # ─────────────────────────────────────────────
55
+ # Model architecture (copy from training script)
56
+ # ─────────────────────────────────────────────
57
+
58
+ class PositionalEncoding(nn.Module):
59
+ def __init__(self, d_model, max_len):
60
+ super().__init__()
61
+ self.max_len = max_len
62
+ self.d_model = d_model
63
+
64
+ def forward(self):
65
+ pos = torch.arange(self.max_len, dtype=torch.float).unsqueeze(1)
66
+ _i = torch.arange(self.d_model, dtype=torch.float).unsqueeze(0)
67
+ _i = 1 / torch.pow(torch.tensor(10000.0), (2 * (_i // 2)) / self.d_model)
68
+ angles = pos * _i
69
+ angles[:, 0::2] = torch.sin(angles[:, 0::2])
70
+ angles[:, 1::2] = torch.cos(angles[:, 1::2])
71
+ return angles[:self.max_len].unsqueeze(0)
72
+
73
+
74
+ def scaled_dot_product_attention(q, k, v, s_mask=None):
75
+ d_k = q.shape[-1]
76
+ scaled = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)
77
+ if s_mask is not None:
78
+ scaled = scaled.permute(1, 0, 2, 3) + s_mask
79
+ scaled = scaled.permute(1, 0, 2, 3)
80
+ attention = F.softmax(scaled, dim=-1)
81
+ return torch.matmul(attention, v), attention
82
+
83
+
84
+ class MultiHeadAttention(nn.Module):
85
+ def __init__(self, _input_dim, d_model, _num_heads=8):
86
+ super().__init__()
87
+ assert d_model % _num_heads == 0
88
+ self.num_heads = _num_heads
89
+ self.d_model = d_model
90
+ self.head_dim = d_model // _num_heads
91
+ self.qkv_layer = nn.Linear(_input_dim, 3 * d_model)
92
+ self.output_layer = nn.Linear(d_model, d_model)
93
+
94
+ def forward(self, mha_x, self_mask=None):
95
+ seq_len, batch_size = mha_x.shape[1], mha_x.shape[0]
96
+ qkv = self.qkv_layer(mha_x)
97
+ qkv = qkv.reshape(batch_size, seq_len, self.num_heads, 3 * self.head_dim).permute(0, 2, 1, 3)
98
+ q, k, v = qkv.chunk(3, dim=-1)
99
+ values, _ = scaled_dot_product_attention(q, k, v, self_mask)
100
+ values = values.permute(0, 2, 1, 3).reshape(batch_size, seq_len, self.head_dim * self.num_heads)
101
+ return self.output_layer(values)
102
+
103
+
104
+ class MultiHeadCrossAttention(nn.Module):
105
+ def __init__(self, d_model, _num_heads=8):
106
+ super().__init__()
107
+ assert d_model % _num_heads == 0
108
+ self.num_heads = _num_heads
109
+ self.head_dim = d_model // _num_heads
110
+ self.kv_layer = nn.Linear(d_model, 2 * d_model)
111
+ self.q_layer = nn.Linear(d_model, d_model)
112
+ self.output_layer = nn.Linear(d_model, d_model)
113
+
114
+ def forward(self, mhca_input, encoder_output, cross_mask=None):
115
+ dec_len, enc_len = mhca_input.shape[1], encoder_output.shape[1]
116
+ batch_size = mhca_input.shape[0]
117
+ kv = self.kv_layer(encoder_output).reshape(batch_size, enc_len, self.num_heads, 2 * self.head_dim).permute(0, 2, 1, 3)
118
+ q = self.q_layer(mhca_input).reshape(batch_size, dec_len, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
119
+ k, v = kv.chunk(2, dim=-1)
120
+ values, _ = scaled_dot_product_attention(q, k, v, cross_mask)
121
+ values = values.permute(0, 2, 1, 3).reshape(batch_size, dec_len, self.head_dim * self.num_heads)
122
+ return self.output_layer(values)
123
+
124
+
125
+ class LayerNorm(nn.Module):
126
+ def __init__(self, normalization_shape, eps=1e-6):
127
+ super().__init__()
128
+ self.eps = eps
129
+ self.gamma = nn.Parameter(torch.ones(normalization_shape))
130
+ self.beta = nn.Parameter(torch.zeros(normalization_shape))
131
+
132
+ def forward(self, layer):
133
+ dim = [-(i + 1) for i in range(len(self.gamma.shape))]
134
+ mean = layer.mean(dim=dim, keepdim=True)
135
+ std = (layer.var(dim=dim, keepdim=True) + self.eps).sqrt()
136
+ return self.gamma * (layer - mean) / std + self.beta
137
+
138
+
139
+ class PositionWiseFeedForward(nn.Module):
140
+ def __init__(self, d_model, hidden, dropout=0.1):
141
+ super().__init__()
142
+ self.linear1 = nn.Linear(d_model, hidden)
143
+ self.linear2 = nn.Linear(hidden, d_model)
144
+ self.gelu = nn.GELU()
145
+ self.dropout = nn.Dropout(p=dropout)
146
+
147
+ def forward(self, x):
148
+ return self.linear2(self.dropout(self.gelu(self.linear1(x))))
149
+
150
+
151
+ class EncoderLayer(nn.Module):
152
+ def __init__(self, d_model, num_heads, ffn_hidden, dropout):
153
+ super().__init__()
154
+ self.self_attn = MultiHeadAttention(d_model, d_model, num_heads)
155
+ self.layer_norm = LayerNorm([d_model])
156
+ self.dropout = nn.Dropout(dropout)
157
+ self.ffn = PositionWiseFeedForward(d_model, ffn_hidden, dropout)
158
+
159
+ def forward(self, x, mask=None):
160
+ res = x
161
+ x = self.layer_norm(self.dropout(self.self_attn(x, mask)) + res)
162
+ res = x
163
+ x = self.layer_norm(self.dropout(self.ffn(x)) + res)
164
+ return x
165
+
166
+
167
+ class SequentialEncoder(nn.Sequential):
168
+ def forward(self, *inputs):
169
+ x, mask = inputs
170
+ for module in self._modules.values():
171
+ x = module(x, mask)
172
+ return x
173
+
174
+
175
+ class Encoder(nn.Module):
176
+ def __init__(self, d_model, ffn_hidden, num_heads, dropout, num_layers):
177
+ super().__init__()
178
+ self.layers = SequentialEncoder(*[EncoderLayer(d_model, num_heads, ffn_hidden, dropout) for _ in range(num_layers)])
179
+
180
+ def forward(self, x, mask):
181
+ return self.layers(x, mask)
182
+
183
+
184
+ class DecoderLayer(nn.Module):
185
+ def __init__(self, d_model, num_heads, ffn_hidden=2048, dropout=0.1):
186
+ super().__init__()
187
+ self.self_attn = MultiHeadAttention(d_model, d_model, num_heads)
188
+ self.cross_attn = MultiHeadCrossAttention(d_model, num_heads)
189
+ self.layer_norm = LayerNorm([d_model])
190
+ self.dropout = nn.Dropout(dropout)
191
+ self.ffn = PositionWiseFeedForward(d_model, ffn_hidden, dropout)
192
+
193
+ def forward(self, x, enc_out, self_mask, cross_mask):
194
+ res = x; x = self.layer_norm(self.dropout(self.self_attn(x, self_mask)) + res)
195
+ res = x; x = self.layer_norm(self.dropout(self.cross_attn(x, enc_out, cross_mask)) + res)
196
+ res = x; x = self.layer_norm(self.dropout(self.ffn(x)) + res)
197
+ return x
198
+
199
+
200
+ class SequentialDecoder(nn.Sequential):
201
+ def forward(self, *inputs):
202
+ x, enc_out, self_mask, cross_mask = inputs
203
+ for module in self._modules.values():
204
+ x = module(x, enc_out, self_mask, cross_mask)
205
+ return x
206
+
207
+
208
+ class Decoder(nn.Module):
209
+ def __init__(self, d_model, ffn_hidden, num_heads, dropout, num_layers):
210
+ super().__init__()
211
+ self.layers = SequentialDecoder(*[DecoderLayer(d_model, num_heads, ffn_hidden, dropout) for _ in range(num_layers)])
212
+
213
+ def forward(self, x, enc_out, self_mask=None, cross_mask=None):
214
+ return self.layers(x, enc_out, self_mask, cross_mask)
215
+
216
+
217
+ class Transformer(nn.Module):
218
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob, num_encoder_layers, num_decoder_layers):
219
+ super().__init__()
220
+ self.encoder = Encoder(d_model, ffn_hidden, num_heads, drop_prob, num_encoder_layers)
221
+ self.decoder = Decoder(d_model, ffn_hidden, num_heads, drop_prob, num_decoder_layers)
222
+
223
+ def forward(self, src, tgt, enc_self_mask=None, dec_self_mask=None, dec_cross_mask=None):
224
+ src = self.encoder(src, enc_self_mask)
225
+ return self.decoder(tgt, src, dec_self_mask, dec_cross_mask)
226
+
227
+
228
+ def get_conv_Lout(L_in, conv):
229
+ return math.floor((L_in + 2 * conv.padding[0] - conv.dilation[0] * (conv.kernel_size[0] - 1) - 1) / conv.stride[0] + 1)
230
+
231
+
232
+ class MMS(nn.Module):
233
+ def __init__(self, vocab_size, d_model=512, nhead=8, num_encoder_layers=6,
234
+ num_decoder_layers=6, dim_feedforward=2048,
235
+ max_encoder_seq_len=100, max_decoder_seq_len=100,
236
+ n_mels=N_MELS, dropout=0.1):
237
+ super().__init__()
238
+ self.transformer = Transformer(d_model, dim_feedforward, nhead, dropout,
239
+ num_encoder_layers, num_decoder_layers)
240
+ self.en_positional_encoding = PositionalEncoding(d_model, max_encoder_seq_len)
241
+ self.de_positional_encoding = PositionalEncoding(d_model, max_decoder_seq_len)
242
+ self.conv1 = nn.Conv1d(n_mels, d_model, kernel_size=3, padding=1)
243
+ self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=3, stride=2, padding=1)
244
+ self.gelu = nn.GELU()
245
+ self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
246
+ self.d_model = d_model
247
+ self.ff = nn.Linear(d_model, vocab_size)
248
+
249
+ def get_encoder_seq_len(self, L_in):
250
+ return get_conv_Lout(get_conv_Lout(L_in, self.conv1), self.conv2)
251
+
252
+ def forward(self, audio, text, enc_self_mask, dec_self_mask, dec_cross_mask, device):
253
+ audio = self.gelu(self.conv1(audio))
254
+ audio = self.gelu(self.conv2(audio))
255
+ audio = audio.permute(0, 2, 1)
256
+ audio += self.en_positional_encoding().to(device)
257
+ text = self.embedding(text) + self.de_positional_encoding().to(device)
258
+ out = self.transformer(audio, text, enc_self_mask, dec_self_mask, dec_cross_mask)
259
+ return self.ff(out)
260
+
261
+
262
+ # ─────────────────────────────────────────────
263
+ # Helpers
264
+ # ─────────────────────────────────────────────
265
+
266
+ def pad_or_trim(array, length=N_SAMPLES, axis=-1, padding=True):
267
+ if array.shape[axis] > length:
268
+ array = array.take(indices=range(length), axis=axis)
269
+ if padding and array.shape[axis] < length:
270
+ pad_widths = [(0, 0)] * array.ndim
271
+ pad_widths[axis] = (0, length - array.shape[axis])
272
+ array = np.pad(array, pad_widths)
273
+ return array
274
+
275
+
276
+ def preprocess_audio(audio_bytes: bytes):
277
+ audio_data, _ = librosa.load(io.BytesIO(audio_bytes), sr=SAMPLE_RATE)
278
+ audio_data = nr.reduce_noise(y=audio_data, sr=SAMPLE_RATE)
279
+ duration = librosa.get_duration(y=audio_data, sr=SAMPLE_RATE)
280
+ orig_len = audio_data.shape[-1]
281
+ modified = pad_or_trim(audio_data, padding=False)
282
+ sgram = librosa.stft(y=modified, n_fft=N_FFT, hop_length=HOP_LENGTH)
283
+ sgram_mag, _ = librosa.magphase(sgram)
284
+ mel = librosa.feature.melspectrogram(S=sgram_mag, sr=SAMPLE_RATE,
285
+ n_fft=N_FFT, hop_length=HOP_LENGTH, n_mels=N_MELS)
286
+ mel_db = librosa.amplitude_to_db(mel, ref=np.min)
287
+ padded = np.pad(mel_db, ((0, 0), (0, N_FRAMES - mel_db.shape[-1])))
288
+ return padded, orig_len, duration
289
+
290
+
291
+ def nlp_preprocessing(sentence: str) -> str:
292
+ sentence = sentence.lower().replace("\n", " ")
293
+ sentence = re.sub(r'[إأآ]', 'ا', sentence)
294
+ sentence = re.sub(r'[^a-zA-Zء-ي\s\d]', '', sentence)
295
+ sentence = re.sub(r'[\u0617-\u061A\u064B-\u065F]', '', sentence)
296
+ sentence = re.sub(r'([a-zA-Z])([ء-ي])|([ء-ي])([a-zA-Z])', r'\1\3 \2\4', sentence)
297
+ sentence = re.sub(r'\s+', ' ', sentence)
298
+ sentence = re.sub(r'\d+', lambda x: num2words(int(x.group()), lang='ar'), sentence)
299
+ return sentence
300
+
301
+
302
+ def tokenize_text(text, max_len=MAX_SEQ_LEN, start_token=True, end_token=True):
303
+ tokens = [char2idx.get(c, char2idx['<UNK>']) for c in text]
304
+ max_len -= (start_token + end_token)
305
+ text_len = len(tokens)
306
+ if text_len < max_len:
307
+ if end_token:
308
+ tokens += [char2idx['<EOS>']]
309
+ tokens += [char2idx['<PAD>']] * (max_len - text_len)
310
+ else:
311
+ tokens = tokens[:max_len]
312
+ if end_token:
313
+ tokens += [char2idx['<EOS>']]
314
+ if start_token:
315
+ tokens.insert(0, char2idx['<SOS>'])
316
+ return tokens
317
+
318
+
319
+ def text_decoder(token_list):
320
+ out = ''
321
+ for token in token_list:
322
+ if isinstance(token, torch.Tensor):
323
+ token = token.item()
324
+ char = idx2char[token]
325
+ if char == '<EOS>':
326
+ return out
327
+ if char not in special_tokens:
328
+ out += char
329
+ return out
330
+
331
+
332
+ def generate_padding_masks(transcription, audio_original_len, conv_func, frames=N_FRAMES):
333
+ batch_size, seq_len = transcription.size()
334
+ audio_len = conv_func(frames)
335
+ look_ahead_mask = torch.triu(torch.full((seq_len, seq_len), True), diagonal=1)
336
+ enc_mask = torch.full([batch_size, audio_len, audio_len], False)
337
+ dec_self = torch.full([batch_size, seq_len, seq_len], False)
338
+ dec_cross = torch.full([batch_size, seq_len, audio_len], False)
339
+
340
+ for i in range(batch_size):
341
+ new_len = conv_func(audio_original_len[i])
342
+ enc_mask[i, new_len:, :] = True
343
+ enc_mask[i, :, new_len:] = True
344
+ dec_cross[i, :, new_len:] = True
345
+ zeros = np.where(transcription[0].cpu().numpy() == 0)[0]
346
+ if len(zeros) > 0:
347
+ idx = zeros[0]
348
+ dec_self[i, idx:, :] = True
349
+ dec_self[i, :, idx:] = True
350
+ dec_cross[i, idx:, :] = True
351
+
352
+ dec_self_mask = torch.where(look_ahead_mask + dec_self, NEG_INFTY, 0.0)
353
+ dec_cross_mask = torch.where(dec_cross, NEG_INFTY, 0.0)
354
+ enc_self_mask = torch.where(enc_mask, NEG_INFTY, 0.0)
355
+ return enc_self_mask, dec_self_mask, dec_cross_mask
356
+
357
+
358
+ def greedy_decode(audio_tensor, orig_len, model, device, max_len=MAX_TEXT_LEN):
359
+ model.eval()
360
+ transcription = ""
361
+ audio_tensor = audio_tensor.to(device)
362
+ with torch.no_grad():
363
+ for i in range(max_len):
364
+ tgt = torch.tensor(tokenize_text(transcription, max_len=MAX_TEXT_LEN, end_token=False),
365
+ dtype=torch.long).unsqueeze(0).to(device)
366
+ enc_mask, dec_self, dec_cross = generate_padding_masks(tgt, [orig_len], model.get_encoder_seq_len)
367
+ out = model(audio_tensor, tgt,
368
+ enc_mask.to(device), dec_self.to(device), dec_cross.to(device), device)
369
+ next_tok = torch.argmax(F.softmax(out, dim=-1)[0, i, :], dim=-1).unsqueeze(0)
370
+ transcription += text_decoder(next_tok)
371
+ if next_tok.item() == char2idx['<EOS>']:
372
+ break
373
+ return transcription
374
+
375
+
376
+ # ─────────────────────────────────────────────
377
+ # App startup / model loading
378
+ # ─────────────────────────────────────────────
379
+
380
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
381
+ model_state = {"model": None}
382
+
383
+ MODEL_PATH = os.getenv("MODEL_PATH", "entire_model.pth")
384
+
385
+
386
+ @asynccontextmanager
387
+ async def lifespan(app: FastAPI):
388
+ # Load model on startup
389
+ if os.path.exists(MODEL_PATH):
390
+ try:
391
+ m = torch.load(MODEL_PATH, map_location=device, weights_only=False)
392
+ m.to(device)
393
+ m.eval()
394
+ model_state["model"] = m
395
+ print(f"✅ Model loaded from {MODEL_PATH} on {device}")
396
+ except Exception as e:
397
+ print(f"⚠️ Could not load model: {e}")
398
+ else:
399
+ print(f"⚠️ Model file not found at {MODEL_PATH}. Upload your .pth file.")
400
+ yield
401
+ # Cleanup
402
+ del model_state["model"]
403
+ gc.collect()
404
+
405
+
406
+ # ─────────────────────────────────────────────
407
+ # FastAPI app
408
+ # ─────────────────────────────────────────────
409
+
410
+ app = FastAPI(
411
+ title="RAFIQ – Arabic Speech-to-Text API",
412
+ description=(
413
+ "**Egyptian Arabic ASR** powered by a custom character-level Transformer.\n\n"
414
+ "Upload an audio file (WAV / MP3 / OGG, ≤ 15 s) and receive the Arabic transcription.\n\n"
415
+ "Built for the **RAFIQ** autism-support platform – FCAI, Beni-Suef University 2026."
416
+ ),
417
+ version="1.0.0",
418
+ lifespan=lifespan,
419
+ )
420
+
421
+
422
+ # ── Response schemas ──────────────────────────
423
+
424
+ class TranscribeResponse(BaseModel):
425
+ transcription: str
426
+ duration_seconds: float
427
+ device: str
428
+ model_loaded: bool
429
+
430
+
431
+ class HealthResponse(BaseModel):
432
+ status: str
433
+ model_loaded: bool
434
+ device: str
435
+ vocab_size: int
436
+ sample_rate: int
437
+ max_audio_seconds: int
438
+
439
+
440
+ # ── Endpoints ────────────────────────────────
441
+
442
+ @app.get("/", tags=["Info"])
443
+ def root():
444
+ return {
445
+ "message": "RAFIQ Arabic ASR API is running 🎙️",
446
+ "docs": "/docs",
447
+ "health": "/health",
448
+ }
449
+
450
+
451
+ @app.get("/health", response_model=HealthResponse, tags=["Info"])
452
+ def health():
453
+ return HealthResponse(
454
+ status="ok",
455
+ model_loaded=model_state["model"] is not None,
456
+ device=str(device),
457
+ vocab_size=vocab_size,
458
+ sample_rate=SAMPLE_RATE,
459
+ max_audio_seconds=CHUNK_LENGTH,
460
+ )
461
+
462
+
463
+ @app.post(
464
+ "/transcribe",
465
+ response_model=TranscribeResponse,
466
+ tags=["ASR"],
467
+ summary="Transcribe Egyptian Arabic audio",
468
+ description=(
469
+ "Upload a WAV / MP3 / OGG audio file (max 15 seconds).\n"
470
+ "The API applies noise reduction, extracts mel-spectrograms, "
471
+ "and runs greedy decoding through the Arabic Transformer model."
472
+ ),
473
+ )
474
+ async def transcribe(
475
+ file: UploadFile = File(..., description="Audio file: WAV, MP3, or OGG. Max 15 seconds.")
476
+ ):
477
+ if model_state["model"] is None:
478
+ raise HTTPException(
479
+ status_code=503,
480
+ detail="Model not loaded. Make sure 'entire_model.pth' is present in the Space.",
481
+ )
482
+
483
+ allowed = {"audio/wav", "audio/x-wav", "audio/mpeg", "audio/mp3", "audio/ogg", "audio/flac"}
484
+ if file.content_type and file.content_type not in allowed:
485
+ raise HTTPException(
486
+ status_code=415,
487
+ detail=f"Unsupported file type: {file.content_type}. Use WAV, MP3, or OGG.",
488
+ )
489
+
490
+ audio_bytes = await file.read()
491
+ if len(audio_bytes) > 50 * 1024 * 1024: # 50 MB guard
492
+ raise HTTPException(status_code=413, detail="File too large (max 50 MB).")
493
+
494
+ try:
495
+ mel, orig_len, duration = preprocess_audio(audio_bytes)
496
+ except Exception as e:
497
+ raise HTTPException(status_code=422, detail=f"Audio preprocessing failed: {e}")
498
+
499
+ audio_tensor = torch.tensor(mel, dtype=torch.float32).unsqueeze(0) # (1, N_MELS, N_FRAMES)
500
+
501
+ try:
502
+ transcription = greedy_decode(audio_tensor, orig_len, model_state["model"], device)
503
+ except Exception as e:
504
+ raise HTTPException(status_code=500, detail=f"Inference failed: {e}")
505
+
506
+ return TranscribeResponse(
507
+ transcription=transcription,
508
+ duration_seconds=round(duration, 2),
509
+ device=str(device),
510
+ model_loaded=True,
511
+ )
entire_model_vol_8_5_5_40.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3affa392f825c9e6947d5eba3e407c68499814a60c2b6863bdd4130ac0df7b35
3
+ size 46244771
hf_space_rafiq_asr.zip ADDED
Binary file (7.42 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ torch
5
+ torchaudio
6
+ librosa
7
+ noisereduce
8
+ num2words
9
+ numpy
10
+ pydub