Athspi commited on
Commit
1d99855
·
verified ·
1 Parent(s): dd03a74

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -90
app.py CHANGED
@@ -9,10 +9,13 @@ from moviepy.video.io.VideoFileClip import VideoFileClip
9
  from moviepy.audio.io.AudioFileClip import AudioFileClip
10
  from werkzeug.utils import secure_filename
11
  from dotenv import load_dotenv
 
 
 
12
 
13
- # Initialize Flask app
14
  load_dotenv()
15
- app = Flask(__name__, template_folder='templates', static_folder='static')
16
 
17
  # Configuration
18
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
@@ -30,9 +33,18 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
30
  os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
31
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
32
  app.config['DOWNLOAD_FOLDER'] = DOWNLOAD_FOLDER
33
- app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB
34
  app.secret_key = os.urandom(24)
35
 
 
 
 
 
 
 
 
 
 
36
  # Voice options
37
  VOICE_CHOICES = {
38
  "Male (Charon)": "Charon",
@@ -51,72 +63,223 @@ You are an expert AI scriptwriter. Your task is to watch the provided video and
51
  Say happily: வணக்கம்! [laugh] எப்படி இருக்கீங்க? Whisper mysteriously: அந்த ரகசியம் எனக்கு மட்டும் தான் தெரியும்.
52
  """
53
 
54
- def generate_tamil_script(video_path):
55
- """Generate Tamil script using Gemini"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  try:
57
- print("Uploading video to Gemini...")
58
- video_file = genai.upload_file(video_path, mime_type="video/mp4")
59
-
60
- # Wait for processing
61
- while video_file.state.name == "PROCESSING":
62
- time.sleep(5)
63
- video_file = genai.get_file(video_file.name)
64
-
65
- if video_file.state.name != "ACTIVE":
66
- raise Exception(f"Gemini processing failed: {video_file.state.name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- print("Generating script...")
69
- model = genai.GenerativeModel(model_name="models/gemini-2.5-flash")
70
- response = model.generate_content([GEMINI_PROMPT, video_file])
71
- genai.delete_file(video_file.name)
 
 
 
 
 
 
 
 
72
 
73
- if hasattr(response, 'text') and response.text:
74
- return " ".join(response.text.strip().splitlines())
75
- raise Exception("No valid script generated")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  except Exception as e:
77
- print(f"Script generation failed: {str(e)}")
78
- raise
 
 
 
 
 
 
 
 
79
 
80
- def generate_audio_track(text, voice, cheerful, output_path):
81
- """Generate audio using TTS API"""
82
  try:
83
- print("Generating audio track...")
84
- payload = {
85
- "text": text,
86
- "voice_name": voice,
87
- "cheerful": cheerful
88
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- response = requests.post(TTS_API_URL, json=payload, timeout=300)
91
- if response.status_code != 200:
92
- raise Exception(f"TTS API error: {response.status_code}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- with open(output_path, "wb") as f:
95
- f.write(response.content)
96
- print("Audio track generated successfully")
97
- except Exception as e:
98
- print(f"Audio generation failed: {str(e)}")
99
- raise
100
 
101
  def replace_video_audio(video_path, audio_path, output_path):
102
- """Replace video audio track"""
103
  video = None
104
  audio = None
105
  try:
106
- print("Replacing audio track...")
107
  video = VideoFileClip(video_path)
108
  audio = AudioFileClip(audio_path)
 
 
109
  video.audio = audio
 
 
110
  video.write_videofile(
111
  output_path,
112
  codec="libx264",
113
  audio_codec="aac",
 
114
  threads=4,
115
- verbose=False
 
116
  )
117
- print("Video processing complete!")
118
  except Exception as e:
119
- print(f"Video processing failed: {str(e)}")
 
 
 
120
  raise
121
  finally:
122
  if video:
@@ -129,65 +292,118 @@ def index():
129
  """Main page"""
130
  return render_template('index.html', voices=VOICE_CHOICES)
131
 
132
- @app.route('/process', methods=['POST'])
133
- def process_video():
134
- """Handle video processing"""
135
  if 'video' not in request.files:
136
- flash("No video file uploaded", "error")
137
- return render_template('index.html', voices=VOICE_CHOICES)
138
 
139
  file = request.files['video']
140
  if file.filename == '':
141
- flash("No file selected", "error")
142
- return render_template('index.html', voices=VOICE_CHOICES)
143
 
144
- # Save uploaded file
145
- filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
 
146
  video_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
147
  file.save(video_path)
148
 
149
  # Get processing options
150
  voice = request.form.get('voice', 'Charon')
151
- cheerful = request.form.get('cheerful') == 'on'
152
 
153
- try:
154
- # Generate script
155
- script = generate_tamil_script(video_path)
156
-
157
- # Generate audio
158
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio:
159
- audio_path = temp_audio.name
160
-
161
- generate_audio_track(script, voice, cheerful, audio_path)
162
-
163
- # Create dubbed video
164
- final_filename = f"dubbed_{filename}"
165
- final_path = os.path.join(app.config['DOWNLOAD_FOLDER'], final_filename)
166
- replace_video_audio(video_path, audio_path, final_path)
167
-
168
- # Cleanup
169
- os.unlink(audio_path)
170
- os.unlink(video_path)
171
-
172
- return jsonify({
173
- 'status': 'success',
174
- 'video_url': url_for('download', filename=final_filename),
175
- 'script': script
176
- })
177
-
178
- except Exception as e:
179
- # Cleanup on error
180
- if os.path.exists(video_path):
181
- os.unlink(video_path)
182
- return jsonify({
183
- 'status': 'error',
184
- 'message': str(e)
185
- }), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  @app.route('/download/<filename>')
188
  def download(filename):
189
  """Serve processed video"""
190
  return send_from_directory(app.config['DOWNLOAD_FOLDER'], filename)
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  if __name__ == '__main__':
193
- app.run(host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from moviepy.audio.io.AudioFileClip import AudioFileClip
10
  from werkzeug.utils import secure_filename
11
  from dotenv import load_dotenv
12
+ import threading
13
+ from datetime import datetime, timedelta
14
+ import logging
15
 
16
+ # Initialize Flask app and load secrets
17
  load_dotenv()
18
+ app = Flask(__name__)
19
 
20
  # Configuration
21
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
 
33
  os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
34
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
35
  app.config['DOWNLOAD_FOLDER'] = DOWNLOAD_FOLDER
36
+ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB
37
  app.secret_key = os.urandom(24)
38
 
39
+ # Processing status tracking
40
+ processing_status = {}
41
+ processing_times = {
42
+ 'upload': 0,
43
+ 'transcription': 0,
44
+ 'tts': 0,
45
+ 'dubbing': 0
46
+ }
47
+
48
  # Voice options
49
  VOICE_CHOICES = {
50
  "Male (Charon)": "Charon",
 
63
  Say happily: வணக்கம்! [laugh] எப்படி இருக்கீங்க? Whisper mysteriously: அந்த ரகசியம் எனக்கு மட்டும் தான் தெரியும்.
64
  """
65
 
66
+ # Configure logging
67
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
68
+ logger = logging.getLogger(__name__)
69
+
70
+ def track_processing_time(task_id, stage, duration):
71
+ """Track processing times for each stage"""
72
+ processing_times[stage] = duration
73
+ if task_id in processing_status:
74
+ processing_status[task_id]['timings'][stage] = duration
75
+
76
+ def estimate_remaining_time(task_id):
77
+ """Estimate remaining processing time"""
78
+ if task_id not in processing_status:
79
+ return "Calculating..."
80
+
81
+ status = processing_status[task_id]
82
+ completed_stages = [s for s in status['timings'] if status['timings'][s] is not None]
83
+
84
+ if len(completed_stages) == 0:
85
+ return "Starting soon..."
86
+
87
+ # Weighted average based on stage complexity
88
+ weights = {
89
+ 'transcription': 2.0,
90
+ 'tts': 1.5,
91
+ 'dubbing': 1.0
92
+ }
93
+
94
+ total_weighted_time = 0
95
+ total_weights = 0
96
+
97
+ for stage in completed_stages:
98
+ weight = weights.get(stage, 1.0)
99
+ total_weighted_time += status['timings'][stage] * weight
100
+ total_weights += weight
101
+
102
+ if total_weights == 0:
103
+ return "Estimating..."
104
+
105
+ avg_time = total_weighted_time / total_weights
106
+ remaining_stages = 4 - len(completed_stages)
107
+ return remaining_stages * avg_time
108
+
109
+ def process_video_background(task_id, video_path, voice, cheerful):
110
+ """Background processing function with enhanced logging"""
111
  try:
112
+ start_time = time.time()
113
+ processing_status[task_id] = {
114
+ 'status': 'processing',
115
+ 'progress': 0,
116
+ 'message': 'Starting transcription',
117
+ 'timings': {'upload': None, 'transcription': None, 'tts': None, 'dubbing': None},
118
+ 'start_time': start_time,
119
+ 'video_duration': get_video_duration(video_path)
120
+ }
121
+
122
+ # Stage 1: Transcription
123
+ processing_status[task_id]['message'] = 'Transcribing video content'
124
+ logger.info(f"Task {task_id}: Starting transcription")
125
+ script_start = time.time()
126
+ script = generate_tamil_script(video_path)
127
+ transcription_time = time.time() - script_start
128
+ track_processing_time(task_id, 'transcription', transcription_time)
129
+ processing_status[task_id]['progress'] = 25
130
+ processing_status[task_id]['script'] = script
131
+ logger.info(f"Task {task_id}: Transcription completed in {transcription_time:.1f}s")
132
+
133
+ # Stage 2: TTS Generation
134
+ processing_status[task_id]['message'] = 'Generating audio narration'
135
+ logger.info(f"Task {task_id}: Starting TTS generation")
136
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio:
137
+ audio_path = temp_audio.name
138
 
139
+ tts_start = time.time()
140
+ generate_audio_track(script, voice, cheerful, audio_path)
141
+ tts_time = time.time() - tts_start
142
+ track_processing_time(task_id, 'tts', tts_time)
143
+ processing_status[task_id]['progress'] = 50
144
+ logger.info(f"Task {task_id}: TTS completed in {tts_time:.1f}s")
145
+
146
+ # Stage 3: Dubbing
147
+ processing_status[task_id]['message'] = 'Creating dubbed video'
148
+ logger.info(f"Task {task_id}: Starting dubbing")
149
+ final_filename = f"dubbed_{task_id}.mp4"
150
+ final_path = os.path.join(app.config['DOWNLOAD_FOLDER'], final_filename)
151
 
152
+ dubbing_start = time.time()
153
+ replace_video_audio(video_path, audio_path, final_path)
154
+ dubbing_time = time.time() - dubbing_start
155
+ track_processing_time(task_id, 'dubbing', dubbing_time)
156
+ processing_status[task_id]['progress'] = 75
157
+ logger.info(f"Task {task_id}: Dubbing completed in {dubbing_time:.1f}s")
158
+
159
+ # Cleanup
160
+ os.unlink(audio_path)
161
+
162
+ # Finalize
163
+ processing_status[task_id].update({
164
+ 'status': 'complete',
165
+ 'progress': 100,
166
+ 'message': 'Processing complete',
167
+ 'result_path': final_path,
168
+ 'end_time': time.time()
169
+ })
170
+ logger.info(f"Task {task_id}: Processing completed successfully")
171
+
172
  except Exception as e:
173
+ logger.error(f"Task {task_id} failed: {str(e)}")
174
+ processing_status[task_id].update({
175
+ 'status': 'error',
176
+ 'message': f'Error: {str(e)}'
177
+ })
178
+ # Cleanup temporary files
179
+ if 'video_path' in locals() and os.path.exists(video_path):
180
+ os.unlink(video_path)
181
+ if 'audio_path' in locals() and os.path.exists(audio_path):
182
+ os.unlink(audio_path)
183
 
184
+ def get_video_duration(video_path):
185
+ """Get duration of video in seconds"""
186
  try:
187
+ with VideoFileClip(video_path) as video:
188
+ return video.duration
189
+ except:
190
+ return 0
191
+
192
+ def generate_tamil_script(video_path):
193
+ """Generate Tamil script using Gemini with retry logic"""
194
+ max_retries = 3
195
+ retry_delay = 10 # seconds
196
+
197
+ for attempt in range(max_retries):
198
+ try:
199
+ video_file = genai.upload_file(video_path, mime_type="video/mp4")
200
+
201
+ # Wait for file processing with timeout
202
+ start_wait = time.time()
203
+ while video_file.state.name == "PROCESSING":
204
+ if time.time() - start_wait > 300: # 5 minutes timeout
205
+ raise TimeoutError("Gemini processing timed out")
206
+ time.sleep(5)
207
+ video_file = genai.get_file(video_file.name)
208
+
209
+ if video_file.state.name != "ACTIVE":
210
+ raise Exception(f"Gemini processing failed: {video_file.state.name}")
211
+
212
+ model = genai.GenerativeModel(model_name="models/gemini-2.5-flash")
213
+ response = model.generate_content([GEMINI_PROMPT, video_file])
214
+ genai.delete_file(video_file.name)
215
+
216
+ if hasattr(response, 'text') and response.text:
217
+ return " ".join(response.text.strip().splitlines())
218
+ raise Exception("No valid script generated")
219
 
220
+ except Exception as e:
221
+ if attempt < max_retries - 1:
222
+ logger.warning(f"Gemini error (attempt {attempt+1}/{max_retries}): {str(e)}")
223
+ time.sleep(retry_delay * (attempt + 1))
224
+ else:
225
+ raise
226
+
227
+ def generate_audio_track(text, voice, cheerful, output_path):
228
+ """Generate audio using TTS API with retry logic"""
229
+ max_retries = 3
230
+ retry_delay = 5 # seconds
231
+
232
+ for attempt in range(max_retries):
233
+ try:
234
+ payload = {
235
+ "text": text,
236
+ "voice_name": voice,
237
+ "cheerful": cheerful
238
+ }
239
+
240
+ response = requests.post(TTS_API_URL, json=payload, timeout=300)
241
+ if response.status_code != 200:
242
+ raise Exception(f"TTS API error: {response.status_code} - {response.text}")
243
+
244
+ with open(output_path, "wb") as f:
245
+ f.write(response.content)
246
+ return
247
 
248
+ except Exception as e:
249
+ if attempt < max_retries - 1:
250
+ logger.warning(f"TTS error (attempt {attempt+1}/{max_retries}): {str(e)}")
251
+ time.sleep(retry_delay * (attempt + 1))
252
+ else:
253
+ raise
254
 
255
  def replace_video_audio(video_path, audio_path, output_path):
256
+ """Replace video audio track with enhanced error handling"""
257
  video = None
258
  audio = None
259
  try:
260
+ # Open video and audio files
261
  video = VideoFileClip(video_path)
262
  audio = AudioFileClip(audio_path)
263
+
264
+ # Set video audio
265
  video.audio = audio
266
+
267
+ # Write output with optimized settings
268
  video.write_videofile(
269
  output_path,
270
  codec="libx264",
271
  audio_codec="aac",
272
+ logger=None,
273
  threads=4,
274
+ preset='medium',
275
+ ffmpeg_params=['-crf', '23', '-movflags', '+faststart']
276
  )
277
+
278
  except Exception as e:
279
+ logger.error(f"Video processing error: {str(e)}")
280
+ # Cleanup partially created file
281
+ if os.path.exists(output_path):
282
+ os.unlink(output_path)
283
  raise
284
  finally:
285
  if video:
 
292
  """Main page"""
293
  return render_template('index.html', voices=VOICE_CHOICES)
294
 
295
+ @app.route('/upload', methods=['POST'])
296
+ def upload_video():
297
+ """Handle video upload and start processing"""
298
  if 'video' not in request.files:
299
+ return jsonify({'error': 'No file uploaded'}), 400
 
300
 
301
  file = request.files['video']
302
  if file.filename == '':
303
+ return jsonify({'error': 'No file selected'}), 400
 
304
 
305
+ # Generate unique task ID
306
+ task_id = str(uuid.uuid4())
307
+ filename = secure_filename(f"{task_id}_{file.filename}")
308
  video_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
309
  file.save(video_path)
310
 
311
  # Get processing options
312
  voice = request.form.get('voice', 'Charon')
313
+ cheerful = request.form.get('cheerful', 'false') == 'true'
314
 
315
+ # Start background processing
316
+ processing_status[task_id] = {
317
+ 'status': 'uploaded',
318
+ 'progress': 0,
319
+ 'message': 'Starting processing',
320
+ 'timings': {'upload': time.time(), 'transcription': None, 'tts': None, 'dubbing': None},
321
+ 'start_time': time.time(),
322
+ 'video_duration': get_video_duration(video_path)
323
+ }
324
+
325
+ thread = threading.Thread(
326
+ target=process_video_background,
327
+ args=(task_id, video_path, voice, cheerful)
328
+ )
329
+ thread.start()
330
+
331
+ return jsonify({
332
+ 'task_id': task_id,
333
+ 'video_duration': processing_status[task_id]['video_duration']
334
+ })
335
+
336
+ @app.route('/status/<task_id>')
337
+ def get_status(task_id):
338
+ """Check processing status"""
339
+ if task_id not in processing_status:
340
+ return jsonify({'error': 'Invalid task ID'}), 404
341
+
342
+ status = processing_status[task_id]
343
+
344
+ # Calculate ETA if processing
345
+ eta = None
346
+ if status['status'] == 'processing':
347
+ elapsed = time.time() - status['start_time']
348
+ remaining = estimate_remaining_time(task_id)
349
+ if isinstance(remaining, (int, float)):
350
+ eta = str(timedelta(seconds=int(remaining)))
351
+
352
+ response = {
353
+ 'status': status['status'],
354
+ 'progress': status.get('progress', 0),
355
+ 'message': status.get('message', ''),
356
+ 'eta': eta
357
+ }
358
+
359
+ if status['status'] == 'complete':
360
+ response['result_url'] = url_for('download', filename=os.path.basename(status['result_path']))
361
+ response['script'] = status.get('script', '')
362
+
363
+ return jsonify(response)
364
 
365
  @app.route('/download/<filename>')
366
  def download(filename):
367
  """Serve processed video"""
368
  return send_from_directory(app.config['DOWNLOAD_FOLDER'], filename)
369
 
370
+ @app.route('/cleanup', methods=['POST'])
371
+ def cleanup():
372
+ """Cleanup old files"""
373
+ try:
374
+ # Cleanup uploads older than 1 hour
375
+ for filename in os.listdir(UPLOAD_FOLDER):
376
+ file_path = os.path.join(UPLOAD_FOLDER, filename)
377
+ if os.path.getmtime(file_path) < time.time() - 3600:
378
+ os.unlink(file_path)
379
+
380
+ # Cleanup downloads older than 24 hours
381
+ for filename in os.listdir(DOWNLOAD_FOLDER):
382
+ file_path = os.path.join(DOWNLOAD_FOLDER, filename)
383
+ if os.path.getmtime(file_path) < time.time() - 86400:
384
+ os.unlink(file_path)
385
+
386
+ return jsonify({'status': 'success', 'message': 'Cleanup completed'})
387
+ except Exception as e:
388
+ return jsonify({'status': 'error', 'message': str(e)}), 500
389
+
390
  if __name__ == '__main__':
391
+ # Schedule cleanup thread
392
+ import schedule
393
+ import time as t
394
+ def cleanup_job():
395
+ with app.app_context():
396
+ app.test_client().post('/cleanup')
397
+
398
+ schedule.every().hour.do(cleanup_job)
399
+
400
+ # Start scheduler in background thread
401
+ def scheduler_thread():
402
+ while True:
403
+ schedule.run_pending()
404
+ t.sleep(1)
405
+
406
+ threading.Thread(target=scheduler_thread, daemon=True).start()
407
+
408
+ # Start Flask app
409
+ app.run(host="0.0.0.0", port=7860, threaded=True)