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

Update app.py

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