Athspi commited on
Commit
8bc1a84
·
verified ·
1 Parent(s): 25df726

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +331 -181
app.py CHANGED
@@ -4,256 +4,406 @@ import tempfile
4
  import uuid
5
  import google.generativeai as genai
6
  import requests
7
- from flask import Flask, request, render_template, send_from_directory, url_for, flash
8
  from moviepy.video.io.VideoFileClip import VideoFileClip
9
  from moviepy.audio.io.AudioFileClip import AudioFileClip
10
- from moviepy.audio.AudioClip import concatenate_audioclips
11
  from werkzeug.utils import secure_filename
12
  from dotenv import load_dotenv
 
 
 
13
 
14
- # --- 1. INITIALIZE FLASK APP AND LOAD SECRETS ---
15
  load_dotenv()
16
  app = Flask(__name__)
17
 
18
- # Load secrets from environment variables
19
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
20
  TTS_API_URL = os.getenv("TTS_API_URL")
21
- TTS_MAX_TOKENS = 30000 # Conservative limit below 32k token threshold
22
 
23
- # Validate required configurations
24
- if not GEMINI_API_KEY:
25
- raise ValueError("SECURITY ERROR: GEMINI_API_KEY not found in .env file!")
26
- if not TTS_API_URL:
27
- raise ValueError("CONFIGURATION ERROR: TTS_API_URL not found in .env file!")
28
 
29
- # Configure Gemini AI
30
  genai.configure(api_key=GEMINI_API_KEY)
31
 
32
- # Configure directories
33
  UPLOAD_FOLDER = 'uploads'
34
  DOWNLOAD_FOLDER = 'downloads'
35
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
36
  os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
37
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
38
  app.config['DOWNLOAD_FOLDER'] = DOWNLOAD_FOLDER
39
- app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100 MB upload limit
40
- app.secret_key = os.urandom(24) # Secure key for flash messages
41
 
42
- # --- 2. APPLICATION CONFIGURATION ---
 
 
 
 
 
 
 
 
 
43
  VOICE_CHOICES = {
44
  "Male (Charon)": "Charon",
45
  "Female (Zephyr)": "Zephyr"
46
  }
47
 
48
  GEMINI_PROMPT = """
49
- You are an expert AI scriptwriter. Your task is to watch the provided video and transcribe ALL spoken dialogue into a SINGLE, CONTINUOUS block of modern, colloquial Tamil.
50
 
51
  **CRITICAL INSTRUCTIONS:**
52
- 1. **Single Script:** Combine all dialogue from all speakers into one continuous script.
53
- 2. **NO Timestamps or Speaker Labels:** Do NOT include any timestamps or speaker identifiers.
54
- 3. **Incorporate Performance:** Add English style prompts (e.g., `Say happily:`, `Whisper mysteriously:`) and performance tags (e.g., `[laugh]`, `[sigh]`) directly into the text for an expressive narration.
55
 
56
  **EXAMPLE OUTPUT:**
57
  Say happily: வணக்கம்! [laugh] எப்படி இருக்கீங்க? Whisper mysteriously: அந்த ரகசியம் எனக்கு மட்டும் தான் தெரியும்.
58
  """
59
 
60
- # --- 3. CORE APPLICATION FUNCTIONS ---
 
 
 
 
 
 
 
 
61
 
62
- def generate_tamil_script(video_file_path):
63
- """Generates a Tamil script from the video using Gemini AI."""
64
- print("Uploading video to Gemini for transcription...")
65
- video_file = genai.upload_file(video_file_path, mime_type="video/mp4")
66
 
67
- # Wait for file processing
68
- while video_file.state.name == "PROCESSING":
69
- time.sleep(5)
70
- video_file = genai.get_file(video_file.name)
71
 
72
- if video_file.state.name != "ACTIVE":
73
- raise Exception(f"Gemini file processing failed: {video_file.state.name}")
74
 
75
- print("Generating script...")
76
- model = genai.GenerativeModel(model_name="models/gemini-2.5-flash")
77
- response = model.generate_content([GEMINI_PROMPT, video_file])
78
- genai.delete_file(video_file.name)
 
 
79
 
