Opera8 commited on
Commit
082d255
·
verified ·
1 Parent(s): e0266c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +333 -494
app.py CHANGED
@@ -16,9 +16,8 @@ from pydantic import BaseModel
16
  # 1. CONFIGURATION & AI SETUP
17
  # ==========================================
18
 
19
- app = FastAPI(title="AI Subtitle Studio Pro")
20
 
21
- # CORS Setup
22
  app.add_middleware(
23
  CORSMiddleware,
24
  allow_origins=["*"],
@@ -26,11 +25,9 @@ app.add_middleware(
26
  allow_headers=["*"],
27
  )
28
 
29
- # Paths
30
  TEMP_DIR = "temp"
31
  os.makedirs(TEMP_DIR, exist_ok=True)
32
 
33
- # Load Model (Global)
34
  print("⚡ [SYSTEM] Initializing AI Neural Network...")
35
  model = WhisperModel("small", device="cpu", compute_type="int8")
36
  print("✅ [SYSTEM] AI Core Ready.")
@@ -50,10 +47,10 @@ class StyleConfig(BaseModel):
50
  fontSize: int
51
  primaryColor: str
52
  outlineColor: str
53
- backType: str # 'solid', 'transparent', 'outline'
54
  outlineWidth: float
55
  marginV: int
56
- alignment: int # 2=Bottom, 5=Top, 10=Center
57
 
58
  class ProcessRequest(BaseModel):
59
  file_id: str
@@ -65,7 +62,6 @@ class ProcessRequest(BaseModel):
65
  # ==========================================
66
 
67
  def hex_to_ass(hex_color, alpha="00"):
68
- """Converts HEX #RRGGBB to ASS &HABGR"""
69
  hex_color = hex_color.lstrip('#')
70
  if len(hex_color) != 6: return "&H00FFFFFF"
71
  r, g, b = hex_color[0:2], hex_color[2:4], hex_color[4:6]
@@ -83,31 +79,31 @@ def format_time_ass(seconds: float):
83
  def generate_ass_file(data: ProcessRequest, output_path: str):
84
  s = data.style
85
 
86
- # Mapping fonts
87
  font_map = {"vazir": "Vazirmatn", "lalezar": "Lalezar"}
88
- font_name = font_map.get(s.font, "Arial")
89
 
90
- # Colors
91
  primary = hex_to_ass(s.primaryColor)
92
  outline = hex_to_ass(s.outlineColor)
93
 
94
- # Background Logic
95
  border_style = 1
96
  back_color = "&H00000000"
97
 
 
98
  if s.backType == 'solid':
99
- border_style = 3 # Opaque Box
100
- outline = hex_to_ass(s.outlineColor, "00") # Fully Opaque
101
  elif s.backType == 'transparent':
102
  border_style = 3
103
- outline = "&H80000000" # 50% Transparent Black
104
- else: # Outline only
105
  border_style = 1
106
-
 
 
107
  header = f"""[Script Info]
108
  ScriptType: v4.00+
109
- PlayResX: 1080
110
- PlayResY: 1920
111
  WrapStyle: 1
112
 
113
  [V4+ Styles]
@@ -122,7 +118,6 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
122
  for seg in data.segments:
123
  start = format_time_ass(seg.start)
124
  end = format_time_ass(seg.end)
125
- # Clean text for ASS
126
  clean_text = seg.text.strip().replace("\n", "\\N")
127
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{clean_text}\n")
128
 
@@ -140,7 +135,6 @@ async def analyze_media(file: UploadFile = File(...)):
140
  with open(input_path, "wb") as f:
141
  shutil.copyfileobj(file.file, f)
142
 
143
- # Whisper Transcribe
144
  segments_gen, _ = model.transcribe(input_path, language="fa", beam_size=5)
145
 
146
  results = []
@@ -160,9 +154,6 @@ async def analyze_media(file: UploadFile = File(...)):
160
  @app.post("/api/render")
161
  async def render_video(data: ProcessRequest):
162
  try:
163
- # Input paths
164
- # We need to find the original file extension since we only have ID
165
- # Simple hack: check common extensions
166
  exts = ["mp4", "mov", "avi", "mkv", "mp3"]
167
  input_path = None
168
  for ext in exts:
@@ -177,15 +168,13 @@ async def render_video(data: ProcessRequest):
177
  ass_path = f"{TEMP_DIR}/{data.file_id}.ass"
178
  output_path = f"{TEMP_DIR}/{data.file_id}_final.mp4"
179
 
180
- # 1. Generate ASS
181
  generate_ass_file(data, ass_path)
182
 
183
- # 2. FFmpeg Burn
184
  cmd = [
185
  "ffmpeg", "-y",
186
  "-i", input_path,
187
  "-vf", f"ass={ass_path}",
188
- "-c:v", "libx264", "-preset", "ultrafast", "-crf", "26",
189
  "-c:a", "aac",
190
  output_path
191
  ]
@@ -204,7 +193,7 @@ async def get_file(filename: str):
204
  return JSONResponse(status_code=404, content={"error": "File missing"})
205
 
206
  # ==========================================
207
- # 5. THE MONSTER FRONTEND (SPA)
208
  # ==========================================
209
 
210
  @app.get("/", response_class=HTMLResponse)
@@ -215,580 +204,430 @@ async def interface():
215
  <head>
216
  <meta charset="UTF-8">
217
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
218
- <title>AI Subtitle Monster</title>
219
- <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;500;700;900&display=swap" rel="stylesheet">
220
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
221
  <style>
222
  :root {
223
- --bg-dark: #0f172a;
224
- --bg-card: #1e293b;
225
- --primary: #6366f1;
226
- --primary-glow: rgba(99, 102, 241, 0.5);
227
- --accent: #ec4899;
228
- --text-main: #f8fafc;
229
- --text-muted: #94a3b8;
230
- --border: #334155;
231
- --success: #10b981;
232
- }
233
-
234
- * { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
235
-
236
  body {
237
  font-family: 'Vazirmatn', sans-serif;
238
  background-color: var(--bg-dark);
239
  color: var(--text-main);
240
  margin: 0;
241
  padding: 0;
242
- overflow-x: hidden;
243
  height: 100vh;
244
  display: flex;
245
  flex-direction: column;
246
- }
247
-
248
- /* --- ANIMATED BACKGROUND --- */
249
- .bg-mesh {
250
- position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1;
251
- background: radial-gradient(circle at 50% 50%, #1e1b4b 0%, #0f172a 100%);
252
  overflow: hidden;
253
  }
254
- .blob {
255
- position: absolute;
256
- filter: blur(80px);
257
- opacity: 0.4;
258
- animation: float 10s infinite ease-in-out;
259
- }
260
- .blob-1 { top: -10%; left: -10%; width: 50vw; height: 50vw; background: var(--primary); }
261
- .blob-2 { bottom: -10%; right: -10%; width: 40vw; height: 40vw; background: var(--accent); animation-delay: -5s; }
262
- @keyframes float { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(30px, 50px); } }
263
-
264
- /* --- LAYOUT --- */
265
- .app-header {
266
  padding: 15px 20px;
267
- background: rgba(30, 41, 59, 0.8);
268
- backdrop-filter: blur(10px);
269
  border-bottom: 1px solid var(--border);
270
  display: flex;
271
  justify-content: space-between;
272
  align-items: center;
273
- z-index: 100;
274
  }
275
- .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; }
276
 
277
- .app-body {
278
  flex: 1;
279
- display: flex;
280
- flex-direction: column;
 
281
  overflow: hidden;
282
  position: relative;
283
  }
 
284
 
285
- /* --- VIEWS SYSTEM --- */
286
- .view {
287
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
288
  padding: 20px;
289
  overflow-y: auto;
290
- transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s;
291
- opacity: 0; transform: scale(0.95); pointer-events: none;
292
- }
293
- .view.active { opacity: 1; transform: scale(1); pointer-events: all; }
294
-
295
- /* --- COMPONENTS --- */
296
- .card {
297
- background: var(--bg-card);
298
- border: 1px solid var(--border);
299
- border-radius: 20px;
300
- padding: 25px;
301
- margin-bottom: 20px;
302
- box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3);
303
- }
304
-
305
- .btn {
306
- width: 100%;
307
- padding: 16px;
308
- border-radius: 14px;
309
- border: none;
310
- font-weight: 800;
311
- font-size: 1rem;
312
- cursor: pointer;
313
- transition: 0.2s;
314
  display: flex;
315
- align-items: center;
316
- justify-content: center;
317
- gap: 10px;
318
- }
319
- .btn-primary {
320
- background: linear-gradient(135deg, var(--primary), #4338ca);
321
- color: white;
322
- box-shadow: 0 0 20px var(--primary-glow);
323
  }
324
- .btn-primary:active { transform: scale(0.98); }
325
-
326
- /* --- UPLOADER --- */
 
327
  .upload-zone {
328
  border: 2px dashed var(--border);
329
- border-radius: 20px;
330
- height: 300px;
331
- display: flex;
332
- flex-direction: column;
333
- justify-content: center;
334
- align-items: center;
335
  cursor: pointer;
336
  transition: 0.3s;
337
- background: rgba(255,255,255,0.02);
338
- }
339
- .upload-zone:hover { border-color: var(--primary); background: rgba(99, 102, 241, 0.05); }
340
- .upload-icon { font-size: 4rem; margin-bottom: 20px; color: var(--text-muted); transition: 0.3s; }
341
- .upload-zone:hover .upload-icon { transform: scale(1.1) rotate(-10deg); color: var(--primary); }
342
-
343
- /* --- EDITOR --- */
344
- .editor-layout {
345
- display: grid;
346
- grid-template-columns: 1fr;
347
- gap: 20px;
348
- height: 100%;
349
- }
350
- @media(min-width: 1024px) {
351
- .editor-layout { grid-template-columns: 350px 1fr; }
352
- }
353
-
354
- .segments-container {
355
- flex: 1;
356
- overflow-y: auto;
357
- padding-right: 5px;
358
  }
 
359
 
360
- .segment-row {
361
- background: rgba(255,255,255,0.03);
362
- border-radius: 12px;
363
- padding: 15px;
364
- margin-bottom: 12px;
365
- border-right: 3px solid transparent;
366
- transition: 0.2s;
367
- }
368
- .segment-row:focus-within { border-right-color: var(--accent); background: rgba(255,255,255,0.06); }
369
-
370
- .seg-time { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 5px; font-family: monospace; }
371
- .seg-input {
372
- width: 100%;
373
- background: transparent;
374
- border: none;
375
- color: var(--text-main);
376
- font-size: 1.1rem;
377
- font-family: inherit;
378
- resize: none;
379
- overflow: hidden;
380
- }
381
-
382
- /* --- SETTINGS PANEL --- */
383
- .settings-panel {
384
- background: var(--bg-card);
385
- border-radius: 16px;
386
- padding: 20px;
387
- height: fit-content;
388
- }
389
- .control-group { margin-bottom: 18px; }
390
- .control-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
391
 
392
- /* Modern Inputs */
393
  input[type="range"] {
394
- width: 100%;
395
- height: 6px;
396
- background: var(--border);
397
- border-radius: 5px;
398
- appearance: none;
399
  }
400
  input[type="range"]::-webkit-slider-thumb {
401
- appearance: none;
402
- width: 18px; height: 18px;
403
- background: var(--primary);
404
- border-radius: 50%;
405
- cursor: pointer;
406
- box-shadow: 0 0 10px var(--primary-glow);
407
  }
408
-
409
  input[type="color"] {
410
- width: 100%; height: 40px;
411
- border: none; border-radius: 8px;
412
- cursor: pointer;
413
- background: transparent;
414
  }
415
-
416
- .style-chips { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px; }
417
  .chip {
418
- padding: 8px 16px;
419
- background: var(--border);
420
- border-radius: 20px;
421
- font-size: 0.85rem;
422
- cursor: pointer;
423
- white-space: nowrap;
424
- transition: 0.2s;
 
 
 
425
  border: 1px solid transparent;
426
  }
427
- .chip.active {
428
- background: rgba(99, 102, 241, 0.2);
429
- color: var(--primary);
430
- border-color: var(--primary);
431
  }
432
 
433
- /* --- PREVIEW BOX (Simulated) --- */
434
- .preview-box {
435
  position: relative;
436
  width: 100%;
437
  aspect-ratio: 16/9;
438
  background: #000;
439
  border-radius: 12px;
440
- margin-bottom: 20px;
441
  display: flex;
442
  align-items: center;
443
  justify-content: center;
444
  overflow: hidden;
445
- background-image: url('https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80');
446
  background-size: cover;
447
- background-position: center;
448
  }
449
  .preview-text {
450
  position: absolute;
451
  text-align: center;
452
  pointer-events: none;
453
  transition: 0.1s;
454
- line-height: 1.4;
455
- max-width: 80%;
456
  }
457
-
458
- /* --- LOADER OVERLAY --- */
459
- .loader-screen {
460
- position: fixed; top:0; left:0; width:100%; height:100%;
461
- background: rgba(15, 23, 42, 0.95);
462
- z-index: 1000;
463
- display: none;
464
- flex-direction: column;
465
- justify-content: center;
466
- align-items: center;
467
- }
468
- .loader-screen.flex { display: flex; }
469
- .dna-loader {
470
- display: flex; gap: 5px; margin-bottom: 20px;
471
  }
472
- .dna-dot {
473
- width: 10px; height: 10px; background: var(--primary);
474
- border-radius: 50%;
475
- animation: bounce 1s infinite ease-in-out;
476
  }
477
- .dna-dot:nth-child(2) { animation-delay: 0.1s; background: var(--accent); }
478
- .dna-dot:nth-child(3) { animation-delay: 0.2s; }
479
- @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-15px); } }
480
 
481
- /* --- RESULT PAGE --- */
482
- .result-video { width: 100%; border-radius: 12px; box-shadow: 0 0 30px rgba(0,0,0,0.5); }
 
 
 
483
 
 
 
 
 
 
 
 
484
  </style>
485
  </head>
486
  <body>
487
 
488
- <!-- BACKGROUND -->
489
- <div class="bg-mesh"><div class="blob blob-1"></div><div class="blob blob-2"></div></div>
490
-
491
- <!-- LOADER -->
492
- <div id="loader" class="loader-screen">
493
- <div class="dna-loader"><div class="dna-dot"></div><div class="dna-dot"></div><div class="dna-dot"></div></div>
494
- <h3 id="loaderMsg">در حال پردازش هوش مصنوعی...</h3>
495
- <p style="color: var(--text-muted); font-size: 0.9rem;">لطفاً صبر کنید</p>
 
 
 
 
 
 
 
 
 
496
  </div>
 
497
 
498
- <!-- HEADER -->
499
- <header class="app-header">
500
- <div class="brand"><i class="fa-solid fa-wand-magic-sparkles"></i> SubMagic</div>
501
- <div style="font-size: 0.8rem; color: var(--text-muted);">v2.0 Pro</div>
502
- </header>
503
-
504
- <!-- BODY -->
505
- <div class="app-body">
506
 
507
- <!-- VIEW 1: UPLOAD -->
508
- <div id="view-upload" class="view active">
509
- <div style="max-width: 600px; margin: 40px auto;">
510
- <div class="card">
511
- <h2 style="text-align: center;">شروع پروژه جدید</h2>
512
- <div class="upload-zone" onclick="document.getElementById('fileIn').click()">
513
- <i class="fa-solid fa-cloud-arrow-up upload-icon"></i>
514
- <h3>ویدیو را انتخاب کنید</h3>
515
- <p style="color: var(--text-muted);">پشتیبانی از MP4, MOV, AVI</p>
516
- <input type="file" id="fileIn" hidden accept="video/*" onchange="handleUpload()">
517
- </div>
518
- </div>
519
  </div>
520
  </div>
521
 
522
- <!-- VIEW 2: EDITOR -->
523
- <div id="view-editor" class="view">
524
- <div class="editor-layout">
525
-
526
- <!-- SIDEBAR SETTINGS -->
527
- <div class="settings-panel">
528
- <h2>🎨 تنظیمات گرافیکی</h2>
529
-
530
- <!-- PREVIEW -->
531
- <div class="preview-box">
532
- <div id="livePreview" class="preview-text">این یک متن تست است</div>
533
- </div>
534
-
535
- <div class="control-group">
536
- <div class="control-label"><span>رنگ متن</span></div>
537
- <input type="color" id="colorMain" value="#FFFFFF" oninput="updatePreview()">
538
- </div>
539
-
540
- <div class="control-group">
541
- <div class="control-label"><span>رنگ کادر/حاشیه</span></div>
542
- <input type="color" id="colorOutline" value="#000000" oninput="updatePreview()">
543
- </div>
544
-
545
- <div class="control-group">
546
- <div class="control-label"><span>نوع پس‌زمینه</span></div>
547
- <div class="style-chips">
548
- <div class="chip active" onclick="setStyle('solid', this)">هرمزی</div>
549
- <div class="chip" onclick="setStyle('transparent', this)">سینمایی</div>
550
- <div class="chip" onclick="setStyle('outline', this)">ساده</div>
551
- </div>
552
- </div>
553
-
554
- <div class="control-group">
555
- <div class="control-label"><span>فونت</span></div>
556
- <div class="style-chips">
557
- <div class="chip active" onclick="setFont('lalezar', this)">لاله زار</div>
558
- <div class="chip" onclick="setFont('vazir', this)">وزیر</div>
559
- </div>
560
- </div>
561
-
562
- <div class="control-group">
563
- <div class="control-label"><span>اندازه متن</span> <span id="lblSize">60</span></div>
564
- <input type="range" id="rngSize" min="30" max="120" value="60" oninput="updatePreview()">
565
- </div>
566
-
567
- <div class="control-group">
568
- <div class="control-label"><span>موقعیت عمودی</span></div>
569
- <input type="range" id="rngPos" min="10" max="500" value="100" oninput="updatePreview()">
570
- </div>
571
-
572
- <button class="btn btn-primary" onclick="startRender()">
573
- <i class="fa-solid fa-rocket"></i> ساخت خروجی نهایی
574
- </button>
575
- </div>
576
-
577
- <!-- SEGMENTS LIST -->
578
- <div class="card" style="display: flex; flex-direction: column; height: 100%;">
579
- <h2>📝 ویرایش متن‌ها</h2>
580
- <div id="segmentsList" class="segments-container">
581
- <!-- Dynamic Content -->
582
- </div>
583
- </div>
584
-
585
  </div>
586
  </div>
587
 
588
- <!-- VIEW 3: RESULT -->
589
- <div id="view-result" class="view">
590
- <div style="max-width: 800px; margin: 20px auto;">
591
- <div class="card" style="text-align: center;">
592
- <h2 style="color: var(--success);">🎉 ویدیو آماده شد!</h2>
593
- <video id="finalPlayer" controls class="result-video"></video>
594
-
595
- <div style="display: flex; gap: 10px; margin-top: 20px;">
596
- <a id="dlBtn" href="#" download class="btn btn-primary" style="background: var(--success);">
597
- <i class="fa-solid fa-download"></i> دانلود ویدیو
598
- </a>
599
- <button class="btn" style="background: var(--border);" onclick="location.reload()">
600
- <i class="fa-solid fa-rotate-right"></i> پروژه جدید
601
- </button>
602
- </div>
603
- </div>
604
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  </div>
606
 
 
 
 
 
607
  </div>
608
 
609
- <!-- LOGIC -->
610
- <script>
611
- // STATE
612
- let appState = {
613
- fileId: null,
614
- segments: [],
615
- style: {
616
- backType: 'solid',
617
- font: 'lalezar'
618
- }
619
- };
620
 
621
- // --- NAVIGATION ---
622
- function switchView(id) {
623
- document.querySelectorAll('.view').forEach(el => el.classList.remove('active'));
624
- document.getElementById(id).classList.add('active');
625
- }
 
 
626
 
627
- function showLoader(msg) {
628
- document.getElementById('loaderMsg').innerText = msg;
629
- document.getElementById('loader').classList.add('flex');
630
- }
 
631
 
632
- function hideLoader() {
633
- document.getElementById('loader').classList.remove('flex');
634
- }
635
 
636
- // --- UPLOAD ---
637
- async function handleUpload() {
638
- const file = document.getElementById('fileIn').files[0];
639
- if(!file) return;
 
 
640
 
641
- showLoader("در حال آپلود و استخراج متن با هوش مصنوعی...");
642
-
643
- const formData = new FormData();
644
- formData.append("file", file);
645
-
646
- try {
647
- const res = await fetch("/api/analyze", { method: "POST", body: formData });
648
- const data = await res.json();
649
-
650
- if(data.error) throw new Error(data.error);
651
-
652
- appState.fileId = data.file_id;
653
- appState.segments = data.segments;
654
-
655
- renderSegments();
656
- switchView('view-editor');
657
- updatePreview();
658
-
659
- } catch(e) {
660
- alert("Error: " + e.message);
661
- } finally {
662
- hideLoader();
663
- }
664
- }
665
 
666
- // --- EDITOR LOGIC ---
667
- function renderSegments() {
668
- const container = document.getElementById('segmentsList');
669
- container.innerHTML = "";
670
-
671
- appState.segments.forEach((seg, idx) => {
672
- const div = document.createElement('div');
673
- div.className = 'segment-row';
674
- div.innerHTML = `
675
- <div class="seg-time">${formatTime(seg.start)} -> ${formatTime(seg.end)}</div>
676
- <textarea class="seg-input" rows="1" oninput="updateSegment(${idx}, this)">${seg.text}</textarea>
677
- `;
678
- container.appendChild(div);
679
- });
680
 
681
- // Auto resize textareas
682
- document.querySelectorAll('.seg-input').forEach(tx => {
683
- tx.style.height = 'auto';
684
- tx.style.height = (tx.scrollHeight) + 'px';
685
- });
686
- }
687
 
688
- function updateSegment(idx, el) {
689
- appState.segments[idx].text = el.value;
690
- document.getElementById('livePreview').innerText = el.value; // Live typing effect
691
- el.style.height = 'auto';
692
- el.style.height = (el.scrollHeight) + 'px';
693
- }
694
 
695
- function formatTime(s) {
696
- const m = Math.floor(s / 60);
697
- const sec = Math.floor(s % 60);
698
- return `${m}:${sec.toString().padStart(2, '0')}`;
699
- }
700
 
701
- // --- SETTINGS & PREVIEW ---
702
- function setStyle(type, el) {
703
- appState.style.backType = type;
704
- el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
705
- el.classList.add('active');
706
  updatePreview();
707
- }
708
 
709
- function setFont(font, el) {
710
- appState.style.font = font;
711
- el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
712
- el.classList.add('active');
713
- updatePreview();
714
  }
 
715
 
716
- function updatePreview() {
717
- const txt = document.getElementById('livePreview');
718
- const size = document.getElementById('rngSize').value;
719
- const pos = document.getElementById('rngPos').value;
720
- const color = document.getElementById('colorMain').value;
721
- const outline = document.getElementById('colorOutline').value;
722
- const font = appState.style.font === 'lalezar' ? 'Lalezar' : 'Vazirmatn';
723
-
724
- document.getElementById('lblSize').innerText = size;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
- // CSS Simulation of ASS Style
727
- txt.style.fontFamily = font;
728
- txt.style.fontSize = (size / 2) + 'px'; // Scale down for preview box
729
- txt.style.color = color;
730
- txt.style.bottom = (pos / 5) + 'px'; // Scale down pos
 
 
731
 
732
- if(appState.style.backType === 'solid') {
733
- txt.style.backgroundColor = outline;
734
- txt.style.textShadow = 'none';
735
- txt.style.padding = '5px 10px';
736
- txt.style.borderRadius = '4px';
737
- } else if (appState.style.backType === 'transparent') {
738
- txt.style.backgroundColor = 'rgba(0,0,0,0.6)';
739
- txt.style.textShadow = 'none';
740
- txt.style.padding = '5px 10px';
741
- txt.style.borderRadius = '4px';
742
- } else { // Outline
743
- txt.style.backgroundColor = 'transparent';
744
- txt.style.webkitTextStroke = `1px ${outline}`;
745
- txt.style.textShadow = `0 0 2px ${outline}`;
746
- txt.style.padding = '0';
747
- }
748
- }
749
 
750
- // --- RENDER ---
751
- async function startRender() {
752
- showLoader("در حال رندر نهایی ویدیو... (ممکن است کمی طول بکشد)");
753
 
754
- const payload = {
755
- file_id: appState.fileId,
756
- segments: appState.segments,
757
- style: {
758
- font: appState.style.font,
759
- fontSize: parseInt(document.getElementById('rngSize').value),
760
- primaryColor: document.getElementById('colorMain').value,
761
- outlineColor: document.getElementById('colorOutline').value,
762
- backType: appState.style.backType,
763
- outlineWidth: 2.0,
764
- marginV: parseInt(document.getElementById('rngPos').value),
765
- alignment: 2
766
- }
767
- };
768
-
769
- try {
770
- const res = await fetch("/api/render", {
771
- method: "POST",
772
- headers: { "Content-Type": "application/json" },
773
- body: JSON.stringify(payload)
774
- });
775
- const data = await res.json();
776
-
777
- if(data.error) throw new Error(data.error);
778
-
779
- document.getElementById('finalPlayer').src = data.url + "?t=" + Date.now();
780
- document.getElementById('dlBtn').href = data.url;
781
-
782
- switchView('view-result');
783
-
784
- } catch(e) {
785
- alert("Render Error: " + e.message);
786
- } finally {
787
- hideLoader();
788
- }
789
  }
 
 
790
 
791
- </script>
792
  </body>
793
  </html>
794
  """
 
16
  # 1. CONFIGURATION & AI SETUP
17
  # ==========================================
18
 
19
+ app = FastAPI(title="AI Subtitle Monster V3")
20
 
 
21
  app.add_middleware(
22
  CORSMiddleware,
23
  allow_origins=["*"],
 
25
  allow_headers=["*"],
26
  )
27
 
 
28
  TEMP_DIR = "temp"
29
  os.makedirs(TEMP_DIR, exist_ok=True)
30
 
 
31
  print("⚡ [SYSTEM] Initializing AI Neural Network...")
32
  model = WhisperModel("small", device="cpu", compute_type="int8")
33
  print("✅ [SYSTEM] AI Core Ready.")
 
47
  fontSize: int
48
  primaryColor: str
49
  outlineColor: str
50
+ backType: str
51
  outlineWidth: float
52
  marginV: int
53
+ alignment: int
54
 
55
  class ProcessRequest(BaseModel):
56
  file_id: str
 
62
  # ==========================================
63
 
64
  def hex_to_ass(hex_color, alpha="00"):
 
65
  hex_color = hex_color.lstrip('#')
66
  if len(hex_color) != 6: return "&H00FFFFFF"
67
  r, g, b = hex_color[0:2], hex_color[2:4], hex_color[4:6]
 
79
  def generate_ass_file(data: ProcessRequest, output_path: str):
80
  s = data.style
81
 
 
82
  font_map = {"vazir": "Vazirmatn", "lalezar": "Lalezar"}
83
+ font_name = font_map.get(s.font, "Vazirmatn")
84
 
 
85
  primary = hex_to_ass(s.primaryColor)
86
  outline = hex_to_ass(s.outlineColor)
87
 
 
88
  border_style = 1
89
  back_color = "&H00000000"
90
 
91
+ # منطق کادرها
92
  if s.backType == 'solid':
93
+ border_style = 3
94
+ outline = hex_to_ass(s.outlineColor, "00")
95
  elif s.backType == 'transparent':
96
  border_style = 3
97
+ outline = "&H80000000"
98
+ else:
99
  border_style = 1
100
+
101
+ # تنظیمات رزولوشن ASS برای هماهنگی اندازه فونت
102
+ # PlayResY روی 1080 تنظیم شده تا اندازه فونت استاندارد باشد
103
  header = f"""[Script Info]
104
  ScriptType: v4.00+
105
+ PlayResX: 1920
106
+ PlayResY: 1080
107
  WrapStyle: 1
108
 
109
  [V4+ Styles]
 
118
  for seg in data.segments:
119
  start = format_time_ass(seg.start)
120
  end = format_time_ass(seg.end)
 
121
  clean_text = seg.text.strip().replace("\n", "\\N")
122
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{clean_text}\n")
123
 
 
135
  with open(input_path, "wb") as f:
136
  shutil.copyfileobj(file.file, f)
137
 
 
138
  segments_gen, _ = model.transcribe(input_path, language="fa", beam_size=5)
139
 
140
  results = []
 
154
  @app.post("/api/render")
155
  async def render_video(data: ProcessRequest):
156
  try:
 
 
 
157
  exts = ["mp4", "mov", "avi", "mkv", "mp3"]
158
  input_path = None
159
  for ext in exts:
 
168
  ass_path = f"{TEMP_DIR}/{data.file_id}.ass"
169
  output_path = f"{TEMP_DIR}/{data.file_id}_final.mp4"
170
 
 
171
  generate_ass_file(data, ass_path)
172
 
 
173
  cmd = [
174
  "ffmpeg", "-y",
175
  "-i", input_path,
176
  "-vf", f"ass={ass_path}",
177
+ "-c:v", "libx264", "-preset", "ultrafast", "-crf", "24",
178
  "-c:a", "aac",
179
  output_path
180
  ]
 
193
  return JSONResponse(status_code=404, content={"error": "File missing"})
194
 
195
  # ==========================================
196
+ # 5. FRONTEND
197
  # ==========================================
198
 
199
  @app.get("/", response_class=HTMLResponse)
 
204
  <head>
205
  <meta charset="UTF-8">
206
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
207
+ <title>SubMagic Pro</title>
208
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;500;700;900&display=swap" rel="stylesheet">
209
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
210
  <style>
211
  :root {
212
+ --bg-dark: #111827;
213
+ --bg-card: #1f2937;
214
+ --primary: #8b5cf6;
215
+ --accent: #f43f5e;
216
+ --text-main: #f9fafb;
217
+ --text-muted: #9ca3af;
218
+ --border: #374151;
219
+ }
220
+ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; outline: none; }
 
 
 
 
221
  body {
222
  font-family: 'Vazirmatn', sans-serif;
223
  background-color: var(--bg-dark);
224
  color: var(--text-main);
225
  margin: 0;
226
  padding: 0;
 
227
  height: 100vh;
228
  display: flex;
229
  flex-direction: column;
 
 
 
 
 
 
230
  overflow: hidden;
231
  }
232
+
233
+ /* --- COMPONENTS --- */
234
+ .header {
 
 
 
 
 
 
 
 
 
235
  padding: 15px 20px;
236
+ background: rgba(31, 41, 55, 0.9);
 
237
  border-bottom: 1px solid var(--border);
238
  display: flex;
239
  justify-content: space-between;
240
  align-items: center;
 
241
  }
242
+ .logo { font-weight: 900; font-size: 1.3rem; background: linear-gradient(45deg, var(--primary), var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
243
 
244
+ .container {
245
  flex: 1;
246
+ display: grid;
247
+ grid-template-columns: 1fr;
248
+ height: 100%;
249
  overflow: hidden;
250
  position: relative;
251
  }
252
+ @media(min-width: 1024px) { .container { grid-template-columns: 400px 1fr; } }
253
 
254
+ .panel {
 
 
255
  padding: 20px;
256
  overflow-y: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  display: flex;
258
+ flex-direction: column;
259
+ gap: 20px;
 
 
 
 
 
 
260
  }
261
+ .panel-left { background: var(--bg-card); border-left: 1px solid var(--border); z-index: 10; }
262
+ .panel-right { background: var(--bg-dark); }
263
+
264
+ /* --- UPLOAD --- */
265
  .upload-zone {
266
  border: 2px dashed var(--border);
267
+ border-radius: 16px;
268
+ padding: 40px 20px;
269
+ text-align: center;
 
 
 
270
  cursor: pointer;
271
  transition: 0.3s;
272
+ margin-top: 50px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  }
274
+ .upload-zone:hover { border-color: var(--primary); background: rgba(139, 92, 246, 0.1); }
275
 
276
+ /* --- CONTROLS --- */
277
+ .control-group { margin-bottom: 15px; }
278
+ .label { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
 
280
  input[type="range"] {
281
+ width: 100%; height: 6px; background: var(--border); border-radius: 5px; appearance: none;
 
 
 
 
282
  }
283
  input[type="range"]::-webkit-slider-thumb {
284
+ appearance: none; width: 18px; height: 18px; background: var(--primary); border-radius: 50%; cursor: pointer;
 
 
 
 
 
285
  }
 
286
  input[type="color"] {
287
+ width: 100%; height: 40px; border: none; border-radius: 8px; background: transparent; cursor: pointer;
 
 
 
288
  }
289
+
290
+ .chips { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px; }
291
  .chip {
292
+ padding: 8px 15px; background: var(--border); border-radius: 20px; font-size: 0.85rem; cursor: pointer; white-space: nowrap; transition: 0.2s;
293
+ }
294
+ .chip.active { background: var(--primary); color: white; }
295
+
296
+ /* --- EDITOR --- */
297
+ .segment-item {
298
+ background: rgba(255,255,255,0.05);
299
+ border-radius: 12px;
300
+ padding: 12px;
301
+ margin-bottom: 10px;
302
  border: 1px solid transparent;
303
  }
304
+ .segment-item:focus-within { border-color: var(--primary); background: rgba(139, 92, 246, 0.1); }
305
+ .time-badge { font-size: 0.75rem; color: var(--accent); font-family: monospace; margin-bottom: 5px; display: block; }
306
+ .text-area {
307
+ width: 100%; background: transparent; border: none; color: var(--text-main); font-family: inherit; font-size: 1rem; resize: none; overflow: hidden;
308
  }
309
 
310
+ /* --- PREVIEW & RESULT --- */
311
+ .preview-container {
312
  position: relative;
313
  width: 100%;
314
  aspect-ratio: 16/9;
315
  background: #000;
316
  border-radius: 12px;
 
317
  display: flex;
318
  align-items: center;
319
  justify-content: center;
320
  overflow: hidden;
321
+ background-image: url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1000&auto=format&fit=crop');
322
  background-size: cover;
323
+ margin-bottom: 20px;
324
  }
325
  .preview-text {
326
  position: absolute;
327
  text-align: center;
328
  pointer-events: none;
329
  transition: 0.1s;
330
+ white-space: pre-wrap;
331
+ max-width: 90%;
332
  }
333
+
334
+ /* --- RESULT VIDEO --- */
335
+ #resultContainer {
336
+ margin-top: 20px;
337
+ padding-top: 20px;
338
+ border-top: 1px solid var(--border);
339
+ animation: fadeIn 0.5s;
 
 
 
 
 
 
 
340
  }
341
+ video { width: 100%; border-radius: 12px; background: #000; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
342
+
343
+ .btn-main {
344
+ width: 100%; padding: 15px; background: var(--primary); color: white; border: none; border-radius: 12px; font-weight: bold; font-size: 1.1rem; cursor: pointer; margin-top: 10px; display: flex; align-items: center; justify-content: center; gap: 10px;
345
  }
346
+ .btn-main:hover { filter: brightness(1.1); transform: translateY(-2px); transition: 0.2s; }
347
+ .btn-dl { background: #10b981; text-decoration: none; color: white; display: block; padding: 12px; text-align: center; border-radius: 10px; margin-top: 10px; font-weight: bold; }
 
348
 
349
+ /* --- LOADER --- */
350
+ .loader { position: fixed; inset: 0; background: rgba(0,0,0,0.9); z-index: 2000; display: none; flex-direction: column; align-items: center; justify-content: center; }
351
+ .spinner { width: 50px; height: 50px; border: 4px solid #333; border-top-color: var(--primary); border-radius: 50%; animation: spin 1s infinite linear; }
352
+ @keyframes spin { to { transform: rotate(360deg); } }
353
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
354
 
355
+ .hidden { display: none !important; }
356
+
357
+ /* Mobile optimization */
358
+ @media(max-width: 1023px) {
359
+ .panel-left { order: 2; border-left: none; border-top: 1px solid var(--border); max-height: 50vh; }
360
+ .panel-right { order: 1; flex: 1; }
361
+ }
362
  </style>
363
  </head>
364
  <body>
365
 
366
+ <div id="loader" class="loader">
367
+ <div class="spinner"></div>
368
+ <p id="loaderText" style="margin-top: 15px; color: #ccc;">در حال پردازش...</p>
369
+ </div>
370
+
371
+ <div class="header">
372
+ <div class="logo"><i class="fa-solid fa-layer-group"></i> SubMagic Pro</div>
373
+ <button onclick="location.reload()" style="background:none; border:none; color:#fff; cursor:pointer;"><i class="fa-solid fa-rotate-right"></i></button>
374
+ </div>
375
+
376
+ <!-- UPLOAD VIEW -->
377
+ <div id="viewUpload" style="padding: 20px; max-width: 600px; margin: 0 auto;">
378
+ <div class="upload-zone" onclick="document.getElementById('fileIn').click()">
379
+ <i class="fa-solid fa-cloud-arrow-up" style="font-size: 3rem; margin-bottom: 15px; color: var(--text-muted);"></i>
380
+ <h3>آپلود ویدیو</h3>
381
+ <p style="color: var(--text-muted);">ویدیو را انتخاب کنید تا هوش مصنوعی آن را زیرنویس کند</p>
382
+ <input type="file" id="fileIn" hidden accept="video/*" onchange="handleUpload()">
383
  </div>
384
+ </div>
385
 
386
+ <!-- EDITOR VIEW -->
387
+ <div id="viewEditor" class="container hidden">
388
+
389
+ <!-- SETTINGS PANEL -->
390
+ <div class="panel panel-left">
391
+ <h3 style="margin: 0 0 15px 0; color: var(--primary);">تنظیمات گرافیکی</h3>
 
 
392
 
393
+ <div class="control-group">
394
+ <div class="label"><span>نوع کادر</span></div>
395
+ <div class="chips">
396
+ <div class="chip active" onclick="setStyle('solid', this)">هرمزی</div>
397
+ <div class="chip" onclick="setStyle('transparent', this)">سینمایی</div>
398
+ <div class="chip" onclick="setStyle('outline', this)">ساده</div>
 
 
 
 
 
 
399
  </div>
400
  </div>
401
 
402
+ <div class="control-group">
403
+ <div class="label"><span>فونت</span></div>
404
+ <div class="chips">
405
+ <div class="chip active" onclick="setFont('lalezar', this)">لاله زار</div>
406
+ <div class="chip" onclick="setFont('vazir', this)">وزیر</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  </div>
408
  </div>
409
 
410
+ <div class="control-group" style="display: flex; gap: 10px;">
411
+ <div style="flex: 1;">
412
+ <div class="label">رنگ متن</div>
413
+ <input type="color" id="colorMain" value="#FFFFFF" oninput="updatePreview()">
 
 
 
 
 
 
 
 
 
 
 
 
414
  </div>
415
+ <div style="flex: 1;">
416
+ <div class="label">رنگ کادر</div>
417
+ <input type="color" id="colorOutline" value="#000000" oninput="updatePreview()">
418
+ </div>
419
+ </div>
420
+
421
+ <div class="control-group">
422
+ <div class="label"><span>سایز متن</span> <span id="lblSize">80</span></div>
423
+ <!-- محدوده اسلایدر را تا 200 افزایش دادم -->
424
+ <input type="range" id="rngSize" min="40" max="200" value="80" oninput="updatePreview()">
425
+ </div>
426
+
427
+ <div class="control-group">
428
+ <div class="label"><span>موقعیت عمودی</span></div>
429
+ <input type="range" id="rngPos" min="20" max="500" value="100" oninput="updatePreview()">
430
  </div>
431
 
432
+ <button class="btn-main" onclick="startRender()">
433
+ <i class="fa-solid fa-wand-magic-sparkles"></i>
434
+ ساخت خروجی نهایی
435
+ </button>
436
  </div>
437
 
438
+ <!-- PREVIEW & TEXT PANEL -->
439
+ <div class="panel panel-right">
440
+ <!-- Live CSS Preview -->
441
+ <div class="preview-container">
442
+ <div id="livePreview" class="preview-text">اینجا پیش‌نمایش زنده زیرنویس شماست</div>
443
+ </div>
 
 
 
 
 
444
 
445
+ <!-- Result Video Container (Hidden Initially) -->
446
+ <div id="resultContainer" class="hidden">
447
+ <h3 style="color: #10b981; margin-bottom: 10px;">✅ ویدیو ساخته شد:</h3>
448
+ <video id="finalPlayer" controls playsinline></video>
449
+ <a id="dlBtn" href="#" download class="btn-dl">دانلود ویدیو</a>
450
+ <hr style="border-color: var(--border); margin: 20px 0;">
451
+ </div>
452
 
453
+ <!-- Text Segments -->
454
+ <div id="segmentsList" style="padding-bottom: 50px;">
455
+ <!-- Segments injected here -->
456
+ </div>
457
+ </div>
458
 
459
+ </div>
 
 
460
 
461
+ <script>
462
+ let appState = {
463
+ fileId: null,
464
+ segments: [],
465
+ style: { backType: 'solid', font: 'lalezar' }
466
+ };
467
 
468
+ // --- UPLOAD ---
469
+ async function handleUpload() {
470
+ const file = document.getElementById('fileIn').files[0];
471
+ if(!file) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
+ document.getElementById('loader').style.display = 'flex';
474
+ document.getElementById('loaderText').innerText = "در حال آپلود و هوش مصنوعی...";
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
+ const formData = new FormData();
477
+ formData.append("file", file);
 
 
 
 
478
 
479
+ try {
480
+ const res = await fetch("/api/analyze", { method: "POST", body: formData });
481
+ const data = await res.json();
482
+
483
+ if(data.error) throw new Error(data.error);
 
484
 
485
+ appState.fileId = data.file_id;
486
+ appState.segments = data.segments;
 
 
 
487
 
488
+ renderSegments();
489
+
490
+ document.getElementById('viewUpload').classList.add('hidden');
491
+ document.getElementById('viewEditor').classList.remove('hidden');
 
492
  updatePreview();
 
493
 
494
+ } catch(e) {
495
+ alert("خطا: " + e.message);
496
+ } finally {
497
+ document.getElementById('loader').style.display = 'none';
 
498
  }
499
+ }
500
 
501
+ // --- RENDER SEGMENTS ---
502
+ function renderSegments() {
503
+ const container = document.getElementById('segmentsList');
504
+ container.innerHTML = "";
505
+
506
+ appState.segments.forEach((seg, idx) => {
507
+ const div = document.createElement('div');
508
+ div.className = 'segment-item';
509
+ div.innerHTML = `
510
+ <span class="time-tag">${Math.round(seg.start)}s - ${Math.round(seg.end)}s</span>
511
+ <textarea class="text-area" rows="1" oninput="updateSegment(${idx}, this)">${seg.text}</textarea>
512
+ `;
513
+ container.appendChild(div);
514
+ });
515
+
516
+ // Auto resize
517
+ document.querySelectorAll('.text-area').forEach(el => {
518
+ el.style.height = 'auto';
519
+ el.style.height = el.scrollHeight + 'px';
520
+ });
521
+ }
522
+
523
+ function updateSegment(idx, el) {
524
+ appState.segments[idx].text = el.value;
525
+ document.getElementById('livePreview').innerText = el.value;
526
+ el.style.height = 'auto';
527
+ el.style.height = el.scrollHeight + 'px';
528
+ }
529
+
530
+ // --- SETTINGS ---
531
+ function setStyle(type, el) {
532
+ appState.style.backType = type;
533
+ el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
534
+ el.classList.add('active');
535
+ updatePreview();
536
+ }
537
+
538
+ function setFont(font, el) {
539
+ appState.style.font = font;
540
+ el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
541
+ el.classList.add('active');
542
+ updatePreview();
543
+ }
544
+
545
+ function updatePreview() {
546
+ const txt = document.getElementById('livePreview');
547
+ const size = parseInt(document.getElementById('rngSize').value);
548
+ const pos = parseInt(document.getElementById('rngPos').value);
549
+ const color = document.getElementById('colorMain').value;
550
+ const outline = document.getElementById('colorOutline').value;
551
+
552
+ document.getElementById('lblSize').innerText = size;
553
+
554
+ // شبیه‌سازی CSS برای پیش‌نمایش (تقریبی)
555
+ // مقیاس‌دهی: سایز ASS با پیکسل CSS فرق دارد، اینجا تقریبی اسکیل میکنیم
556
+ txt.style.fontFamily = appState.style.font === 'lalezar' ? 'Lalezar' : 'Vazirmatn';
557
+ txt.style.fontSize = (size * 0.6) + 'px';
558
+ txt.style.color = color;
559
+ txt.style.bottom = (pos / 4) + 'px'; // Scale pos for preview box
560
+
561
+ if(appState.style.backType === 'solid') {
562
+ txt.style.backgroundColor = outline;
563
+ txt.style.textShadow = 'none';
564
+ txt.style.padding = '5px 15px';
565
+ txt.style.webkitTextStroke = '0';
566
+ } else if (appState.style.backType === 'transparent') {
567
+ txt.style.backgroundColor = 'rgba(0,0,0,0.6)';
568
+ txt.style.textShadow = 'none';
569
+ txt.style.padding = '5px 15px';
570
+ txt.style.webkitTextStroke = '0';
571
+ } else {
572
+ txt.style.backgroundColor = 'transparent';
573
+ txt.style.padding = '0';
574
+ txt.style.webkitTextStroke = `1px ${outline}`;
575
+ txt.style.textShadow = `2px 2px 0 ${outline}`;
576
+ }
577
+ txt.style.borderRadius = '8px';
578
+ }
579
+
580
+ // --- FINAL RENDER ---
581
+ async function startRender() {
582
+ document.getElementById('loader').style.display = 'flex';
583
+ document.getElementById('loaderText').innerText = "در حال رندر ویدیو نهایی...";
584
+
585
+ const payload = {
586
+ file_id: appState.fileId,
587
+ segments: appState.segments,
588
+ style: {
589
+ font: appState.style.font,
590
+ fontSize: parseInt(document.getElementById('rngSize').value),
591
+ primaryColor: document.getElementById('colorMain').value,
592
+ outlineColor: document.getElementById('colorOutline').value,
593
+ backType: appState.style.backType,
594
+ outlineWidth: 2.0,
595
+ marginV: parseInt(document.getElementById('rngPos').value),
596
+ alignment: 2
597
+ }
598
+ };
599
 
600
+ try {
601
+ const res = await fetch("/api/render", {
602
+ method: "POST",
603
+ headers: { "Content-Type": "application/json" },
604
+ body: JSON.stringify(payload)
605
+ });
606
+ const data = await res.json();
607
 
608
+ if(data.error) throw new Error(data.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
610
+ // نمایش نتیجه در همان صفحه
611
+ const resultBox = document.getElementById('resultContainer');
612
+ resultBox.classList.remove('hidden');
613
 
614
+ const player = document.getElementById('finalPlayer');
615
+ // اضافه کردن زمان برای جلوگیری از کش شدن ویدیو قبلی
616
+ player.src = data.url + "?t=" + Date.now();
617
+
618
+ document.getElementById('dlBtn').href = data.url;
619
+
620
+ // اسکرول نرم به ویدیو
621
+ resultBox.scrollIntoView({ behavior: 'smooth' });
622
+
623
+ } catch(e) {
624
+ alert("خطا: " + e.message);
625
+ } finally {
626
+ document.getElementById('loader').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  }
628
+ }
629
+ </script>
630
 
 
631
  </body>
632
  </html>
633
  """