Opera8 commited on
Commit
7f7b879
·
verified ·
1 Parent(s): 682dcb4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -85
app.py CHANGED
@@ -3,7 +3,7 @@ import shutil
3
  import subprocess
4
  import uuid
5
  from datetime import timedelta
6
- from fastapi import FastAPI, UploadFile, File, BackgroundTasks
7
  from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
8
  from faster_whisper import WhisperModel
9
 
@@ -13,15 +13,44 @@ app = FastAPI()
13
  TEMP_DIR = "temp"
14
  os.makedirs(TEMP_DIR, exist_ok=True)
15
 
16
- # لود کردن مدل هوش مصنوعی (فقط یک بار اجرا می‌شود)
17
  print("Loading Whisper AI...")
18
  model = WhisperModel("small", device="cpu", compute_type="int8")
19
  print("Model Loaded!")
20
 
21
- # --- توابع کمکی ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def format_timestamp(seconds: float):
24
- """تبدیل زمان به فرمت ASS"""
25
  td = timedelta(seconds=seconds)
26
  total_seconds = int(td.total_seconds())
27
  hours = total_seconds // 3600
@@ -30,10 +59,58 @@ def format_timestamp(seconds: float):
30
  centisecs = int(td.microseconds / 10000)
31
  return f"{hours:01d}:{minutes:02d}:{secs:02d}.{centisecs:02d}"
32
 
33
- def create_styled_ass(segments, output_file):
34
- """ساخت زیرنویس با استایل زرد و مشکی جذاب (هرمزی)"""
35
- # تنظیمات استایل: فونت Noto Sans Arabic، سایز بزرگ، کادر زرد
36
- ass_content = """[Script Info]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  ScriptType: v4.00+
38
  PlayResX: 1080
39
  PlayResY: 1920
@@ -41,11 +118,12 @@ WrapStyle: 0
41
 
42
  [V4+ Styles]
43
  Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
44
- Style: Default,Noto Sans Arabic UI,60,&H00000000,&H000000FF,&H00000000,&H0000D7FF,1,0,0,0,100,100,0,0,3,2,0,2,10,10,180,1
45
 
46
  [Events]
47
  Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
48
  """
 
49
  with open(output_file, "w", encoding="utf-8") as f:
50
  f.write(ass_content)
51
  for segment in segments:
@@ -55,13 +133,12 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
55
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}\n")
56
 
57
  def burn_subtitles(video_path, ass_path, output_path):
58
- """چسباندن زیرنویس به ویدیو"""
59
  try:
60
  subprocess.run([
61
  "ffmpeg", "-y",
62
  "-i", video_path,
63
  "-vf", f"ass={ass_path}",
64
- "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28", # تنظیم سرعت بالا
65
  "-c:a", "copy",
66
  output_path
67
  ], check=True)
@@ -80,59 +157,80 @@ async def home():
80
  <head>
81
  <meta charset="UTF-8">
82
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
- <title>زیرنویس‌ساز هوشمند اینستاگرام</title>
84
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;700;900&display=swap" rel="stylesheet">
85
  <style>
