Opera8 commited on
Commit
4ada954
·
verified ·
1 Parent(s): b276a45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -170
app.py CHANGED
@@ -2,10 +2,14 @@ import os
2
  import shutil
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
 
10
  app = FastAPI()
11
 
@@ -18,28 +22,35 @@ 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
 
@@ -48,7 +59,7 @@ FONTS_CONFIG = {
48
  "lalezar": "Lalezar"
49
  }
50
 
51
- # --- توابع ---
52
 
53
  def format_timestamp(seconds: float):
54
  td = timedelta(seconds=seconds)
@@ -59,58 +70,12 @@ def format_timestamp(seconds: float):
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,18 +83,17 @@ WrapStyle: 0
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:
130
- start = format_timestamp(segment.start)
131
- end = format_timestamp(segment.end)
132
- text = segment.text.strip()
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):
@@ -138,13 +102,13 @@ def burn_subtitles(video_path, ass_path, output_path):
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)
145
  return True
146
  except Exception as e:
147
- print(f"Error: {e}")
148
  return False
149
 
150
  # --- روت‌ها ---
@@ -157,122 +121,193 @@ async def home():
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
 
237
  <script>
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
  }
278
  </script>
@@ -280,45 +315,59 @@ async def home():
280
  </html>
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"})
 
2
  import shutil
3
  import subprocess
4
  import uuid
5
+ import json
6
  from datetime import timedelta
7
+ from fastapi import FastAPI, UploadFile, File, Form, Body
8
  from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
  from faster_whisper import WhisperModel
11
+ from pydantic import BaseModel
12
+ from typing import List
13
 
14
  app = FastAPI()
15
 
 
22
  model = WhisperModel("small", device="cpu", compute_type="int8")
23
  print("Model Loaded!")
24
 
25
+ # --- مدل‌های داده (برای دریافت اطلاعات از سمت کاربر) ---
26
+ class SegmentData(BaseModel):
27
+ start: float
28
+ end: float
29
+ text: str
30
 
31
+ class BurnRequest(BaseModel):
32
+ file_id: str
33
+ style: str
34
+ font: str
35
+ segments: List[SegmentData]
36
+
37
+ # --- تعریف استایل‌ها ---
38
  STYLES_CONFIG = {
39
  "hormozi": {
40
  "name": "هرمزی (زرد)",
41
+ "ass_header": "Style: Default,{font_name},60,&H00000000,&H000000FF,&H00000000,&H0000FFFF,1,0,0,0,100,100,0,0,3,2,0,2,10,10,180,1"
 
42
  },
43
  "cinema": {
44
+ "name": "سینمایی (کادر تیره)",
45
+ "ass_header": "Style: Default,{font_name},50,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,3,0,0,2,10,10,50,1"
 
46
  },
47
  "neon": {
48
  "name": "نئون (آبی)",
49
+ "ass_header": "Style: Default,{font_name},60,&H00FFFFFF,&H000000FF,&H00FF0000,&H00000000,1,0,0,0,100,100,0,0,1,3,1,2,10,10,150,1"
 
50
  },
51
  "clean": {
52
+ "name": "ساده (سفید با حاشیه)",
53
+ "ass_header": "Style: Default,{font_name},55,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,1,2,10,10,100,1"
 
54
  }
55
  }
56
 
 
59
  "lalezar": "Lalezar"
60
  }
61
 
62
+ # --- توابع کمکی ---
63
 
64
  def format_timestamp(seconds: float):
65
  td = timedelta(seconds=seconds)
 
70
  centisecs = int(td.microseconds / 10000)
71
  return f"{hours:01d}:{minutes:02d}:{secs:02d}.{centisecs:02d}"
72
 
73
+ def create_ass_file(segments, output_file, style_key, font_key):
 
 
 
 
74
  font_name = FONTS_CONFIG.get(font_key, "Vazirmatn")
75
+ style_template = STYLES_CONFIG.get(style_key, STYLES_CONFIG["hormozi"])["ass_header"]
76
+ final_style = style_template.replace("{font_name}", font_name)
 
 
 
 
 
77
 
