Opera8 commited on
Commit
4af161c
·
verified ·
1 Parent(s): 0ed3ea4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +15 -527
app.py CHANGED
@@ -2,11 +2,10 @@ import os
2
  import shutil
3
  import subprocess
4
  import uuid
5
- import json
6
  from datetime import timedelta
7
  from typing import List
8
  from fastapi import FastAPI, UploadFile, File
9
- from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from faster_whisper import WhisperModel
12
  from pydantic import BaseModel
@@ -28,7 +27,6 @@ model = WhisperModel("small", device="cpu", compute_type="int8")
28
  print("✅ [SYSTEM] AI Core Ready.")
29
 
30
  # --- MODELS ---
31
-
32
  class SubtitleSegment(BaseModel):
33
  id: int
34
  start: float
@@ -51,7 +49,6 @@ class ProcessRequest(BaseModel):
51
  style: StyleConfig
52
 
53
  # --- HELPERS ---
54
-
55
  def hex_to_ass(hex_color, alpha="00"):
56
  hex_color = hex_color.lstrip('#')
57
  if len(hex_color) != 6: return "&H00FFFFFF"
@@ -70,7 +67,13 @@ def format_time_ass(seconds: float):
70
  def generate_ass_file(data: ProcessRequest, output_path: str):
71
  s = data.style
72
 
73
- font_map = {"vazir": "Vazirmatn", "lalezar": "Lalezar"}
 
 
 
 
 
 
74
  font_name = font_map.get(s.font, "Arial")
75
 
76
  primary = hex_to_ass(s.primaryColor)
@@ -79,7 +82,6 @@ def generate_ass_file(data: ProcessRequest, output_path: str):
79
  border_style = 1
80
  back_color = "&H00000000"
81
 
82
- # تنظیمات دقیق‌تر کادر برای دیده شدن بهتر
83
  if s.backType == 'solid':
84
  border_style = 3
85
  outline = hex_to_ass(s.outlineColor, "00")
@@ -89,7 +91,6 @@ def generate_ass_file(data: ProcessRequest, output_path: str):
89
  else:
90
  border_style = 1
91
 
