aladhefafalquran commited on
Commit
df2fada
·
0 Parent(s):

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ venv/
8
+ env/
9
+ .env
10
+
11
+ # Output files
12
+ output/*
13
+ !output/.gitkeep
14
+ uploads/*
15
+ !uploads/.gitkeep
16
+
17
+ # Temporary files
18
+ *.tmp
19
+ *.temp
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies (FFmpeg, etc.)
7
+ RUN apt-get update && apt-get install -y \
8
+ ffmpeg \
9
+ git \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements first for caching
13
+ COPY requirements.txt .
14
+
15
+ # Install Python dependencies
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # Copy application code
19
+ COPY . .
20
+
21
+ # Create output directory
22
+ RUN mkdir -p /app/output /app/uploads
23
+
24
+ # Set environment variables
25
+ ENV PYTHONUNBUFFERED=1
26
+ ENV HF_SPACES=1
27
+
28
+ # Expose port (HF Spaces uses 7860)
29
+ EXPOSE 7860
30
+
31
+ # Run the application
32
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Quran SRT Generator
3
+ emoji: 📖
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # Quran SRT Generator
12
+
13
+ Generate synchronized subtitles (SRT) for Quran recitation videos with multiple language support.
14
+
15
+ ## Features
16
+
17
+ - **Multiple Languages**: Arabic, English, Turkish, French, Urdu, and more
18
+ - **Multiple Reciters**: 20+ famous Quran reciters
19
+ - **Video Generation**: Create videos with black background and burned-in subtitles
20
+ - **Accurate Timing**: Syncs subtitles with actual audio from AlQuran API
21
+ - **Download Options**: SRT files, JSON timing data, and MP4 videos
22
+
23
+ ## How to Use
24
+
25
+ 1. Select a Surah (chapter)
26
+ 2. Choose ayah range (start/end verses)
27
+ 3. Select languages for subtitles
28
+ 4. Choose a reciter
29
+ 5. Click "Generate" and wait for processing
30
+ 6. Download your SRT files or video
31
+
32
+ ## Supported Languages
33
+
34
+ Arabic, English, Turkish, French, German, Spanish, Indonesian, Urdu, Bengali, Russian, Chinese, Japanese, Korean, Malay, Dutch, Italian
35
+
36
+ ## Credits
37
+
38
+ - Quran text and audio from [AlQuran Cloud API](https://alquran.cloud/)
39
+ - Audio transcription powered by OpenAI Whisper
app.py ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quran SRT Generator - FastAPI Backend
3
+ Hugging Face Spaces Version
4
+ """
5
+
6
+ import os
7
+ import asyncio
8
+ import uuid
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Optional, List
12
+ from dataclasses import asdict
13
+
14
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks
15
+ from fastapi.staticfiles import StaticFiles
16
+ from fastapi.templating import Jinja2Templates
17
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
18
+ from fastapi.requests import Request
19
+ from pydantic import BaseModel
20
+
21
+ from src.quran_data import fetch_surah_list, fetch_all_translations, download_full_quran, check_cached_data, set_offline_mode, is_offline_mode
22
+ from src.audio_processor import get_processor, WHISPER_AVAILABLE
23
+ from src.verse_matcher import match_video_to_verses, MatchedVerse
24
+ from src.subtitle_generator import SubtitleGenerator
25
+ from src.video_generator import generate_quran_video, VideoGenerator
26
+
27
+ # Initialize FastAPI app
28
+ app = FastAPI(
29
+ title="Quran SRT Generator",
30
+ description="Generate subtitles for Quran recitation videos with Arabic, English, and Turkish translations",
31
+ version="1.0.0"
32
+ )
33
+
34
+ # Setup directories
35
+ BASE_DIR = Path(__file__).parent
36
+ UPLOAD_DIR = BASE_DIR / "uploads"
37
+ OUTPUT_DIR = BASE_DIR / "output"
38
+ STATIC_DIR = BASE_DIR / "static"
39
+ TEMPLATES_DIR = BASE_DIR / "templates"
40
+
41
+ UPLOAD_DIR.mkdir(exist_ok=True)
42
+ OUTPUT_DIR.mkdir(exist_ok=True)
43
+ STATIC_DIR.mkdir(exist_ok=True)
44
+
45
+ # Mount static files and templates
46
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
47
+ app.mount("/output", StaticFiles(directory=str(OUTPUT_DIR)), name="output")
48
+ templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
49
+
50
+ # Processing status tracking
51
+ processing_status = {}
52
+
53
+
54
+ class ProcessingRequest(BaseModel):
55
+ surah: int
56
+ start_ayah: int = 1
57
+ end_ayah: Optional[int] = None
58
+ separator: str = " | "
59
+ languages: List[str] = ["arabic", "english", "turkish"]
60
+ whisper_model: str = "medium"
61
+
62
+
63
+ class ProcessingStatus(BaseModel):
64
+ task_id: str
65
+ status: str
66
+ progress: int
67
+ message: str
68
+ output_files: Optional[dict] = None
69
+ error: Optional[str] = None
70
+
71
+
72
+ @app.get("/", response_class=HTMLResponse)
73
+ async def home(request: Request):
74
+ """Serve the main web interface"""
75
+ return templates.TemplateResponse("index.html", {
76
+ "request": request,
77
+ "whisper_available": WHISPER_AVAILABLE
78
+ })
79
+
80
+
81
+ @app.get("/api/surahs")
82
+ async def get_surahs():
83
+ """Get list of all surahs"""
84
+ try:
85
+ surahs = await fetch_surah_list()
86
+ return {"success": True, "surahs": surahs}
87
+ except Exception as e:
88
+ return {"success": False, "error": str(e)}
89
+
90
+
91
+ @app.get("/api/surah/{surah_number}")
92
+ async def get_surah(surah_number: int):
93
+ """Get surah verses with all translations"""
94
+ try:
95
+ verses = await fetch_all_translations(surah_number)
96
+ return {
97
+ "success": True,
98
+ "surah": surah_number,
99
+ "verses": verses
100
+ }
101
+ except Exception as e:
102
+ return {"success": False, "error": str(e)}
103
+
104
+
105
+ @app.get("/api/reciters")
106
+ async def get_reciters():
107
+ """Get list of available audio reciters"""
108
+ reciters = [
109
+ {"identifier": "ar.alafasy", "englishName": "Mishary Rashid Alafasy", "style": "Murattal"},
110
+ {"identifier": "ar.abdurrahmaansudais", "englishName": "Abdurrahmaan As-Sudais", "style": "Murattal"},
111
+ {"identifier": "ar.husary", "englishName": "Mahmoud Khalil Al-Husary", "style": "Murattal"},
112
+ {"identifier": "ar.minshawi", "englishName": "Mohamed Siddiq Al-Minshawi", "style": "Murattal"},
113
+ {"identifier": "ar.abdulsamad", "englishName": "Abdul Samad", "style": "Murattal"},
114
+ {"identifier": "ar.shaatree", "englishName": "Abu Bakr Ash-Shaatree", "style": "Murattal"},
115
+ {"identifier": "ar.ahmedajamy", "englishName": "Ahmed ibn Ali al-Ajamy", "style": "Murattal"},
116
+ {"identifier": "ar.hudhaify", "englishName": "Ali Al-Hudhaify", "style": "Murattal"},
117
+ {"identifier": "ar.ibrahimakhbar", "englishName": "Ibrahim Al-Akhdar", "style": "Murattal"},
118
+ {"identifier": "ar.mahermuaiqly", "englishName": "Maher Al-Muaiqly", "style": "Murattal"},
119
+ {"identifier": "ar.muhammadayyoub", "englishName": "Muhammad Ayyub", "style": "Murattal"},
120
+ {"identifier": "ar.muhammadjibreel", "englishName": "Muhammad Jibreel", "style": "Murattal"},
121
+ {"identifier": "ar.saaborinah", "englishName": "Saad Al-Ghamdi", "style": "Murattal"},
122
+ {"identifier": "ar.parhizgar", "englishName": "Shahriar Parhizgar", "style": "Murattal"},
123
+ {"identifier": "ar.aaboromali", "englishName": "Abdullah Awad al-Juhani", "style": "Murattal"},
124
+ {"identifier": "ar.haborouni", "englishName": "Hani Ar-Rifai", "style": "Murattal"},
125
+ {"identifier": "ar.abdullahbasfar", "englishName": "Abdullah Basfar", "style": "Murattal"},
126
+ {"identifier": "ar.ibrahimwalkil", "englishName": "Ibrahim Walk (English)", "style": "Translation"},
127
+ {"identifier": "ar.husarymujawwad", "englishName": "Al-Husary (Mujawwad)", "style": "Mujawwad"},
128
+ {"identifier": "ar.minshawimujawwad", "englishName": "Al-Minshawi (Mujawwad)", "style": "Mujawwad"},
129
+ ]
130
+ return {"success": True, "reciters": reciters}
131
+
132
+
133
+ @app.post("/api/upload")
134
+ async def upload_video(
135
+ surah: int = Form(...),
136
+ start_ayah: int = Form(1),
137
+ end_ayah: Optional[int] = Form(None),
138
+ separator: str = Form(" | "),
139
+ languages: str = Form('["arabic", "english"]'),
140
+ mode: str = Form("video"),
141
+ model_size: str = Form("medium"),
142
+ reciter: str = Form("ar.alafasy"),
143
+ file: Optional[UploadFile] = File(None),
144
+ image: Optional[UploadFile] = File(None)
145
+ ):
146
+ """Upload video/image and start processing"""
147
+ try:
148
+ selected_languages = json.loads(languages)
149
+ except:
150
+ selected_languages = ["arabic", "english"]
151
+
152
+ task_id = str(uuid.uuid4())
153
+ upload_path = None
154
+ image_path = None
155
+
156
+ if mode == "video":
157
+ if not file or not file.filename:
158
+ raise HTTPException(400, "No video/audio file provided")
159
+
160
+ allowed_extensions = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".mp3", ".wav", ".m4a"}
161
+ file_ext = Path(file.filename).suffix.lower()
162
+
163
+ if file_ext not in allowed_extensions:
164
+ raise HTTPException(400, f"Unsupported file format: {file_ext}")
165
+
166
+ upload_path = UPLOAD_DIR / f"{task_id}{file_ext}"
167
+ try:
168
+ with open(upload_path, "wb") as f:
169
+ content = await file.read()
170
+ f.write(content)
171
+ except Exception as e:
172
+ raise HTTPException(500, f"Failed to save file: {e}")
173
+
174
+ elif mode == "image":
175
+ if not image or not image.filename:
176
+ raise HTTPException(400, "No image file provided")
177
+
178
+ allowed_extensions = {".jpg", ".jpeg", ".png", ".webp"}
179
+ file_ext = Path(image.filename).suffix.lower()
180
+
181
+ if file_ext not in allowed_extensions:
182
+ raise HTTPException(400, f"Unsupported image format: {file_ext}")
183
+
184
+ image_path = UPLOAD_DIR / f"{task_id}_bg{file_ext}"
185
+ try:
186
+ with open(image_path, "wb") as f:
187
+ content = await image.read()
188
+ f.write(content)
189
+ except Exception as e:
190
+ raise HTTPException(500, f"Failed to save image: {e}")
191
+
192
+ processing_status[task_id] = {
193
+ "status": "pending",
194
+ "progress": 0,
195
+ "message": "Upload complete. Starting processing...",
196
+ "output_files": None,
197
+ "error": None,
198
+ "settings": {
199
+ "mode": mode,
200
+ "surah": surah,
201
+ "start_ayah": start_ayah,
202
+ "end_ayah": end_ayah,
203
+ "separator": separator,
204
+ "languages": selected_languages,
205
+ "model_size": model_size,
206
+ "reciter": reciter,
207
+ "file_path": str(upload_path) if upload_path else None,
208
+ "image_path": str(image_path) if image_path else None
209
+ }
210
+ }
211
+
212
+ return {
213
+ "success": True,
214
+ "task_id": task_id,
215
+ "message": "Upload successful. Processing will start shortly."
216
+ }
217
+
218
+
219
+ @app.post("/api/process/{task_id}")
220
+ async def start_processing(task_id: str, background_tasks: BackgroundTasks):
221
+ """Start processing an uploaded video"""
222
+ if task_id not in processing_status:
223
+ raise HTTPException(404, "Task not found")
224
+
225
+ status = processing_status[task_id]
226
+ if status["status"] not in ["pending", "error"]:
227
+ return {"success": False, "message": "Task already processing or completed"}
228
+
229
+ background_tasks.add_task(process_video_task, task_id)
230
+
231
+ processing_status[task_id]["status"] = "processing"
232
+ processing_status[task_id]["message"] = "Processing started..."
233
+
234
+ return {"success": True, "message": "Processing started"}
235
+
236
+
237
+ async def process_video_task(task_id: str):
238
+ """Background task to process video"""
239
+ status = processing_status[task_id]
240
+ settings = status["settings"]
241
+ mode = settings.get("mode", "video")
242
+
243
+ try:
244
+ status["progress"] = 10
245
+ status["message"] = "Loading Quran data..."
246
+
247
+ selected_languages = settings.get("languages", ["arabic", "english", "turkish"])
248
+ verses_data = await fetch_all_translations(settings["surah"], selected_languages)
249
+
250
+ if mode == "video":
251
+ status["progress"] = 20
252
+ status["message"] = "Extracting audio from video..."
253
+
254
+ processor = get_processor(settings.get("model_size", "medium"))
255
+
256
+ status["progress"] = 30
257
+ status["message"] = "Transcribing audio (this may take a few minutes)..."
258
+
259
+ loop = asyncio.get_event_loop()
260
+ transcription = await loop.run_in_executor(
261
+ None,
262
+ lambda: processor.transcribe_video(settings["file_path"])
263
+ )
264
+
265
+ segments = processor.get_segments_with_timing(transcription)
266
+
267
+ status["progress"] = 70
268
+ status["message"] = "Matching verses..."
269
+
270
+ matched_verses = await match_video_to_verses(
271
+ segments,
272
+ surah=settings["surah"],
273
+ start_ayah=settings["start_ayah"],
274
+ end_ayah=settings["end_ayah"],
275
+ languages=selected_languages
276
+ )
277
+ else:
278
+ status["progress"] = 30
279
+ status["message"] = "Downloading audio from reciter..."
280
+
281
+ start_ayah = settings.get("start_ayah", 1)
282
+ end_ayah = settings.get("end_ayah")
283
+ reciter = settings.get("reciter", "ar.alafasy")
284
+
285
+ video_gen = VideoGenerator(output_dir=str(OUTPUT_DIR))
286
+ ayah_timings = []
287
+
288
+ try:
289
+ audio_path, ayah_timings = await video_gen.download_surah_audio(
290
+ surah=settings["surah"],
291
+ reciter=reciter,
292
+ start_ayah=start_ayah,
293
+ end_ayah=end_ayah
294
+ )
295
+ except Exception as e:
296
+ print(f"Audio download failed: {e}")
297
+ audio_path = None
298
+ ayah_timings = []
299
+
300
+ status["progress"] = 50
301
+ status["message"] = "Generating verse timings..."
302
+
303
+ filtered_verses = {}
304
+ for ayah_num, verse in verses_data.items():
305
+ if ayah_num >= start_ayah:
306
+ if end_ayah is None or ayah_num <= end_ayah:
307
+ filtered_verses[ayah_num] = verse
308
+
309
+ timing_lookup = {t[0]: (t[1], t[2]) for t in ayah_timings}
310
+
311
+ matched_verses = []
312
+ current_time = 0.0
313
+ default_duration = 5.0
314
+
315
+ for ayah_num in sorted(filtered_verses.keys()):
316
+ verse = filtered_verses[ayah_num]
317
+
318
+ if ayah_num in timing_lookup:
319
+ start_time, end_time = timing_lookup[ayah_num]
320
+ else:
321
+ start_time = current_time
322
+ end_time = current_time + default_duration
323
+ current_time = end_time
324
+
325
+ extra_translations = {}
326
+ for lang in selected_languages:
327
+ if lang not in ["arabic", "english", "turkish"]:
328
+ extra_translations[lang] = verse.get(lang, "")
329
+
330
+ matched_verses.append(MatchedVerse(
331
+ surah=settings["surah"],
332
+ ayah=ayah_num,
333
+ arabic=verse.get("arabic", ""),
334
+ english=verse.get("english", ""),
335
+ turkish=verse.get("turkish", ""),
336
+ start_time=start_time,
337
+ end_time=end_time,
338
+ confidence=1.0,
339
+ segment_text="",
340
+ translations=extra_translations
341
+ ))
342
+
343
+ status["progress"] = 60
344
+ status["message"] = f"Generated timing for {len(matched_verses)} verses..."
345
+ status["audio_path"] = audio_path
346
+
347
+ status["progress"] = 90
348
+ status["message"] = "Generating subtitle files..."
349
+
350
+ generator = SubtitleGenerator(
351
+ output_dir=str(OUTPUT_DIR),
352
+ separator=settings["separator"],
353
+ languages=settings.get("languages", ["arabic", "english", "turkish"])
354
+ )
355
+
356
+ base_filename = f"quran_surah{settings['surah']}_{task_id[:8]}"
357
+ output_files = generator.generate_all(matched_verses, base_filename)
358
+
359
+ if mode in ["image", "black"] and status.get("audio_path"):
360
+ status["progress"] = 95
361
+ status["message"] = "Generating video with burned-in subtitles..."
362
+
363
+ try:
364
+ video_output = str(OUTPUT_DIR / f"{base_filename}_video.mp4")
365
+ srt_for_video = output_files.get("srt_combined")
366
+
367
+ bg_image = settings.get("image_path") if mode == "image" else None
368
+
369
+ video_gen = VideoGenerator(output_dir=str(OUTPUT_DIR))
370
+ video_path = video_gen.create_video_with_subtitles(
371
+ audio_path=status["audio_path"],
372
+ srt_path=srt_for_video,
373
+ output_path=video_output,
374
+ background_image=bg_image
375
+ )
376
+
377
+ output_files["video"] = video_path
378
+ status["video_file"] = video_path
379
+ except Exception as e:
380
+ print(f"Video generation failed: {e}")
381
+
382
+ verses_data = []
383
+ for v in matched_verses:
384
+ verse_dict = {
385
+ "surah": v.surah,
386
+ "ayah": v.ayah,
387
+ "arabic": v.arabic,
388
+ "english": v.english,
389
+ "turkish": v.turkish,
390
+ "start_time": v.start_time,
391
+ "end_time": v.end_time,
392
+ "confidence": v.confidence
393
+ }
394
+ if v.translations:
395
+ for lang, text in v.translations.items():
396
+ verse_dict[lang] = text
397
+ verses_data.append(verse_dict)
398
+
399
+ status["progress"] = 100
400
+ status["status"] = "completed"
401
+ status["message"] = f"Processing complete! Generated {len(matched_verses)} verse subtitles."
402
+ status["output_files"] = output_files
403
+ status["verses"] = verses_data
404
+ status["video_file"] = settings.get("file_path")
405
+
406
+ except Exception as e:
407
+ status["status"] = "error"
408
+ status["error"] = str(e)
409
+ status["message"] = f"Error: {str(e)}"
410
+
411
+
412
+ @app.get("/api/status/{task_id}")
413
+ async def get_status(task_id: str):
414
+ """Get processing status"""
415
+ if task_id not in processing_status:
416
+ raise HTTPException(404, "Task not found")
417
+ return processing_status[task_id]
418
+
419
+
420
+ @app.get("/api/download/{task_id}/{file_type}")
421
+ async def download_file(task_id: str, file_type: str):
422
+ """Download generated file"""
423
+ if task_id not in processing_status:
424
+ raise HTTPException(404, "Task not found")
425
+
426
+ status = processing_status[task_id]
427
+ if status["status"] != "completed":
428
+ raise HTTPException(400, "Processing not complete")
429
+
430
+ output_files = status.get("output_files", {})
431
+
432
+ if file_type == "srt_combined":
433
+ file_path = output_files.get("srt_combined")
434
+ elif file_type == "json":
435
+ file_path = output_files.get("json")
436
+ elif file_type == "video":
437
+ file_path = output_files.get("video")
438
+ if file_path:
439
+ return FileResponse(
440
+ file_path,
441
+ filename=Path(file_path).name,
442
+ media_type="video/mp4"
443
+ )
444
+ elif file_type.startswith("srt_"):
445
+ lang = file_type.replace("srt_", "")
446
+ file_path = output_files.get("srt_separate", {}).get(lang)
447
+ else:
448
+ raise HTTPException(400, "Invalid file type")
449
+
450
+ if not file_path or not Path(file_path).exists():
451
+ raise HTTPException(404, "File not found")
452
+
453
+ return FileResponse(
454
+ file_path,
455
+ filename=Path(file_path).name,
456
+ media_type="application/octet-stream"
457
+ )
458
+
459
+
460
+ @app.post("/api/download-quran")
461
+ async def download_quran_data(background_tasks: BackgroundTasks):
462
+ """Download full Quran data for offline use"""
463
+ background_tasks.add_task(download_full_quran)
464
+ return {"success": True, "message": "Downloading Quran data in background..."}
465
+
466
+
467
+ @app.get("/api/health")
468
+ async def health_check():
469
+ """Health check endpoint"""
470
+ return {
471
+ "status": "healthy",
472
+ "whisper_available": WHISPER_AVAILABLE,
473
+ "version": "1.0.0"
474
+ }
475
+
476
+
477
+ @app.get("/api/cache-status")
478
+ async def get_cache_status():
479
+ """Get information about cached Quran data"""
480
+ cache_info = check_cached_data()
481
+ return {
482
+ "success": True,
483
+ "offline_mode": is_offline_mode(),
484
+ "cache": cache_info
485
+ }
486
+
487
+
488
+ @app.post("/api/offline-mode")
489
+ async def toggle_offline_mode(enabled: bool = True):
490
+ """Toggle offline mode on/off"""
491
+ set_offline_mode(enabled)
492
+ return {
493
+ "success": True,
494
+ "offline_mode": is_offline_mode(),
495
+ "message": f"Offline mode {'enabled' if enabled else 'disabled'}"
496
+ }
497
+
498
+
499
+ @app.get("/api/verses/{task_id}")
500
+ async def get_verses(task_id: str):
501
+ """Get verses data for editing"""
502
+ if task_id not in processing_status:
503
+ raise HTTPException(404, "Task not found")
504
+
505
+ status = processing_status[task_id]
506
+ if status["status"] != "completed":
507
+ raise HTTPException(400, "Processing not complete")
508
+
509
+ return {
510
+ "success": True,
511
+ "verses": status.get("verses", []),
512
+ "settings": status.get("settings", {})
513
+ }
514
+
515
+
516
+ class VerseUpdate(BaseModel):
517
+ index: int
518
+ start_time: float
519
+ end_time: float
520
+
521
+
522
+ class VersesUpdateRequest(BaseModel):
523
+ verses: List[VerseUpdate]
524
+
525
+
526
+ @app.post("/api/verses/{task_id}/update")
527
+ async def update_verses(task_id: str, request: VersesUpdateRequest):
528
+ """Update verse timings"""
529
+ if task_id not in processing_status:
530
+ raise HTTPException(404, "Task not found")
531
+
532
+ status = processing_status[task_id]
533
+ if status["status"] != "completed":
534
+ raise HTTPException(400, "Processing not complete")
535
+
536
+ verses = status.get("verses", [])
537
+
538
+ for update in request.verses:
539
+ if 0 <= update.index < len(verses):
540
+ verses[update.index]["start_time"] = update.start_time
541
+ verses[update.index]["end_time"] = update.end_time
542
+
543
+ status["verses"] = verses
544
+
545
+ return {"success": True, "message": "Timings updated"}
546
+
547
+
548
+ @app.post("/api/regenerate/{task_id}")
549
+ async def regenerate_subtitles(task_id: str):
550
+ """Regenerate SRT/JSON files with updated timings"""
551
+ if task_id not in processing_status:
552
+ raise HTTPException(404, "Task not found")
553
+
554
+ status = processing_status[task_id]
555
+ if status["status"] != "completed":
556
+ raise HTTPException(400, "Processing not complete")
557
+
558
+ verses_data = status.get("verses", [])
559
+ settings = status.get("settings", {})
560
+
561
+ matched_verses = []
562
+ for v in verses_data:
563
+ extra_translations = {}
564
+ for key, value in v.items():
565
+ if key not in ["surah", "ayah", "arabic", "english", "turkish", "start_time", "end_time", "confidence"]:
566
+ extra_translations[key] = value
567
+
568
+ matched_verses.append(MatchedVerse(
569
+ surah=v["surah"],
570
+ ayah=v["ayah"],
571
+ arabic=v.get("arabic", ""),
572
+ english=v.get("english", ""),
573
+ turkish=v.get("turkish", ""),
574
+ start_time=v["start_time"],
575
+ end_time=v["end_time"],
576
+ confidence=v.get("confidence", 1.0),
577
+ segment_text="",
578
+ translations=extra_translations if extra_translations else None
579
+ ))
580
+
581
+ generator = SubtitleGenerator(
582
+ output_dir=str(OUTPUT_DIR),
583
+ separator=settings.get("separator", " | "),
584
+ languages=settings.get("languages", ["arabic", "english", "turkish"])
585
+ )
586
+
587
+ base_filename = f"quran_surah{settings['surah']}_{task_id[:8]}"
588
+ output_files = generator.generate_all(matched_verses, base_filename)
589
+
590
+ status["output_files"] = output_files
591
+
592
+ return {
593
+ "success": True,
594
+ "message": "Subtitles regenerated with updated timings",
595
+ "output_files": output_files
596
+ }
597
+
598
+
599
+ @app.get("/api/video/{task_id}")
600
+ async def get_video(task_id: str):
601
+ """Stream video file for preview"""
602
+ if task_id not in processing_status:
603
+ raise HTTPException(404, "Task not found")
604
+
605
+ status = processing_status[task_id]
606
+
607
+ video_path = status.get("video_file")
608
+ if not video_path or not Path(video_path).exists():
609
+ output_files = status.get("output_files", {})
610
+ video_path = output_files.get("video")
611
+
612
+ if not video_path or not Path(video_path).exists():
613
+ raise HTTPException(404, "Video file not found")
614
+
615
+ return FileResponse(
616
+ video_path,
617
+ media_type="video/mp4",
618
+ filename=Path(video_path).name
619
+ )
620
+
621
+
622
+ @app.delete("/api/task/{task_id}")
623
+ async def cleanup_task(task_id: str):
624
+ """Clean up task and delete video file"""
625
+ if task_id not in processing_status:
626
+ raise HTTPException(404, "Task not found")
627
+
628
+ status = processing_status[task_id]
629
+ video_path = status.get("video_file")
630
+
631
+ if video_path and Path(video_path).exists():
632
+ try:
633
+ os.remove(video_path)
634
+ except:
635
+ pass
636
+
637
+ del processing_status[task_id]
638
+
639
+ return {"success": True, "message": "Task cleaned up"}
640
+
641
+
642
+ # Run with HF Spaces port (7860)
643
+ if __name__ == "__main__":
644
+ import uvicorn
645
+ port = int(os.environ.get("PORT", 7860))
646
+ print("Starting Quran SRT Generator (HF Spaces)...")
647
+ print(f"Whisper available: {WHISPER_AVAILABLE}")
648
+ print(f"\nRunning on port {port}")
649
+ uvicorn.run(app, host="0.0.0.0", port=port)
output/.gitkeep ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quran SRT Generator - HF Spaces Dependencies
2
+
3
+ # Web Framework
4
+ fastapi==0.109.0
5
+ uvicorn[standard]==0.27.0
6
+ python-multipart==0.0.6
7
+ jinja2==3.1.3
8
+
9
+ # HTTP Client (for Quran API)
10
+ httpx==0.26.0
11
+
12
+ # Audio Processing (Whisper)
13
+ openai-whisper==20231117
14
+
15
+ # Utilities
16
+ pydantic==2.5.3
17
+
18
+ # Additional for production
19
+ aiofiles==23.2.1
src/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Quran SRT Generator
2
+ # Core modules for processing Quran recitation videos
src/audio_processor.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio Processor
3
+ Extracts audio from video and transcribes using Whisper
4
+ Optimized for Arabic Quran recitation
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Optional, List, Dict
12
+ import json
13
+
14
+ # Try to import whisper
15
+ try:
16
+ import whisper
17
+ WHISPER_AVAILABLE = True
18
+ except ImportError:
19
+ WHISPER_AVAILABLE = False
20
+ print("Warning: Whisper not installed. Install with: pip install openai-whisper")
21
+
22
+
23
+ class AudioProcessor:
24
+ def __init__(self, model_size: str = "medium"):
25
+ """
26
+ Initialize the audio processor
27
+
28
+ Args:
29
+ model_size: Whisper model size
30
+ - "tiny": Fastest, least accurate
31
+ - "base": Fast, basic accuracy
32
+ - "small": Good balance
33
+ - "medium": Recommended for Arabic (best balance)
34
+ - "large": Most accurate, slowest (requires more VRAM)
35
+ """
36
+ self.model_size = model_size
37
+ self.model = None
38
+ self.temp_dir = Path(tempfile.gettempdir()) / "quran_srt"
39
+ self.temp_dir.mkdir(exist_ok=True)
40
+
41
+ def load_model(self):
42
+ """Load Whisper model (lazy loading)"""
43
+ if not WHISPER_AVAILABLE:
44
+ raise RuntimeError("Whisper is not installed. Run: pip install openai-whisper")
45
+
46
+ if self.model is None:
47
+ print(f"Loading Whisper {self.model_size} model...")
48
+ self.model = whisper.load_model(self.model_size)
49
+ print("Model loaded successfully!")
50
+
51
+ return self.model
52
+
53
+ def extract_audio(self, video_path: str, output_path: Optional[str] = None) -> str:
54
+ """
55
+ Extract audio from video file using FFmpeg
56
+
57
+ Args:
58
+ video_path: Path to the video file
59
+ output_path: Optional output path for audio file
60
+
61
+ Returns:
62
+ Path to the extracted audio file
63
+ """
64
+ video_path = Path(video_path)
65
+
66
+ if not video_path.exists():
67
+ raise FileNotFoundError(f"Video file not found: {video_path}")
68
+
69
+ if output_path is None:
70
+ output_path = self.temp_dir / f"{video_path.stem}_audio.wav"
71
+ else:
72
+ output_path = Path(output_path)
73
+
74
+ # FFmpeg command to extract audio as WAV (16kHz for Whisper)
75
+ cmd = [
76
+ "ffmpeg",
77
+ "-i", str(video_path),
78
+ "-vn", # No video
79
+ "-acodec", "pcm_s16le", # PCM format
80
+ "-ar", "16000", # 16kHz sample rate (Whisper optimal)
81
+ "-ac", "1", # Mono
82
+ "-y", # Overwrite output
83
+ str(output_path)
84
+ ]
85
+
86
+ try:
87
+ result = subprocess.run(
88
+ cmd,
89
+ capture_output=True,
90
+ text=True,
91
+ check=True
92
+ )
93
+ print(f"Audio extracted to: {output_path}")
94
+ return str(output_path)
95
+
96
+ except subprocess.CalledProcessError as e:
97
+ raise RuntimeError(f"FFmpeg error: {e.stderr}")
98
+ except FileNotFoundError:
99
+ raise RuntimeError("FFmpeg not found. Please install FFmpeg.")
100
+
101
+ def transcribe(
102
+ self,
103
+ audio_path: str,
104
+ language: str = "ar",
105
+ task: str = "transcribe"
106
+ ) -> Dict:
107
+ """
108
+ Transcribe audio using Whisper
109
+
110
+ Args:
111
+ audio_path: Path to audio file
112
+ language: Language code ("ar" for Arabic)
113
+ task: "transcribe" for same language, "translate" for English
114
+
115
+ Returns:
116
+ Transcription result with segments and timestamps
117
+ """
118
+ model = self.load_model()
119
+
120
+ print(f"Transcribing audio: {audio_path}")
121
+ print("This may take a few minutes depending on the video length...")
122
+
123
+ result = model.transcribe(
124
+ audio_path,
125
+ language=language,
126
+ task=task,
127
+ word_timestamps=True, # Get word-level timestamps
128
+ verbose=False,
129
+ initial_prompt="بسم الله الرحمن الرحيم", # Help with Quran context
130
+ )
131
+
132
+ return result
133
+
134
+ def transcribe_video(
135
+ self,
136
+ video_path: str,
137
+ language: str = "ar"
138
+ ) -> Dict:
139
+ """
140
+ Full pipeline: extract audio and transcribe
141
+
142
+ Args:
143
+ video_path: Path to video file
144
+ language: Language code
145
+
146
+ Returns:
147
+ Transcription result with segments
148
+ """
149
+ # Extract audio
150
+ audio_path = self.extract_audio(video_path)
151
+
152
+ # Transcribe
153
+ result = self.transcribe(audio_path, language=language)
154
+
155
+ # Clean up temp audio file
156
+ try:
157
+ os.remove(audio_path)
158
+ except:
159
+ pass
160
+
161
+ return result
162
+
163
+ def get_segments_with_timing(self, transcription: Dict) -> List[Dict]:
164
+ """
165
+ Extract segments with precise timing from transcription
166
+
167
+ Args:
168
+ transcription: Whisper transcription result
169
+
170
+ Returns:
171
+ List of segments with start, end, and text
172
+ """
173
+ segments = []
174
+
175
+ for segment in transcription.get("segments", []):
176
+ segments.append({
177
+ "id": segment.get("id", len(segments)),
178
+ "start": segment.get("start", 0),
179
+ "end": segment.get("end", 0),
180
+ "text": segment.get("text", "").strip(),
181
+ "words": segment.get("words", []),
182
+ "confidence": segment.get("avg_logprob", 0)
183
+ })
184
+
185
+ return segments
186
+
187
+
188
+ class MockAudioProcessor:
189
+ """
190
+ Mock processor for testing without Whisper installed
191
+ """
192
+
193
+ def __init__(self, model_size: str = "medium"):
194
+ self.model_size = model_size
195
+
196
+ def transcribe_video(self, video_path: str, language: str = "ar") -> Dict:
197
+ """Return mock transcription for testing"""
198
+ return {
199
+ "text": "بسم الله الرحمن الرحيم الحمد لله رب العالمين",
200
+ "segments": [
201
+ {
202
+ "id": 0,
203
+ "start": 0.0,
204
+ "end": 3.5,
205
+ "text": "بسم الله الرحمن الرحيم",
206
+ "words": []
207
+ },
208
+ {
209
+ "id": 1,
210
+ "start": 3.5,
211
+ "end": 6.0,
212
+ "text": "الحمد لله رب العالمين",
213
+ "words": []
214
+ }
215
+ ],
216
+ "language": "ar"
217
+ }
218
+
219
+ def get_segments_with_timing(self, transcription: Dict) -> List[Dict]:
220
+ return transcription.get("segments", [])
221
+
222
+
223
+ def get_processor(model_size: str = "medium") -> AudioProcessor:
224
+ """
225
+ Get appropriate processor based on Whisper availability
226
+ """
227
+ if WHISPER_AVAILABLE:
228
+ return AudioProcessor(model_size)
229
+ else:
230
+ print("Using mock processor (Whisper not installed)")
231
+ return MockAudioProcessor(model_size)
232
+
233
+
234
+ # For testing
235
+ if __name__ == "__main__":
236
+ processor = get_processor()
237
+ print(f"Whisper available: {WHISPER_AVAILABLE}")
238
+ print(f"Processor type: {type(processor).__name__}")
239
+
240
+ # Test with mock data
241
+ if not WHISPER_AVAILABLE:
242
+ result = processor.transcribe_video("test.mp4")
243
+ segments = processor.get_segments_with_timing(result)
244
+
245
+ print("\nMock transcription result:")
246
+ for seg in segments:
247
+ print(f"[{seg['start']:.2f} - {seg['end']:.2f}] {seg['text']}")
src/quran_data.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quran Data Handler
3
+ Fetches and caches Quran verses with translations from free APIs
4
+ Primary source: api.alquran.cloud (reliable, no rate limits)
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import httpx
10
+ import asyncio
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ DATA_DIR = Path(__file__).parent.parent / "data"
15
+ DATA_DIR.mkdir(exist_ok=True)
16
+
17
+ # API Base URL
18
+ API_BASE = "https://api.alquran.cloud/v1"
19
+
20
+ # Global offline mode setting
21
+ OFFLINE_MODE = False
22
+
23
+
24
+ def set_offline_mode(enabled: bool):
25
+ """Enable or disable offline mode"""
26
+ global OFFLINE_MODE
27
+ OFFLINE_MODE = enabled
28
+
29
+
30
+ def is_offline_mode() -> bool:
31
+ """Check if offline mode is enabled"""
32
+ return OFFLINE_MODE
33
+
34
+
35
+ def check_cached_data() -> dict:
36
+ """
37
+ Check what data is available offline
38
+ Returns dict with cache status
39
+ """
40
+ cache_info = {
41
+ "surah_list": (DATA_DIR / "surah_list.json").exists(),
42
+ "surahs_cached": [],
43
+ "languages_cached": {}
44
+ }
45
+
46
+ # Check which surahs are cached for each language
47
+ for lang in EDITIONS.keys():
48
+ cache_info["languages_cached"][lang] = []
49
+ for surah_num in range(1, 115):
50
+ cache_file = DATA_DIR / f"surah_{surah_num}_{lang}.json"
51
+ if cache_file.exists():
52
+ cache_info["languages_cached"][lang].append(surah_num)
53
+ if surah_num not in cache_info["surahs_cached"]:
54
+ cache_info["surahs_cached"].append(surah_num)
55
+
56
+ cache_info["total_cached"] = len(cache_info["surahs_cached"])
57
+ cache_info["is_complete"] = cache_info["total_cached"] == 114
58
+
59
+ return cache_info
60
+
61
+
62
+ def is_surah_cached(surah: int, languages: list = None) -> bool:
63
+ """Check if a surah is cached for the specified languages"""
64
+ if languages is None:
65
+ languages = ["arabic", "english", "turkish"]
66
+
67
+ for lang in languages:
68
+ cache_file = DATA_DIR / f"surah_{surah}_{lang}.json"
69
+ if not cache_file.exists():
70
+ return False
71
+ return True
72
+
73
+ # Translation editions (verified high-quality)
74
+ EDITIONS = {
75
+ "arabic": "quran-uthmani", # Original Arabic (Uthmani script)
76
+ "english": "en.sahih", # Saheeh International
77
+ "turkish": "tr.diyanet", # Diyanet Isleri (official Turkish)
78
+ "urdu": "ur.jalandhry", # Fateh Muhammad Jalandhry
79
+ "french": "fr.hamidullah", # Muhammad Hamidullah
80
+ "german": "de.aburida", # Abu Rida Muhammad
81
+ "indonesian": "id.indonesian", # Indonesian Ministry of Religious Affairs
82
+ "spanish": "es.cortes", # Julio Cortes
83
+ "russian": "ru.kuliev", # Elmir Kuliev
84
+ "bengali": "bn.bengali", # Muhiuddin Khan
85
+ "chinese": "zh.majian", # Ma Jian
86
+ "dutch": "nl.siregar", # Sofian Siregar
87
+ "italian": "it.piccardo", # Hamza Piccardo
88
+ "japanese": "ja.japanese", # Japanese
89
+ "korean": "ko.korean", # Korean
90
+ "malay": "ms.basmeih", # Abdullah Basmeih
91
+ "persian": "fa.ansarian", # Hussain Ansarian
92
+ "portuguese": "pt.elhayek", # Samir El-Hayek
93
+ "swedish": "sv.bernstrom", # Knut Bernstrom
94
+ "thai": "th.thai", # Thai
95
+ }
96
+
97
+ # Surah metadata cache
98
+ SURAH_INFO = None
99
+
100
+
101
+ async def fetch_surah_list() -> list:
102
+ """Fetch list of all surahs with metadata"""
103
+ global SURAH_INFO
104
+
105
+ cache_file = DATA_DIR / "surah_list.json"
106
+
107
+ if cache_file.exists():
108
+ with open(cache_file, "r", encoding="utf-8") as f:
109
+ SURAH_INFO = json.load(f)
110
+ return SURAH_INFO
111
+
112
+ async with httpx.AsyncClient(timeout=30.0) as client:
113
+ try:
114
+ response = await client.get(f"{API_BASE}/surah")
115
+ data = response.json()
116
+
117
+ if data.get("status") == "OK":
118
+ SURAH_INFO = data["data"]
119
+ with open(cache_file, "w", encoding="utf-8") as f:
120
+ json.dump(SURAH_INFO, f, ensure_ascii=False, indent=2)
121
+ return SURAH_INFO
122
+ except Exception as e:
123
+ print(f"Error fetching surah list: {e}")
124
+
125
+ return []
126
+
127
+
128
+ async def fetch_surah_translation(surah: int, language: str = "arabic") -> Optional[list]:
129
+ """
130
+ Fetch entire surah with translation
131
+ Returns list of verses with text
132
+ """
133
+ edition = EDITIONS.get(language, "quran-uthmani")
134
+ cache_file = DATA_DIR / f"surah_{surah}_{language}.json"
135
+
136
+ # Check cache first
137
+ if cache_file.exists():
138
+ with open(cache_file, "r", encoding="utf-8") as f:
139
+ return json.load(f)
140
+
141
+ # If offline mode is enabled and no cache, return None
142
+ if OFFLINE_MODE:
143
+ print(f"Offline mode: Surah {surah} in {language} not cached")
144
+ return None
145
+
146
+ async with httpx.AsyncClient(timeout=30.0) as client:
147
+ try:
148
+ url = f"{API_BASE}/surah/{surah}/{edition}"
149
+ response = await client.get(url)
150
+ data = response.json()
151
+
152
+ if data.get("status") == "OK":
153
+ ayahs = data["data"]["ayahs"]
154
+ verses = []
155
+ for ayah in ayahs:
156
+ verses.append({
157
+ "ayah": ayah["numberInSurah"],
158
+ "text": ayah["text"]
159
+ })
160
+
161
+ # Cache the result
162
+ with open(cache_file, "w", encoding="utf-8") as f:
163
+ json.dump(verses, f, ensure_ascii=False, indent=2)
164
+
165
+ return verses
166
+ except Exception as e:
167
+ print(f"Error fetching surah {surah} in {language}: {e}")
168
+
169
+ return None
170
+
171
+
172
+ async def fetch_all_translations(surah: int, languages: list = None) -> dict:
173
+ """
174
+ Fetch surah in specified languages (default: Arabic, English, Turkish)
175
+ Returns dict with verses indexed by ayah number
176
+ """
177
+ if languages is None:
178
+ languages = ["arabic", "english", "turkish"]
179
+
180
+ results = {}
181
+
182
+ # Fetch all requested languages in parallel
183
+ tasks = {lang: fetch_surah_translation(surah, lang) for lang in languages}
184
+ translations = await asyncio.gather(*tasks.values())
185
+
186
+ # Map results back to language keys
187
+ for lang, translation in zip(tasks.keys(), translations):
188
+ if translation:
189
+ for verse in translation:
190
+ ayah = verse.get("ayah")
191
+ if ayah:
192
+ if ayah not in results:
193
+ results[ayah] = {"ayah": ayah, "surah": surah}
194
+ results[ayah][lang] = verse.get("text", "")
195
+
196
+ return results
197
+
198
+
199
+ async def download_full_quran():
200
+ """
201
+ Download entire Quran in all languages for offline use
202
+ This is optional but recommended for faster processing
203
+ """
204
+ print("Downloading Quran data for offline use...")
205
+
206
+ surah_list = await fetch_surah_list()
207
+ total = len(surah_list)
208
+
209
+ for i, surah in enumerate(surah_list, 1):
210
+ surah_num = surah["number"]
211
+ print(f"Downloading Surah {surah_num}/{total}: {surah['englishName']}...")
212
+
213
+ await fetch_all_translations(surah_num)
214
+
215
+ # Small delay to be respectful to the API
216
+ await asyncio.sleep(0.2)
217
+
218
+ print("Download complete! Quran data cached locally.")
219
+
220
+
221
+ def get_cached_surah(surah: int) -> Optional[dict]:
222
+ """
223
+ Get cached surah data if available
224
+ Returns None if not cached
225
+ """
226
+ result = {}
227
+
228
+ for lang in ["arabic", "english", "turkish"]:
229
+ cache_file = DATA_DIR / f"surah_{surah}_{lang}.json"
230
+ if cache_file.exists():
231
+ with open(cache_file, "r", encoding="utf-8") as f:
232
+ verses = json.load(f)
233
+ for verse in verses:
234
+ ayah = verse.get("ayah")
235
+ if ayah:
236
+ if ayah not in result:
237
+ result[ayah] = {"ayah": ayah, "surah": surah}
238
+ result[ayah][lang] = verse.get("text", "")
239
+
240
+ return result if result else None
241
+
242
+
243
+ def search_verse_by_text(arabic_text: str, surah: int) -> Optional[dict]:
244
+ """
245
+ Search for a verse by its Arabic text within a surah
246
+ Uses fuzzy matching for Whisper transcription errors
247
+ """
248
+ from difflib import SequenceMatcher
249
+
250
+ cached = get_cached_surah(surah)
251
+ if not cached:
252
+ return None
253
+
254
+ best_match = None
255
+ best_ratio = 0
256
+
257
+ # Clean the input text
258
+ clean_input = normalize_arabic(arabic_text)
259
+
260
+ for ayah, verse in cached.items():
261
+ if "arabic" in verse:
262
+ clean_verse = normalize_arabic(verse["arabic"])
263
+
264
+ # Calculate similarity
265
+ ratio = SequenceMatcher(None, clean_input, clean_verse).ratio()
266
+
267
+ if ratio > best_ratio and ratio > 0.5: # Minimum 50% match
268
+ best_ratio = ratio
269
+ best_match = verse
270
+ best_match["match_confidence"] = ratio
271
+
272
+ return best_match
273
+
274
+
275
+ def normalize_arabic(text: str) -> str:
276
+ """
277
+ Normalize Arabic text for comparison
278
+ Removes diacritics and normalizes characters
279
+ """
280
+ # Remove diacritics (tashkeel)
281
+ diacritics = [
282
+ '\u064B', '\u064C', '\u064D', '\u064E', '\u064F',
283
+ '\u0650', '\u0651', '\u0652', '\u0653', '\u0654',
284
+ '\u0655', '\u0656', '\u0657', '\u0658', '\u0659',
285
+ '\u065A', '\u065B', '\u065C', '\u065D', '\u065E',
286
+ '\u065F', '\u0670'
287
+ ]
288
+
289
+ for d in diacritics:
290
+ text = text.replace(d, '')
291
+
292
+ # Normalize alef variations
293
+ text = text.replace('أ', 'ا')
294
+ text = text.replace('إ', 'ا')
295
+ text = text.replace('آ', 'ا')
296
+ text = text.replace('ٱ', 'ا')
297
+
298
+ # Normalize other characters
299
+ text = text.replace('ة', 'ه')
300
+ text = text.replace('ى', 'ي')
301
+
302
+ # Remove extra spaces
303
+ text = ' '.join(text.split())
304
+
305
+ return text
306
+
307
+
308
+ # For testing
309
+ if __name__ == "__main__":
310
+ async def test():
311
+ print("Testing Quran Data Handler...")
312
+
313
+ # Test fetching surah list
314
+ surahs = await fetch_surah_list()
315
+ print(f"Found {len(surahs)} surahs")
316
+
317
+ # Test fetching Al-Fatiha in all languages
318
+ print("\nFetching Al-Fatiha (Surah 1)...")
319
+ verses = await fetch_all_translations(1)
320
+
321
+ for ayah, verse in sorted(verses.items()):
322
+ print(f"\nAyah {ayah}:")
323
+ print(f" AR: {verse.get('arabic', 'N/A')}")
324
+ print(f" EN: {verse.get('english', 'N/A')}")
325
+ print(f" TR: {verse.get('turkish', 'N/A')}")
326
+
327
+ asyncio.run(test())
src/subtitle_generator.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Subtitle Generator
3
+ Generates SRT and JSON files from matched verses
4
+ Supports multiple languages on the same line with separators
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+ from datetime import timedelta
11
+ from dataclasses import asdict
12
+
13
+ from .verse_matcher import MatchedVerse
14
+
15
+
16
+ class SubtitleGenerator:
17
+ def __init__(
18
+ self,
19
+ output_dir: str = "output",
20
+ separator: str = " | ",
21
+ languages: List[str] = None
22
+ ):
23
+ self.output_dir = Path(output_dir)
24
+ self.output_dir.mkdir(exist_ok=True)
25
+ self.separator = separator
26
+ self.languages = languages or ["arabic", "english", "turkish"]
27
+
28
+ def _format_timestamp(self, seconds: float) -> str:
29
+ td = timedelta(seconds=seconds)
30
+ total_seconds = int(td.total_seconds())
31
+ hours = total_seconds // 3600
32
+ minutes = (total_seconds % 3600) // 60
33
+ secs = total_seconds % 60
34
+ milliseconds = int((seconds - int(seconds)) * 1000)
35
+ return f"{hours:02d}:{minutes:02d}:{secs:02d},{milliseconds:03d}"
36
+
37
+ def _format_verse_text(self, verse: MatchedVerse) -> str:
38
+ parts = []
39
+
40
+ # RTL Unicode markers for Arabic text
41
+ RLE = '\u202B' # Right-to-Left Embedding
42
+ PDF = '\u202C' # Pop Directional Formatting
43
+
44
+ for lang in self.languages:
45
+ if hasattr(verse, 'get_translation'):
46
+ text = verse.get_translation(lang)
47
+ else:
48
+ text = getattr(verse, lang, "")
49
+ if text:
50
+ # Wrap Arabic text with RTL markers for proper rendering
51
+ if lang == "arabic":
52
+ text = f"{RLE}{text}{PDF}"
53
+ parts.append(text)
54
+
55
+ return "\n".join(parts)
56
+
57
+ def generate_srt(
58
+ self,
59
+ verses: List[MatchedVerse],
60
+ filename: str = "quran_subtitles.srt"
61
+ ) -> str:
62
+ output_path = self.output_dir / filename
63
+ srt_content = []
64
+
65
+ for i, verse in enumerate(verses, 1):
66
+ srt_content.append(str(i))
67
+ start = self._format_timestamp(verse.start_time)
68
+ end = self._format_timestamp(verse.end_time)
69
+ srt_content.append(f"{start} --> {end}")
70
+ text = self._format_verse_text(verse)
71
+ srt_content.append(text)
72
+ srt_content.append("")
73
+
74
+ with open(output_path, "w", encoding="utf-8") as f:
75
+ f.write("\n".join(srt_content))
76
+
77
+ print(f"SRT file saved: {output_path}")
78
+ return str(output_path)
79
+
80
+ def generate_srt_separate(
81
+ self,
82
+ verses: List[MatchedVerse],
83
+ base_filename: str = "quran_subtitles"
84
+ ) -> dict:
85
+ output_paths = {}
86
+
87
+ for lang in self.languages:
88
+ filename = f"{base_filename}_{lang}.srt"
89
+ output_path = self.output_dir / filename
90
+ srt_content = []
91
+
92
+ for i, verse in enumerate(verses, 1):
93
+ srt_content.append(str(i))
94
+ start = self._format_timestamp(verse.start_time)
95
+ end = self._format_timestamp(verse.end_time)
96
+ srt_content.append(f"{start} --> {end}")
97
+
98
+ if hasattr(verse, 'get_translation'):
99
+ text = verse.get_translation(lang)
100
+ else:
101
+ text = getattr(verse, lang, "")
102
+ srt_content.append(text)
103
+ srt_content.append("")
104
+
105
+ with open(output_path, "w", encoding="utf-8") as f:
106
+ f.write("\n".join(srt_content))
107
+
108
+ output_paths[lang] = str(output_path)
109
+ print(f"SRT file saved: {output_path}")
110
+
111
+ return output_paths
112
+
113
+ def generate_json(
114
+ self,
115
+ verses: List[MatchedVerse],
116
+ filename: str = "quran_timing.json",
117
+ include_confidence: bool = True
118
+ ) -> str:
119
+ output_path = self.output_dir / filename
120
+
121
+ data = {
122
+ "version": "1.0",
123
+ "generator": "Quran SRT Generator",
124
+ "total_verses": len(verses),
125
+ "languages": self.languages,
126
+ "separator": self.separator,
127
+ "verses": []
128
+ }
129
+
130
+ for verse in verses:
131
+ verse_data = {
132
+ "index": verses.index(verse) + 1,
133
+ "surah": verse.surah,
134
+ "ayah": verse.ayah,
135
+ "timing": {
136
+ "start": verse.start_time,
137
+ "end": verse.end_time,
138
+ "start_formatted": self._format_timestamp(verse.start_time),
139
+ "end_formatted": self._format_timestamp(verse.end_time),
140
+ "duration": round(verse.end_time - verse.start_time, 3)
141
+ },
142
+ "text": {
143
+ "arabic": verse.arabic,
144
+ "english": verse.english,
145
+ "turkish": verse.turkish,
146
+ "combined": self._format_verse_text(verse)
147
+ }
148
+ }
149
+
150
+ if include_confidence:
151
+ verse_data["confidence"] = round(verse.confidence, 4)
152
+
153
+ data["verses"].append(verse_data)
154
+
155
+ if verses:
156
+ data["summary"] = {
157
+ "surah": verses[0].surah,
158
+ "first_ayah": min(v.ayah for v in verses),
159
+ "last_ayah": max(v.ayah for v in verses),
160
+ "total_duration": round(verses[-1].end_time - verses[0].start_time, 3),
161
+ "average_verse_duration": round(
162
+ sum(v.end_time - v.start_time for v in verses) / len(verses), 3
163
+ )
164
+ }
165
+
166
+ with open(output_path, "w", encoding="utf-8") as f:
167
+ json.dump(data, f, ensure_ascii=False, indent=2)
168
+
169
+ print(f"JSON file saved: {output_path}")
170
+ return str(output_path)
171
+
172
+ def generate_all(
173
+ self,
174
+ verses: List[MatchedVerse],
175
+ base_filename: str = "quran_subtitles"
176
+ ) -> dict:
177
+ outputs = {}
178
+ outputs["srt_combined"] = self.generate_srt(verses, f"{base_filename}_combined.srt")
179
+ outputs["srt_separate"] = self.generate_srt_separate(verses, base_filename)
180
+ outputs["json"] = self.generate_json(verses, f"{base_filename}_timing.json")
181
+ return outputs
182
+
183
+
184
+ def generate_subtitles(
185
+ verses: List[MatchedVerse],
186
+ output_dir: str = "output",
187
+ separator: str = " | ",
188
+ languages: List[str] = None,
189
+ base_filename: str = "quran_subtitles"
190
+ ) -> dict:
191
+ generator = SubtitleGenerator(
192
+ output_dir=output_dir,
193
+ separator=separator,
194
+ languages=languages
195
+ )
196
+ return generator.generate_all(verses, base_filename)
src/verse_matcher.py ADDED
@@ -0,0 +1,398 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Verse Matcher
3
+ Matches transcribed Arabic text to Quran verses
4
+ Uses fuzzy matching to handle Whisper transcription variations
5
+ """
6
+
7
+ import asyncio
8
+ import re
9
+ from difflib import SequenceMatcher
10
+ from typing import List, Dict, Optional, Tuple
11
+ from dataclasses import dataclass
12
+
13
+ from .quran_data import (
14
+ fetch_all_translations,
15
+ normalize_arabic,
16
+ get_cached_surah,
17
+ fetch_surah_list
18
+ )
19
+
20
+
21
+ @dataclass
22
+ class MatchedVerse:
23
+ """Represents a matched Quran verse with timing"""
24
+ surah: int
25
+ ayah: int
26
+ arabic: str
27
+ english: str
28
+ turkish: str
29
+ start_time: float
30
+ end_time: float
31
+ confidence: float
32
+ segment_text: str # Original transcribed text
33
+ # Additional languages stored as dict
34
+ translations: dict = None
35
+
36
+ def __post_init__(self):
37
+ if self.translations is None:
38
+ self.translations = {}
39
+
40
+ def get_translation(self, lang: str) -> str:
41
+ """Get translation for a language"""
42
+ if lang == "arabic":
43
+ return self.arabic
44
+ elif lang == "english":
45
+ return self.english
46
+ elif lang == "turkish":
47
+ return self.turkish
48
+ return self.translations.get(lang, "")
49
+
50
+
51
+ class VerseMatcher:
52
+ def __init__(self, surah_number: int, start_ayah: int = 1, end_ayah: Optional[int] = None):
53
+ """
54
+ Initialize verse matcher for a specific surah range
55
+
56
+ Args:
57
+ surah_number: The surah to match against
58
+ start_ayah: Starting ayah number
59
+ end_ayah: Ending ayah number (None for all)
60
+ """
61
+ self.surah = surah_number
62
+ self.start_ayah = start_ayah
63
+ self.end_ayah = end_ayah
64
+ self.verses = {}
65
+ self.verse_list = [] # Ordered list for sequential matching
66
+
67
+ async def load_verses(self, languages: List[str] = None):
68
+ """Load verses from Quran API"""
69
+ if languages is None:
70
+ languages = ["arabic", "english", "turkish"]
71
+
72
+ print(f"Loading Surah {self.surah} verses...")
73
+
74
+ self.verses = await fetch_all_translations(self.surah, languages)
75
+
76
+ # Filter by ayah range
77
+ if self.end_ayah:
78
+ self.verses = {
79
+ k: v for k, v in self.verses.items()
80
+ if self.start_ayah <= k <= self.end_ayah
81
+ }
82
+ else:
83
+ self.verses = {
84
+ k: v for k, v in self.verses.items()
85
+ if k >= self.start_ayah
86
+ }
87
+
88
+ # Create ordered list
89
+ self.verse_list = [
90
+ self.verses[k] for k in sorted(self.verses.keys())
91
+ ]
92
+
93
+ print(f"Loaded {len(self.verses)} verses")
94
+ return self.verses
95
+
96
+ def _calculate_similarity(self, text1: str, text2: str) -> float:
97
+ """Calculate similarity ratio between two texts"""
98
+ norm1 = normalize_arabic(text1)
99
+ norm2 = normalize_arabic(text2)
100
+ return SequenceMatcher(None, norm1, norm2).ratio()
101
+
102
+ def _find_best_match(self, segment_text: str, search_start: int = 0) -> Tuple[Optional[Dict], float, int]:
103
+ """
104
+ Find the best matching verse for a text segment
105
+
106
+ Args:
107
+ segment_text: Transcribed text to match
108
+ search_start: Start searching from this verse index
109
+
110
+ Returns:
111
+ Tuple of (matched_verse, confidence, verse_index)
112
+ """
113
+ best_match = None
114
+ best_confidence = 0
115
+ best_index = search_start
116
+
117
+ normalized_segment = normalize_arabic(segment_text)
118
+
119
+ # Search through verses starting from search_start
120
+ for i, verse in enumerate(self.verse_list[search_start:], search_start):
121
+ if "arabic" not in verse:
122
+ continue
123
+
124
+ verse_text = verse["arabic"]
125
+ normalized_verse = normalize_arabic(verse_text)
126
+
127
+ # Calculate different types of matches
128
+
129
+ # 1. Full match
130
+ full_ratio = self._calculate_similarity(segment_text, verse_text)
131
+
132
+ # 2. Segment is part of verse
133
+ if normalized_segment in normalized_verse:
134
+ partial_ratio = len(normalized_segment) / len(normalized_verse)
135
+ full_ratio = max(full_ratio, 0.7 + (partial_ratio * 0.3))
136
+
137
+ # 3. Verse is part of segment (for combined segments)
138
+ if normalized_verse in normalized_segment:
139
+ partial_ratio = len(normalized_verse) / len(normalized_segment)
140
+ full_ratio = max(full_ratio, 0.6 + (partial_ratio * 0.4))
141
+
142
+ # 4. Word overlap
143
+ segment_words = set(normalized_segment.split())
144
+ verse_words = set(normalized_verse.split())
145
+ if segment_words and verse_words:
146
+ overlap = len(segment_words & verse_words)
147
+ word_ratio = overlap / max(len(segment_words), len(verse_words))
148
+ full_ratio = max(full_ratio, word_ratio * 0.8)
149
+
150
+ if full_ratio > best_confidence:
151
+ best_confidence = full_ratio
152
+ best_match = verse
153
+ best_index = i
154
+
155
+ return best_match, best_confidence, best_index
156
+
157
+ def match_segments(
158
+ self,
159
+ segments: List[Dict],
160
+ min_confidence: float = 0.3,
161
+ languages: List[str] = None
162
+ ) -> List[MatchedVerse]:
163
+ """
164
+ Match transcribed segments to Quran verses
165
+
166
+ Args:
167
+ segments: List of transcribed segments with timing
168
+ min_confidence: Minimum confidence threshold
169
+ languages: List of languages to include
170
+
171
+ Returns:
172
+ List of matched verses with timing
173
+ """
174
+ if languages is None:
175
+ languages = ["arabic", "english", "turkish"]
176
+
177
+ matched = []
178
+ current_verse_index = 0
179
+ pending_segments = []
180
+
181
+ for segment in segments:
182
+ text = segment.get("text", "").strip()
183
+ if not text:
184
+ continue
185
+
186
+ start_time = segment.get("start", 0)
187
+ end_time = segment.get("end", 0)
188
+
189
+ # Try to match this segment
190
+ verse, confidence, verse_index = self._find_best_match(
191
+ text,
192
+ search_start=current_verse_index
193
+ )
194
+
195
+ if verse and confidence >= min_confidence:
196
+ # Build extra translations
197
+ extra_translations = {}
198
+ for lang in languages:
199
+ if lang not in ["arabic", "english", "turkish"]:
200
+ extra_translations[lang] = verse.get(lang, "")
201
+
202
+ # Good match found
203
+ matched_verse = MatchedVerse(
204
+ surah=verse.get("surah", self.surah),
205
+ ayah=verse.get("ayah", 0),
206
+ arabic=verse.get("arabic", ""),
207
+ english=verse.get("english", ""),
208
+ turkish=verse.get("turkish", ""),
209
+ start_time=start_time,
210
+ end_time=end_time,
211
+ confidence=confidence,
212
+ segment_text=text,
213
+ translations=extra_translations if extra_translations else None
214
+ )
215
+ matched.append(matched_verse)
216
+
217
+ # Move to next verse for sequential matching
218
+ current_verse_index = verse_index + 1
219
+
220
+ else:
221
+ # Low confidence - might be partial verse
222
+ # Try combining with next segment
223
+ pending_segments.append(segment)
224
+
225
+ # Handle any remaining pending segments
226
+ if pending_segments:
227
+ combined_text = " ".join(s.get("text", "") for s in pending_segments)
228
+ verse, confidence, _ = self._find_best_match(combined_text, current_verse_index)
229
+
230
+ if verse and confidence >= min_confidence:
231
+ # Build extra translations
232
+ extra_translations = {}
233
+ for lang in languages:
234
+ if lang not in ["arabic", "english", "turkish"]:
235
+ extra_translations[lang] = verse.get(lang, "")
236
+
237
+ matched_verse = MatchedVerse(
238
+ surah=verse.get("surah", self.surah),
239
+ ayah=verse.get("ayah", 0),
240
+ arabic=verse.get("arabic", ""),
241
+ english=verse.get("english", ""),
242
+ turkish=verse.get("turkish", ""),
243
+ start_time=pending_segments[0].get("start", 0),
244
+ end_time=pending_segments[-1].get("end", 0),
245
+ confidence=confidence,
246
+ segment_text=combined_text,
247
+ translations=extra_translations if extra_translations else None
248
+ )
249
+ matched.append(matched_verse)
250
+
251
+ return matched
252
+
253
+ def sequential_match(
254
+ self,
255
+ segments: List[Dict],
256
+ languages: List[str] = None,
257
+ bismillah_included: bool = True
258
+ ) -> List[MatchedVerse]:
259
+ """
260
+ Sequential matching - assumes verses are recited in order
261
+ This is more accurate for continuous recitation
262
+
263
+ Args:
264
+ segments: Transcribed segments
265
+ languages: List of languages to include
266
+ bismillah_included: Whether Bismillah is recited (not for Surah 9)
267
+
268
+ Returns:
269
+ List of matched verses
270
+ """
271
+ if languages is None:
272
+ languages = ["arabic", "english", "turkish"]
273
+
274
+ if not self.verse_list:
275
+ print("No verses loaded. Call load_verses() first.")
276
+ return []
277
+
278
+ matched = []
279
+ segment_index = 0
280
+ verse_index = 0
281
+
282
+ # Combine all segment text for analysis
283
+ all_text = " ".join(s.get("text", "") for s in segments)
284
+ all_text_normalized = normalize_arabic(all_text)
285
+
286
+ # Process each verse in order
287
+ for verse in self.verse_list:
288
+ if "arabic" not in verse:
289
+ continue
290
+
291
+ verse_text = verse["arabic"]
292
+ verse_normalized = normalize_arabic(verse_text)
293
+
294
+ # Find where this verse appears in the transcription
295
+ position = all_text_normalized.find(verse_normalized[:20]) # First 20 chars
296
+
297
+ # Find corresponding segment
298
+ best_segment = None
299
+ best_overlap = 0
300
+
301
+ for i, segment in enumerate(segments[segment_index:], segment_index):
302
+ seg_text = normalize_arabic(segment.get("text", ""))
303
+
304
+ # Check overlap with verse
305
+ overlap = len(set(seg_text.split()) & set(verse_normalized.split()))
306
+ if overlap > best_overlap:
307
+ best_overlap = overlap
308
+ best_segment = segment
309
+ segment_index = i
310
+
311
+ if best_segment:
312
+ # Build extra translations
313
+ extra_translations = {}
314
+ for lang in languages:
315
+ if lang not in ["arabic", "english", "turkish"]:
316
+ extra_translations[lang] = verse.get(lang, "")
317
+
318
+ matched_verse = MatchedVerse(
319
+ surah=verse.get("surah", self.surah),
320
+ ayah=verse.get("ayah", 0),
321
+ arabic=verse_text,
322
+ english=verse.get("english", ""),
323
+ turkish=verse.get("turkish", ""),
324
+ start_time=best_segment.get("start", 0),
325
+ end_time=best_segment.get("end", 0),
326
+ confidence=0.8, # Assumed high for sequential
327
+ segment_text=best_segment.get("text", ""),
328
+ translations=extra_translations if extra_translations else None
329
+ )
330
+ matched.append(matched_verse)
331
+
332
+ return matched
333
+
334
+
335
+ async def match_video_to_verses(
336
+ segments: List[Dict],
337
+ surah: int,
338
+ start_ayah: int = 1,
339
+ end_ayah: Optional[int] = None,
340
+ languages: List[str] = None
341
+ ) -> List[MatchedVerse]:
342
+ """
343
+ Main function to match video segments to Quran verses
344
+
345
+ Args:
346
+ segments: Transcribed segments from Whisper
347
+ surah: Surah number
348
+ start_ayah: Starting ayah
349
+ end_ayah: Ending ayah (None for all)
350
+ languages: List of languages to include in output
351
+
352
+ Returns:
353
+ List of matched verses with timing
354
+ """
355
+ if languages is None:
356
+ languages = ["arabic", "english", "turkish"]
357
+
358
+ matcher = VerseMatcher(surah, start_ayah, end_ayah)
359
+ await matcher.load_verses(languages)
360
+
361
+ # Try sequential matching first (more accurate for recitation)
362
+ matched = matcher.sequential_match(segments, languages)
363
+
364
+ # Fall back to fuzzy matching if sequential didn't work well
365
+ if len(matched) < len(matcher.verse_list) * 0.5:
366
+ print("Sequential matching incomplete, trying fuzzy matching...")
367
+ matched = matcher.match_segments(segments, languages=languages)
368
+
369
+ return matched
370
+
371
+
372
+ # For testing
373
+ if __name__ == "__main__":
374
+ async def test():
375
+ # Mock segments (simulating Whisper output)
376
+ mock_segments = [
377
+ {"id": 0, "start": 0.0, "end": 3.5, "text": "بسم الله الرحمن الرحيم"},
378
+ {"id": 1, "start": 3.5, "end": 6.5, "text": "الحمد لله رب العالمين"},
379
+ {"id": 2, "start": 6.5, "end": 9.0, "text": "الرحمن الرحيم"},
380
+ {"id": 3, "start": 9.0, "end": 12.0, "text": "مالك يوم الدين"},
381
+ {"id": 4, "start": 12.0, "end": 15.5, "text": "اياك نعبد واياك نستعين"},
382
+ {"id": 5, "start": 15.5, "end": 19.0, "text": "اهدنا الصراط المستقيم"},
383
+ {"id": 6, "start": 19.0, "end": 25.0, "text": "صراط الذين انعمت عليهم غير المغضوب عليهم ولا الضالين"},
384
+ ]
385
+
386
+ print("Testing Verse Matcher with Al-Fatiha...")
387
+ matched = await match_video_to_verses(mock_segments, surah=1)
388
+
389
+ print(f"\nMatched {len(matched)} verses:\n")
390
+ for verse in matched:
391
+ print(f"[{verse.start_time:.1f}s - {verse.end_time:.1f}s] Ayah {verse.ayah}")
392
+ print(f" AR: {verse.arabic[:50]}...")
393
+ print(f" EN: {verse.english[:50]}...")
394
+ print(f" TR: {verse.turkish[:50]}...")
395
+ print(f" Confidence: {verse.confidence:.2%}")
396
+ print()
397
+
398
+ asyncio.run(test())
src/video_generator.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video Generator
3
+ Creates videos with black screen or image background,
4
+ audio from reciters, and burned-in subtitles
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ import asyncio
10
+ import httpx
11
+ from pathlib import Path
12
+ from typing import Optional, List
13
+ import tempfile
14
+
15
+
16
+ class VideoGenerator:
17
+ def __init__(self, output_dir: str = "output", temp_dir: str = None):
18
+ self.output_dir = Path(output_dir)
19
+ self.output_dir.mkdir(exist_ok=True)
20
+ self.temp_dir = Path(temp_dir) if temp_dir else Path(tempfile.gettempdir()) / "quran_video"
21
+ self.temp_dir.mkdir(exist_ok=True)
22
+
23
+ async def download_surah_audio(
24
+ self,
25
+ surah: int,
26
+ reciter: str = "ar.alafasy",
27
+ start_ayah: int = 1,
28
+ end_ayah: Optional[int] = None
29
+ ) -> tuple:
30
+ print(f"Downloading audio for Surah {surah} by {reciter}...")
31
+ base_url = "https://api.alquran.cloud/v1"
32
+
33
+ async with httpx.AsyncClient(timeout=60.0) as client:
34
+ response = await client.get(f"{base_url}/surah/{surah}")
35
+ data = response.json()
36
+
37
+ if data.get("status") != "OK":
38
+ raise Exception(f"Failed to get surah info: {data}")
39
+
40
+ total_ayahs = data["data"]["numberOfAyahs"]
41
+ if end_ayah is None:
42
+ end_ayah = total_ayahs
43
+
44
+ audio_files = []
45
+ ayah_durations = []
46
+
47
+ for ayah in range(start_ayah, end_ayah + 1):
48
+ print(f" Downloading ayah {ayah}/{end_ayah}...")
49
+
50
+ ayah_response = await client.get(
51
+ f"{base_url}/ayah/{surah}:{ayah}/{reciter}"
52
+ )
53
+ ayah_data = ayah_response.json()
54
+
55
+ if ayah_data.get("status") != "OK":
56
+ print(f" Warning: Could not get ayah {ayah}")
57
+ continue
58
+
59
+ audio_url = ayah_data["data"].get("audio")
60
+ if not audio_url:
61
+ print(f" Warning: No audio URL for ayah {ayah}")
62
+ continue
63
+
64
+ audio_response = await client.get(audio_url)
65
+ audio_path = self.temp_dir / f"ayah_{surah}_{ayah}.mp3"
66
+
67
+ with open(audio_path, "wb") as f:
68
+ f.write(audio_response.content)
69
+
70
+ duration = self.get_audio_duration(str(audio_path))
71
+ ayah_durations.append((ayah, duration))
72
+ audio_files.append(str(audio_path))
73
+
74
+ if not audio_files:
75
+ raise Exception("No audio files downloaded")
76
+
77
+ ayah_timings = []
78
+ current_time = 0.0
79
+ for ayah_num, duration in ayah_durations:
80
+ start_time = current_time
81
+ end_time = current_time + duration
82
+ ayah_timings.append((ayah_num, start_time, end_time))
83
+ current_time = end_time
84
+
85
+ combined_audio = self.temp_dir / f"surah_{surah}_combined.mp3"
86
+
87
+ if len(audio_files) == 1:
88
+ import shutil
89
+ shutil.copy(audio_files[0], combined_audio)
90
+ else:
91
+ concat_list = self.temp_dir / "concat_list.txt"
92
+ with open(concat_list, "w") as f:
93
+ for audio_file in audio_files:
94
+ f.write(f"file '{audio_file}'\n")
95
+
96
+ cmd = [
97
+ "ffmpeg", "-y",
98
+ "-f", "concat",
99
+ "-safe", "0",
100
+ "-i", str(concat_list),
101
+ "-c", "copy",
102
+ str(combined_audio)
103
+ ]
104
+
105
+ result = subprocess.run(cmd, capture_output=True, text=True)
106
+ if result.returncode != 0:
107
+ print(f"FFmpeg error: {result.stderr}")
108
+ raise Exception("Failed to combine audio files")
109
+
110
+ print(f"Audio downloaded: {combined_audio}")
111
+ return str(combined_audio), ayah_timings
112
+
113
+ def get_audio_duration(self, audio_path: str) -> float:
114
+ cmd = [
115
+ "ffprobe",
116
+ "-v", "error",
117
+ "-show_entries", "format=duration",
118
+ "-of", "default=noprint_wrappers=1:nokey=1",
119
+ audio_path
120
+ ]
121
+ result = subprocess.run(cmd, capture_output=True, text=True)
122
+ if result.returncode == 0:
123
+ return float(result.stdout.strip())
124
+ return 0
125
+
126
+ def create_video_with_subtitles(
127
+ self,
128
+ audio_path: str,
129
+ srt_path: str,
130
+ output_path: str,
131
+ background_image: Optional[str] = None,
132
+ resolution: str = "1920x1080",
133
+ font_size: int = 24,
134
+ font_color: str = "white",
135
+ bg_color: str = "black"
136
+ ) -> str:
137
+ print(f"Creating video with subtitles...")
138
+
139
+ width, height = resolution.split("x")
140
+ duration = self.get_audio_duration(audio_path)
141
+
142
+ alignment = 8 # Top center
143
+
144
+ style = (
145
+ f"FontName=Arial,"
146
+ f"FontSize={font_size},"
147
+ f"PrimaryColour=&Hffffff&,"
148
+ f"OutlineColour=&H000000&,"
149
+ f"Outline=2,"
150
+ f"Alignment={alignment},"
151
+ f"MarginV=30,"
152
+ f"MarginL=50,"
153
+ f"MarginR=50,"
154
+ f"BorderStyle=1"
155
+ )
156
+
157
+ if background_image and Path(background_image).exists():
158
+ input_args = [
159
+ "-loop", "1",
160
+ "-i", background_image,
161
+ "-i", audio_path,
162
+ ]
163
+ filter_complex = (
164
+ f"[0:v]scale={width}:{height}:force_original_aspect_ratio=decrease,"
165
+ f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2:color={bg_color},"
166
+ f"subtitles='{srt_path.replace(chr(92), '/').replace(':', chr(92)+':')}':force_style='{style}'[v]"
167
+ )
168
+ else:
169
+ input_args = [
170
+ "-f", "lavfi",
171
+ "-i", f"color=c={bg_color}:s={resolution}:d={duration}",
172
+ "-i", audio_path,
173
+ ]
174
+ srt_escaped = srt_path.replace("\\", "/").replace(":", "\\:")
175
+ filter_complex = (
176
+ f"[0:v]subtitles='{srt_escaped}':force_style='{style}'[v]"
177
+ )
178
+
179
+ cmd = [
180
+ "ffmpeg", "-y",
181
+ *input_args,
182
+ "-filter_complex", filter_complex,
183
+ "-map", "[v]",
184
+ "-map", "1:a",
185
+ "-c:v", "libx264",
186
+ "-preset", "medium",
187
+ "-crf", "23",
188
+ "-c:a", "aac",
189
+ "-b:a", "192k",
190
+ "-shortest",
191
+ "-movflags", "+faststart",
192
+ output_path
193
+ ]
194
+
195
+ print(f"Running FFmpeg: {' '.join(cmd)}")
196
+
197
+ result = subprocess.run(cmd, capture_output=True, text=True)
198
+ if result.returncode != 0:
199
+ print(f"FFmpeg error: {result.stderr}")
200
+ cmd_simple = [
201
+ "ffmpeg", "-y",
202
+ "-f", "lavfi",
203
+ "-i", f"color=c={bg_color}:s={resolution}:d={duration}",
204
+ "-i", audio_path,
205
+ "-c:v", "libx264",
206
+ "-preset", "medium",
207
+ "-crf", "23",
208
+ "-c:a", "aac",
209
+ "-b:a", "192k",
210
+ "-shortest",
211
+ "-movflags", "+faststart",
212
+ output_path
213
+ ]
214
+ result = subprocess.run(cmd_simple, capture_output=True, text=True)
215
+ if result.returncode != 0:
216
+ raise Exception(f"Failed to create video: {result.stderr}")
217
+ print("Warning: Created video without burned-in subtitles")
218
+
219
+ print(f"Video created: {output_path}")
220
+ return output_path
221
+
222
+ def cleanup_temp_files(self, surah: int = None):
223
+ try:
224
+ for f in self.temp_dir.glob("*.mp3"):
225
+ if surah is None or f"surah_{surah}" in f.name or f"ayah_{surah}_" in f.name:
226
+ f.unlink()
227
+ for f in self.temp_dir.glob("*.txt"):
228
+ f.unlink()
229
+ except Exception as e:
230
+ print(f"Cleanup error: {e}")
231
+
232
+
233
+ async def generate_quran_video(
234
+ surah: int,
235
+ reciter: str,
236
+ srt_path: str,
237
+ output_path: str,
238
+ start_ayah: int = 1,
239
+ end_ayah: Optional[int] = None,
240
+ background_image: Optional[str] = None,
241
+ output_dir: str = "output"
242
+ ) -> str:
243
+ generator = VideoGenerator(output_dir=output_dir)
244
+
245
+ try:
246
+ audio_path = await generator.download_surah_audio(
247
+ surah=surah,
248
+ reciter=reciter,
249
+ start_ayah=start_ayah,
250
+ end_ayah=end_ayah
251
+ )
252
+
253
+ video_path = generator.create_video_with_subtitles(
254
+ audio_path=audio_path,
255
+ srt_path=srt_path,
256
+ output_path=output_path,
257
+ background_image=background_image
258
+ )
259
+
260
+ return video_path
261
+
262
+ finally:
263
+ generator.cleanup_temp_files(surah)
templates/index.html ADDED
@@ -0,0 +1,1976 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Quran SRT Generator</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ :root {
15
+ --primary: #1a5f4a;
16
+ --primary-light: #2d8a6e;
17
+ --secondary: #c9a227;
18
+ --bg-dark: #0f1419;
19
+ --bg-card: #1a1f26;
20
+ --bg-input: #0d1117;
21
+ --text: #e8e8e8;
22
+ --text-muted: #8b949e;
23
+ --border: #30363d;
24
+ --success: #2ea043;
25
+ --error: #f85149;
26
+ --warning: #d29922;
27
+ }
28
+
29
+ body {
30
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
31
+ background: var(--bg-dark);
32
+ color: var(--text);
33
+ min-height: 100vh;
34
+ line-height: 1.6;
35
+ }
36
+
37
+ .container {
38
+ max-width: 1200px;
39
+ margin: 0 auto;
40
+ padding: 2rem;
41
+ }
42
+
43
+ header {
44
+ text-align: center;
45
+ margin-bottom: 2rem;
46
+ padding: 2rem;
47
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
48
+ border-radius: 16px;
49
+ }
50
+
51
+ header h1 {
52
+ font-size: 2.5rem;
53
+ margin-bottom: 0.5rem;
54
+ color: white;
55
+ }
56
+
57
+ header p {
58
+ color: rgba(255, 255, 255, 0.85);
59
+ font-size: 1.1rem;
60
+ }
61
+
62
+ .arabic-text {
63
+ font-family: 'Traditional Arabic', 'Scheherazade', serif;
64
+ font-size: 1.8rem;
65
+ margin-bottom: 1rem;
66
+ color: var(--secondary);
67
+ }
68
+
69
+ .card {
70
+ background: var(--bg-card);
71
+ border: 1px solid var(--border);
72
+ border-radius: 12px;
73
+ padding: 1.5rem;
74
+ margin-bottom: 1.5rem;
75
+ }
76
+
77
+ .card h2 {
78
+ color: var(--primary-light);
79
+ margin-bottom: 1rem;
80
+ font-size: 1.3rem;
81
+ }
82
+
83
+ .form-group {
84
+ margin-bottom: 1.25rem;
85
+ }
86
+
87
+ label {
88
+ display: block;
89
+ margin-bottom: 0.5rem;
90
+ color: var(--text);
91
+ font-weight: 500;
92
+ }
93
+
94
+ select, input[type="number"], input[type="text"] {
95
+ width: 100%;
96
+ padding: 0.75rem 1rem;
97
+ background: var(--bg-input);
98
+ border: 1px solid var(--border);
99
+ border-radius: 8px;
100
+ color: var(--text);
101
+ font-size: 1rem;
102
+ transition: border-color 0.2s;
103
+ }
104
+
105
+ select:focus, input:focus {
106
+ outline: none;
107
+ border-color: var(--primary-light);
108
+ }
109
+
110
+ .row {
111
+ display: grid;
112
+ grid-template-columns: 1fr 1fr;
113
+ gap: 1rem;
114
+ }
115
+
116
+ .row-3 {
117
+ grid-template-columns: 1fr 1fr 1fr;
118
+ }
119
+
120
+ .upload-area {
121
+ border: 2px dashed var(--border);
122
+ border-radius: 12px;
123
+ padding: 3rem 2rem;
124
+ text-align: center;
125
+ cursor: pointer;
126
+ transition: all 0.3s;
127
+ background: var(--bg-input);
128
+ }
129
+
130
+ .upload-area:hover, .upload-area.dragover {
131
+ border-color: var(--primary-light);
132
+ background: rgba(45, 138, 110, 0.1);
133
+ }
134
+
135
+ .upload-area.has-file {
136
+ border-color: var(--success);
137
+ border-style: solid;
138
+ }
139
+
140
+ .upload-icon {
141
+ font-size: 3rem;
142
+ margin-bottom: 1rem;
143
+ }
144
+
145
+ .upload-area p {
146
+ color: var(--text-muted);
147
+ }
148
+
149
+ .file-name {
150
+ color: var(--success);
151
+ font-weight: 500;
152
+ margin-top: 0.5rem;
153
+ }
154
+
155
+ input[type="file"] {
156
+ display: none;
157
+ }
158
+
159
+ .btn {
160
+ display: inline-block;
161
+ padding: 0.875rem 2rem;
162
+ background: var(--primary);
163
+ color: white;
164
+ border: none;
165
+ border-radius: 8px;
166
+ font-size: 1rem;
167
+ font-weight: 600;
168
+ cursor: pointer;
169
+ transition: all 0.2s;
170
+ text-decoration: none;
171
+ }
172
+
173
+ .btn:hover {
174
+ background: var(--primary-light);
175
+ transform: translateY(-1px);
176
+ }
177
+
178
+ .btn:disabled {
179
+ background: var(--border);
180
+ cursor: not-allowed;
181
+ transform: none;
182
+ }
183
+
184
+ .btn-block {
185
+ display: block;
186
+ width: 100%;
187
+ text-align: center;
188
+ }
189
+
190
+ .btn-secondary {
191
+ background: transparent;
192
+ border: 2px solid var(--primary);
193
+ color: var(--primary-light);
194
+ }
195
+
196
+ .btn-secondary:hover {
197
+ background: var(--primary);
198
+ color: white;
199
+ }
200
+
201
+ .btn-sm {
202
+ padding: 0.5rem 1rem;
203
+ font-size: 0.875rem;
204
+ }
205
+
206
+ .btn-warning {
207
+ background: var(--warning);
208
+ }
209
+
210
+ .btn-warning:hover {
211
+ background: #e5a922;
212
+ }
213
+
214
+ .progress-container {
215
+ display: none;
216
+ margin-top: 1.5rem;
217
+ }
218
+
219
+ .progress-container.show {
220
+ display: block;
221
+ }
222
+
223
+ .progress-bar {
224
+ height: 8px;
225
+ background: var(--border);
226
+ border-radius: 4px;
227
+ overflow: hidden;
228
+ margin-bottom: 0.75rem;
229
+ }
230
+
231
+ .progress-fill {
232
+ height: 100%;
233
+ background: linear-gradient(90deg, var(--primary), var(--primary-light));
234
+ border-radius: 4px;
235
+ transition: width 0.3s ease;
236
+ width: 0%;
237
+ }
238
+
239
+ .progress-text {
240
+ color: var(--text-muted);
241
+ font-size: 0.9rem;
242
+ }
243
+
244
+ .alert {
245
+ padding: 1rem 1.25rem;
246
+ border-radius: 8px;
247
+ margin-bottom: 1rem;
248
+ }
249
+
250
+ .alert-warning {
251
+ background: rgba(201, 162, 39, 0.15);
252
+ border: 1px solid var(--secondary);
253
+ color: var(--secondary);
254
+ }
255
+
256
+ .alert-error {
257
+ background: rgba(248, 81, 73, 0.15);
258
+ border: 1px solid var(--error);
259
+ color: var(--error);
260
+ }
261
+
262
+ .alert-success {
263
+ background: rgba(46, 160, 67, 0.15);
264
+ border: 1px solid var(--success);
265
+ color: var(--success);
266
+ }
267
+
268
+ /* ================================
269
+ MODE SELECTOR
270
+ ================================ */
271
+
272
+ .mode-selector {
273
+ display: grid;
274
+ grid-template-columns: repeat(3, 1fr);
275
+ gap: 1rem;
276
+ margin-bottom: 1rem;
277
+ }
278
+
279
+ .mode-option input {
280
+ display: none;
281
+ }
282
+
283
+ .mode-card {
284
+ display: flex;
285
+ flex-direction: column;
286
+ align-items: center;
287
+ padding: 1.25rem;
288
+ background: var(--bg-input);
289
+ border: 2px solid var(--border);
290
+ border-radius: 12px;
291
+ cursor: pointer;
292
+ transition: all 0.2s;
293
+ }
294
+
295
+ .mode-card:hover {
296
+ border-color: var(--primary);
297
+ }
298
+
299
+ .mode-option input:checked + .mode-card {
300
+ border-color: var(--primary-light);
301
+ background: rgba(45, 138, 110, 0.15);
302
+ }
303
+
304
+ .mode-icon {
305
+ font-size: 2rem;
306
+ margin-bottom: 0.5rem;
307
+ }
308
+
309
+ .mode-label {
310
+ font-weight: 600;
311
+ color: var(--text);
312
+ }
313
+
314
+ .mode-desc {
315
+ font-size: 0.75rem;
316
+ color: var(--text-muted);
317
+ margin-top: 0.25rem;
318
+ }
319
+
320
+ .upload-section {
321
+ margin-top: 1rem;
322
+ }
323
+
324
+ .info-box {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 1rem;
328
+ padding: 1.5rem;
329
+ background: var(--bg-input);
330
+ border: 1px solid var(--border);
331
+ border-radius: 12px;
332
+ }
333
+
334
+ .info-icon {
335
+ font-size: 2.5rem;
336
+ }
337
+
338
+ .info-box p {
339
+ color: var(--text-muted);
340
+ }
341
+
342
+ /* ================================
343
+ CHECKBOX GROUP
344
+ ================================ */
345
+
346
+ .checkbox-group {
347
+ display: flex;
348
+ flex-wrap: wrap;
349
+ gap: 1rem;
350
+ }
351
+
352
+ .checkbox-option {
353
+ display: flex;
354
+ align-items: center;
355
+ gap: 0.5rem;
356
+ padding: 0.75rem 1rem;
357
+ background: var(--bg-input);
358
+ border: 1px solid var(--border);
359
+ border-radius: 8px;
360
+ cursor: pointer;
361
+ transition: all 0.2s;
362
+ }
363
+
364
+ .checkbox-option:hover {
365
+ border-color: var(--primary);
366
+ }
367
+
368
+ .checkbox-option input {
369
+ width: 18px;
370
+ height: 18px;
371
+ accent-color: var(--primary-light);
372
+ }
373
+
374
+ .checkbox-option input:checked + .checkbox-label {
375
+ color: var(--primary-light);
376
+ }
377
+
378
+ .checkbox-label {
379
+ font-weight: 500;
380
+ }
381
+
382
+ @media (max-width: 768px) {
383
+ .mode-selector {
384
+ grid-template-columns: 1fr;
385
+ }
386
+
387
+ .checkbox-group {
388
+ flex-direction: column;
389
+ }
390
+ }
391
+
392
+ /* ================================
393
+ EDITOR SECTION
394
+ ================================ */
395
+
396
+ .editor-section {
397
+ display: none;
398
+ }
399
+
400
+ .editor-section.show {
401
+ display: block;
402
+ }
403
+
404
+ .editor-layout {
405
+ display: grid;
406
+ grid-template-columns: 1fr 1fr;
407
+ gap: 1.5rem;
408
+ }
409
+
410
+ @media (max-width: 1024px) {
411
+ .editor-layout {
412
+ grid-template-columns: 1fr;
413
+ }
414
+ }
415
+
416
+ /* Video Preview */
417
+ .video-preview {
418
+ position: sticky;
419
+ top: 1rem;
420
+ }
421
+
422
+ .video-container {
423
+ position: relative;
424
+ background: #000;
425
+ border-radius: 12px;
426
+ overflow: hidden;
427
+ }
428
+
429
+ .video-container video {
430
+ width: 100%;
431
+ display: block;
432
+ }
433
+
434
+ .subtitle-overlay {
435
+ position: absolute;
436
+ bottom: 60px;
437
+ left: 50%;
438
+ transform: translateX(-50%);
439
+ width: 90%;
440
+ text-align: center;
441
+ padding: 0.75rem 1rem;
442
+ background: rgba(0, 0, 0, 0.8);
443
+ border-radius: 8px;
444
+ font-size: 1rem;
445
+ line-height: 1.5;
446
+ pointer-events: none;
447
+ }
448
+
449
+ .subtitle-overlay .arabic {
450
+ font-family: 'Traditional Arabic', 'Scheherazade', serif;
451
+ font-size: 1.4rem;
452
+ color: var(--secondary);
453
+ margin-bottom: 0.25rem;
454
+ }
455
+
456
+ .subtitle-overlay .english {
457
+ color: #fff;
458
+ font-size: 0.95rem;
459
+ }
460
+
461
+ .subtitle-overlay .turkish {
462
+ color: #aaa;
463
+ font-size: 0.9rem;
464
+ }
465
+
466
+ .video-controls {
467
+ display: flex;
468
+ gap: 0.5rem;
469
+ margin-top: 1rem;
470
+ flex-wrap: wrap;
471
+ }
472
+
473
+ .video-controls .btn {
474
+ flex: 1;
475
+ min-width: 80px;
476
+ }
477
+
478
+ .current-time {
479
+ text-align: center;
480
+ padding: 0.5rem;
481
+ background: var(--bg-input);
482
+ border-radius: 8px;
483
+ font-family: monospace;
484
+ font-size: 1.2rem;
485
+ margin-top: 0.5rem;
486
+ }
487
+
488
+ /* Timing Editor */
489
+ .verse-list {
490
+ max-height: 600px;
491
+ overflow-y: auto;
492
+ }
493
+
494
+ .verse-item {
495
+ background: var(--bg-input);
496
+ border: 1px solid var(--border);
497
+ border-radius: 8px;
498
+ padding: 1rem;
499
+ margin-bottom: 0.75rem;
500
+ transition: all 0.2s;
501
+ }
502
+
503
+ .verse-item:hover {
504
+ border-color: var(--primary);
505
+ }
506
+
507
+ .verse-item.active {
508
+ border-color: var(--secondary);
509
+ background: rgba(201, 162, 39, 0.1);
510
+ }
511
+
512
+ .verse-item.editing {
513
+ border-color: var(--primary-light);
514
+ box-shadow: 0 0 0 2px rgba(45, 138, 110, 0.3);
515
+ }
516
+
517
+ .verse-header {
518
+ display: flex;
519
+ justify-content: space-between;
520
+ align-items: center;
521
+ margin-bottom: 0.5rem;
522
+ }
523
+
524
+ .verse-number {
525
+ background: var(--primary);
526
+ color: white;
527
+ padding: 0.25rem 0.75rem;
528
+ border-radius: 20px;
529
+ font-size: 0.875rem;
530
+ font-weight: 600;
531
+ }
532
+
533
+ .verse-confidence {
534
+ font-size: 0.75rem;
535
+ color: var(--text-muted);
536
+ }
537
+
538
+ .verse-text {
539
+ font-family: 'Traditional Arabic', 'Scheherazade', serif;
540
+ font-size: 1.2rem;
541
+ color: var(--secondary);
542
+ margin-bottom: 0.5rem;
543
+ direction: rtl;
544
+ text-align: right;
545
+ }
546
+
547
+ .verse-translation {
548
+ font-size: 0.85rem;
549
+ color: var(--text-muted);
550
+ margin-bottom: 0.75rem;
551
+ }
552
+
553
+ .timing-inputs {
554
+ display: grid;
555
+ grid-template-columns: 1fr 1fr auto;
556
+ gap: 0.5rem;
557
+ align-items: end;
558
+ }
559
+
560
+ .timing-inputs label {
561
+ font-size: 0.75rem;
562
+ color: var(--text-muted);
563
+ margin-bottom: 0.25rem;
564
+ }
565
+
566
+ .timing-inputs input {
567
+ padding: 0.5rem;
568
+ font-family: monospace;
569
+ font-size: 0.9rem;
570
+ }
571
+
572
+ .timing-inputs .btn {
573
+ padding: 0.5rem 0.75rem;
574
+ }
575
+
576
+ /* Download Section */
577
+ .download-section {
578
+ margin-top: 1.5rem;
579
+ }
580
+
581
+ .download-grid {
582
+ display: grid;
583
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
584
+ gap: 1rem;
585
+ margin-top: 1rem;
586
+ }
587
+
588
+ .download-btn {
589
+ display: flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ gap: 0.5rem;
593
+ padding: 1rem;
594
+ background: var(--bg-input);
595
+ border: 1px solid var(--border);
596
+ border-radius: 8px;
597
+ color: var(--text);
598
+ text-decoration: none;
599
+ transition: all 0.2s;
600
+ }
601
+
602
+ .download-btn:hover {
603
+ border-color: var(--primary-light);
604
+ background: rgba(45, 138, 110, 0.1);
605
+ }
606
+
607
+ .download-btn .icon {
608
+ font-size: 1.5rem;
609
+ }
610
+
611
+ /* Features */
612
+ .features {
613
+ display: grid;
614
+ grid-template-columns: repeat(3, 1fr);
615
+ gap: 1rem;
616
+ margin-top: 2rem;
617
+ }
618
+
619
+ .feature {
620
+ text-align: center;
621
+ padding: 1.5rem;
622
+ background: var(--bg-card);
623
+ border-radius: 12px;
624
+ border: 1px solid var(--border);
625
+ }
626
+
627
+ .feature-icon {
628
+ font-size: 2rem;
629
+ margin-bottom: 0.75rem;
630
+ }
631
+
632
+ .feature h3 {
633
+ color: var(--primary-light);
634
+ margin-bottom: 0.5rem;
635
+ }
636
+
637
+ .feature p {
638
+ color: var(--text-muted);
639
+ font-size: 0.9rem;
640
+ }
641
+
642
+ footer {
643
+ text-align: center;
644
+ padding: 2rem;
645
+ color: var(--text-muted);
646
+ font-size: 0.9rem;
647
+ }
648
+
649
+ /* Tabs */
650
+ .tabs {
651
+ display: flex;
652
+ gap: 0.5rem;
653
+ margin-bottom: 1rem;
654
+ border-bottom: 1px solid var(--border);
655
+ padding-bottom: 0.5rem;
656
+ }
657
+
658
+ .tab {
659
+ padding: 0.5rem 1rem;
660
+ background: transparent;
661
+ border: none;
662
+ color: var(--text-muted);
663
+ cursor: pointer;
664
+ border-radius: 8px 8px 0 0;
665
+ transition: all 0.2s;
666
+ }
667
+
668
+ .tab:hover {
669
+ color: var(--text);
670
+ background: var(--bg-input);
671
+ }
672
+
673
+ .tab.active {
674
+ color: var(--primary-light);
675
+ background: var(--bg-input);
676
+ border-bottom: 2px solid var(--primary-light);
677
+ }
678
+
679
+ .hidden {
680
+ display: none !important;
681
+ }
682
+
683
+ @media (max-width: 768px) {
684
+ .container {
685
+ padding: 1rem;
686
+ }
687
+
688
+ header h1 {
689
+ font-size: 1.8rem;
690
+ }
691
+
692
+ .row, .row-3 {
693
+ grid-template-columns: 1fr;
694
+ }
695
+
696
+ .features {
697
+ grid-template-columns: 1fr;
698
+ }
699
+
700
+ .timing-inputs {
701
+ grid-template-columns: 1fr 1fr;
702
+ }
703
+
704
+ .timing-inputs .btn {
705
+ grid-column: span 2;
706
+ }
707
+ }
708
+ </style>
709
+ </head>
710
+ <body>
711
+ <div class="container">
712
+ <header>
713
+ <div class="arabic-text">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</div>
714
+ <h1>Quran SRT Generator</h1>
715
+ <p>Generate multilingual subtitles for Quran recitation videos</p>
716
+ </header>
717
+
718
+ {% if not whisper_available %}
719
+ <div class="alert alert-warning">
720
+ <strong>Note:</strong> Whisper is not installed. The app will work in demo mode.
721
+ Install with: <code>pip install openai-whisper</code>
722
+ </div>
723
+ {% endif %}
724
+
725
+ <!-- UPLOAD SECTION -->
726
+ <div id="uploadSection">
727
+ <form id="uploadForm">
728
+ <div class="card">
729
+ <h2>1. Select Surah & Range</h2>
730
+
731
+ <div class="form-group">
732
+ <label for="surah">Surah</label>
733
+ <select id="surah" name="surah" required>
734
+ <option value="">Loading surahs...</option>
735
+ </select>
736
+ </div>
737
+
738
+ <div class="row">
739
+ <div class="form-group">
740
+ <label for="startAyah">Start Ayah</label>
741
+ <input type="number" id="startAyah" name="start_ayah" value="1" min="1">
742
+ </div>
743
+ <div class="form-group">
744
+ <label for="endAyah">End Ayah (leave empty for all)</label>
745
+ <input type="number" id="endAyah" name="end_ayah" min="1">
746
+ </div>
747
+ </div>
748
+ </div>
749
+
750
+ <div class="card">
751
+ <h2>2. Input Mode</h2>
752
+
753
+ <div class="mode-selector">
754
+ <label class="mode-option">
755
+ <input type="radio" name="inputMode" value="video" checked>
756
+ <div class="mode-card">
757
+ <span class="mode-icon">🎬</span>
758
+ <span class="mode-label">Video/Audio</span>
759
+ <span class="mode-desc">Upload existing recitation</span>
760
+ </div>
761
+ </label>
762
+ <label class="mode-option">
763
+ <input type="radio" name="inputMode" value="image">
764
+ <div class="mode-card">
765
+ <span class="mode-icon">🖼️</span>
766
+ <span class="mode-label">Image</span>
767
+ <span class="mode-desc">Upload background image</span>
768
+ </div>
769
+ </label>
770
+ <label class="mode-option">
771
+ <input type="radio" name="inputMode" value="black">
772
+ <div class="mode-card">
773
+ <span class="mode-icon">⬛</span>
774
+ <span class="mode-label">Black Screen</span>
775
+ <span class="mode-desc">Plain black background</span>
776
+ </div>
777
+ </label>
778
+ </div>
779
+
780
+ <!-- Video/Audio Upload -->
781
+ <div id="videoUploadArea" class="upload-section">
782
+ <div class="upload-area" id="uploadArea">
783
+ <div class="upload-icon">📁</div>
784
+ <p>Drag & drop your video/audio file here</p>
785
+ <p>or click to browse</p>
786
+ <p class="file-name" id="fileName"></p>
787
+ <input type="file" id="fileInput" name="file" accept=".mp4,.mkv,.avi,.mov,.webm,.mp3,.wav,.m4a">
788
+ </div>
789
+ </div>
790
+
791
+ <!-- Image Upload -->
792
+ <div id="imageUploadArea" class="upload-section hidden">
793
+ <div class="upload-area" id="imageUpload">
794
+ <div class="upload-icon">🖼️</div>
795
+ <p>Drag & drop background image</p>
796
+ <p>or click to browse</p>
797
+ <p class="file-name" id="imageName"></p>
798
+ <input type="file" id="imageInput" name="image" accept=".jpg,.jpeg,.png,.webp">
799
+ </div>
800
+ </div>
801
+
802
+ <!-- Black Screen Info -->
803
+ <div id="blackScreenInfo" class="upload-section hidden">
804
+ <div class="info-box">
805
+ <span class="info-icon">⬛</span>
806
+ <p>Video will be generated with a black background</p>
807
+ </div>
808
+ </div>
809
+
810
+ <!-- Audio Reciter (for image/black modes) -->
811
+ <div id="reciterSection" class="hidden" style="margin-top: 1rem;">
812
+ <div class="form-group">
813
+ <label for="reciter">Audio Reciter</label>
814
+ <select id="reciter" name="reciter">
815
+ <option value="">Loading reciters...</option>
816
+ </select>
817
+ </div>
818
+ </div>
819
+ </div>
820
+
821
+ <div class="card">
822
+ <h2>3. Languages</h2>
823
+ <p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1rem;">Select which languages to include in subtitles</p>
824
+
825
+ <div class="checkbox-group" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;">
826
+ <label class="checkbox-option">
827
+ <input type="checkbox" name="languages" value="arabic" checked>
828
+ <span class="checkbox-label">Arabic (Original)</span>
829
+ </label>
830
+ <label class="checkbox-option">
831
+ <input type="checkbox" name="languages" value="english" checked>
832
+ <span class="checkbox-label">English (Saheeh)</span>
833
+ </label>
834
+ <label class="checkbox-option">
835
+ <input type="checkbox" name="languages" value="turkish">
836
+ <span class="checkbox-label">Turkish (Diyanet)</span>
837
+ </label>
838
+ <label class="checkbox-option">
839
+ <input type="checkbox" name="languages" value="urdu">
840
+ <span class="checkbox-label">Urdu</span>
841
+ </label>
842
+ <label class="checkbox-option">
843
+ <input type="checkbox" name="languages" value="french">
844
+ <span class="checkbox-label">French</span>
845
+ </label>
846
+ <label class="checkbox-option">
847
+ <input type="checkbox" name="languages" value="german">
848
+ <span class="checkbox-label">German</span>
849
+ </label>
850
+ <label class="checkbox-option">
851
+ <input type="checkbox" name="languages" value="indonesian">
852
+ <span class="checkbox-label">Indonesian</span>
853
+ </label>
854
+ <label class="checkbox-option">
855
+ <input type="checkbox" name="languages" value="spanish">
856
+ <span class="checkbox-label">Spanish</span>
857
+ </label>
858
+ <label class="checkbox-option">
859
+ <input type="checkbox" name="languages" value="russian">
860
+ <span class="checkbox-label">Russian</span>
861
+ </label>
862
+ <label class="checkbox-option">
863
+ <input type="checkbox" name="languages" value="bengali">
864
+ <span class="checkbox-label">Bengali</span>
865
+ </label>
866
+ <label class="checkbox-option">
867
+ <input type="checkbox" name="languages" value="chinese">
868
+ <span class="checkbox-label">Chinese</span>
869
+ </label>
870
+ <label class="checkbox-option">
871
+ <input type="checkbox" name="languages" value="malay">
872
+ <span class="checkbox-label">Malay</span>
873
+ </label>
874
+ <label class="checkbox-option">
875
+ <input type="checkbox" name="languages" value="persian">
876
+ <span class="checkbox-label">Persian/Farsi</span>
877
+ </label>
878
+ <label class="checkbox-option">
879
+ <input type="checkbox" name="languages" value="dutch">
880
+ <span class="checkbox-label">Dutch</span>
881
+ </label>
882
+ <label class="checkbox-option">
883
+ <input type="checkbox" name="languages" value="italian">
884
+ <span class="checkbox-label">Italian</span>
885
+ </label>
886
+ <label class="checkbox-option">
887
+ <input type="checkbox" name="languages" value="portuguese">
888
+ <span class="checkbox-label">Portuguese</span>
889
+ </label>
890
+ </div>
891
+ </div>
892
+
893
+ <div class="card">
894
+ <h2>4. Options</h2>
895
+
896
+ <div class="row">
897
+ <div class="form-group">
898
+ <label for="separator">Language Separator</label>
899
+ <input type="text" id="separator" name="separator" value=" | ">
900
+ </div>
901
+ <div class="form-group" id="whisperModelGroup">
902
+ <label for="modelSize">Whisper Model</label>
903
+ <select id="modelSize" name="model_size">
904
+ <option value="tiny">Tiny (Fastest)</option>
905
+ <option value="base">Base (Fast)</option>
906
+ <option value="small">Small (Balanced)</option>
907
+ <option value="medium" selected>Medium (Recommended)</option>
908
+ <option value="large">Large (Most Accurate)</option>
909
+ </select>
910
+ </div>
911
+ </div>
912
+ </div>
913
+
914
+ <!-- Offline Mode -->
915
+ <div class="card">
916
+ <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
917
+ <div>
918
+ <h2 style="margin: 0;">Offline Mode</h2>
919
+ <p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 0.25rem;">
920
+ Download Quran data (~5-10 MB) for faster processing
921
+ </p>
922
+ </div>
923
+ <div style="display: flex; gap: 0.75rem; align-items: center;">
924
+ <label class="checkbox-option" style="margin: 0;">
925
+ <input type="checkbox" id="offlineModeToggle">
926
+ <span class="checkbox-label">Use Offline Only</span>
927
+ </label>
928
+ <button type="button" class="btn btn-secondary" id="downloadQuranBtn">
929
+ Download Quran Data
930
+ </button>
931
+ </div>
932
+ </div>
933
+ <div id="cacheStatus" style="margin-top: 1rem; padding: 0.75rem; background: var(--bg-input); border-radius: 8px; display: none;">
934
+ <p style="color: var(--text-muted); font-size: 0.9rem;">
935
+ <span id="cacheStatusText">Checking cache...</span>
936
+ </p>
937
+ </div>
938
+ <div id="downloadProgress" class="hidden" style="margin-top: 1rem;">
939
+ <div class="progress-bar">
940
+ <div class="progress-fill" id="quranProgressFill"></div>
941
+ </div>
942
+ <p class="progress-text" id="quranProgressText">Downloading...</p>
943
+ </div>
944
+ </div>
945
+
946
+ <button type="submit" class="btn btn-block" id="submitBtn">
947
+ Generate Subtitles
948
+ </button>
949
+
950
+ <div class="progress-container" id="progressContainer">
951
+ <div class="progress-bar">
952
+ <div class="progress-fill" id="progressFill"></div>
953
+ </div>
954
+ <p class="progress-text" id="progressText">Uploading...</p>
955
+ </div>
956
+ </form>
957
+ </div>
958
+
959
+ <!-- EDITOR SECTION -->
960
+ <div class="editor-section" id="editorSection">
961
+ <div class="card">
962
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
963
+ <h2 style="margin: 0;">Edit Timing & Preview</h2>
964
+ <button class="btn btn-secondary btn-sm" id="backBtn">Back to Upload</button>
965
+ </div>
966
+
967
+ <div class="alert alert-success" id="editorMessage"></div>
968
+
969
+ <div class="editor-layout">
970
+ <!-- Video Preview -->
971
+ <div class="video-preview">
972
+ <div class="video-container">
973
+ <video id="videoPlayer" controls>
974
+ Your browser does not support the video tag.
975
+ </video>
976
+ <div class="subtitle-overlay" id="subtitleOverlay">
977
+ <div class="arabic"></div>
978
+ <div class="english"></div>
979
+ <div class="turkish"></div>
980
+ </div>
981
+ </div>
982
+ <div class="current-time" id="currentTime">00:00:00.000</div>
983
+ <div class="video-controls">
984
+ <button class="btn btn-sm" id="seekBack5">-5s</button>
985
+ <button class="btn btn-sm" id="seekBack1">-1s</button>
986
+ <button class="btn btn-sm btn-secondary" id="playPause">Play</button>
987
+ <button class="btn btn-sm" id="seekForward1">+1s</button>
988
+ <button class="btn btn-sm" id="seekForward5">+5s</button>
989
+ </div>
990
+ <div class="video-controls" style="margin-top: 0.5rem;">
991
+ <button class="btn btn-sm btn-warning" id="setStartTime">Set Start</button>
992
+ <button class="btn btn-sm btn-warning" id="setEndTime">Set End</button>
993
+ </div>
994
+ </div>
995
+
996
+ <!-- Verse List -->
997
+ <div class="timing-editor">
998
+ <h3 style="margin-bottom: 1rem;">Verses (click to edit)</h3>
999
+ <div class="verse-list" id="verseList">
1000
+ <!-- Verses will be inserted here -->
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+
1006
+ <!-- Download Section -->
1007
+ <div class="card download-section">
1008
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1009
+ <h2 style="margin: 0;">Download Subtitles</h2>
1010
+ <button class="btn" id="regenerateBtn">Regenerate with New Timings</button>
1011
+ </div>
1012
+
1013
+ <div class="download-grid" id="downloadGrid">
1014
+ <!-- Downloads will be inserted here -->
1015
+ </div>
1016
+ </div>
1017
+ </div>
1018
+
1019
+ <div class="features" id="featuresSection">
1020
+ <div class="feature">
1021
+ <div class="feature-icon">🌍</div>
1022
+ <h3>3 Languages</h3>
1023
+ <p>Arabic, English & Turkish on one line</p>
1024
+ </div>
1025
+ <div class="feature">
1026
+ <div class="feature-icon">⚡</div>
1027
+ <h3>AI-Powered</h3>
1028
+ <p>OpenAI Whisper for accurate transcription</p>
1029
+ </div>
1030
+ <div class="feature">
1031
+ <div class="feature-icon">📋</div>
1032
+ <h3>JSON Export</h3>
1033
+ <p>Precise timing for video editors</p>
1034
+ </div>
1035
+ </div>
1036
+
1037
+ <footer>
1038
+ <p>Quran SRT Generator - 100% Free & Open Source</p>
1039
+ <p>Translations from trusted sources (Saheeh International, Diyanet)</p>
1040
+ </footer>
1041
+ </div>
1042
+
1043
+ <script>
1044
+ // ================================
1045
+ // STATE
1046
+ // ================================
1047
+ let selectedFile = null;
1048
+ let selectedImage = null;
1049
+ let currentTaskId = null;
1050
+ let statusPollInterval = null;
1051
+ let verses = [];
1052
+ let selectedVerseIndex = null;
1053
+ let currentMode = 'video'; // 'video', 'image', 'black'
1054
+ let surahData = {}; // Store surah info including ayah count
1055
+
1056
+ // ================================
1057
+ // DOM ELEMENTS
1058
+ // ================================
1059
+ const uploadForm = document.getElementById('uploadForm');
1060
+ const uploadArea = document.getElementById('uploadArea');
1061
+ const fileInput = document.getElementById('fileInput');
1062
+ const fileName = document.getElementById('fileName');
1063
+ const surahSelect = document.getElementById('surah');
1064
+ const submitBtn = document.getElementById('submitBtn');
1065
+ const progressContainer = document.getElementById('progressContainer');
1066
+ const progressFill = document.getElementById('progressFill');
1067
+ const progressText = document.getElementById('progressText');
1068
+
1069
+ const uploadSection = document.getElementById('uploadSection');
1070
+ const editorSection = document.getElementById('editorSection');
1071
+ const featuresSection = document.getElementById('featuresSection');
1072
+
1073
+ const videoPlayer = document.getElementById('videoPlayer');
1074
+ const subtitleOverlay = document.getElementById('subtitleOverlay');
1075
+ const currentTimeDisplay = document.getElementById('currentTime');
1076
+ const verseList = document.getElementById('verseList');
1077
+ const downloadGrid = document.getElementById('downloadGrid');
1078
+ const editorMessage = document.getElementById('editorMessage');
1079
+
1080
+ // Mode elements
1081
+ const videoUploadArea = document.getElementById('videoUploadArea');
1082
+ const imageUploadArea = document.getElementById('imageUploadArea');
1083
+ const blackScreenInfo = document.getElementById('blackScreenInfo');
1084
+ const reciterSection = document.getElementById('reciterSection');
1085
+ const whisperModelGroup = document.getElementById('whisperModelGroup');
1086
+ const reciterSelect = document.getElementById('reciter');
1087
+ const imageUpload = document.getElementById('imageUpload');
1088
+ const imageInput = document.getElementById('imageInput');
1089
+ const imageName = document.getElementById('imageName');
1090
+
1091
+ // ================================
1092
+ // INITIALIZATION
1093
+ // ================================
1094
+ async function loadSurahs() {
1095
+ try {
1096
+ const response = await fetch('/api/surahs');
1097
+ const data = await response.json();
1098
+
1099
+ if (data.success) {
1100
+ surahSelect.innerHTML = '<option value="">Select a Surah</option>';
1101
+ data.surahs.forEach(surah => {
1102
+ // Store surah data for ayah validation
1103
+ surahData[surah.number] = {
1104
+ name: surah.name,
1105
+ englishName: surah.englishName,
1106
+ numberOfAyahs: surah.numberOfAyahs
1107
+ };
1108
+
1109
+ const option = document.createElement('option');
1110
+ option.value = surah.number;
1111
+ option.textContent = `${surah.number}. ${surah.englishName} (${surah.name}) - ${surah.numberOfAyahs} ayahs`;
1112
+ surahSelect.appendChild(option);
1113
+ });
1114
+ }
1115
+ } catch (error) {
1116
+ console.error('Failed to load surahs:', error);
1117
+ surahSelect.innerHTML = '<option value="">Failed to load surahs</option>';
1118
+ }
1119
+ }
1120
+
1121
+ // Update ayah range inputs when surah is selected
1122
+ surahSelect.addEventListener('change', () => {
1123
+ const surahNum = parseInt(surahSelect.value);
1124
+ const startAyah = document.getElementById('startAyah');
1125
+ const endAyah = document.getElementById('endAyah');
1126
+
1127
+ if (surahNum && surahData[surahNum]) {
1128
+ const maxAyahs = surahData[surahNum].numberOfAyahs;
1129
+
1130
+ // Set max values
1131
+ startAyah.max = maxAyahs;
1132
+ endAyah.max = maxAyahs;
1133
+
1134
+ // Set placeholder with max info
1135
+ endAyah.placeholder = `Max: ${maxAyahs}`;
1136
+
1137
+ // Reset values if they exceed max
1138
+ if (parseInt(startAyah.value) > maxAyahs) {
1139
+ startAyah.value = 1;
1140
+ }
1141
+ if (parseInt(endAyah.value) > maxAyahs) {
1142
+ endAyah.value = '';
1143
+ }
1144
+
1145
+ // Validate start ayah
1146
+ startAyah.value = Math.max(1, Math.min(parseInt(startAyah.value) || 1, maxAyahs));
1147
+ } else {
1148
+ startAyah.max = '';
1149
+ endAyah.max = '';
1150
+ endAyah.placeholder = '';
1151
+ }
1152
+ });
1153
+
1154
+ // Validate ayah inputs on change
1155
+ document.getElementById('startAyah').addEventListener('change', (e) => {
1156
+ const surahNum = parseInt(surahSelect.value);
1157
+ if (surahNum && surahData[surahNum]) {
1158
+ const max = surahData[surahNum].numberOfAyahs;
1159
+ let val = parseInt(e.target.value) || 1;
1160
+ e.target.value = Math.max(1, Math.min(val, max));
1161
+ }
1162
+ });
1163
+
1164
+ document.getElementById('endAyah').addEventListener('change', (e) => {
1165
+ const surahNum = parseInt(surahSelect.value);
1166
+ if (surahNum && surahData[surahNum]) {
1167
+ const max = surahData[surahNum].numberOfAyahs;
1168
+ const startVal = parseInt(document.getElementById('startAyah').value) || 1;
1169
+ let val = parseInt(e.target.value);
1170
+ if (val) {
1171
+ e.target.value = Math.max(startVal, Math.min(val, max));
1172
+ }
1173
+ }
1174
+ });
1175
+
1176
+ async function loadReciters() {
1177
+ try {
1178
+ const response = await fetch('/api/reciters');
1179
+ const data = await response.json();
1180
+
1181
+ if (data.success) {
1182
+ reciterSelect.innerHTML = '';
1183
+ data.reciters.forEach(reciter => {
1184
+ const option = document.createElement('option');
1185
+ option.value = reciter.identifier;
1186
+ option.textContent = reciter.style
1187
+ ? `${reciter.englishName} (${reciter.style})`
1188
+ : reciter.englishName;
1189
+ reciterSelect.appendChild(option);
1190
+ });
1191
+ }
1192
+ } catch (error) {
1193
+ console.error('Failed to load reciters:', error);
1194
+ reciterSelect.innerHTML = '<option value="ar.alafasy">Mishary Alafasy</option>';
1195
+ }
1196
+ }
1197
+
1198
+ // ================================
1199
+ // MODE SWITCHING
1200
+ // ================================
1201
+ document.querySelectorAll('input[name="inputMode"]').forEach(radio => {
1202
+ radio.addEventListener('change', (e) => {
1203
+ currentMode = e.target.value;
1204
+ updateModeUI();
1205
+ });
1206
+ });
1207
+
1208
+ function updateModeUI() {
1209
+ // Hide all upload areas
1210
+ videoUploadArea.classList.add('hidden');
1211
+ imageUploadArea.classList.add('hidden');
1212
+ blackScreenInfo.classList.add('hidden');
1213
+ reciterSection.classList.add('hidden');
1214
+ whisperModelGroup.classList.remove('hidden');
1215
+
1216
+ // Show relevant area based on mode
1217
+ if (currentMode === 'video') {
1218
+ videoUploadArea.classList.remove('hidden');
1219
+ } else if (currentMode === 'image') {
1220
+ imageUploadArea.classList.remove('hidden');
1221
+ reciterSection.classList.remove('hidden');
1222
+ whisperModelGroup.classList.add('hidden');
1223
+ } else if (currentMode === 'black') {
1224
+ blackScreenInfo.classList.remove('hidden');
1225
+ reciterSection.classList.remove('hidden');
1226
+ whisperModelGroup.classList.add('hidden');
1227
+ }
1228
+ }
1229
+
1230
+ // ================================
1231
+ // FILE UPLOAD (Video/Audio)
1232
+ // ================================
1233
+ uploadArea.addEventListener('click', () => fileInput.click());
1234
+
1235
+ uploadArea.addEventListener('dragover', (e) => {
1236
+ e.preventDefault();
1237
+ uploadArea.classList.add('dragover');
1238
+ });
1239
+
1240
+ uploadArea.addEventListener('dragleave', () => {
1241
+ uploadArea.classList.remove('dragover');
1242
+ });
1243
+
1244
+ uploadArea.addEventListener('drop', (e) => {
1245
+ e.preventDefault();
1246
+ uploadArea.classList.remove('dragover');
1247
+ const files = e.dataTransfer.files;
1248
+ if (files.length > 0) {
1249
+ handleFile(files[0]);
1250
+ }
1251
+ });
1252
+
1253
+ fileInput.addEventListener('change', (e) => {
1254
+ if (e.target.files.length > 0) {
1255
+ handleFile(e.target.files[0]);
1256
+ }
1257
+ });
1258
+
1259
+ function handleFile(file) {
1260
+ selectedFile = file;
1261
+ fileName.textContent = file.name;
1262
+ uploadArea.classList.add('has-file');
1263
+ }
1264
+
1265
+ // ================================
1266
+ // IMAGE UPLOAD
1267
+ // ================================
1268
+ imageUpload.addEventListener('click', () => imageInput.click());
1269
+
1270
+ imageUpload.addEventListener('dragover', (e) => {
1271
+ e.preventDefault();
1272
+ imageUpload.classList.add('dragover');
1273
+ });
1274
+
1275
+ imageUpload.addEventListener('dragleave', () => {
1276
+ imageUpload.classList.remove('dragover');
1277
+ });
1278
+
1279
+ imageUpload.addEventListener('drop', (e) => {
1280
+ e.preventDefault();
1281
+ imageUpload.classList.remove('dragover');
1282
+ const files = e.dataTransfer.files;
1283
+ if (files.length > 0) {
1284
+ handleImage(files[0]);
1285
+ }
1286
+ });
1287
+
1288
+ imageInput.addEventListener('change', (e) => {
1289
+ if (e.target.files.length > 0) {
1290
+ handleImage(e.target.files[0]);
1291
+ }
1292
+ });
1293
+
1294
+ function handleImage(file) {
1295
+ selectedImage = file;
1296
+ imageName.textContent = file.name;
1297
+ imageUpload.classList.add('has-file');
1298
+ }
1299
+
1300
+ // ================================
1301
+ // LANGUAGE SELECTION
1302
+ // ================================
1303
+ function getSelectedLanguages() {
1304
+ const languages = [];
1305
+ document.querySelectorAll('input[name="languages"]:checked').forEach(cb => {
1306
+ languages.push(cb.value);
1307
+ });
1308
+ return languages;
1309
+ }
1310
+
1311
+ // ================================
1312
+ // OFFLINE MODE & CACHE
1313
+ // ================================
1314
+ async function checkCacheStatus() {
1315
+ try {
1316
+ const response = await fetch('/api/cache-status');
1317
+ const data = await response.json();
1318
+
1319
+ if (data.success) {
1320
+ const cacheDiv = document.getElementById('cacheStatus');
1321
+ const cacheText = document.getElementById('cacheStatusText');
1322
+ const offlineToggle = document.getElementById('offlineModeToggle');
1323
+
1324
+ cacheDiv.style.display = 'block';
1325
+ offlineToggle.checked = data.offline_mode;
1326
+
1327
+ if (data.cache.is_complete) {
1328
+ cacheText.innerHTML = `<span style="color: var(--success);">All 114 surahs cached.</span> Ready for offline use.`;
1329
+ } else if (data.cache.total_cached > 0) {
1330
+ cacheText.innerHTML = `${data.cache.total_cached}/114 surahs cached. <a href="#" onclick="event.preventDefault(); document.getElementById('downloadQuranBtn').click();">Download remaining</a>`;
1331
+ } else {
1332
+ cacheText.innerHTML = `No data cached. <a href="#" onclick="event.preventDefault(); document.getElementById('downloadQuranBtn').click();">Download for offline use</a>`;
1333
+ }
1334
+ }
1335
+ } catch (error) {
1336
+ console.error('Failed to check cache status:', error);
1337
+ }
1338
+ }
1339
+
1340
+ // Toggle offline mode
1341
+ document.getElementById('offlineModeToggle').addEventListener('change', async (e) => {
1342
+ const enabled = e.target.checked;
1343
+ try {
1344
+ const response = await fetch(`/api/offline-mode?enabled=${enabled}`, { method: 'POST' });
1345
+ const data = await response.json();
1346
+
1347
+ if (!data.success) {
1348
+ e.target.checked = !enabled; // Revert
1349
+ alert('Failed to toggle offline mode');
1350
+ }
1351
+ } catch (error) {
1352
+ e.target.checked = !enabled; // Revert
1353
+ console.error('Failed to toggle offline mode:', error);
1354
+ }
1355
+ });
1356
+
1357
+ document.getElementById('downloadQuranBtn').addEventListener('click', async () => {
1358
+ const btn = document.getElementById('downloadQuranBtn');
1359
+ const progress = document.getElementById('downloadProgress');
1360
+ const progressFill = document.getElementById('quranProgressFill');
1361
+ const progressText = document.getElementById('quranProgressText');
1362
+
1363
+ btn.disabled = true;
1364
+ btn.textContent = 'Downloading...';
1365
+ progress.classList.remove('hidden');
1366
+
1367
+ try {
1368
+ const response = await fetch('/api/download-quran', { method: 'POST' });
1369
+ const data = await response.json();
1370
+
1371
+ if (data.success) {
1372
+ // Poll for progress
1373
+ let percent = 0;
1374
+ const interval = setInterval(() => {
1375
+ percent += 2;
1376
+ if (percent >= 100) {
1377
+ clearInterval(interval);
1378
+ progressFill.style.width = '100%';
1379
+ progressText.textContent = 'Download complete!';
1380
+ btn.textContent = 'Downloaded';
1381
+ // Refresh cache status
1382
+ setTimeout(checkCacheStatus, 1000);
1383
+ } else {
1384
+ progressFill.style.width = percent + '%';
1385
+ progressText.textContent = `Downloading... ${percent}%`;
1386
+ }
1387
+ }, 500);
1388
+ }
1389
+ } catch (error) {
1390
+ progressText.textContent = 'Error: ' + error.message;
1391
+ btn.disabled = false;
1392
+ btn.textContent = 'Retry Download';
1393
+ }
1394
+ });
1395
+
1396
+ // ================================
1397
+ // FORM SUBMISSION
1398
+ // ================================
1399
+ uploadForm.addEventListener('submit', async (e) => {
1400
+ e.preventDefault();
1401
+
1402
+ const languages = getSelectedLanguages();
1403
+ if (languages.length === 0) {
1404
+ alert('Please select at least one language');
1405
+ return;
1406
+ }
1407
+
1408
+ // Validate based on mode
1409
+ if (currentMode === 'video' && !selectedFile) {
1410
+ alert('Please select a video or audio file');
1411
+ return;
1412
+ }
1413
+ if (currentMode === 'image' && !selectedImage) {
1414
+ alert('Please select a background image');
1415
+ return;
1416
+ }
1417
+
1418
+ if (!surahSelect.value) {
1419
+ alert('Please select a surah');
1420
+ return;
1421
+ }
1422
+
1423
+ const formData = new FormData();
1424
+ formData.append('mode', currentMode);
1425
+ formData.append('surah', surahSelect.value);
1426
+ formData.append('start_ayah', document.getElementById('startAyah').value || 1);
1427
+ formData.append('end_ayah', document.getElementById('endAyah').value || '');
1428
+ formData.append('separator', document.getElementById('separator').value);
1429
+ formData.append('languages', JSON.stringify(languages));
1430
+
1431
+ if (currentMode === 'video') {
1432
+ formData.append('file', selectedFile);
1433
+ formData.append('model_size', document.getElementById('modelSize').value);
1434
+ } else if (currentMode === 'image') {
1435
+ formData.append('image', selectedImage);
1436
+ formData.append('reciter', reciterSelect.value);
1437
+ } else if (currentMode === 'black') {
1438
+ formData.append('reciter', reciterSelect.value);
1439
+ }
1440
+
1441
+ submitBtn.disabled = true;
1442
+ progressContainer.classList.add('show');
1443
+ updateProgress(0, currentMode === 'video' ? 'Uploading file...' : 'Starting...');
1444
+
1445
+ try {
1446
+ const uploadResponse = await fetch('/api/upload', {
1447
+ method: 'POST',
1448
+ body: formData
1449
+ });
1450
+ const uploadData = await uploadResponse.json();
1451
+
1452
+ if (!uploadData.success) {
1453
+ throw new Error(uploadData.message || 'Upload failed');
1454
+ }
1455
+
1456
+ currentTaskId = uploadData.task_id;
1457
+ updateProgress(5, 'File uploaded. Starting processing...');
1458
+
1459
+ const processResponse = await fetch(`/api/process/${currentTaskId}`, {
1460
+ method: 'POST'
1461
+ });
1462
+ const processData = await processResponse.json();
1463
+
1464
+ if (!processData.success) {
1465
+ throw new Error(processData.message || 'Failed to start processing');
1466
+ }
1467
+
1468
+ startStatusPolling();
1469
+
1470
+ } catch (error) {
1471
+ console.error('Error:', error);
1472
+ updateProgress(0, `Error: ${error.message}`);
1473
+ submitBtn.disabled = false;
1474
+ }
1475
+ });
1476
+
1477
+ function startStatusPolling() {
1478
+ statusPollInterval = setInterval(async () => {
1479
+ try {
1480
+ const response = await fetch(`/api/status/${currentTaskId}`);
1481
+ const status = await response.json();
1482
+
1483
+ updateProgress(status.progress, status.message);
1484
+
1485
+ if (status.status === 'completed') {
1486
+ clearInterval(statusPollInterval);
1487
+ showEditor(status);
1488
+ } else if (status.status === 'error') {
1489
+ clearInterval(statusPollInterval);
1490
+ updateProgress(0, `Error: ${status.error}`);
1491
+ submitBtn.disabled = false;
1492
+ }
1493
+ } catch (error) {
1494
+ console.error('Status poll error:', error);
1495
+ }
1496
+ }, 1000);
1497
+ }
1498
+
1499
+ function updateProgress(percent, message) {
1500
+ progressFill.style.width = `${percent}%`;
1501
+ progressText.textContent = message;
1502
+ }
1503
+
1504
+ // ================================
1505
+ // EDITOR
1506
+ // ================================
1507
+ function showEditor(status) {
1508
+ uploadSection.classList.add('hidden');
1509
+ featuresSection.classList.add('hidden');
1510
+ editorSection.classList.add('show');
1511
+ progressContainer.classList.remove('show');
1512
+ submitBtn.disabled = false;
1513
+
1514
+ editorMessage.textContent = status.message;
1515
+ verses = status.verses || [];
1516
+
1517
+ // Check if video is available
1518
+ const settings = status.settings || {};
1519
+ const videoPreview = document.querySelector('.video-preview');
1520
+ const hasVideo = status.video_file || (settings.mode === 'video' && settings.file_path);
1521
+ const hasGeneratedVideo = status.output_files && status.output_files.video;
1522
+
1523
+ // Reset video preview HTML
1524
+ videoPreview.innerHTML = `
1525
+ <div class="video-container">
1526
+ <video id="videoPlayer" controls>
1527
+ Your browser does not support the video tag.
1528
+ </video>
1529
+ <div class="subtitle-overlay" id="subtitleOverlay">
1530
+ <div class="arabic"></div>
1531
+ <div class="english"></div>
1532
+ <div class="turkish"></div>
1533
+ </div>
1534
+ </div>
1535
+ <div class="current-time" id="currentTime">00:00:00.000</div>
1536
+ <div class="video-controls">
1537
+ <button class="btn btn-sm" id="seekBack5">-5s</button>
1538
+ <button class="btn btn-sm" id="seekBack1">-1s</button>
1539
+ <button class="btn btn-sm btn-secondary" id="playPause">Play</button>
1540
+ <button class="btn btn-sm" id="seekForward1">+1s</button>
1541
+ <button class="btn btn-sm" id="seekForward5">+5s</button>
1542
+ </div>
1543
+ <div class="video-controls" style="margin-top: 0.5rem;">
1544
+ <button class="btn btn-sm btn-warning" id="setStartTime">Set Start</button>
1545
+ <button class="btn btn-sm btn-warning" id="setEndTime">Set End</button>
1546
+ </div>
1547
+ `;
1548
+
1549
+ // Re-bind video player reference
1550
+ const newVideoPlayer = document.getElementById('videoPlayer');
1551
+
1552
+ if (hasVideo || hasGeneratedVideo) {
1553
+ // Load video
1554
+ newVideoPlayer.src = `/api/video/${currentTaskId}`;
1555
+ videoPreview.style.display = 'block';
1556
+
1557
+ // Re-setup video event listeners
1558
+ setupVideoControls(newVideoPlayer);
1559
+ } else {
1560
+ // No video available
1561
+ videoPreview.innerHTML = `
1562
+ <div style="background: var(--bg-input); border-radius: 12px; padding: 3rem; text-align: center;">
1563
+ <div style="font-size: 3rem; margin-bottom: 1rem;">📋</div>
1564
+ <p style="color: var(--text-muted);">Video is being generated...</p>
1565
+ <p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 0.5rem;">If audio download failed, edit timing manually.</p>
1566
+ </div>
1567
+ `;
1568
+ }
1569
+
1570
+ // Render verse list
1571
+ renderVerseList();
1572
+
1573
+ // Setup download buttons based on selected languages and video availability
1574
+ setupDownloadButtons(settings.languages || ['arabic', 'english', 'turkish'], hasGeneratedVideo);
1575
+ }
1576
+
1577
+ function setupVideoControls(player) {
1578
+ // Time update
1579
+ player.addEventListener('timeupdate', () => {
1580
+ const time = player.currentTime;
1581
+ const hours = Math.floor(time / 3600);
1582
+ const minutes = Math.floor((time % 3600) / 60);
1583
+ const seconds = Math.floor(time % 60);
1584
+ const ms = Math.floor((time % 1) * 1000);
1585
+ document.getElementById('currentTime').textContent =
1586
+ `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
1587
+
1588
+ updateSubtitleOverlay(time);
1589
+ highlightActiveVerse(time);
1590
+ });
1591
+
1592
+ // Control buttons
1593
+ document.getElementById('playPause').onclick = () => {
1594
+ if (player.paused) {
1595
+ player.play();
1596
+ document.getElementById('playPause').textContent = 'Pause';
1597
+ } else {
1598
+ player.pause();
1599
+ document.getElementById('playPause').textContent = 'Play';
1600
+ }
1601
+ };
1602
+
1603
+ document.getElementById('seekBack5').onclick = () => {
1604
+ player.currentTime = Math.max(0, player.currentTime - 5);
1605
+ };
1606
+ document.getElementById('seekBack1').onclick = () => {
1607
+ player.currentTime = Math.max(0, player.currentTime - 1);
1608
+ };
1609
+ document.getElementById('seekForward1').onclick = () => {
1610
+ player.currentTime = Math.min(player.duration, player.currentTime + 1);
1611
+ };
1612
+ document.getElementById('seekForward5').onclick = () => {
1613
+ player.currentTime = Math.min(player.duration, player.currentTime + 5);
1614
+ };
1615
+
1616
+ document.getElementById('setStartTime').onclick = () => {
1617
+ if (selectedVerseIndex !== null) {
1618
+ verses[selectedVerseIndex].start_time = player.currentTime;
1619
+ const input = document.querySelector(`input[data-index="${selectedVerseIndex}"][data-field="start_time"]`);
1620
+ if (input) input.value = player.currentTime.toFixed(3);
1621
+ } else {
1622
+ alert('Please select a verse first');
1623
+ }
1624
+ };
1625
+
1626
+ document.getElementById('setEndTime').onclick = () => {
1627
+ if (selectedVerseIndex !== null) {
1628
+ verses[selectedVerseIndex].end_time = player.currentTime;
1629
+ const input = document.querySelector(`input[data-index="${selectedVerseIndex}"][data-field="end_time"]`);
1630
+ if (input) input.value = player.currentTime.toFixed(3);
1631
+ } else {
1632
+ alert('Please select a verse first');
1633
+ }
1634
+ };
1635
+ }
1636
+
1637
+ function renderVerseList() {
1638
+ verseList.innerHTML = '';
1639
+
1640
+ verses.forEach((verse, index) => {
1641
+ const item = document.createElement('div');
1642
+ item.className = 'verse-item';
1643
+ item.dataset.index = index;
1644
+
1645
+ item.innerHTML = `
1646
+ <div class="verse-header">
1647
+ <span class="verse-number">Ayah ${verse.ayah}</span>
1648
+ <span class="verse-confidence">${(verse.confidence * 100).toFixed(0)}% match</span>
1649
+ </div>
1650
+ <div class="verse-text">${verse.arabic}</div>
1651
+ <div class="verse-translation">${verse.english}</div>
1652
+ <div class="timing-inputs">
1653
+ <div>
1654
+ <label>Start (seconds)</label>
1655
+ <input type="number" step="0.001" value="${verse.start_time.toFixed(3)}"
1656
+ data-field="start_time" data-index="${index}">
1657
+ </div>
1658
+ <div>
1659
+ <label>End (seconds)</label>
1660
+ <input type="number" step="0.001" value="${verse.end_time.toFixed(3)}"
1661
+ data-field="end_time" data-index="${index}">
1662
+ </div>
1663
+ <button class="btn btn-sm" onclick="jumpToVerse(${index})">Jump</button>
1664
+ </div>
1665
+ `;
1666
+
1667
+ item.addEventListener('click', (e) => {
1668
+ if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'BUTTON') {
1669
+ selectVerse(index);
1670
+ }
1671
+ });
1672
+
1673
+ // Input change handlers
1674
+ item.querySelectorAll('input').forEach(input => {
1675
+ input.addEventListener('change', (e) => {
1676
+ const idx = parseInt(e.target.dataset.index);
1677
+ const field = e.target.dataset.field;
1678
+ verses[idx][field] = parseFloat(e.target.value);
1679
+ });
1680
+ });
1681
+
1682
+ verseList.appendChild(item);
1683
+ });
1684
+ }
1685
+
1686
+ function selectVerse(index) {
1687
+ selectedVerseIndex = index;
1688
+
1689
+ // Update UI
1690
+ document.querySelectorAll('.verse-item').forEach((item, i) => {
1691
+ item.classList.toggle('editing', i === index);
1692
+ });
1693
+
1694
+ // Jump to verse
1695
+ jumpToVerse(index);
1696
+ }
1697
+
1698
+ function jumpToVerse(index) {
1699
+ if (verses[index]) {
1700
+ videoPlayer.currentTime = verses[index].start_time;
1701
+ }
1702
+ }
1703
+
1704
+ // ================================
1705
+ // VIDEO PLAYER
1706
+ // ================================
1707
+ videoPlayer.addEventListener('timeupdate', () => {
1708
+ const time = videoPlayer.currentTime;
1709
+
1710
+ // Update time display
1711
+ const hours = Math.floor(time / 3600);
1712
+ const minutes = Math.floor((time % 3600) / 60);
1713
+ const seconds = Math.floor(time % 60);
1714
+ const ms = Math.floor((time % 1) * 1000);
1715
+ currentTimeDisplay.textContent =
1716
+ `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
1717
+
1718
+ // Update subtitle overlay
1719
+ updateSubtitleOverlay(time);
1720
+
1721
+ // Highlight active verse
1722
+ highlightActiveVerse(time);
1723
+ });
1724
+
1725
+ function updateSubtitleOverlay(time) {
1726
+ const activeVerse = verses.find(v => time >= v.start_time && time <= v.end_time);
1727
+
1728
+ if (activeVerse) {
1729
+ subtitleOverlay.style.display = 'block';
1730
+ subtitleOverlay.querySelector('.arabic').textContent = activeVerse.arabic;
1731
+ subtitleOverlay.querySelector('.english').textContent = activeVerse.english;
1732
+ subtitleOverlay.querySelector('.turkish').textContent = activeVerse.turkish;
1733
+ } else {
1734
+ subtitleOverlay.style.display = 'none';
1735
+ }
1736
+ }
1737
+
1738
+ function highlightActiveVerse(time) {
1739
+ document.querySelectorAll('.verse-item').forEach((item, index) => {
1740
+ const verse = verses[index];
1741
+ const isActive = time >= verse.start_time && time <= verse.end_time;
1742
+ item.classList.toggle('active', isActive);
1743
+ });
1744
+ }
1745
+
1746
+ // Video controls
1747
+ document.getElementById('playPause').addEventListener('click', () => {
1748
+ if (videoPlayer.paused) {
1749
+ videoPlayer.play();
1750
+ document.getElementById('playPause').textContent = 'Pause';
1751
+ } else {
1752
+ videoPlayer.pause();
1753
+ document.getElementById('playPause').textContent = 'Play';
1754
+ }
1755
+ });
1756
+
1757
+ document.getElementById('seekBack5').addEventListener('click', () => {
1758
+ videoPlayer.currentTime = Math.max(0, videoPlayer.currentTime - 5);
1759
+ });
1760
+
1761
+ document.getElementById('seekBack1').addEventListener('click', () => {
1762
+ videoPlayer.currentTime = Math.max(0, videoPlayer.currentTime - 1);
1763
+ });
1764
+
1765
+ document.getElementById('seekForward1').addEventListener('click', () => {
1766
+ videoPlayer.currentTime = Math.min(videoPlayer.duration, videoPlayer.currentTime + 1);
1767
+ });
1768
+
1769
+ document.getElementById('seekForward5').addEventListener('click', () => {
1770
+ videoPlayer.currentTime = Math.min(videoPlayer.duration, videoPlayer.currentTime + 5);
1771
+ });
1772
+
1773
+ document.getElementById('setStartTime').addEventListener('click', () => {
1774
+ if (selectedVerseIndex !== null) {
1775
+ verses[selectedVerseIndex].start_time = videoPlayer.currentTime;
1776
+ const input = document.querySelector(`input[data-index="${selectedVerseIndex}"][data-field="start_time"]`);
1777
+ if (input) input.value = videoPlayer.currentTime.toFixed(3);
1778
+ } else {
1779
+ alert('Please select a verse first');
1780
+ }
1781
+ });
1782
+
1783
+ document.getElementById('setEndTime').addEventListener('click', () => {
1784
+ if (selectedVerseIndex !== null) {
1785
+ verses[selectedVerseIndex].end_time = videoPlayer.currentTime;
1786
+ const input = document.querySelector(`input[data-index="${selectedVerseIndex}"][data-field="end_time"]`);
1787
+ if (input) input.value = videoPlayer.currentTime.toFixed(3);
1788
+ } else {
1789
+ alert('Please select a verse first');
1790
+ }
1791
+ });
1792
+
1793
+ // ================================
1794
+ // REGENERATE & DOWNLOAD
1795
+ // ================================
1796
+ document.getElementById('regenerateBtn').addEventListener('click', async () => {
1797
+ const btn = document.getElementById('regenerateBtn');
1798
+ btn.disabled = true;
1799
+ btn.textContent = 'Regenerating...';
1800
+
1801
+ try {
1802
+ // Update verses on server
1803
+ const updates = verses.map((v, i) => ({
1804
+ index: i,
1805
+ start_time: v.start_time,
1806
+ end_time: v.end_time
1807
+ }));
1808
+
1809
+ await fetch(`/api/verses/${currentTaskId}/update`, {
1810
+ method: 'POST',
1811
+ headers: { 'Content-Type': 'application/json' },
1812
+ body: JSON.stringify({ verses: updates })
1813
+ });
1814
+
1815
+ // Regenerate files
1816
+ const response = await fetch(`/api/regenerate/${currentTaskId}`, {
1817
+ method: 'POST'
1818
+ });
1819
+ const data = await response.json();
1820
+
1821
+ if (data.success) {
1822
+ editorMessage.textContent = 'Subtitles regenerated with updated timings!';
1823
+ editorMessage.className = 'alert alert-success';
1824
+ } else {
1825
+ throw new Error(data.message || 'Regeneration failed');
1826
+ }
1827
+
1828
+ } catch (error) {
1829
+ editorMessage.textContent = `Error: ${error.message}`;
1830
+ editorMessage.className = 'alert alert-error';
1831
+ } finally {
1832
+ btn.disabled = false;
1833
+ btn.textContent = 'Regenerate with New Timings';
1834
+ }
1835
+ });
1836
+
1837
+ function setupDownloadButtons(languages, hasVideo = false) {
1838
+ downloadGrid.innerHTML = '';
1839
+
1840
+ // Language display names and icons
1841
+ const langInfo = {
1842
+ 'arabic': { label: 'Arabic', icon: 'AR' },
1843
+ 'english': { label: 'English', icon: 'EN' },
1844
+ 'turkish': { label: 'Turkish', icon: 'TR' },
1845
+ 'urdu': { label: 'Urdu', icon: 'UR' },
1846
+ 'french': { label: 'French', icon: 'FR' },
1847
+ 'german': { label: 'German', icon: 'DE' },
1848
+ 'indonesian': { label: 'Indonesian', icon: 'ID' },
1849
+ 'spanish': { label: 'Spanish', icon: 'ES' },
1850
+ 'russian': { label: 'Russian', icon: 'RU' },
1851
+ 'bengali': { label: 'Bengali', icon: 'BN' },
1852
+ 'chinese': { label: 'Chinese', icon: 'ZH' },
1853
+ 'malay': { label: 'Malay', icon: 'MS' },
1854
+ 'persian': { label: 'Persian', icon: 'FA' },
1855
+ 'dutch': { label: 'Dutch', icon: 'NL' },
1856
+ 'italian': { label: 'Italian', icon: 'IT' },
1857
+ 'portuguese': { label: 'Portuguese', icon: 'PT' }
1858
+ };
1859
+
1860
+ const files = [];
1861
+
1862
+ // Add video download if available
1863
+ if (hasVideo) {
1864
+ files.push({
1865
+ id: 'video',
1866
+ label: 'Download Video',
1867
+ icon: '🎬',
1868
+ desc: 'MP4 with subtitles',
1869
+ highlight: true
1870
+ });
1871
+ }
1872
+
1873
+ files.push({ id: 'srt_combined', label: 'Combined SRT', icon: '📄', desc: `All ${languages.length} languages` });
1874
+
1875
+ // Add individual language SRT downloads
1876
+ languages.forEach(lang => {
1877
+ const info = langInfo[lang] || { label: lang, icon: lang.substring(0, 2).toUpperCase() };
1878
+ files.push({
1879
+ id: `srt_${lang}`,
1880
+ label: `${info.label} SRT`,
1881
+ icon: info.icon,
1882
+ desc: `${info.label} only`
1883
+ });
1884
+ });
1885
+
1886
+ // Add JSON timing file
1887
+ files.push({ id: 'json', label: 'JSON Timing', icon: '⏱️', desc: 'For video editors' });
1888
+
1889
+ files.forEach(file => {
1890
+ const btn = document.createElement('a');
1891
+ btn.href = `/api/download/${currentTaskId}/${file.id}`;
1892
+ btn.className = 'download-btn' + (file.highlight ? ' highlight' : '');
1893
+ btn.innerHTML = `
1894
+ <span class="icon">${file.icon}</span>
1895
+ <div>
1896
+ <strong>${file.label}</strong>
1897
+ <br><small>${file.desc}</small>
1898
+ </div>
1899
+ `;
1900
+ if (file.highlight) {
1901
+ btn.style.background = 'linear-gradient(135deg, var(--primary), var(--primary-dark))';
1902
+ btn.style.color = 'white';
1903
+ }
1904
+ downloadGrid.appendChild(btn);
1905
+ });
1906
+ }
1907
+
1908
+ // ================================
1909
+ // BACK BUTTON
1910
+ // ================================
1911
+ document.getElementById('backBtn').addEventListener('click', () => {
1912
+ if (confirm('Are you sure? Your edited timings will be lost.')) {
1913
+ editorSection.classList.remove('show');
1914
+ uploadSection.classList.remove('hidden');
1915
+ featuresSection.classList.remove('hidden');
1916
+
1917
+ // Cleanup
1918
+ fetch(`/api/task/${currentTaskId}`, { method: 'DELETE' });
1919
+ currentTaskId = null;
1920
+ verses = [];
1921
+ selectedVerseIndex = null;
1922
+ videoPlayer.src = '';
1923
+ }
1924
+ });
1925
+
1926
+ // ================================
1927
+ // KEYBOARD SHORTCUTS
1928
+ // ================================
1929
+ document.addEventListener('keydown', (e) => {
1930
+ if (editorSection.classList.contains('show') && !e.target.matches('input')) {
1931
+ switch(e.key) {
1932
+ case ' ':
1933
+ e.preventDefault();
1934
+ document.getElementById('playPause').click();
1935
+ break;
1936
+ case 'ArrowLeft':
1937
+ e.preventDefault();
1938
+ videoPlayer.currentTime -= e.shiftKey ? 5 : 1;
1939
+ break;
1940
+ case 'ArrowRight':
1941
+ e.preventDefault();
1942
+ videoPlayer.currentTime += e.shiftKey ? 5 : 1;
1943
+ break;
1944
+ case 'ArrowUp':
1945
+ e.preventDefault();
1946
+ if (selectedVerseIndex > 0) selectVerse(selectedVerseIndex - 1);
1947
+ break;
1948
+ case 'ArrowDown':
1949
+ e.preventDefault();
1950
+ if (selectedVerseIndex < verses.length - 1) selectVerse(selectedVerseIndex + 1);
1951
+ break;
1952
+ case 's':
1953
+ if (e.ctrlKey || e.metaKey) {
1954
+ e.preventDefault();
1955
+ document.getElementById('regenerateBtn').click();
1956
+ } else {
1957
+ document.getElementById('setStartTime').click();
1958
+ }
1959
+ break;
1960
+ case 'e':
1961
+ document.getElementById('setEndTime').click();
1962
+ break;
1963
+ }
1964
+ }
1965
+ });
1966
+
1967
+ // ================================
1968
+ // INITIALIZE
1969
+ // ================================
1970
+ loadSurahs();
1971
+ loadReciters();
1972
+ updateModeUI();
1973
+ checkCacheStatus();
1974
+ </script>
1975
+ </body>
1976
+ </html>
uploads/.gitkeep ADDED
File without changes