78
+ header = f"""[Script Info]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ScriptType: v4.00+
80
  PlayResX: 1080
81
  PlayResY: 1920
 
83
 
84
  [V4+ Styles]
85
  Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
86
+ {final_style}
87
 
88
  [Events]
89
  Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
90
  """
 
91
  with open(output_file, "w", encoding="utf-8") as f:
92
+ f.write(header)
93
+ for seg in segments:
94
+ start = format_timestamp(seg.start)
95
+ end = format_timestamp(seg.end)
96
+ text = seg.text.strip().replace("\n", " ")
97
  f.write(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}\n")
98
 
99
  def burn_subtitles(video_path, ass_path, output_path):
 
102
  "ffmpeg", "-y",
103
  "-i", video_path,
104
  "-vf", f"ass={ass_path}",
105
+ "-c:v", "libx264", "-preset", "ultrafast", "-crf", "26",
106
  "-c:a", "copy",
107
  output_path
108
  ], check=True)
109
  return True
110
  except Exception as e:
111
+ print(f"FFmpeg Error: {e}")
112
  return False
113
 
114
  # --- روت‌ها ---
 
121
  <head>
122
  <meta charset="UTF-8">
123
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
124
+ <title>ویرایشگر زیرنویس هوشمند</title>
125
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;700;900&display=swap" rel="stylesheet">
126
  <style>