92
- # تغییر مهم: ScaledBorderAndShadow برای اینکه در رزولوشن‌های بالا حاشیه محو نشود
93
  header = f"""[Script Info]
94
  ScriptType: v4.00+
95
  PlayResX: 1080
@@ -112,7 +113,11 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
112
  clean_text = seg.text.strip().replace("\n", "\\N")
113
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{clean_text}\n")
114
 
115
- # --- API ---
 
 
 
 
116
 
117
  @app.post("/api/analyze")
118
  async def analyze_media(file: UploadFile = File(...)):
@@ -143,6 +148,7 @@ async def analyze_media(file: UploadFile = File(...)):
143
  @app.post("/api/render")
144
  async def render_video(data: ProcessRequest):
145
  try:
 
146
  exts = ["mp4", "mov", "avi", "mkv", "mp3"]
147
  input_path = None
148
  for ext in exts:
@@ -179,522 +185,4 @@ async def get_file(filename: str):
179
  path = f"{TEMP_DIR}/{filename}"
180
  if os.path.exists(path):
181
  return FileResponse(path)
182
- return JSONResponse(status_code=404, content={"error": "File missing"})
183
-
184
- # --- FRONTEND ---
185
-
186
- @app.get("/", response_class=HTMLResponse)
187
- async def interface():
188
- return """
189
- <!DOCTYPE html>
190
- <html lang="fa" dir="rtl">
191
- <head>
192
- <meta charset="UTF-8">
193
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
194
- <title>AI Subtitle Monster</title>
195
- <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;500;700;900&display=swap" rel="stylesheet">
196
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
197
- <style>
198
- :root {
199
- --bg-dark: #0f172a;
200
- --bg-card: #1e293b;
201
- --primary: #8b5cf6;
202
- --primary-glow: rgba(139, 92, 246, 0.5);
203
- --accent: #f43f5e;
204
- --text-main: #f8fafc;
205
- --text-muted: #94a3b8;
206
- --border: #334155;
207
- --success: #10b981;
208
- }
209
-
210
- * { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
211
-
212
- body {
213
- font-family: 'Vazirmatn', sans-serif;
214
- background-color: var(--bg-dark);
215
- color: var(--text-main);
216
- margin: 0;
217
- padding: 0;
218
- overflow-x: hidden;
219
- min-height: 100vh;
220
- display: flex;
221
- flex-direction: column;
222
- }
223
-
224
- .bg-mesh {
225
- position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1;
226
- background: radial-gradient(circle at 50% 50%, #1e1b4b 0%, #0f172a 100%);
227
- overflow: hidden;
228
- }
229
- .blob {
230
- position: absolute;
231
- filter: blur(80px);
232
- opacity: 0.4;
233
- animation: float 10s infinite ease-in-out;
234
- }
235
- .blob-1 { top: -10%; left: -10%; width: 50vw; height: 50vw; background: var(--primary); }
236
- .blob-2 { bottom: -10%; right: -10%; width: 40vw; height: 40vw; background: var(--accent); animation-delay: -5s; }
237
- @keyframes float { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(30px, 50px); } }
238
-
239
- .app-header {
240
- padding: 15px 20px;
241
- background: rgba(30, 41, 59, 0.8);
242
- backdrop-filter: blur(10px);
243
- border-bottom: 1px solid var(--border);
244
- display: flex;
245
- justify-content: space-between;
246
- align-items: center;
247
- z-index: 100;
248
- position: sticky;
249
- top: 0;
250
- }
251
- .brand { font-weight: 900; font-size: 1.4rem; background: linear-gradient(to right, var(--primary), var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
252
-
253
- .app-body {
254
- flex: 1;
255
- display: flex;
256
- flex-direction: column;
257
- padding: 20px;
258
- max-width: 1200px;
259
- margin: 0 auto;
260
- width: 100%;
261
- }
262
-
263
- /* --- VIEWS --- */
264
- .view { display: none; animation: fadeIn 0.4s ease; }
265
- .view.active { display: block; }
266
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
267
-
268
- .card {
269
- background: var(--bg-card);
270
- border: 1px solid var(--border);
271
- border-radius: 20px;
272
- padding: 25px;
273
- margin-bottom: 20px;
274
- box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3);
275
- }
276
-
277
- .btn {
278
- width: 100%;
279
- padding: 16px;
280
- border-radius: 14px;
281
- border: none;
282
- font-weight: 800;
283
- font-size: 1rem;
284
- cursor: pointer;
285
- transition: 0.2s;
286
- display: flex;
287
- align-items: center;
288
- justify-content: center;
289
- gap: 10px;
290
- color: white;
291
- }
292
- .btn-primary {
293
- background: linear-gradient(135deg, var(--primary), #4338ca);
294
- box-shadow: 0 0 20px var(--primary-glow);
295
- }
296
- .btn-primary:active { transform: scale(0.98); }
297
-
298
- .upload-zone {
299
- border: 2px dashed var(--border);
300
- border-radius: 20px;
301
- height: 300px;
302
- display: flex;
303
- flex-direction: column;
304
- justify-content: center;
305
- align-items: center;
306
- cursor: pointer;
307
- transition: 0.3s;
308
- background: rgba(255,255,255,0.02);
309
- }
310
- .upload-zone:hover { border-color: var(--primary); background: rgba(99, 102, 241, 0.05); }
311
- .upload-icon { font-size: 4rem; margin-bottom: 20px; color: var(--text-muted); }
312
-
313
- /* --- EDITOR LAYOUT --- */
314
- .editor-grid {
315
- display: grid;
316
- grid-template-columns: 1fr;
317
- gap: 20px;
318
- }
319
- @media(min-width: 1024px) {
320
- .editor-grid { grid-template-columns: 350px 1fr; }
321
- }
322
-
323
- .segment-row {
324
- background: rgba(255,255,255,0.03);
325
- border-radius: 12px;
326
- padding: 15px;
327
- margin-bottom: 12px;
328
- border-right: 3px solid transparent;
329
- transition: 0.2s;
330
- }
331
- .segment-row:focus-within { border-right-color: var(--accent); background: rgba(255,255,255,0.06); }
332
-
333
- .seg-time { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 5px; font-family: monospace; }
334
-
335
- /* اصلاح استایل تکست باکس برای دیده شدن متن */
336
- .seg-input {
337
- width: 100%;
338
- background: rgba(0, 0, 0, 0.2); /* پس‌زمینه تیره */
339
- border: 1px solid var(--border); /* کادر مشخص */
340
- border-radius: 8px;
341
- color: #fff; /* متن سفید */
342
- font-size: 1.1rem;
343
- font-family: inherit;
344
- resize: vertical;
345
- padding: 10px;
346
- min-height: 60px;
347
- }
348
- .seg-input:focus {
349
- border-color: var(--primary);
350
- background: rgba(0, 0, 0, 0.3);
351
- }
352
-
353
- .control-group { margin-bottom: 18px; }
354
- .control-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
355
-
356
- input[type="range"] {
357
- width: 100%; height: 6px; background: var(--border);
358
- border-radius: 5px; appearance: none;
359
- }
360
- input[type="range"]::-webkit-slider-thumb {
361
- appearance: none; width: 20px; height: 20px;
362
- background: var(--primary); border-radius: 50%; cursor: pointer;
363
- }
364
- input[type="color"] {
365
- width: 100%; height: 40px; border: none; border-radius: 8px;
366
- cursor: pointer; background: transparent;
367
- }
368
-
369
- .style-chips { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px; }
370
- .chip {
371
- padding: 8px 16px; background: var(--border); border-radius: 20px;
372
- font-size: 0.85rem; cursor: pointer; white-space: nowrap; transition: 0.2s;
373
- }
374
- .chip.active { background: rgba(99, 102, 241, 0.2); color: var(--primary); border: 1px solid var(--primary); }
375
-
376
- .preview-box {
377
- position: relative; width: 100%; aspect-ratio: 16/9;
378
- background: #000; border-radius: 12px; margin-bottom: 20px;
379
- display: flex; align-items: center; justify-content: center; overflow: hidden;
380
- background-image: url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1000&auto=format&fit=crop');
381
- background-size: cover;
382
- }
383
- .preview-text {
384
- position: absolute; text-align: center; pointer-events: none;
385
- transition: 0.1s; line-height: 1.4; max-width: 80%;
386
- }
387
-
388
- /* --- RESULT SECTION INLINE --- */
389
- #inlineResult {
390
- margin-bottom: 20px; /* فاصله از دکمه */
391
- padding: 20px;
392
- border: 2px solid var(--success);
393
- border-radius: 16px;
394
- background: rgba(16, 185, 129, 0.05);
395
- text-align: center;
396
- display: none;
397
- animation: slideDown 0.5s ease;
398
- }
399
- @keyframes slideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
400
-
401
- .result-video { width: 100%; border-radius: 12px; margin-top: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
402
- .dl-btn {
403
- display: inline-block; margin-top: 15px; padding: 12px 30px;
404
- background: var(--success); color: white; text-decoration: none;
405
- border-radius: 10px; font-weight: bold;
406
- }
407
-
408
- /* LOADER */
409
- .loader-screen {
410
- position: fixed; top:0; left:0; width:100%; height:100%;
411
- background: rgba(15, 23, 42, 0.95); z-index: 1000;
412
- display: none; flex-direction: column; justify-content: center; align-items: center;
413
- }
414
- .loader-screen.flex { display: flex; }
415
- .spinner { width: 50px; height: 50px; border: 5px solid var(--border); border-top: 5px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; }
416
- @keyframes spin { 100% { transform: rotate(360deg); } }
417
-
418
- </style>
419
- </head>
420
- <body>
421
-
422
- <div class="bg-mesh"><div class="blob blob-1"></div><div class="blob blob-2"></div></div>
423
-
424
- <div id="loader" class="loader-screen">
425
- <div class="spinner"></div>
426
- <h3 id="loaderMsg">در حال پردازش...</h3>
427
- </div>
428
-
429
- <header class="app-header">
430
- <div class="brand"><i class="fa-solid fa-bolt"></i> SubMaster</div>
431
- </header>
432
-
433
- <div class="app-body">
434
-
435
- <!-- VIEW 1: UPLOAD -->
436
- <div id="view-upload" class="view active">
437
- <div style="max-width: 600px; margin: 40px auto;">
438
- <div class="card">
439
- <h2 style="text-align: center; color: var(--text-main);">آپلود ویدیو</h2>
440
- <div class="upload-zone" onclick="document.getElementById('fileIn').click()">
441
- <i class="fa-solid fa-cloud-arrow-up upload-icon"></i>
442
- <h3>انتخاب فایل</h3>
443
- <input type="file" id="fileIn" hidden accept="video/*" onchange="handleUpload()">
444
- </div>
445
- </div>
446
- </div>
447
- </div>
448
-
449
- <!-- VIEW 2: EDITOR -->
450
- <div id="view-editor" class="view">
451
- <div class="editor-grid">
452
-
453
- <!-- SETTINGS PANEL -->
454
- <div class="settings-panel card">
455
- <h2 style="color: var(--primary); margin-top:0;">🎨 استایل و خروجی</h2>
456
-
457
- <!-- INLINE RESULT AREA (MOVED UP) -->
458
- <div id="inlineResult">
459
- <h3 style="color: var(--success); margin:0;">✅ ویدیو آماده شد!</h3>
460
- <video id="finalPlayer" controls class="result-video"></video>
461
- <a id="dlBtn" href="#" download class="dl-btn">دانلود ویدیو</a>
462
- </div>
463
-
464
- <div class="preview-box">
465
- <div id="livePreview" class="preview-text">متن نمونه</div>
466
- </div>
467
-
468
- <div class="control-group">
469
- <div class="control-label"><span>رنگ متن</span></div>
470
- <input type="color" id="colorMain" value="#FFFFFF" oninput="updatePreview()">
471
- </div>
472
-
473
- <div class="control-group">
474
- <div class="control-label"><span>رنگ کادر</span></div>
475
- <input type="color" id="colorOutline" value="#000000" oninput="updatePreview()">
476
- </div>
477
-
478
- <div class="control-group">
479
- <div class="control-label"><span>نوع کادر</span></div>
480
- <div class="style-chips">
481
- <div class="chip active" onclick="setStyle('solid', this)">پُر رنگ</div>
482
- <div class="chip" onclick="setStyle('transparent', this)">شیشه‌ای</div>
483
- <div class="chip" onclick="setStyle('outline', this)">حاشیه</div>
484
- </div>
485
- </div>
486
-
487
- <div class="control-group">
488
- <div class="control-label"><span>فونت</span></div>
489
- <div class="style-chips">
490
- <div class="chip active" onclick="setFont('lalezar', this)">لاله زار</div>
491
- <div class="chip" onclick="setFont('vazir', this)">وزیر</div>
492
- </div>
493
- </div>
494
-
495
- <div class="control-group">
496
- <div class="control-label"><span>سایز متن</span> <span id="lblSize">100</span></div>
497
- <!-- افزایش بازه برای فونت‌های بزرگ -->
498
- <input type="range" id="rngSize" min="30" max="300" value="100" oninput="updatePreview()">
499
- </div>
500
-
501
- <div class="control-group">
502
- <div class="control-label"><span>موقعیت عمودی</span></div>
503
- <input type="range" id="rngPos" min="10" max="600" value="150" oninput="updatePreview()">
504
- </div>
505
-
506
- <button class="btn btn-primary" onclick="startRender()">
507
- <i class="fa-solid fa-wand-magic-sparkles"></i> شروع ساخت / بروزرسانی
508
- </button>
509
-
510
- </div>
511
-
512
- <!-- TEXT EDITOR -->
513
- <div class="card" style="display: flex; flex-direction: column; height: 80vh;">
514
- <h2>📝 ویرایش متن زیرنویس</h2>
515
- <div id="segmentsList" style="flex:1; overflow-y:auto;"></div>
516
- </div>
517
-
518
- </div>
519
- </div>
520
-
521
- </div>
522
-
523
- <script>
524
- let appState = {
525
- fileId: null,
526
- segments: [],
527
- style: {
528
- backType: 'solid',
529
- font: 'lalezar'
530
- }
531
- };
532
-
533
- // --- UPLOAD ---
534
- async function handleUpload() {
535
- const file = document.getElementById('fileIn').files[0];
536
- if(!file) return;
537
-
538
- showLoader("در حال استخراج متن...");
539
-
540
- const formData = new FormData();
541
- formData.append("file", file);
542
-
543
- try {
544
- const res = await fetch("/api/analyze", { method: "POST", body: formData });
545
- const data = await res.json();
546
-
547
- if(data.error) throw new Error(data.error);
548
-
549
- appState.fileId = data.file_id;
550
- appState.segments = data.segments;
551
-
552
- renderSegments();
553
-
554
- document.getElementById('view-upload').classList.remove('active');
555
- document.getElementById('view-editor').classList.add('active');
556
- updatePreview();
557
-
558
- } catch(e) {
559
- alert("Error: " + e.message);
560
- } finally {
561
- hideLoader();
562
- }
563
- }
564
-
565
- // --- EDITOR ---
566
- function renderSegments() {
567
- const container = document.getElementById('segmentsList');
568
- container.innerHTML = "";
569
-
570
- appState.segments.forEach((seg, idx) => {
571
- const div = document.createElement('div');
572
- div.className = 'segment-row';
573
- div.innerHTML = `
574
- <div class="seg-time">${formatTime(seg.start)} -> ${formatTime(seg.end)}</div>
575
- <textarea class="seg-input" rows="1" oninput="updateSegment(${idx}, this)">${seg.text}</textarea>
576
- `;
577
- container.appendChild(div);
578
- });
579
- }
580
-
581
- function updateSegment(idx, el) {
582
- appState.segments[idx].text = el.value;
583
- document.getElementById('livePreview').innerText = el.value;
584
- el.style.height = 'auto';
585
- el.style.height = (el.scrollHeight) + 'px';
586
- }
587
-
588
- function formatTime(s) {
589
- const m = Math.floor(s / 60);
590
- const sec = Math.floor(s % 60);
591
- return `${m}:${sec.toString().padStart(2, '0')}`;
592
- }
593
-
594
- // --- PREVIEW ---
595
- function setStyle(type, el) {
596
- appState.style.backType = type;
597
- el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
598
- el.classList.add('active');
599
- updatePreview();
600
- }
601
-
602
- function setFont(font, el) {
603
- appState.style.font = font;
604
- el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
605
- el.classList.add('active');
606
- updatePreview();
607
- }
608
-
609
- function updatePreview() {
610
- const txt = document.getElementById('livePreview');
611
- const size = document.getElementById('rngSize').value;
612
- const pos = document.getElementById('rngPos').value;
613
- const color = document.getElementById('colorMain').value;
614
- const outline = document.getElementById('colorOutline').value;
615
- const font = appState.style.font === 'lalezar' ? 'Lalezar' : 'Vazirmatn';
616
-
617
- document.getElementById('lblSize').innerText = size;
618
-
619
- txt.style.fontFamily = font;
620
- // تقسیم بر ۵ برای واقعی‌تر شدن پیش‌نمایش
621
- txt.style.fontSize = (size / 5) + 'px';
622
- txt.style.color = color;
623
- txt.style.bottom = (pos / 6) + 'px';
624
-
625
- if(appState.style.backType === 'solid') {
626
- txt.style.backgroundColor = outline;
627
- txt.style.textShadow = 'none';
628
- txt.style.padding = '2px 8px';
629
- txt.style.borderRadius = '4px';
630
- } else if (appState.style.backType === 'transparent') {
631
- txt.style.backgroundColor = 'rgba(0,0,0,0.6)';
632
- txt.style.textShadow = 'none';
633
- txt.style.padding = '2px 8px';
634
- txt.style.borderRadius = '4px';
635
- } else {
636
- txt.style.backgroundColor = 'transparent';
637
- txt.style.webkitTextStroke = `1px ${outline}`;
638
- txt.style.textShadow = `0 0 2px ${outline}`;
639
- txt.style.padding = '0';
640
- }
641
- }
642
-
643
- // --- RENDER ---
644
- async function startRender() {
645
- showLoader("در حال ساخت ویدیو...");
646
-
647
- const payload = {
648
- file_id: appState.fileId,
649
- segments: appState.segments,
650
- style: {
651
- font: appState.style.font,
652
- fontSize: parseInt(document.getElementById('rngSize').value),
653
- primaryColor: document.getElementById('colorMain').value,
654
- outlineColor: document.getElementById('colorOutline').value,
655
- backType: appState.style.backType,
656
- outlineWidth: 2.0,
657
- marginV: parseInt(document.getElementById('rngPos').value),
658
- alignment: 2
659
- }
660
- };
661
-
662
- try {
663
- const res = await fetch("/api/render", {
664
- method: "POST",
665
- headers: { "Content-Type": "application/json" },
666
- body: JSON.stringify(payload)
667
- });
668
- const data = await res.json();
669
-
670
- if(data.error) throw new Error(data.error);
671
-
672
- const resultBox = document.getElementById('inlineResult');
673
- resultBox.style.display = 'block';
674
-
675
- // اضافه کردن زمان برای جلوگیری از کش
676
- document.getElementById('finalPlayer').src = data.url + "?t=" + Date.now();
677
- document.getElementById('dlBtn').href = data.url;
678
-
679
- // اسکرول به نتیجه
680
- resultBox.scrollIntoView({behavior: 'smooth'});
681
-
682
- } catch(e) {
683
- alert("Render Error: " + e.message);
684
- } finally {
685
- hideLoader();
686
- }
687
- }
688
-
689
- function showLoader(msg) {
690
- document.getElementById('loaderMsg').innerText = msg;
691
- document.getElementById('loader').classList.add('flex');
692
- }
693
- function hideLoader() {
694
- document.getElementById('loader').classList.remove('flex');
695
- }
696
-
697
- </script>
698
- </body>
699
- </html>
700
- """
 
2
  import shutil
3
  import subprocess
4
  import uuid
 
5
  from datetime import timedelta
6
  from typing import List
7
  from fastapi import FastAPI, UploadFile, File
8
+ from fastapi.responses import FileResponse, JSONResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from faster_whisper import WhisperModel
11
  from pydantic import BaseModel
 
27
  print("✅ [SYSTEM] AI Core Ready.")
28
 
29
  # --- MODELS ---
 
30
  class SubtitleSegment(BaseModel):
31
  id: int
32
  start: float
 
49
  style: StyleConfig
50
 
51
  # --- HELPERS ---
 
52
  def hex_to_ass(hex_color, alpha="00"):
53
  hex_color = hex_color.lstrip('#')
54
  if len(hex_color) != 6: return "&H00FFFFFF"
 
67
  def generate_ass_file(data: ProcessRequest, output_path: str):
68
  s = data.style
69
 
70
+ # مپینگ فونت‌ها (باید دقیقاً با نام فایل‌های دانلود شده در Dockerfile یکی باشد)
71
+ font_map = {
72
+ "vazir": "Vazirmatn",
73
+ "lalezar": "Lalezar",
74
+ "roboto": "Roboto",
75
+ "bangers": "Bangers"
76
+ }
77
  font_name = font_map.get(s.font, "Arial")
78
 
79
  primary = hex_to_ass(s.primaryColor)
 
82
  border_style = 1
83
  back_color = "&H00000000"
84
 
 
85
  if s.backType == 'solid':
86
  border_style = 3
87
  outline = hex_to_ass(s.outlineColor, "00")
 
91
  else:
92
  border_style = 1
93
 
 
94
  header = f"""[Script Info]
95
  ScriptType: v4.00+
96
  PlayResX: 1080
 
113
  clean_text = seg.text.strip().replace("\n", "\\N")
114
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{clean_text}\n")
115
 
116
+ # --- ROUTES ---
117
+
118
+ @app.get("/")
119
+ async def home():
120
+ return FileResponse("index.html")
121
 
122
  @app.post("/api/analyze")
123
  async def analyze_media(file: UploadFile = File(...)):
 
148
  @app.post("/api/render")
149
  async def render_video(data: ProcessRequest):
150
  try:
151
+ # پیدا کردن فایل اصلی
152
  exts = ["mp4", "mov", "avi", "mkv", "mp3"]
153
  input_path = None
154
  for ext in exts:
 
185
  path = f"{TEMP_DIR}/{filename}"
186
  if os.path.exists(path):
187
  return FileResponse(path)
188
+ return JSONResponse(status_code=404, content={"error": "File missing"})