86
- :root { --primary: #FFD700; --dark: #1a1a1a; }
87
- body { font-family: 'Vazirmatn', sans-serif; background: #f0f2f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
88
- .container { background: white; width: 90%; max-width: 450px; padding: 2rem; border-radius: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); text-align: center; }
89
- h1 { color: var(--dark); font-weight: 900; margin-bottom: 5px; }
90
- p { color: #666; font-size: 0.9rem; margin-bottom: 2rem; }
91
 
92
- .upload-box { border: 2px dashed #ddd; padding: 2rem; border-radius: 16px; cursor: pointer; transition: 0.3s; background: #fafafa; position: relative; overflow: hidden; }
93
- .upload-box:hover { border-color: var(--primary); background: #fffdf0; }
94
- .upload-icon { font-size: 3rem; color: #ccc; margin-bottom: 10px; }
95
 
96
- #videoPreview { width: 100%; border-radius: 12px; display: none; margin-top: 1rem; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
97
 
98
- .btn { background: var(--dark); color: var(--primary); border: none; padding: 1rem 2rem; font-size: 1.1rem; font-weight: bold; border-radius: 12px; cursor: pointer; width: 100%; margin-top: 1.5rem; transition: 0.3s; display: none; }
99
- .btn:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.2); }
100
 
101
- .loader { display: none; margin-top: 20px; }
102
- .spinner { width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 10px; }
 
 
 
103
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
104
- .status-text { font-size: 0.9rem; color: #555; font-weight: bold; }
105
 
106
- .result-area { display: none; margin-top: 2rem; animation: fadeIn 0.5s; }
107
- .download-btn { text-decoration: none; background: #25D366; color: white; padding: 10px 20px; border-radius: 10px; display: inline-block; font-weight: bold; margin-top: 10px; }
 
108
 
109
  @keyframes fadeIn { from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:translateY(0)} }
110
  </style>
111
  </head>
112
  <body>
113
  <div class="container">
114
- <h1>زیرنویس‌ساز جادویی ✨</h1>
115
- <p>ویدیو بده، با زیرنویس هرمزی تحویل بگیر!</p>
116
 
117
- <div class="upload-box" onclick="document.getElementById('fileInput').click()">
118
- <div class="upload-icon">📂</div>
119
  <div id="fileName">برای انتخاب ویدیو کلیک کنید</div>
120
  <input type="file" id="fileInput" accept="video/*" hidden onchange="handleFile(this)">
121
  </div>
122
 
123
- <button id="startBtn" class="btn" onclick="uploadAndProcess()">شروع ساخت زیرنویس ⚡</button>
124
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  <div id="loader" class="loader">
126
  <div class="spinner"></div>
127
- <div class="status-text" id="statusText">در حال گوش دادن به ویدیو...</div>
128
- <small style="color:#999; display:block; margin-top:5px;">(ممکن است ۱ تا ۳ دقیقه طول بکشد)</small>
129
  </div>
130
 
131
- <div id="resultArea" class="result-area">
132
- <h3>✅ ویدیو آماده شد!</h3>
133
- <video id="finalVideo" controls style="width:100%; border-radius:12px;"></video>
134
- <a id="downloadLink" class="download-btn" download>دانلود ویدیو</a>
135
- <button onclick="location.reload()" style="background:none; border:none; color:#666; margin-top:15px; cursor:pointer; text-decoration:underline;">ساخت ویدیو جدید</button>
 
136
  </div>
137
  </div>
138
 
@@ -140,50 +238,40 @@ async def home():
140
  let selectedFile = null;
141
 
142
  function handleFile(input) {
143
- if (input.files && input.files[0]) {
144
  selectedFile = input.files[0];
145
  document.getElementById('fileName').innerText = selectedFile.name;
146
- document.getElementById('startBtn').style.display = 'block';
147
  }
148
  }
149
 
150
- async function uploadAndProcess() {
151
  if (!selectedFile) return;
152
 
153
- // UI Updates
154
- document.querySelector('.upload-box').style.display = 'none';
155
- document.getElementById('startBtn').style.display = 'none';
 
156
  document.getElementById('loader').style.display = 'block';
157
 
158
  const formData = new FormData();
159
  formData.append("file", selectedFile);
 
 
160
 
161
  try {
162
- // Step 1: Upload & Process
163
- document.getElementById('statusText').innerText = "هوش مصنوعی در حال نوشتن زیرنویس...";
164
 
165
- const response = await fetch("/api/process", {
166
- method: "POST",
167
- body: formData
168
- });
169
-
170
- if (!response.ok) throw new Error("Server Error");
171
-
172
- const data = await response.json();
173
 
174
- // Step 2: Show Result
175
  document.getElementById('loader').style.display = 'none';
176
- const resultArea = document.getElementById('resultArea');
177
- resultArea.style.display = 'block';
178
-
179
- const videoPlayer = document.getElementById('finalVideo');
180
- videoPlayer.src = data.url;
181
-
182
- const downloadLink = document.getElementById('downloadLink');
183
- downloadLink.href = data.url;
184
 
185
- } catch (error) {
186
- alert("خطایی رخ داد. لطفا دوباره تلاش کنید.");
187
  location.reload();
188
  }
189
  }
@@ -193,42 +281,44 @@ async def home():
193
  """
194
 
195
  @app.post("/api/process")
196
- async def process_video(file: UploadFile = File(...)):
 
 
 
 
197
  try:
198
  job_id = str(uuid.uuid4())
199
- input_path = f"{TEMP_DIR}/{job_id}_input.mp4"
200
  ass_path = f"{TEMP_DIR}/{job_id}.ass"
201
- output_path = f"{TEMP_DIR}/{job_id}_final.mp4"
202
 
203
- # 1. ذخیره فایل ورودی
204
  with open(input_path, "wb") as buffer:
205
  shutil.copyfileobj(file.file, buffer)
206
 
207
- # 2. تبدیل صدا به متن (فارسی)
208
  segments, _ = model.transcribe(input_path, language="fa", beam_size=5)
209
- segments = list(segments) # اجرا
210
 
211
  if not segments:
212
- return JSONResponse(status_code=400, content={"error": "No speech found"})
213
 
214
- # 3. ساخت فایل زیرنویس گرافیکی
215
- create_styled_ass(segments, ass_path)
216
 
217
- # 4. چسباندن زیرنویس
218
  if burn_subtitles(input_path, ass_path, output_path):
219
- # پاک کردن فایل‌های اضافی
220
  os.remove(input_path)
221
  os.remove(ass_path)
222
- return {"url": f"/download/{job_id}_final.mp4"}
223
  else:
224
- return JSONResponse(status_code=500, content={"error": "FFmpeg failed"})
225
 
226
  except Exception as e:
227
  return JSONResponse(status_code=500, content={"error": str(e)})
228
 
229
  @app.get("/download/{filename}")
230
  async def download_file(filename: str):
231
- file_path = f"{TEMP_DIR}/{filename}"
232
- if os.path.exists(file_path):
233
- return FileResponse(file_path)
234
- return JSONResponse(status_code=404, content={"error": "File not found"})
 
3
  import subprocess
4
  import uuid
5
  from datetime import timedelta
6
+ from fastapi import FastAPI, UploadFile, File, Form
7
  from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
8
  from faster_whisper import WhisperModel
9
 
 
13
  TEMP_DIR = "temp"
14
  os.makedirs(TEMP_DIR, exist_ok=True)
15
 
16
+ # لود مدل
17
  print("Loading Whisper AI...")
18
  model = WhisperModel("small", device="cpu", compute_type="int8")
19
  print("Model Loaded!")
20
 
21
+ # --- تعریف استایل‌ها و فونت‌ها ---
22
+
23
+ STYLES_CONFIG = {
24
+ "hormozi": {
25
+ "name": "هرمزی (زرد)",
26
+ # زرد جیغ پشت متن، متن مشکی
27
+ "ass_style": "BackColour=&H0000FFFF,PrimaryColour=&H00000000,BorderStyle=3,Outline=0,Shadow=0"
28
+ },
29
+ "cinema": {
30
+ "name": "سینمایی (مشکی)",
31
+ # مشکی نیمه شفاف پشت متن، متن سفید
32
+ "ass_style": "BackColour=&H80000000,PrimaryColour=&H00FFFFFF,BorderStyle=3,Outline=0,Shadow=0"
33
+ },
34
+ "neon": {
35
+ "name": "نئون (آبی)",
36
+ # متن سفید با حاشیه ضخیم آبی، بدون کادر
37
+ "ass_style": "BackColour=&H00000000,PrimaryColour=&H00FFFFFF,OutlineColour=&H00FF0000,BorderStyle=1,Outline=3,Shadow=1"
38
+ },
39
+ "clean": {
40
+ "name": "ساده (سفید)",
41
+ # متن سفید با حاشیه مشکی باریک
42
+ "ass_style": "BackColour=&H00000000,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=2,Shadow=1"
43
+ }
44
+ }
45
+
46
+ FONTS_CONFIG = {
47
+ "vazir": "Vazirmatn",
48
+ "lalezar": "Lalezar"
49
+ }
50
+
51
+ # --- توابع ---
52
 
53
  def format_timestamp(seconds: float):
 
54
  td = timedelta(seconds=seconds)
55
  total_seconds = int(td.total_seconds())
56
  hours = total_seconds // 3600
 
59
  centisecs = int(td.microseconds / 10000)
60
  return f"{hours:01d}:{minutes:02d}:{secs:02d}.{centisecs:02d}"
61
 
62
+ def create_custom_ass(segments, output_file, style_key, font_key):
63
+ """ساخت فایل زیرنویس با تنظیمات کاربر"""
64
+
65
+ # دریافت تنظیمات انتخاب شده
66
+ style_settings = STYLES_CONFIG.get(style_key, STYLES_CONFIG["hormozi"])["ass_style"]
67
+ font_name = FONTS_CONFIG.get(font_key, "Vazirmatn")
68
+
69
+ # هدر فایل ASS
70
+ ass_content = f"""[Script Info]
71
+ ScriptType: v4.00+
72
+ PlayResX: 1080
73
+ PlayResY: 1920
74
+ WrapStyle: 0
75
+
76
+ [V4+ Styles]
77
+ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
78
+ Style: Default,{font_name},70,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,2,0,2,10,10,180,1
79
+
80
+ [Events]
81
+ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
82
+ """
83
+ # جایگزینی استایل انتخاب شده در هدر (Trick برای ساده سازی)
84
+ # ما خط استایل پیش فرض را با تنظیمات کاربر آپدیت میکنیم
85
+ base_style = f"Style: Default,{font_name},65,{{primary}},&H000000FF,{{outline}},{{back}},-1,0,0,0,100,100,0,0,{{border}},{{out_width}},{{shadow}},2,10,10,180,1"
86
+
87
+ # پارس کردن تنظیمات ساده به فرمت کامل ASS
88
+ # این یک روش ساده‌سازی شده است. برای کنترل دقیق‌تر، مقادیر را مستقیم جاگذاری می‌کنیم:
89
+
90
+ final_style_line = f"Style: Default,{font_name},60,PrimaryColour,SecondaryColour,OutlineColour,BackColour,-1,0,0,0,100,100,0,0,BorderStyle,Outline,Shadow,2,10,10,150,1"
91
+
92
+ # اعمال تغییرات استایل روی رشته
93
+ # مقادیر پیش فرض
94
+ params = {
95
+ "PrimaryColour": "&H00FFFFFF", "OutlineColour": "&H00000000", "BackColour": "&H00000000",
96
+ "BorderStyle": "1", "Outline": "2", "Shadow": "0"
97
+ }
98
+
99
+ # آپدیت با انتخاب کاربر
100
+ user_settings = style_settings.split(",")
101
+ for setting in user_settings:
102
+ key, value = setting.split("=")
103
+ params[key] = value
104
+
105
+ real_style_line = f"Style: Default,{font_name},65,{params['PrimaryColour']},{params['PrimaryColour']},{params['OutlineColour']},{params['BackColour']},-1,0,0,0,100,100,0,0,{params['BorderStyle']},{params['Outline']},{params['Shadow']},2,10,10,150,1"
106
+
107
+ # بازنویسی هدر با استایل نهایی
108
+ ass_content = ass_content.replace(
109
+ "Style: Default,Noto Sans Arabic UI,60,&H00000000,&H000000FF,&H00000000,&H0000D7FF,1,0,0,0,100,100,0,0,3,2,0,2,10,10,180,1",
110
+ real_style_line
111
+ )
112
+ # روش مطمئن‌تر: ساخت هدر جدید
113
+ ass_content = f"""[Script Info]
114
  ScriptType: v4.00+
115
  PlayResX: 1080
116
  PlayResY: 1920
 
118
 
119
  [V4+ Styles]
120
  Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
121
+ {real_style_line}
122
 
123
  [Events]
124
  Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
125
  """
126
+
127
  with open(output_file, "w", encoding="utf-8") as f:
128
  f.write(ass_content)
129
  for segment in segments:
 
133
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}\n")
134
 
135
  def burn_subtitles(video_path, ass_path, output_path):
 
136
  try:
137
  subprocess.run([
138
  "ffmpeg", "-y",
139
  "-i", video_path,
140
  "-vf", f"ass={ass_path}",
141
+ "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28",
142
  "-c:a", "copy",
143
  output_path
144
  ], check=True)
 
157
  <head>
158
  <meta charset="UTF-8">
159
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
160
+ <title>استودیو زیرنویس هوشمند</title>
161
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;700;900&display=swap" rel="stylesheet">
162
  <style>
163
+ :root { --primary: #6C63FF; --bg: #F4F7FE; --card: #ffffff; }
164
+ body { font-family: 'Vazirmatn', sans-serif; background: var(--bg); margin: 0; padding: 20px; display: flex; justify-content: center; min-height: 100vh; }
165
+ .container { background: var(--card); width: 100%; max-width: 500px; padding: 2rem; border-radius: 24px; box-shadow: 0 10px 40px rgba(0,0,0,0.05); }
166
+
167
+ h1 { text-align: center; color: #2B3674; margin-bottom: 30px; }
168
 
169
+ .upload-area { border: 2px dashed #E0E5F2; border-radius: 16px; padding: 40px; text-align: center; cursor: pointer; transition: 0.3s; margin-bottom: 20px; }
170
+ .upload-area:hover { border-color: var(--primary); background: #F4F7FE; }
171
+ .upload-icon { font-size: 40px; margin-bottom: 10px; }
172
 
173
+ .options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px; }
174
 
175
+ label { display: block; margin-bottom: 8px; font-weight: bold; color: #2B3674; font-size: 0.9rem; }
176
+ select { width: 100%; padding: 12px; border-radius: 12px; border: 1px solid #E0E5F2; font-family: 'Vazirmatn'; font-size: 1rem; outline: none; }
177
 
178
+ .btn { background: var(--primary); color: white; border: none; width: 100%; padding: 15px; border-radius: 16px; font-size: 1.1rem; font-weight: bold; cursor: pointer; transition: 0.2s; margin-top: 10px; display: none; }
179
+ .btn:hover { background: #5751D1; transform: translateY(-2px); }
180
+
181
+ .loader { display: none; text-align: center; margin-top: 20px; }
182
+ .spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 10px; }
183
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
184
 
185
+ .result-box { display: none; margin-top: 30px; text-align: center; animation: fadeIn 0.5s; }
186
+ video { width: 100%; border-radius: 12px; margin-bottom: 15px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
187
+ .dl-btn { background: #05CD99; color: white; text-decoration: none; padding: 12px 30px; border-radius: 12px; font-weight: bold; display: inline-block; }
188
 
189
  @keyframes fadeIn { from{opacity:0; transform:translateY(10px)} to{opacity:1; transform:translateY(0)} }
190
  </style>
191
  </head>
192
  <body>
193
  <div class="container">
194
+ <h1>✨ زیرنویس‌ساز جادویی</h1>
 
195
 
196
+ <div class="upload-area" onclick="document.getElementById('fileInput').click()">
197
+ <div class="upload-icon">📹</div>
198
  <div id="fileName">برای انتخاب ویدیو کلیک کنید</div>
199
  <input type="file" id="fileInput" accept="video/*" hidden onchange="handleFile(this)">
200
  </div>
201
 
202
+ <div class="options-grid">
203
+ <div>
204
+ <label>🎨 استایل زیرنویس</label>
205
+ <select id="styleSelect">
206
+ <option value="hormozi">هرمزی (کادر زرد)</option>
207
+ <option value="cinema">سینمایی (کادر مشکی)</option>
208
+ <option value="neon">نئون (آبی)</option>
209
+ <option value="clean">ساده (سفید)</option>
210
+ </select>
211
+ </div>
212
+ <div>
213
+ <label>✒️ فونت</label>
214
+ <select id="fontSelect">
215
+ <option value="vazir">وزیر (استاندارد)</option>
216
+ <option value="lalezar">لاله زار (ضخیم)</option>
217
+ </select>
218
+ </div>
219
+ </div>
220
+
221
+ <button id="processBtn" class="btn" onclick="startProcess()">شروع پردازش ⚡</button>
222
+
223
  <div id="loader" class="loader">
224
  <div class="spinner"></div>
225
+ <span id="statusText">در حال گوش دادن و نوشتن...</span>
 
226
  </div>
227
 
228
+ <div id="result" class="result-box">
229
+ <video id="finalVideo" controls></video>
230
+ <br>
231
+ <a id="dlLink" class="dl-btn" download>دانلود ویدیو نهایی</a>
232
+ <br><br>
233
+ <a href="javascript:location.reload()" style="color: #A3AED0; text-decoration: none; font-size: 0.9rem;">شروع دوباره</a>
234
  </div>
235
  </div>
236
 
 
238
  let selectedFile = null;
239
 
240
  function handleFile(input) {
241
+ if (input.files[0]) {
242
  selectedFile = input.files[0];
243
  document.getElementById('fileName').innerText = selectedFile.name;
244
+ document.getElementById('processBtn').style.display = 'block';
245
  }
246
  }
247
 
248
+ async function startProcess() {
249
  if (!selectedFile) return;
250
 
251
+ // UI setup
252
+ document.querySelector('.upload-area').style.display = 'none';
253
+ document.querySelector('.options-grid').style.display = 'none';
254
+ document.getElementById('processBtn').style.display = 'none';
255
  document.getElementById('loader').style.display = 'block';
256
 
257
  const formData = new FormData();
258
  formData.append("file", selectedFile);
259
+ formData.append("style", document.getElementById('styleSelect').value);
260
+ formData.append("font", document.getElementById('fontSelect').value);
261
 
262
  try {
263
+ const res = await fetch("/api/process", { method: "POST", body: formData });
264
+ if (!res.ok) throw new Error("خطا در سرور");
265
 
266
+ const data = await res.json();
 
 
 
 
 
 
 
267
 
 
268
  document.getElementById('loader').style.display = 'none';
269
+ document.getElementById('result').style.display = 'block';
270
+ document.getElementById('finalVideo').src = data.url;
271
+ document.getElementById('dlLink').href = data.url;
 
 
 
 
 
272
 
273
+ } catch (e) {
274
+ alert("مشکلی پیش آمد: " + e.message);
275
  location.reload();
276
  }
277
  }
 
281
  """
282
 
283
  @app.post("/api/process")
284
+ async def process_video(
285
+ file: UploadFile = File(...),
286
+ style: str = Form("hormozi"),
287
+ font: str = Form("vazir")
288
+ ):
289
  try:
290
  job_id = str(uuid.uuid4())
291
+ input_path = f"{TEMP_DIR}/{job_id}_in.mp4"
292
  ass_path = f"{TEMP_DIR}/{job_id}.ass"
293
+ output_path = f"{TEMP_DIR}/{job_id}_out.mp4"
294
 
295
+ # ذخیره
296
  with open(input_path, "wb") as buffer:
297
  shutil.copyfileobj(file.file, buffer)
298
 
299
+ # پردازش متن
300
  segments, _ = model.transcribe(input_path, language="fa", beam_size=5)
301
+ segments = list(segments)
302
 
303
  if not segments:
304
+ return JSONResponse(status_code=400, content={"error": "متنی یافت نشد"})
305
 
306
+ # ساخت زیرنویس با استایل انتخابی
307
+ create_custom_ass(segments, ass_path, style, font)
308
 
309
+ # چسباندن
310
  if burn_subtitles(input_path, ass_path, output_path):
 
311
  os.remove(input_path)
312
  os.remove(ass_path)
313
+ return {"url": f"/download/{job_id}_out.mp4"}
314
  else:
315
+ return JSONResponse(status_code=500, content={"error": "Burn Error"})
316
 
317
  except Exception as e:
318
  return JSONResponse(status_code=500, content={"error": str(e)})
319
 
320
  @app.get("/download/{filename}")
321
  async def download_file(filename: str):
322
+ path = f"{TEMP_DIR}/{filename}"
323
+ if os.path.exists(path): return FileResponse(path)
324
+ return JSONResponse(status_code=404, content={"error": "Not Found"})