127
+ :root { --primary: #4361ee; --bg: #f8f9fa; --card: #ffffff; --border: #e9ecef; }
128
+ body { font-family: 'Vazirmatn', sans-serif; background: var(--bg); margin: 0; padding: 20px; }
 
129
 
130
+ .container { max-width: 800px; margin: 0 auto; background: var(--card); padding: 25px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.05); }
131
+ h1 { text-align: center; color: #2b2d42; margin-bottom: 30px; }
132
 
133
+ /* آپلودر */
134
+ .upload-area { border: 2px dashed var(--primary); border-radius: 16px; padding: 40px; text-align: center; cursor: pointer; background: #f0f4ff; transition: 0.3s; }
135
+ .upload-area:hover { background: #e0e7ff; }
136
 
137
+ /* ادیتور */
138
+ .editor-area { display: none; margin-top: 30px; }
139
+ .segment-box { background: #fff; border: 1px solid var(--border); padding: 15px; border-radius: 12px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.02); }
140
+ .time-badge { background: #e9ecef; padding: 5px 10px; border-radius: 8px; font-size: 0.8rem; color: #495057; white-space: nowrap; direction: ltr; }
141
+ .text-input { flex-grow: 1; padding: 10px; border: 1px solid #ced4da; border-radius: 8px; font-family: 'Vazirmatn'; font-size: 1rem; }
142
+ .text-input:focus { border-color: var(--primary); outline: none; }
143
 
144
+ /* تنظیمات */
145
+ .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px; background: #f8f9fa; padding: 15px; border-radius: 12px; }
146
+ select { width: 100%; padding: 10px; border-radius: 8px; border: 1px solid #ced4da; font-family: 'Vazirmatn'; }
147
 
148
+ .btn { background: var(--primary); color: white; border: none; width: 100%; padding: 15px; border-radius: 12px; font-size: 1.1rem; font-weight: bold; cursor: pointer; transition: 0.2s; margin-top: 10px; }
149
+ .btn:disabled { background: #ccc; cursor: not-allowed; }
150
+ .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(67, 97, 238, 0.3); }
151
 
152
+ .loader { display: none; text-align: center; margin: 20px 0; }
153
  .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; }
154
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
155
 
156
+ #finalResult { display: none; text-align: center; margin-top: 30px; }
157
+ video { width: 100%; max-height: 500px; border-radius: 12px; background: #000; }
158
+ .dl-btn { display: inline-block; background: #2ec4b6; color: white; text-decoration: none; padding: 12px 30px; border-radius: 10px; font-weight: bold; margin-top: 10px; }
 
 
159
  </style>
160
  </head>
161
  <body>
162
  <div class="container">
163
+ <h1>✂️ ویرایشگر زیرنویس هوشمند</h1>
164
 
165
+ <!-- مرحله ۱: آپلود -->
166
+ <div class="upload-area" id="uploadArea" onclick="document.getElementById('fileInput').click()">
167
+ <div style="font-size: 40px;">📂</div>
168
+ <p id="uploadText">برای انتخاب ویدیو کلیک کنید</p>
169
+ <input type="file" id="fileInput" accept="video/*" hidden onchange="handleUpload(this)">
170
  </div>
171
 
172
+ <div id="analyzeLoader" class="loader">
173
+ <div class="spinner"></div>
174
+ <span>در حال استخراج متن از ویدیو... (لطفا صبر کنید)</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
 
177
+ <!-- مرحله ۲: ویرایش -->
178
+ <div id="editorArea" class="editor-area">
179
+ <div class="settings-grid">
180
+ <div>
181
+ <label>استایل:</label>
182
+ <select id="styleSelect">
183
+ <option value="hormozi">هرمزی (زرد)</option>
184
+ <option value="cinema">سینمایی (تیره)</option>
185
+ <option value="neon">نئون (آبی)</option>
186
+ <option value="clean">ساده</option>
187
+ </select>
188
+ </div>
189
+ <div>
190
+ <label>فونت:</label>
191
+ <select id="fontSelect">
192
+ <option value="vazir">وزیر (رسمی)</option>
193
+ <option value="lalezar">لاله زار (تیتر)</option>
194
+ </select>
195
+ </div>
196
+ </div>
197
 
198
+ <h3>📝 متن‌ها را ویرایش کنید:</h3>
199
+ <div id="segmentsContainer"></div>
200
+
201
+ <button id="burnBtn" class="btn" onclick="burnVideo()">🔥 ساخت ویدیو نهایی</button>
202
+ </div>
203
+
204
+ <div id="burnLoader" class="loader">
205
  <div class="spinner"></div>
206
+ <span>در حال چسباندن زیرنویس‌ها...</span>
207
  </div>
208
 
209
+ <!-- مرحله ۳: دانلود -->
210
+ <div id="finalResult">
211
+ <h3>✅ ویدیو آماده شد!</h3>
212
+ <video id="previewVideo" controls></video>
213
  <br>
214
+ <a id="downloadLink" class="dl-btn" download>دانلود ویدیو</a>
215
  <br><br>
216
+ <button onclick="location.reload()" style="background:none; border:none; color:#666; cursor:pointer;">شروع دوباره</button>
217
  </div>
218
  </div>
219
 
220
  <script>
221
+ let currentFileId = null;
222
+ let currentSegments = [];
223
+
224
+ async function handleUpload(input) {
225
+ const file = input.files[0];
226
+ if (!file) return;
227
+
228
+ // UI Update
229
+ document.getElementById('uploadArea').style.display = 'none';
230
+ document.getElementById('analyzeLoader').style.display = 'block';
231
+
232
+ const formData = new FormData();
233
+ formData.append("file", file);
234
 
235
+ try {
236
+ const res = await fetch("/api/analyze", { method: "POST", body: formData });
237
+ const data = await res.json();
238
+
239
+ if (data.error) throw new Error(data.error);
240
+
241
+ currentFileId = data.file_id;
242
+ currentSegments = data.segments;
243
+
244
+ renderEditor();
245
+ } catch (e) {
246
+ alert("خطا: " + e.message);
247
+ location.reload();
248
  }
249
  }
250
 
251
+ function renderEditor() {
252
+ document.getElementById('analyzeLoader').style.display = 'none';
253
+ const container = document.getElementById('segmentsContainer');
254
+ container.innerHTML = "";
255
 
256
+ currentSegments.forEach((seg, index) => {
257
+ const div = document.createElement('div');
258
+ div.className = 'segment-box';
259
+
260
+ const time = document.createElement('div');
261
+ time.className = 'time-badge';
262
+ // نمایش زمان به فرمت ساده (ثانیه)
263
+ time.innerText = `${Math.floor(seg.start)}s - ${Math.floor(seg.end)}s`;
264
+
265
+ const input = document.createElement('input');
266
+ input.type = 'text';
267
+ input.className = 'text-input';
268
+ input.value = seg.text.trim();
269
+ input.oninput = (e) => { currentSegments[index].text = e.target.value; };
270
 
271
+ div.appendChild(time);
272
+ div.appendChild(input);
273
+ container.appendChild(div);
274
+ });
275
+
276
+ document.getElementById('editorArea').style.display = 'block';
277
+ }
278
+
279
+ async function burnVideo() {
280
+ document.getElementById('editorArea').style.display = 'none';
281
+ document.getElementById('burnLoader').style.display = 'block';
282
+
283
+ const payload = {
284
+ file_id: currentFileId,
285
+ style: document.getElementById('styleSelect').value,
286
+ font: document.getElementById('fontSelect').value,
287
+ segments: currentSegments
288
+ };
289
 
290
  try {
291
+ const res = await fetch("/api/burn", {
292
+ method: "POST",
293
+ headers: { "Content-Type": "application/json" },
294
+ body: JSON.stringify(payload)
295
+ });
296
  const data = await res.json();
297
+
298
+ if (data.error) throw new Error(data.error);
299
+
300
+ document.getElementById('burnLoader').style.display = 'none';
301
+ document.getElementById('finalResult').style.display = 'block';
302
 
303
+ const videoEl = document.getElementById('previewVideo');
304
+ videoEl.src = data.url;
305
+ document.getElementById('downloadLink').href = data.url;
 
306
 
307
  } catch (e) {
308
+ alert("خطا در ساخت ویدیو: " + e.message);
309
+ document.getElementById('burnLoader').style.display = 'none';
310
+ document.getElementById('editorArea').style.display = 'block';
311
  }
312
  }
313
  </script>
 
315
  </html>
316
  """
317
 
318
+ @app.post("/api/analyze")
319
+ async def analyze_video(file: UploadFile = File(...)):
 
 
 
 
320
  try:
321
+ file_id = str(uuid.uuid4())
322
+ file_path = f"{TEMP_DIR}/{file_id}_raw.mp4"
323
+
324
+ with open(file_path, "wb") as f:
325
+ shutil.copyfileobj(file.file, f)
326
+
327
+ # استخراج متن
328
+ segments_gen, _ = model.transcribe(file_path, language="fa", beam_size=5)
329
 
330
+ # تبدیل به فرمت JSON قابل ارسال
331
+ segments_data = []
332
+ for seg in segments_gen:
333
+ segments_data.append({
334
+ "start": seg.start,
335
+ "end": seg.end,
336
+ "text": seg.text
337
+ })
338
 
339
+ return {"file_id": file_id, "segments": segments_data}
340
+
341
+ except Exception as e:
342
+ return JSONResponse(status_code=500, content={"error": str(e)})
343
+
344
+ @app.post("/api/burn")
345
+ async def burn_video_api(data: BurnRequest):
346
+ try:
347
+ input_path = f"{TEMP_DIR}/{data.file_id}_raw.mp4"
348
+ ass_path = f"{TEMP_DIR}/{data.file_id}.ass"
349
+ output_path = f"{TEMP_DIR}/{data.file_id}_final.mp4"
350
 
351
+ if not os.path.exists(input_path):
352
+ return JSONResponse(status_code=404, content={"error": "فایل یافت نشد (منقضی شده)"})
353
 
354
+ # 1. ساخت فایل ASS از روی دیتای ویرایش شده کاربر
355
+ create_ass_file(data.segments, ass_path, data.style, data.font)
356
 
357
+ # 2. چسباندن
358
  if burn_subtitles(input_path, ass_path, output_path):
359
+ # پاک کردن فایل‌های موقت
360
  os.remove(input_path)
361
  os.remove(ass_path)
362
+ return {"url": f"/download/{data.file_id}_final.mp4"}
363
  else:
364
+ return JSONResponse(status_code=500, content={"error": "خطا در پردازش ویدیو"})
365
 
366
  except Exception as e:
367
  return JSONResponse(status_code=500, content={"error": str(e)})
368
 
369
  @app.get("/download/{filename}")
370
+ async def download(filename: str):
371
  path = f"{TEMP_DIR}/{filename}"
372
  if os.path.exists(path): return FileResponse(path)
373
  return JSONResponse(status_code=404, content={"error": "Not Found"})