ulduldp commited on
Commit
4cd5d22
·
verified ·
1 Parent(s): 3172482

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +370 -0
app.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template_string, request, jsonify
2
+ import os
3
+ import uuid
4
+ import subprocess
5
+ import tempfile
6
+ import shutil
7
+ import textwrap
8
+ from werkzeug.utils import secure_filename
9
+ from faster_whisper import WhisperModel
10
+
11
+ app = Flask(__name__)
12
+
13
+ UPLOAD_FOLDER = "uploads"
14
+ OUTPUT_FOLDER = "static/videos"
15
+ SUBTITLE_FOLDER = "subtitles"
16
+
17
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
18
+ os.makedirs(OUTPUT_FOLDER, exist_ok=True)
19
+ os.makedirs(SUBTITLE_FOLDER, exist_ok=True)
20
+
21
+ # Load model once
22
+ model = WhisperModel(
23
+ "base",
24
+ device="cpu",
25
+ compute_type="int8"
26
+ )
27
+
28
+ HTML = """
29
+ <!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>Photo + Audio To Video</title>
35
+ <style>
36
+ *{
37
+ margin:0;
38
+ padding:0;
39
+ box-sizing:border-box;
40
+ font-family:Arial;
41
+ }
42
+ body{
43
+ background:#0f0f0f;
44
+ color:white;
45
+ min-height:100vh;
46
+ display:flex;
47
+ justify-content:center;
48
+ align-items:center;
49
+ padding:20px;
50
+ }
51
+ .container{
52
+ width:100%;
53
+ max-width:500px;
54
+ background:#1b1b1b;
55
+ border-radius:20px;
56
+ padding:25px;
57
+ box-shadow:0 0 20px rgba(0,0,0,0.4);
58
+ }
59
+ h1{
60
+ text-align:center;
61
+ margin-bottom:25px;
62
+ font-size:28px;
63
+ }
64
+ .upload-box{
65
+ border:2px dashed #444;
66
+ padding:20px;
67
+ border-radius:15px;
68
+ margin-bottom:20px;
69
+ }
70
+ label{
71
+ display:block;
72
+ margin-bottom:8px;
73
+ color:#ccc;
74
+ }
75
+ input{
76
+ width:100%;
77
+ padding:12px;
78
+ background:#2a2a2a;
79
+ border:none;
80
+ border-radius:10px;
81
+ color:white;
82
+ margin-bottom:15px;
83
+ }
84
+ button{
85
+ width:100%;
86
+ padding:15px;
87
+ border:none;
88
+ border-radius:12px;
89
+ background:#00aaff;
90
+ color:white;
91
+ font-size:18px;
92
+ cursor:pointer;
93
+ transition:0.3s;
94
+ }
95
+ button:hover{
96
+ opacity:0.9;
97
+ }
98
+ #loading{
99
+ display:none;
100
+ text-align:center;
101
+ margin-top:20px;
102
+ }
103
+ video{
104
+ width:100%;
105
+ margin-top:20px;
106
+ border-radius:15px;
107
+ display:none;
108
+ }
109
+ .download-btn{
110
+ display:none;
111
+ margin-top:15px;
112
+ text-align:center;
113
+ }
114
+ .download-btn a{
115
+ display:inline-block;
116
+ background:#22c55e;
117
+ color:white;
118
+ text-decoration:none;
119
+ padding:12px 20px;
120
+ border-radius:10px;
121
+ }
122
+ .preview{
123
+ margin-top:15px;
124
+ width:100%;
125
+ border-radius:15px;
126
+ display:none;
127
+ }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="container">
132
+ <h1>Photo + Audio → Video</h1>
133
+
134
+ <form id="form">
135
+ <div class="upload-box">
136
+ <label>Select Photo</label>
137
+ <input type="file" id="image" name="image" accept="image/*" required>
138
+
139
+ <img id="preview" class="preview">
140
+
141
+ <label>Select Audio (mp3/wav)</label>
142
+ <input type="file" name="audio" accept="audio/*" required>
143
+ </div>
144
+
145
+ <button type="submit">Generate Video</button>
146
+ </form>
147
+
148
+ <div id="loading">Generating Video...</div>
149
+
150
+ <video id="video" controls></video>
151
+
152
+ <div class="download-btn" id="downloadDiv">
153
+ <a id="downloadBtn" download>Download Video</a>
154
+ </div>
155
+ </div>
156
+
157
+ <script>
158
+ const form = document.getElementById("form");
159
+ const loading = document.getElementById("loading");
160
+ const video = document.getElementById("video");
161
+ const downloadBtn = document.getElementById("downloadBtn");
162
+ const downloadDiv = document.getElementById("downloadDiv");
163
+ const preview = document.getElementById("preview");
164
+
165
+ document.getElementById("image").addEventListener("change", function(e){
166
+ const file = e.target.files[0];
167
+ if(file){
168
+ preview.src = URL.createObjectURL(file);
169
+ preview.style.display = "block";
170
+ }
171
+ });
172
+
173
+ form.addEventListener("submit", async (e)=>{
174
+ e.preventDefault();
175
+
176
+ loading.style.display = "block";
177
+ video.style.display = "none";
178
+ downloadDiv.style.display = "none";
179
+
180
+ const formData = new FormData(form);
181
+
182
+ try{
183
+ const response = await fetch("/generate",{
184
+ method:"POST",
185
+ body:formData
186
+ });
187
+
188
+ const data = await response.json();
189
+
190
+ loading.style.display = "none";
191
+
192
+ if(data.video_url){
193
+ video.src = data.video_url + "?t=" + new Date().getTime();
194
+ video.style.display = "block";
195
+
196
+ downloadBtn.href = data.video_url;
197
+ downloadDiv.style.display = "block";
198
+ }else{
199
+ alert(data.error || "Failed");
200
+ }
201
+
202
+ }catch(err){
203
+ loading.style.display = "none";
204
+ alert("Server Error");
205
+ }
206
+ });
207
+ </script>
208
+ </body>
209
+ </html>
210
+ """
211
+
212
+ def ass_time(seconds: float) -> str:
213
+ if seconds < 0:
214
+ seconds = 0
215
+ h = int(seconds // 3600)
216
+ m = int((seconds % 3600) // 60)
217
+ s = seconds % 60
218
+ return f"{h}:{m:02d}:{s:05.2f}"
219
+
220
+ def ass_escape(text: str) -> str:
221
+ # Escape ASS special chars
222
+ text = text.replace("\\", "\\\\")
223
+ text = text.replace("{", "\\{").replace("}", "\\}")
224
+ text = text.replace("\n", " ")
225
+ return text
226
+
227
+ def make_ass_subtitles(segments, ass_path):
228
+ header = """[Script Info]
229
+ ScriptType: v4.00+
230
+ PlayResX: 1280
231
+ PlayResY: 720
232
+ ScaledBorderAndShadow: yes
233
+
234
+ [V4+ Styles]
235
+ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
236
+ Style: Default,Arial,26,&H00FFFFFF,&H000000FF,&H00000000,&HEE000000,0,0,0,0,100,100,0,0,3,0,0,2,40,40,35,1
237
+
238
+ [Events]
239
+ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
240
+ """
241
+ lines = [header]
242
+
243
+ for seg in segments:
244
+ start = ass_time(seg["start"])
245
+ end = ass_time(seg["end"])
246
+
247
+ text = textwrap.fill(seg["text"].strip(), width=36)
248
+ text = ass_escape(text).replace("\n", r"\N")
249
+
250
+ lines.append(
251
+ f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}\n"
252
+ )
253
+
254
+ with open(ass_path, "w", encoding="utf-8") as f:
255
+ f.writelines(lines)
256
+
257
+ @app.route("/")
258
+ def home():
259
+ return render_template_string(HTML)
260
+
261
+ @app.route("/generate", methods=["POST"])
262
+ def generate():
263
+ if "image" not in request.files or "audio" not in request.files:
264
+ return jsonify({"error": "Missing files"})
265
+
266
+ image = request.files["image"]
267
+ audio = request.files["audio"]
268
+
269
+ if not image.filename or not audio.filename:
270
+ return jsonify({"error": "Please upload both image and audio"})
271
+
272
+ uid = str(uuid.uuid4())
273
+
274
+ image_name = secure_filename(image.filename)
275
+ audio_name = secure_filename(audio.filename)
276
+
277
+ image_path = os.path.join(UPLOAD_FOLDER, uid + "_" + image_name)
278
+ audio_path = os.path.join(UPLOAD_FOLDER, uid + "_" + audio_name)
279
+
280
+ output_filename = uid + ".mp4"
281
+ output_path = os.path.join(OUTPUT_FOLDER, output_filename)
282
+
283
+ ass_path = os.path.join(SUBTITLE_FOLDER, uid + ".ass")
284
+
285
+ image.save(image_path)
286
+ audio.save(audio_path)
287
+
288
+ try:
289
+ # 1) Transcribe audio
290
+ segments_iter, info = model.transcribe(
291
+ audio_path,
292
+ beam_size=5,
293
+ vad_filter=True
294
+ )
295
+
296
+ transcript = []
297
+ full_text_parts = []
298
+
299
+ for segment in segments_iter:
300
+ text = segment.text.strip()
301
+ if not text:
302
+ continue
303
+
304
+ transcript.append({
305
+ "start": round(segment.start, 2),
306
+ "end": round(segment.end, 2),
307
+ "text": text
308
+ })
309
+ full_text_parts.append(text)
310
+
311
+ full_text = " ".join(full_text_parts).strip()
312
+
313
+ # 2) Create ASS subtitles
314
+ make_ass_subtitles(transcript, ass_path)
315
+
316
+ # 3) FFmpeg command with smooth animation + subtitles
317
+ # Subtle zoom/pan effect + subtitles at bottom
318
+ filter_complex = (
319
+ f"[0:v]scale=1280:720:force_original_aspect_ratio=increase,"
320
+ f"crop=1280:720,"
321
+ f"zoompan=z='min(zoom+0.0008,1.08)':"
322
+ f"x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':"
323
+ f"d=1:s=1280x720:fps=30,"
324
+ f"subtitles='{ass_path.replace(\"'\", \"\\\\'\")}'[v]"
325
+ )
326
+
327
+ cmd = [
328
+ "ffmpeg",
329
+ "-y",
330
+ "-loop", "1",
331
+ "-i", image_path,
332
+ "-i", audio_path,
333
+ "-filter_complex", filter_complex,
334
+ "-map", "[v]",
335
+ "-map", "1:a:0",
336
+ "-c:v", "libx264",
337
+ "-pix_fmt", "yuv420p",
338
+ "-c:a", "aac",
339
+ "-b:a", "192k",
340
+ "-shortest",
341
+ output_path
342
+ ]
343
+
344
+ subprocess.run(
345
+ cmd,
346
+ stdout=subprocess.PIPE,
347
+ stderr=subprocess.PIPE,
348
+ check=True
349
+ )
350
+
351
+ return jsonify({
352
+ "video_url": f"/static/videos/{output_filename}",
353
+ "transcript": transcript,
354
+ "full_text": full_text,
355
+ "language": getattr(info, "language", None)
356
+ })
357
+
358
+ except subprocess.CalledProcessError as e:
359
+ return jsonify({
360
+ "error": "FFmpeg failed",
361
+ "details": e.stderr.decode("utf-8", errors="ignore")
362
+ })
363
+ except Exception as e:
364
+ return jsonify({
365
+ "error": "Processing failed",
366
+ "details": str(e)
367
+ })
368
+
369
+ if __name__ == "__main__":
370
+ app.run(host="0.0.0.0", port=7860)