Manar312 commited on
Commit
3393b91
·
verified ·
1 Parent(s): 74aa0c0

Upload 6 files

Browse files
Files changed (6) hide show
  1. .gitignore +9 -0
  2. Dockerfile +28 -0
  3. README.md +34 -5
  4. app (1).py +448 -0
  5. app.py +448 -0
  6. requirements.txt +11 -0
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.pth
4
+ *.pt
5
+ *.egg-info/
6
+ .env
7
+ .venv/
8
+ dist/
9
+ build/
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Base image ──────────────────────────────────────────────────────────────
2
+ FROM python:3.10-slim
3
+
4
+ # ── System deps (librosa needs libsndfile) ───────────────────────────────────
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ libsndfile1 \
7
+ ffmpeg \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # ── Working directory ────────────────────────────────────────────────────────
11
+ WORKDIR /app
12
+
13
+ # ── Python deps (cached layer) ───────────────────────────────────────────────
14
+ COPY requirements.txt .
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # ── Copy application code ────────────────────────────────────────────────────
18
+ COPY app.py .
19
+
20
+ # ── Copy model weights (add your .pth file here before building) ─────────────
21
+ # If you push the model file to the Space repo, Docker will pick it up here.
22
+ # COPY entire_model.pth .
23
+
24
+ # ── HF Spaces listens on port 7860 ──────────────────────────────────────────
25
+ EXPOSE 7860
26
+
27
+ # ── Launch ───────────────────────────────────────────────────────────────────
28
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,39 @@
1
  ---