80
- if hasattr(response, 'text') and response.text:
81
- return " ".join(response.text.strip().splitlines())
82
- raise Exception("No valid script was generated by Gemini.")
83
-
84
- def split_text_for_tts(text, max_tokens=TTS_MAX_TOKENS):
85
- """Splits text into chunks that fit within TTS token limits."""
86
- words = text.split()
87
- chunks = []
88
- current_chunk = []
89
- current_length = 0
90
 
91
- for word in words:
92
- word_length = len(word) + 1 # +1 for space
93
- if current_length + word_length > max_tokens:
94
- chunks.append(" ".join(current_chunk))
95
- current_chunk = [word]
96
- current_length = word_length
97
- else:
98
- current_chunk.append(word)
99
- current_length += word_length
100
 
101
- if current_chunk:
102
- chunks.append(" ".join(current_chunk))
103
 
104
- return chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
- def generate_audio_with_retry(text_chunk, voice_name, is_cheerful, max_retries=3, retry_delay=2):
107
- """Generates audio with retry logic for API failures."""
 
 
 
108
  for attempt in range(max_retries):
109
  try:
110
- payload = {
111
- "text": text_chunk,
112
- "voice_name": voice_name,
113
- "cheerful": is_cheerful
114
- }
115
 
116
- response = requests.post(TTS_API_URL, json=payload, timeout=300)
117
- response.raise_for_status()
 
 
 
 
 
 
 
 
118
 
119
- if response.status_code == 200:
120
- return response.content
 
121
 
122
- except requests.exceptions.RequestException as e:
123
- print(f"TTS API attempt {attempt + 1} failed: {str(e)}")
 
 
 
124
  if attempt < max_retries - 1:
125
- time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
 
126
  else:
127
- raise Exception(f"TTS API failed after {max_retries} attempts: {str(e)}")
128
 
129
- def generate_long_audio(script_text, voice_name, is_cheerful, output_path):
130
- """Handles long audio generation by splitting text and combining results."""
131
- print("Processing long audio generation...")
132
- text_chunks = split_text_for_tts(script_text)
133
- audio_clips = []
134
- temp_files = []
135
 
136
- try:
137
- for i, chunk in enumerate(text_chunks):
138
- print(f"Processing chunk {i+1}/{len(text_chunks)}")
139
- chunk_audio = generate_audio_with_retry(chunk, voice_name, is_cheerful)
 
 
 
140
 
141
- # Save chunk to temporary file
142
- temp_file = f"temp_chunk_{i}.wav"
143
- with open(temp_file, "wb") as f:
144
- f.write(chunk_audio)
145
- temp_files.append(temp_file)
146
 
147
- # Load audio clip
148
- audio_clip = AudioFileClip(temp_file)
149
- audio_clips.append(audio_clip)
150
 
151
- # Combine all audio clips
152
- print("Combining audio chunks...")
153
- final_audio = concatenate_audioclips(audio_clips)
154
- final_audio.write_audiofile(output_path)
155
-
156
- finally:
157
- # Clean up temporary files
158
- for temp_file in temp_files:
159
- if os.path.exists(temp_file):
160
- os.remove(temp_file)
161
-
162
- # Close audio clips
163
- for clip in audio_clips:
164
- clip.close()
165
 
166
- def replace_video_audio(video_path, new_audio_path, output_path):
167
- """Replaces the audio track of a video file."""
168
- print("Replacing video audio...")
169
- video_clip = None
170
- audio_clip = None
171
-
172
  try:
173
- video_clip = VideoFileClip(video_path)
174
- audio_clip = AudioFileClip(new_audio_path)
175
- video_clip.audio = audio_clip
176
- video_clip.write_videofile(
 
 
 
 
 
177
  output_path,
178
  codec="libx264",
179
  audio_codec="aac",
180
- logger='bar'
 
 
 
181
  )
 
 
 
 
 
 
 
182
  finally:
183
- if audio_clip:
184
- audio_clip.close()
185
- if video_clip:
186
- video_clip.close()
187
-
188
- # --- 4. FLASK ROUTES ---
189
 
190
- @app.route('/', methods=['GET'])
191
  def index():
192
- """Render the main upload page."""
193
  return render_template('index.html', voices=VOICE_CHOICES)
194
 
195
- @app.route('/process', methods=['POST'])
196
- def process_video():
197
- """Handle video upload and processing."""
198
- input_video_path = None
199
- temp_audio_path = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  try:
202
- # Validate file upload
203
- if 'video' not in request.files or request.files['video'].filename == '':
204
- flash("Please upload a video file.", "error")
205
- return render_template('index.html', voices=VOICE_CHOICES)
206
-
207
- # Save uploaded file
208
- file = request.files['video']
209
- filename = secure_filename(file.filename)
210
- input_video_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
211
- file.save(input_video_path)
212
-
213
- # Get processing options
214
- voice_choice = request.form.get('voice', 'Charon')
215
- is_cheerful = request.form.get('tone') == 'on'
216
-
217
- # Generate script
218
- script = generate_tamil_script(input_video_path)
219
-
220
- # Create temporary audio file
221
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio:
222
- temp_audio_path = temp_audio.name
223
-
224
- # Generate audio with retry and chunking
225
- generate_long_audio(script, voice_choice, is_cheerful, temp_audio_path)
226
-
227
- # Create dubbed video
228
- final_video_name = f"dubbed_{filename}"
229
- final_video_path = os.path.join(app.config['DOWNLOAD_FOLDER'], final_video_name)
230
- replace_video_audio(input_video_path, temp_audio_path, final_video_path)
231
-
232
- flash("Video processing complete!", "success")
233
- return render_template(
234
- 'index.html',
235
- voices=VOICE_CHOICES,
236
- result_video=url_for('serve_video', filename=final_video_name),
237
- script=script
238
- )
239
 
 
 
 
 
 
 
 
240
  except Exception as e:
241
- print(f"Processing error: {str(e)}")
242
- flash(f"An error occurred: {str(e)}", "error")
243
- return render_template('index.html', voices=VOICE_CHOICES)
244
-
245
- finally:
246
- # Clean up temporary files
247
- if input_video_path and os.path.exists(input_video_path):
248
- os.remove(input_video_path)
249
- if temp_audio_path and os.path.exists(temp_audio_path):
250
- os.remove(temp_audio_path)
251
-
252
- @app.route('/downloads/<filename>')
253
- def serve_video(filename):
254
- """Serve the processed video file."""
255
- return send_from_directory(app.config['DOWNLOAD_FOLDER'], filename)
256
 
257
- # --- 5. APPLICATION ENTRY POINT ---
258
  if __name__ == '__main__':
259
- app.run(host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import uuid
5
  import google.generativeai as genai
6
  import requests
7
+ from flask import Flask, request, render_template, send_from_directory, url_for, flash, jsonify
8
  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
+ 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")
22
  TTS_API_URL = os.getenv("TTS_API_URL")
 
23
 
24
+ if not GEMINI_API_KEY or not TTS_API_URL:
25
+ raise ValueError("Missing required environment variables")
 
 
 
26
 
 
27
  genai.configure(api_key=GEMINI_API_KEY)
28
 
29
+ # File storage setup
30
  UPLOAD_FOLDER = 'uploads'
31
  DOWNLOAD_FOLDER = 'downloads'
32
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
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",
51
  "Female (Zephyr)": "Zephyr"
52
  }
53
 
54
  GEMINI_PROMPT = """
55
+ You are an AI scriptwriter. Your task is to watch the provided video and transcribe ALL spoken dialogue into a SINGLE, CONTINUOUS block of modern, colloquial Tamil.
56
 
57
  **CRITICAL INSTRUCTIONS:**
58
+ 1. **Single Script:** Combine all dialogue into one continuous script.
59
+ 2. **NO Timestamps or Speaker Labels:** Do NOT include any timestamps or speaker identifiers.
60
+ 3. **Incorporate Performance:** Add English style prompts (e.g., `Say happily:`, `Whisper mysteriously:`) and performance tags (e.g., `[laugh]`, `[sigh]`) directly into the text for an expressive narration.
61
 
62
  **EXAMPLE OUTPUT:**
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:
286
+ video.close()
287
+ if audio:
288
+ audio.close()
 
 
289
 
290
+ @app.route('/')
291
  def index():
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)