2
- title: TTS
3
- emoji: 🐢
4
- colorFrom: gray
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Arabic Speech-to-Text
3
+ emoji: 🎙️
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # 🎙️ Arabic Speech-to-Text (Egyptian Dialect)
12
+
13
+ A custom Transformer-based ASR model trained on Egyptian Arabic speech.
14
+
15
+ ## API Endpoints
16
+
17
+ | Method | Path | Description |
18
+ |--------|------|-------------|
19
+ | GET | `/` | Health check |
20
+ | GET | `/vocab` | View vocabulary |
21
+ | POST | `/transcribe` | Upload audio → get transcription |
22
+
23
+ Interactive docs: **`/docs`** (Swagger UI)
24
+ Alternative docs: **`/redoc`**
25
+
26
+ ## How to use
27
+
28
+ 1. Open `/docs` in your browser.
29
+ 2. Click **POST /transcribe → Try it out**.
30
+ 3. Upload a WAV/MP3 file.
31
+ 4. Click **Execute** and read the transcription.
32
+
33
+ ## Model details
34
+
35
+ - Architecture: Custom Encoder-Decoder Transformer
36
+ - Encoder input: Mel-Spectrogram (128 mels, 15-sec chunks)
37
+ - Decoder: Character-level greedy decoding
38
+ - Language: Egyptian Arabic
39
+ - Vocabulary size: ~75 characters (Arabic + English + special tokens)
app (1).py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import gc
4
+ import re
5
+ import string
6
+ import tempfile
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ import librosa
13
+ import noisereduce as nr
14
+ from num2words import num2words
15
+
16
+ from fastapi import FastAPI, File, UploadFile, HTTPException
17
+ from fastapi.responses import JSONResponse
18
+ import uvicorn
19
+
20
+ # ─────────────────────────────────────────────
21
+ # Constants
22
+ # ─────────────────────────────────────────────
23
+ N_MELS = 128
24
+ SAMPLE_RATE = 16000
25
+ HOP_LENGTH = 512
26
+ N_FFT = 2048
27
+ CHUNK_LENGTH = 15
28
+ N_SAMPLES = SAMPLE_RATE * CHUNK_LENGTH
29
+ N_FRAMES = math.ceil(N_SAMPLES / HOP_LENGTH)
30
+ MAX_TEXT_LEN = 70
31
+ MAX_SEQ_LEN = 70
32
+ NEG_INFTY = -1e9
33
+
34
+ MODEL_PATH = "entire_model.pth" # ضع وزن الموديل هنا
35
+
36
+ # ─────────────────────────────────────────────
37
+ # Vocabulary
38
+ # ─────────────────────────────────────────────
39
+ special_tokens = ['<PAD>', '<UNK>', '<SOS>', '<EOS>']
40
+ english_chars = list(string.ascii_lowercase + ' ')
41
+ arabic_chars = list("ابتثجحخدذرزسشصضطظعغفقكلمنهويئءىةؤ")
42
+ vocab = special_tokens + english_chars + arabic_chars
43
+ char2idx = {c: i for i, c in enumerate(vocab)}
44
+ idx2char = {i: c for i, c in enumerate(vocab)}
45
+ VOCAB_SIZE = len(vocab)
46
+
47
+ # ─────────────────────────────────────────────
48
+ # Model Architecture (same as notebook)
49
+ # ─────────────────────────────────────────────
50
+ class MultiHeadAttention(nn.Module):
51
+ def __init__(self, d_model, num_heads):
52
+ super().__init__()
53
+ assert d_model % num_heads == 0
54
+ self.d_model = d_model
55
+ self.num_heads = num_heads
56
+ self.head_dim = d_model // num_heads
57
+ self.qkv_layer = nn.Linear(d_model, 3 * d_model)
58
+ self.linear = nn.Linear(d_model, d_model)
59
+
60
+ def forward(self, x, mask=None):
61
+ B, L, _ = x.shape
62
+ qkv = self.qkv_layer(x).reshape(B, L, self.num_heads, 3 * self.head_dim)
63
+ qkv = qkv.permute(0, 2, 1, 3)
64
+ q, k, v = qkv.chunk(3, dim=-1)
65
+ attn = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
66
+ if mask is not None:
67
+ attn += mask
68
+ attn = F.softmax(attn, dim=-1)
69
+ out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, L, self.d_model)
70
+ return self.linear(out)
71
+
72
+
73
+ class MultiHeadCrossAttention(nn.Module):
74
+ def __init__(self, d_model, num_heads):
75
+ super().__init__()
76
+ assert d_model % num_heads == 0
77
+ self.d_model = d_model
78
+ self.num_heads = num_heads
79
+ self.head_dim = d_model // num_heads
80
+ self.kv_layer = nn.Linear(d_model, 2 * d_model)
81
+ self.q_layer = nn.Linear(d_model, d_model)
82
+ self.linear = nn.Linear(d_model, d_model)
83
+
84
+ def forward(self, x, y, mask=None):
85
+ B, L, _ = x.shape
86
+ kv = self.kv_layer(y).reshape(B, y.shape[1], self.num_heads, 2 * self.head_dim).permute(0, 2, 1, 3)
87
+ k, v = kv.chunk(2, dim=-1)
88
+ q = self.q_layer(x).reshape(B, L, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
89
+ attn = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
90
+ if mask is not None:
91
+ attn += mask
92
+ attn = F.softmax(attn, dim=-1)
93
+ out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, L, self.d_model)
94
+ return self.linear(out)
95
+
96
+
97
+ class LayerNormalization(nn.Module):
98
+ def __init__(self, d_model, eps=1e-5):
99
+ super().__init__()
100
+ self.gamma = nn.Parameter(torch.ones(d_model))
101
+ self.beta = nn.Parameter(torch.zeros(d_model))
102
+ self.eps = eps
103
+
104
+ def forward(self, x):
105
+ mean = x.mean(-1, keepdim=True)
106
+ var = ((x - mean) ** 2).mean(-1, keepdim=True)
107
+ return self.gamma * (x - mean) / torch.sqrt(var + self.eps) + self.beta
108
+
109
+
110
+ class PositionwiseFeedForward(nn.Module):
111
+ def __init__(self, d_model, hidden, drop_prob=0.1):
112
+ super().__init__()
113
+ self.net = nn.Sequential(
114
+ nn.Linear(d_model, hidden), nn.ReLU(), nn.Dropout(drop_prob), nn.Linear(hidden, d_model)
115
+ )
116
+ def forward(self, x): return self.net(x)
117
+
118
+
119
+ class EncoderLayer(nn.Module):
120
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob):
121
+ super().__init__()
122
+ self.attn = MultiHeadAttention(d_model, num_heads)
123
+ self.norm1 = LayerNormalization(d_model)
124
+ self.ff = PositionwiseFeedForward(d_model, ffn_hidden, drop_prob)
125
+ self.norm2 = LayerNormalization(d_model)
126
+ self.drop = nn.Dropout(drop_prob)
127
+
128
+ def forward(self, x, mask=None):
129
+ x = self.norm1(x + self.drop(self.attn(x, mask)))
130
+ x = self.norm2(x + self.drop(self.ff(x)))
131
+ return x
132
+
133
+
134
+ class DecoderLayer(nn.Module):
135
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob):
136
+ super().__init__()
137
+ self.self_attn = MultiHeadAttention(d_model, num_heads)
138
+ self.norm1 = LayerNormalization(d_model)
139
+ self.cross_attn = MultiHeadCrossAttention(d_model, num_heads)
140
+ self.norm2 = LayerNormalization(d_model)
141
+ self.ff = PositionwiseFeedForward(d_model, ffn_hidden, drop_prob)
142
+ self.norm3 = LayerNormalization(d_model)
143
+ self.drop = nn.Dropout(drop_prob)
144
+
145
+ def forward(self, x, enc_out, self_mask=None, cross_mask=None):
146
+ x = self.norm1(x + self.drop(self.self_attn(x, self_mask)))
147
+ x = self.norm2(x + self.drop(self.cross_attn(x, enc_out, cross_mask)))
148
+ x = self.norm3(x + self.drop(self.ff(x)))
149
+ return x
150
+
151
+
152
+ class SequentialEncoder(nn.Sequential):
153
+ def forward(self, *args):
154
+ x, mask = args
155
+ for layer in self:
156
+ x = layer(x, mask)
157
+ return x
158
+
159
+
160
+ class SequentialDecoder(nn.Sequential):
161
+ def forward(self, *args):
162
+ x, enc_out, self_mask, cross_mask = args
163
+ for layer in self:
164
+ x = layer(x, enc_out, self_mask, cross_mask)
165
+ return x
166
+
167
+
168
+ class Encoder(nn.Module):
169
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob, num_layers):
170
+ super().__init__()
171
+ self.layers = SequentialEncoder(*[EncoderLayer(d_model, ffn_hidden, num_heads, drop_prob) for _ in range(num_layers)])
172
+
173
+ def forward(self, x, mask=None):
174
+ return self.layers(x, mask)
175
+
176
+
177
+ class Decoder(nn.Module):
178
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob, num_layers):
179
+ super().__init__()
180
+ self.layers = SequentialDecoder(*[DecoderLayer(d_model, ffn_hidden, num_heads, drop_prob) for _ in range(num_layers)])
181
+
182
+ def forward(self, x, enc_out, self_mask=None, cross_mask=None):
183
+ return self.layers(x, enc_out, self_mask, cross_mask)
184
+
185
+
186
+ class Transformer(nn.Module):
187
+ def __init__(self, d_model=512, num_heads=8, num_encoder_layers=6, num_decoder_layers=6, ffn_hidden=2048, drop_prob=0.1):
188
+ super().__init__()
189
+ self.encoder = Encoder(d_model, ffn_hidden, num_heads, drop_prob, num_encoder_layers)
190
+ self.decoder = Decoder(d_model, ffn_hidden, num_heads, drop_prob, num_decoder_layers)
191
+
192
+ def forward(self, src, tgt, encoder_self_attention_mask=None, decoder_self_attention_mask=None, decoder_cross_attention_mask=None):
193
+ src = self.encoder(src, encoder_self_attention_mask)
194
+ return self.decoder(tgt, src, decoder_self_attention_mask, decoder_cross_attention_mask)
195
+
196
+
197
+ class PositionalEncoding(nn.Module):
198
+ def __init__(self, d_model, max_seq_len):
199
+ super().__init__()
200
+ pe = torch.zeros(max_seq_len, d_model)
201
+ pos = torch.arange(max_seq_len, dtype=torch.float).unsqueeze(1)
202
+ div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
203
+ pe[:, 0::2] = torch.sin(pos * div)
204
+ pe[:, 1::2] = torch.cos(pos * div)
205
+ self.register_buffer('pe', pe.unsqueeze(0))
206
+
207
+ def forward(self): return self.pe
208
+
209
+
210
+ def get_conv_Lout(L_in, conv):
211
+ return math.floor((L_in + 2 * conv.padding[0] - conv.dilation[0] * (conv.kernel_size[0] - 1) - 1) / conv.stride[0] + 1)
212
+
213
+
214
+ class MMS(nn.Module):
215
+ def __init__(self, vocab_size, d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6,
216
+ dim_feedforward=2048, max_encoder_seq_len=100, max_decoder_seq_len=100, n_mels=N_MELS, dropout=0.1):
217
+ super().__init__()
218
+ self.transformer = Transformer(d_model, nhead, num_encoder_layers, num_decoder_layers, dim_feedforward, dropout)
219
+ self.en_positional_encoding = PositionalEncoding(d_model, max_encoder_seq_len)
220
+ self.de_positional_encoding = PositionalEncoding(d_model, max_decoder_seq_len)
221
+ self.conv1 = nn.Conv1d(n_mels, d_model, kernel_size=3, padding=1)
222
+ self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=3, stride=2, padding=1)
223
+ self.gelu = nn.GELU()
224
+ self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
225
+ self.d_model = d_model
226
+ self.ff = nn.Linear(d_model, vocab_size)
227
+
228
+ def get_encoder_seq_len(self, L_in):
229
+ return get_conv_Lout(get_conv_Lout(L_in, self.conv1), self.conv2)
230
+
231
+ def forward(self, audio, text, enc_mask, dec_mask, cross_mask, device):
232
+ audio = self.gelu(self.conv1(audio))
233
+ audio = self.gelu(self.conv2(audio))
234
+ audio = audio.permute(0, 2, 1)
235
+ audio += self.en_positional_encoding().to(device)
236
+ text = self.embedding(text) + self.de_positional_encoding().to(device)
237
+ out = self.transformer(audio, text, enc_mask, dec_mask, cross_mask)
238
+ return self.ff(out)
239
+
240
+ # ─────────────────────────────────────────────
241
+ # Helpers
242
+ # ─────────────────────────────────────────────
243
+ def pad_or_trim(array, length=N_SAMPLES, axis=-1):
244
+ if array.shape[axis] > length:
245
+ array = array.take(indices=range(length), axis=axis)
246
+ if array.shape[axis] < length:
247
+ pad_widths = [(0, 0)] * array.ndim
248
+ pad_widths[axis] = (0, length - array.shape[axis])
249
+ array = np.pad(array, pad_widths)
250
+ return array
251
+
252
+
253
+ def preprocess_audio(file_path):
254
+ audio_data, _ = librosa.load(file_path, sr=SAMPLE_RATE)
255
+ audio_data = nr.reduce_noise(y=audio_data, sr=SAMPLE_RATE)
256
+ duration = librosa.get_duration(y=audio_data, sr=SAMPLE_RATE)
257
+ modified = pad_or_trim(audio_data, padding=False)
258
+ sgram = librosa.stft(y=modified, n_fft=N_FFT, hop_length=HOP_LENGTH)
259
+ sgram_mag, _ = librosa.magphase(sgram)
260
+ mel = librosa.feature.melspectrogram(S=sgram_mag, sr=SAMPLE_RATE, n_fft=N_FFT, hop_length=HOP_LENGTH, n_mels=N_MELS)
261
+ mel_db = librosa.amplitude_to_db(mel, ref=np.min)
262
+ orig_len = mel_db.shape[-1]
263
+ padded = np.pad(mel_db, ((0, 0), (0, N_FRAMES - mel_db.shape[-1]))) if orig_len < N_FRAMES else mel_db[:, :N_FRAMES]
264
+ return padded, orig_len, duration
265
+
266
+
267
+ def nlp_preprocessing(sentence):
268
+ sentence = sentence.lower().replace("\n", " ")
269
+ sentence = re.sub(r'[إأآ]', 'ا', sentence)
270
+ sentence = re.sub(r'[^a-zA-Zء-ي\s\d]', '', sentence)
271
+ sentence = re.sub(r'[\u0617-\u061A\u064B-\u065F]', '', sentence)
272
+ sentence = re.sub(r'([a-zA-Z])([ء-ي])|([ء-ي])([a-zA-Z])', r'\1\3 \2\4', sentence)
273
+ sentence = re.sub(r'\s+', ' ', sentence)
274
+ return re.sub(r'\d+', lambda x: num2words(int(x.group()), lang='ar'), sentence)
275
+
276
+
277
+ def tokenize_text(text, max_len=MAX_SEQ_LEN, start_token=True, end_token=True):
278
+ tokens = [char2idx.get(c, char2idx['<UNK>']) for c in text]
279
+ max_len -= (start_token + end_token)
280
+ text_len = len(tokens)
281
+ if text_len < max_len:
282
+ if end_token: tokens += [char2idx['<EOS>']]
283
+ tokens += [char2idx['<PAD>']] * (max_len - text_len)
284
+ else:
285
+ tokens = tokens[:max_len]
286
+ if end_token: tokens += [char2idx['<EOS>']]
287
+ if start_token: tokens.insert(0, char2idx['<SOS>'])
288
+ return tokens
289
+
290
+
291
+ def text_decoder(tokens):
292
+ result = ""
293
+ for t in tokens.tolist() if hasattr(tokens, 'tolist') else [tokens.item()]:
294
+ ch = idx2char.get(t, '')
295
+ if ch in ('<PAD>', '<SOS>', '<EOS>', '<UNK>'): break
296
+ result += ch
297
+ return result
298
+
299
+
300
+ def generate_padding_masks(transcription, audio_original_len, conv_func, frames=N_FRAMES):
301
+ batch_size, seq_len = transcription.size()
302
+ audio_len = conv_func(frames)
303
+ look_ahead_mask = torch.triu(torch.full((seq_len, seq_len), True), diagonal=1)
304
+ enc_mask = torch.full([batch_size, audio_len, audio_len], False)
305
+ dec_self_mask = torch.full([batch_size, seq_len, seq_len], False)
306
+ dec_cross_mask = torch.full([batch_size, seq_len, audio_len], False)
307
+
308
+ for i in range(batch_size):
309
+ new_len = conv_func(audio_original_len[i])
310
+ enc_mask[i, new_len:, :] = True
311
+ enc_mask[i, :, new_len:] = True
312
+ dec_cross_mask[i, :, new_len:] = True
313
+ zero_idx = np.where(transcription[0].cpu().numpy() == 0)[0]
314
+ if len(zero_idx):
315
+ idx = zero_idx[0]
316
+ dec_self_mask[i, idx:, :] = True
317
+ dec_self_mask[i, :, idx:] = True
318
+ dec_cross_mask[i, idx:, :] = True
319
+
320
+ dec_self_attn = torch.where(look_ahead_mask | dec_self_mask, NEG_INFTY, 0.0)
321
+ dec_cross_attn = torch.where(dec_cross_mask, NEG_INFTY, 0.0)
322
+ enc_attn = torch.where(enc_mask, NEG_INFTY, 0.0)
323
+ return enc_attn, dec_self_attn, dec_cross_attn
324
+
325
+
326
+ # ─────────────────────────────────────────────
327
+ # Load model
328
+ # ─────────────────────────────────────────────
329
+ DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
330
+ model = None
331
+
332
+ def load_model():
333
+ global model
334
+ if not os.path.exists(MODEL_PATH):
335
+ print(f"⚠️ Model file '{MODEL_PATH}' not found — transcription endpoint will return an error.")
336
+ return
337
+ try:
338
+ model = torch.load(MODEL_PATH, map_location=DEVICE, weights_only=False)
339
+ model.to(DEVICE)
340
+ model.eval()
341
+ print("✅ Model loaded successfully.")
342
+ except Exception as e:
343
+ print(f"❌ Failed to load model: {e}")
344
+
345
+
346
+ # ─────────────────────────────────────────────
347
+ # Greedy decoder
348
+ # ─────────────────────────────────────────────
349
+ def greedy_decode(audio_tensor, org_len):
350
+ transcription = ""
351
+ audio_tensor = audio_tensor.to(DEVICE)
352
+ with torch.no_grad():
353
+ for i in range(MAX_TEXT_LEN):
354
+ tok = torch.tensor(tokenize_text(transcription, max_len=MAX_TEXT_LEN, end_token=False), dtype=torch.long)
355
+ tok = tok.unsqueeze(0).to(DEVICE)
356
+ enc_mask, dec_mask, cross_mask = generate_padding_masks(tok, [org_len], model.get_encoder_seq_len)
357
+ output = model(audio_tensor, tok, enc_mask.to(DEVICE), dec_mask.to(DEVICE), cross_mask.to(DEVICE), DEVICE)
358
+ output = F.softmax(output, dim=-1)
359
+ next_token = torch.argmax(output[0, i, :], dim=-1)
360
+ ch = idx2char.get(next_token.item(), '')
361
+ if ch == '<EOS>': break
362
+ transcription += '' if ch in ('<PAD>', '<SOS>', '<UNK>') else ch
363
+ return transcription
364
+
365
+
366
+ # ─────────────────────────────────────────────
367
+ # FastAPI
368
+ # ─────────────────────────────────────────────
369
+ app = FastAPI(
370
+ title="Arabic Speech-to-Text API",
371
+ description="""
372
+ ## 🎙️ Arabic Speech-to-Text (Egyptian Dialect)
373
+
374
+ Upload an audio file and get the Arabic transcription.
375
+
376
+ ### Notes
377
+ - Supports **WAV / MP3 / FLAC / OGG**
378
+ - Best results with **Egyptian Arabic** speech
379
+ - Max audio duration: **15 seconds** (longer files are trimmed)
380
+ - The model performs noise reduction automatically
381
+ """,
382
+ version="1.0.0",
383
+ )
384
+
385
+
386
+ @app.on_event("startup")
387
+ async def startup_event():
388
+ load_model()
389
+
390
+
391
+ @app.get("/", tags=["Health"])
392
+ def root():
393
+ """Health check endpoint."""
394
+ return {"status": "ok", "model_loaded": model is not None}
395
+
396
+
397
+ @app.get("/vocab", tags=["Info"])
398
+ def get_vocab():
399
+ """Return the vocabulary used by the model."""
400
+ return {"vocab_size": VOCAB_SIZE, "vocab": vocab}
401
+
402
+
403
+ @app.post("/transcribe", tags=["Transcription"])
404
+ async def transcribe(file: UploadFile = File(..., description="Audio file (wav / mp3 / flac / ogg)")):
405
+ """
406
+ Upload an audio file and receive the Arabic transcription.
407
+
408
+ - **file**: audio file to transcribe
409
+ """
410
+ if model is None:
411
+ raise HTTPException(status_code=503, detail="Model not loaded. Make sure 'entire_model.pth' is present.")
412
+
413
+ allowed = {"audio/wav", "audio/wave", "audio/x-wav", "audio/mpeg",
414
+ "audio/mp3", "audio/flac", "audio/ogg", "audio/x-flac"}
415
+ if file.content_type and file.content_type not in allowed:
416
+ raise HTTPException(status_code=415, detail=f"Unsupported media type: {file.content_type}")
417
+
418
+ suffix = os.path.splitext(file.filename)[-1] if file.filename else ".wav"
419
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
420
+ tmp.write(await file.read())
421
+ tmp_path = tmp.name
422
+
423
+ try:
424
+ mel, orig_len, duration = preprocess_audio(tmp_path)
425
+ except Exception as e:
426
+ os.unlink(tmp_path)
427
+ raise HTTPException(status_code=400, detail=f"Audio processing error: {e}")
428
+ finally:
429
+ if os.path.exists(tmp_path):
430
+ os.unlink(tmp_path)
431
+
432
+ audio_tensor = torch.tensor(mel, dtype=torch.float32).unsqueeze(0) # (1, n_mels, n_frames)
433
+
434
+ try:
435
+ result = greedy_decode(audio_tensor, orig_len)
436
+ except Exception as e:
437
+ raise HTTPException(status_code=500, detail=f"Inference error: {e}")
438
+
439
+ gc.collect()
440
+ return JSONResponse({
441
+ "transcription": result,
442
+ "duration_seconds": round(duration, 2),
443
+ "original_frames": orig_len,
444
+ })
445
+
446
+
447
+ if __name__ == "__main__":
448
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)
app.py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import gc
4
+ import re
5
+ import string
6
+ import tempfile
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ import librosa
13
+ import noisereduce as nr
14
+ from num2words import num2words
15
+
16
+ from fastapi import FastAPI, File, UploadFile, HTTPException
17
+ from fastapi.responses import JSONResponse
18
+ import uvicorn
19
+
20
+ # ─────────────────────────────────────────────
21
+ # Constants
22
+ # ─────────────────────────────────────────────
23
+ N_MELS = 128
24
+ SAMPLE_RATE = 16000
25
+ HOP_LENGTH = 512
26
+ N_FFT = 2048
27
+ CHUNK_LENGTH = 15
28
+ N_SAMPLES = SAMPLE_RATE * CHUNK_LENGTH
29
+ N_FRAMES = math.ceil(N_SAMPLES / HOP_LENGTH)
30
+ MAX_TEXT_LEN = 70
31
+ MAX_SEQ_LEN = 70
32
+ NEG_INFTY = -1e9
33
+
34
+ MODEL_PATH = "entire_model.pth" # ضع وزن الموديل هنا
35
+
36
+ # ─────────────────────────────────────────────
37
+ # Vocabulary
38
+ # ─────────────────────────────────────────────
39
+ special_tokens = ['<PAD>', '<UNK>', '<SOS>', '<EOS>']
40
+ english_chars = list(string.ascii_lowercase + ' ')
41
+ arabic_chars = list("ابتثجحخدذرزسشصضطظعغفقكلمنهويئءىةؤ")
42
+ vocab = special_tokens + english_chars + arabic_chars
43
+ char2idx = {c: i for i, c in enumerate(vocab)}
44
+ idx2char = {i: c for i, c in enumerate(vocab)}
45
+ VOCAB_SIZE = len(vocab)
46
+
47
+ # ─────────────────────────────────────────────
48
+ # Model Architecture (same as notebook)
49
+ # ─────────────────────────────────────────────
50
+ class MultiHeadAttention(nn.Module):
51
+ def __init__(self, d_model, num_heads):
52
+ super().__init__()
53
+ assert d_model % num_heads == 0
54
+ self.d_model = d_model
55
+ self.num_heads = num_heads
56
+ self.head_dim = d_model // num_heads
57
+ self.qkv_layer = nn.Linear(d_model, 3 * d_model)
58
+ self.linear = nn.Linear(d_model, d_model)
59
+
60
+ def forward(self, x, mask=None):
61
+ B, L, _ = x.shape
62
+ qkv = self.qkv_layer(x).reshape(B, L, self.num_heads, 3 * self.head_dim)
63
+ qkv = qkv.permute(0, 2, 1, 3)
64
+ q, k, v = qkv.chunk(3, dim=-1)
65
+ attn = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
66
+ if mask is not None:
67
+ attn += mask
68
+ attn = F.softmax(attn, dim=-1)
69
+ out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, L, self.d_model)
70
+ return self.linear(out)
71
+
72
+
73
+ class MultiHeadCrossAttention(nn.Module):
74
+ def __init__(self, d_model, num_heads):
75
+ super().__init__()
76
+ assert d_model % num_heads == 0
77
+ self.d_model = d_model
78
+ self.num_heads = num_heads
79
+ self.head_dim = d_model // num_heads
80
+ self.kv_layer = nn.Linear(d_model, 2 * d_model)
81
+ self.q_layer = nn.Linear(d_model, d_model)
82
+ self.linear = nn.Linear(d_model, d_model)
83
+
84
+ def forward(self, x, y, mask=None):
85
+ B, L, _ = x.shape
86
+ kv = self.kv_layer(y).reshape(B, y.shape[1], self.num_heads, 2 * self.head_dim).permute(0, 2, 1, 3)
87
+ k, v = kv.chunk(2, dim=-1)
88
+ q = self.q_layer(x).reshape(B, L, self.num_heads, self.head_dim).permute(0, 2, 1, 3)
89
+ attn = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
90
+ if mask is not None:
91
+ attn += mask
92
+ attn = F.softmax(attn, dim=-1)
93
+ out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, L, self.d_model)
94
+ return self.linear(out)
95
+
96
+
97
+ class LayerNormalization(nn.Module):
98
+ def __init__(self, d_model, eps=1e-5):
99
+ super().__init__()
100
+ self.gamma = nn.Parameter(torch.ones(d_model))
101
+ self.beta = nn.Parameter(torch.zeros(d_model))
102
+ self.eps = eps
103
+
104
+ def forward(self, x):
105
+ mean = x.mean(-1, keepdim=True)
106
+ var = ((x - mean) ** 2).mean(-1, keepdim=True)
107
+ return self.gamma * (x - mean) / torch.sqrt(var + self.eps) + self.beta
108
+
109
+
110
+ class PositionwiseFeedForward(nn.Module):
111
+ def __init__(self, d_model, hidden, drop_prob=0.1):
112
+ super().__init__()
113
+ self.net = nn.Sequential(
114
+ nn.Linear(d_model, hidden), nn.ReLU(), nn.Dropout(drop_prob), nn.Linear(hidden, d_model)
115
+ )
116
+ def forward(self, x): return self.net(x)
117
+
118
+
119
+ class EncoderLayer(nn.Module):
120
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob):
121
+ super().__init__()
122
+ self.attn = MultiHeadAttention(d_model, num_heads)
123
+ self.norm1 = LayerNormalization(d_model)
124
+ self.ff = PositionwiseFeedForward(d_model, ffn_hidden, drop_prob)
125
+ self.norm2 = LayerNormalization(d_model)
126
+ self.drop = nn.Dropout(drop_prob)
127
+
128
+ def forward(self, x, mask=None):
129
+ x = self.norm1(x + self.drop(self.attn(x, mask)))
130
+ x = self.norm2(x + self.drop(self.ff(x)))
131
+ return x
132
+
133
+
134
+ class DecoderLayer(nn.Module):
135
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob):
136
+ super().__init__()
137
+ self.self_attn = MultiHeadAttention(d_model, num_heads)
138
+ self.norm1 = LayerNormalization(d_model)
139
+ self.cross_attn = MultiHeadCrossAttention(d_model, num_heads)
140
+ self.norm2 = LayerNormalization(d_model)
141
+ self.ff = PositionwiseFeedForward(d_model, ffn_hidden, drop_prob)
142
+ self.norm3 = LayerNormalization(d_model)
143
+ self.drop = nn.Dropout(drop_prob)
144
+
145
+ def forward(self, x, enc_out, self_mask=None, cross_mask=None):
146
+ x = self.norm1(x + self.drop(self.self_attn(x, self_mask)))
147
+ x = self.norm2(x + self.drop(self.cross_attn(x, enc_out, cross_mask)))
148
+ x = self.norm3(x + self.drop(self.ff(x)))
149
+ return x
150
+
151
+
152
+ class SequentialEncoder(nn.Sequential):
153
+ def forward(self, *args):
154
+ x, mask = args
155
+ for layer in self:
156
+ x = layer(x, mask)
157
+ return x
158
+
159
+
160
+ class SequentialDecoder(nn.Sequential):
161
+ def forward(self, *args):
162
+ x, enc_out, self_mask, cross_mask = args
163
+ for layer in self:
164
+ x = layer(x, enc_out, self_mask, cross_mask)
165
+ return x
166
+
167
+
168
+ class Encoder(nn.Module):
169
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob, num_layers):
170
+ super().__init__()
171
+ self.layers = SequentialEncoder(*[EncoderLayer(d_model, ffn_hidden, num_heads, drop_prob) for _ in range(num_layers)])
172
+
173
+ def forward(self, x, mask=None):
174
+ return self.layers(x, mask)
175
+
176
+
177
+ class Decoder(nn.Module):
178
+ def __init__(self, d_model, ffn_hidden, num_heads, drop_prob, num_layers):
179
+ super().__init__()
180
+ self.layers = SequentialDecoder(*[DecoderLayer(d_model, ffn_hidden, num_heads, drop_prob) for _ in range(num_layers)])
181
+
182
+ def forward(self, x, enc_out, self_mask=None, cross_mask=None):
183
+ return self.layers(x, enc_out, self_mask, cross_mask)
184
+
185
+
186
+ class Transformer(nn.Module):
187
+ def __init__(self, d_model=512, num_heads=8, num_encoder_layers=6, num_decoder_layers=6, ffn_hidden=2048, drop_prob=0.1):
188
+ super().__init__()
189
+ self.encoder = Encoder(d_model, ffn_hidden, num_heads, drop_prob, num_encoder_layers)
190
+ self.decoder = Decoder(d_model, ffn_hidden, num_heads, drop_prob, num_decoder_layers)
191
+
192
+ def forward(self, src, tgt, encoder_self_attention_mask=None, decoder_self_attention_mask=None, decoder_cross_attention_mask=None):
193
+ src = self.encoder(src, encoder_self_attention_mask)
194
+ return self.decoder(tgt, src, decoder_self_attention_mask, decoder_cross_attention_mask)
195
+
196
+
197
+ class PositionalEncoding(nn.Module):
198
+ def __init__(self, d_model, max_seq_len):
199
+ super().__init__()
200
+ pe = torch.zeros(max_seq_len, d_model)
201
+ pos = torch.arange(max_seq_len, dtype=torch.float).unsqueeze(1)
202
+ div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
203
+ pe[:, 0::2] = torch.sin(pos * div)
204
+ pe[:, 1::2] = torch.cos(pos * div)
205
+ self.register_buffer('pe', pe.unsqueeze(0))
206
+
207
+ def forward(self): return self.pe
208
+
209
+
210
+ def get_conv_Lout(L_in, conv):
211
+ return math.floor((L_in + 2 * conv.padding[0] - conv.dilation[0] * (conv.kernel_size[0] - 1) - 1) / conv.stride[0] + 1)
212
+
213
+
214
+ class MMS(nn.Module):
215
+ def __init__(self, vocab_size, d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6,
216
+ dim_feedforward=2048, max_encoder_seq_len=100, max_decoder_seq_len=100, n_mels=N_MELS, dropout=0.1):
217
+ super().__init__()
218
+ self.transformer = Transformer(d_model, nhead, num_encoder_layers, num_decoder_layers, dim_feedforward, dropout)
219
+ self.en_positional_encoding = PositionalEncoding(d_model, max_encoder_seq_len)
220
+ self.de_positional_encoding = PositionalEncoding(d_model, max_decoder_seq_len)
221
+ self.conv1 = nn.Conv1d(n_mels, d_model, kernel_size=3, padding=1)
222
+ self.conv2 = nn.Conv1d(d_model, d_model, kernel_size=3, stride=2, padding=1)
223
+ self.gelu = nn.GELU()
224
+ self.embedding = nn.Embedding(vocab_size, d_model, padding_idx=0)
225
+ self.d_model = d_model
226
+ self.ff = nn.Linear(d_model, vocab_size)
227
+
228
+ def get_encoder_seq_len(self, L_in):
229
+ return get_conv_Lout(get_conv_Lout(L_in, self.conv1), self.conv2)
230
+
231
+ def forward(self, audio, text, enc_mask, dec_mask, cross_mask, device):
232
+ audio = self.gelu(self.conv1(audio))
233
+ audio = self.gelu(self.conv2(audio))
234
+ audio = audio.permute(0, 2, 1)
235
+ audio += self.en_positional_encoding().to(device)
236
+ text = self.embedding(text) + self.de_positional_encoding().to(device)
237
+ out = self.transformer(audio, text, enc_mask, dec_mask, cross_mask)
238
+ return self.ff(out)
239
+
240
+ # ─────────────────────────────────────────────
241
+ # Helpers
242
+ # ─────────────────────────────────────────────
243
+ def pad_or_trim(array, length=N_SAMPLES, axis=-1):
244
+ if array.shape[axis] > length:
245
+ array = array.take(indices=range(length), axis=axis)
246
+ if array.shape[axis] < length:
247
+ pad_widths = [(0, 0)] * array.ndim
248
+ pad_widths[axis] = (0, length - array.shape[axis])
249
+ array = np.pad(array, pad_widths)
250
+ return array
251
+
252
+
253
+ def preprocess_audio(file_path):
254
+ audio_data, _ = librosa.load(file_path, sr=SAMPLE_RATE)
255
+ audio_data = nr.reduce_noise(y=audio_data, sr=SAMPLE_RATE)
256
+ duration = librosa.get_duration(y=audio_data, sr=SAMPLE_RATE)
257
+ modified = pad_or_trim(audio_data, padding=False)
258
+ sgram = librosa.stft(y=modified, n_fft=N_FFT, hop_length=HOP_LENGTH)
259
+ sgram_mag, _ = librosa.magphase(sgram)
260
+ mel = librosa.feature.melspectrogram(S=sgram_mag, sr=SAMPLE_RATE, n_fft=N_FFT, hop_length=HOP_LENGTH, n_mels=N_MELS)
261
+ mel_db = librosa.amplitude_to_db(mel, ref=np.min)
262
+ orig_len = mel_db.shape[-1]
263
+ padded = np.pad(mel_db, ((0, 0), (0, N_FRAMES - mel_db.shape[-1]))) if orig_len < N_FRAMES else mel_db[:, :N_FRAMES]
264
+ return padded, orig_len, duration
265
+
266
+
267
+ def nlp_preprocessing(sentence):
268
+ sentence = sentence.lower().replace("\n", " ")
269
+ sentence = re.sub(r'[إأآ]', 'ا', sentence)
270
+ sentence = re.sub(r'[^a-zA-Zء-ي\s\d]', '', sentence)
271
+ sentence = re.sub(r'[\u0617-\u061A\u064B-\u065F]', '', sentence)
272
+ sentence = re.sub(r'([a-zA-Z])([ء-ي])|([ء-ي])([a-zA-Z])', r'\1\3 \2\4', sentence)
273
+ sentence = re.sub(r'\s+', ' ', sentence)
274
+ return re.sub(r'\d+', lambda x: num2words(int(x.group()), lang='ar'), sentence)
275
+
276
+
277
+ def tokenize_text(text, max_len=MAX_SEQ_LEN, start_token=True, end_token=True):
278
+ tokens = [char2idx.get(c, char2idx['<UNK>']) for c in text]
279
+ max_len -= (start_token + end_token)
280
+ text_len = len(tokens)
281
+ if text_len < max_len:
282
+ if end_token: tokens += [char2idx['<EOS>']]
283
+ tokens += [char2idx['<PAD>']] * (max_len - text_len)
284
+ else:
285
+ tokens = tokens[:max_len]
286
+ if end_token: tokens += [char2idx['<EOS>']]
287
+ if start_token: tokens.insert(0, char2idx['<SOS>'])
288
+ return tokens
289
+
290
+
291
+ def text_decoder(tokens):
292
+ result = ""
293
+ for t in tokens.tolist() if hasattr(tokens, 'tolist') else [tokens.item()]:
294
+ ch = idx2char.get(t, '')
295
+ if ch in ('<PAD>', '<SOS>', '<EOS>', '<UNK>'): break
296
+ result += ch
297
+ return result
298
+
299
+
300
+ def generate_padding_masks(transcription, audio_original_len, conv_func, frames=N_FRAMES):
301
+ batch_size, seq_len = transcription.size()
302
+ audio_len = conv_func(frames)
303
+ look_ahead_mask = torch.triu(torch.full((seq_len, seq_len), True), diagonal=1)
304
+ enc_mask = torch.full([batch_size, audio_len, audio_len], False)
305
+ dec_self_mask = torch.full([batch_size, seq_len, seq_len], False)
306
+ dec_cross_mask = torch.full([batch_size, seq_len, audio_len], False)
307
+
308
+ for i in range(batch_size):
309
+ new_len = conv_func(audio_original_len[i])
310
+ enc_mask[i, new_len:, :] = True
311
+ enc_mask[i, :, new_len:] = True
312
+ dec_cross_mask[i, :, new_len:] = True
313
+ zero_idx = np.where(transcription[0].cpu().numpy() == 0)[0]
314
+ if len(zero_idx):
315
+ idx = zero_idx[0]
316
+ dec_self_mask[i, idx:, :] = True
317
+ dec_self_mask[i, :, idx:] = True
318
+ dec_cross_mask[i, idx:, :] = True
319
+
320
+ dec_self_attn = torch.where(look_ahead_mask | dec_self_mask, NEG_INFTY, 0.0)
321
+ dec_cross_attn = torch.where(dec_cross_mask, NEG_INFTY, 0.0)
322
+ enc_attn = torch.where(enc_mask, NEG_INFTY, 0.0)
323
+ return enc_attn, dec_self_attn, dec_cross_attn
324
+
325
+
326
+ # ─────────────────────────────────────────────
327
+ # Load model
328
+ # ─────────────────────────────────────────────
329
+ DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
330
+ model = None
331
+
332
+ def load_model():
333
+ global model
334
+ if not os.path.exists(MODEL_PATH):
335
+ print(f"⚠️ Model file '{MODEL_PATH}' not found — transcription endpoint will return an error.")
336
+ return
337
+ try:
338
+ model = torch.load(MODEL_PATH, map_location=DEVICE, weights_only=False)
339
+ model.to(DEVICE)
340
+ model.eval()
341
+ print("✅ Model loaded successfully.")
342
+ except Exception as e:
343
+ print(f"❌ Failed to load model: {e}")
344
+
345
+
346
+ # ─────────────────────────────────────────────
347
+ # Greedy decoder
348
+ # ─────────────────────────────────────────────
349
+ def greedy_decode(audio_tensor, org_len):
350
+ transcription = ""
351
+ audio_tensor = audio_tensor.to(DEVICE)
352
+ with torch.no_grad():
353
+ for i in range(MAX_TEXT_LEN):
354
+ tok = torch.tensor(tokenize_text(transcription, max_len=MAX_TEXT_LEN, end_token=False), dtype=torch.long)
355
+ tok = tok.unsqueeze(0).to(DEVICE)
356
+ enc_mask, dec_mask, cross_mask = generate_padding_masks(tok, [org_len], model.get_encoder_seq_len)
357
+ output = model(audio_tensor, tok, enc_mask.to(DEVICE), dec_mask.to(DEVICE), cross_mask.to(DEVICE), DEVICE)
358
+ output = F.softmax(output, dim=-1)
359
+ next_token = torch.argmax(output[0, i, :], dim=-1)
360
+ ch = idx2char.get(next_token.item(), '')
361
+ if ch == '<EOS>': break
362
+ transcription += '' if ch in ('<PAD>', '<SOS>', '<UNK>') else ch
363
+ return transcription
364
+
365
+
366
+ # ─────────────────────────────────────────────
367
+ # FastAPI
368
+ # ─────────────────────────────────────────────
369
+ app = FastAPI(
370
+ title="Arabic Speech-to-Text API",
371
+ description="""
372
+ ## 🎙️ Arabic Speech-to-Text (Egyptian Dialect)
373
+
374
+ Upload an audio file and get the Arabic transcription.
375
+
376
+ ### Notes
377
+ - Supports **WAV / MP3 / FLAC / OGG**
378
+ - Best results with **Egyptian Arabic** speech
379
+ - Max audio duration: **15 seconds** (longer files are trimmed)
380
+ - The model performs noise reduction automatically
381
+ """,
382
+ version="1.0.0",
383
+ )
384
+
385
+
386
+ @app.on_event("startup")
387
+ async def startup_event():
388
+ load_model()
389
+
390
+
391
+ @app.get("/", tags=["Health"])
392
+ def root():
393
+ """Health check endpoint."""
394
+ return {"status": "ok", "model_loaded": model is not None}
395
+
396
+
397
+ @app.get("/vocab", tags=["Info"])
398
+ def get_vocab():
399
+ """Return the vocabulary used by the model."""
400
+ return {"vocab_size": VOCAB_SIZE, "vocab": vocab}
401
+
402
+
403
+ @app.post("/transcribe", tags=["Transcription"])
404
+ async def transcribe(file: UploadFile = File(..., description="Audio file (wav / mp3 / flac / ogg)")):
405
+ """
406
+ Upload an audio file and receive the Arabic transcription.
407
+
408
+ - **file**: audio file to transcribe
409
+ """
410
+ if model is None:
411
+ raise HTTPException(status_code=503, detail="Model not loaded. Make sure 'entire_model.pth' is present.")
412
+
413
+ allowed = {"audio/wav", "audio/wave", "audio/x-wav", "audio/mpeg",
414
+ "audio/mp3", "audio/flac", "audio/ogg", "audio/x-flac"}
415
+ if file.content_type and file.content_type not in allowed:
416
+ raise HTTPException(status_code=415, detail=f"Unsupported media type: {file.content_type}")
417
+
418
+ suffix = os.path.splitext(file.filename)[-1] if file.filename else ".wav"
419
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
420
+ tmp.write(await file.read())
421
+ tmp_path = tmp.name
422
+
423
+ try:
424
+ mel, orig_len, duration = preprocess_audio(tmp_path)
425
+ except Exception as e:
426
+ os.unlink(tmp_path)
427
+ raise HTTPException(status_code=400, detail=f"Audio processing error: {e}")
428
+ finally:
429
+ if os.path.exists(tmp_path):
430
+ os.unlink(tmp_path)
431
+
432
+ audio_tensor = torch.tensor(mel, dtype=torch.float32).unsqueeze(0) # (1, n_mels, n_frames)
433
+
434
+ try:
435
+ result = greedy_decode(audio_tensor, orig_len)
436
+ except Exception as e:
437
+ raise HTTPException(status_code=500, detail=f"Inference error: {e}")
438
+
439
+ gc.collect()
440
+ return JSONResponse({
441
+ "transcription": result,
442
+ "duration_seconds": round(duration, 2),
443
+ "original_frames": orig_len,
444
+ })
445
+
446
+
447
+ if __name__ == "__main__":
448
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.29.0
3
+ python-multipart==0.0.9
4
+ torch==2.2.2
5
+ torchaudio==2.2.2
6
+ librosa==0.10.2
7
+ noisereduce==3.0.2
8
+ num2words==0.5.13
9
+ numpy==1.26.4
10
+ pydub==0.25.1
11
+ soundfile==0.12.1