Athspi commited on
Commit
08c3547
·
verified ·
1 Parent(s): 16fcd54

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -68
app.py CHANGED
@@ -1,125 +1,126 @@
1
  import os
2
  import time
3
  import tempfile
 
4
  import google.generativeai as genai
5
  import requests
 
6
  from flask import Flask, request, render_template, send_from_directory, url_for, flash
7
  from moviepy.video.io.VideoFileClip import VideoFileClip
8
  from moviepy.audio.io.AudioFileClip import AudioFileClip
9
  from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
10
  from werkzeug.utils import secure_filename
 
11
 
12
  # --- 1. INITIALIZE FLASK APP AND LOAD SECRETS ---
13
-
14
  app = Flask(__name__)
15
 
16
- # Load secrets from environment variables (the secure way)
17
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
18
  TTS_API_URL = os.getenv("TTS_API_URL")
19
 
20
- # Check if secrets were loaded correctly
21
  if not GEMINI_API_KEY:
22
- raise ValueError("SECURITY ERROR: GEMINI_API_KEY secret not found! Please set it as an environment variable.")
23
  if not TTS_API_URL:
24
- raise ValueError("CONFIGURATION ERROR: TTS_API_URL secret not found! Please set it as an environment variable.")
25
 
26
- # Configure the Gemini API
27
  genai.configure(api_key=GEMINI_API_KEY)
28
 
29
- # Configure directories and app settings
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'] = 100 * 1024 * 1024 # 100 MB upload limit
37
- app.secret_key = os.urandom(24) # Secure key for flash messages
38
 
39
  # --- 2. VOICE CHOICES & GEMINI PROMPT ---
40
-
41
- VOICE_CHOICES = {
42
- "Male (Charon)": "Charon",
43
- "Female (Zephyr)": "Zephyr"
44
- }
45
-
46
  GEMINI_PROMPT = """
47
- 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.
48
-
49
- **CRITICAL INSTRUCTIONS:**
50
- 1. **Single Script:** Combine all dialogue into one continuous script.
51
- 2. **NO Timestamps or Speaker Labels:** Do NOT include any timestamps or speaker identifiers.
52
- 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.
53
-
54
- **EXAMPLE OUTPUT:**
55
- Say happily: வணக்கம்! [laugh] எப்படி இருக்கீங்க? Whisper mysteriously: அந்த ரகசியம் எனக்கு மட்டும் தான் தெரியும்.
56
  """
57
 
58
  # --- 3. CORE LOGIC HELPER FUNCTIONS ---
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def generate_tamil_script(video_file_path):
61
- """Generates a single, continuous Tamil script from the video using Gemini."""
62
  print("Uploading file to Gemini for transcription...")
63
  video_file = genai.upload_file(video_file_path, mime_type="video/mp4")
64
-
65
  print("Waiting for Gemini file processing...")
66
  while video_file.state.name == "PROCESSING":
67
  time.sleep(5)
68
  video_file = genai.get_file(video_file.name)
69
  if video_file.state.name != "ACTIVE":
70
  raise Exception(f"Gemini file processing failed: {video_file.state.name}")
71
-
72
  print("Generating narrator script...")
73
  model = genai.GenerativeModel(model_name="models/gemini-2.5-flash")
74
  response = model.generate_content([GEMINI_PROMPT, video_file])
75
  genai.delete_file(video_file.name)
76
-
77
- if response.text:
78
  return " ".join(response.text.strip().splitlines())
79
  raise Exception("No valid script was generated by Gemini.")
80
 
81
  def generate_single_audio_track(dialogue_text, voice_name, is_cheerful, output_path):
82
- """Generates one audio track for the entire script via TTS API."""
83
  print(f"Requesting audio from TTS API (Voice: {voice_name}, Cheerful: {is_cheerful})")
84
  payload = {"text": dialogue_text, "voice_name": voice_name, "cheerful": is_cheerful}
85
  response = requests.post(TTS_API_URL, json=payload, timeout=300)
86
  if response.status_code == 200:
87
  with open(output_path, "wb") as f:
88
  f.write(response.content)
89
- print(f"Audio track saved to {output_path}")
90
  return True
91
  raise Exception(f"Error from TTS API: {response.status_code} - {response.text}")
92
 
93
  def replace_video_audio(video_path, new_audio_path, output_path):
94
- """
95
- Replaces the audio of a video with a new audio file.
96
- This function now uses the modern, correct method for MoviePy.
97
- """
98
  print("Replacing video audio using MoviePy...")
99
  video_clip = None
100
  audio_clip = None
101
  try:
102
  video_clip = VideoFileClip(video_path)
103
  audio_clip = AudioFileClip(new_audio_path)
104
-
105
- # --- THE CORRECTED CODE ---
106
- # In modern MoviePy, you assign the audio clip directly to the .audio attribute.
107
- # The .set_audio() method is deprecated and causes the error.
108
  video_clip.audio = audio_clip
109
-
110
- # Write the modified video_clip object to a file.
111
  video_clip.write_videofile(output_path, codec="libx264", audio_codec="aac", logger='bar')
112
- print(f"Successfully created final video at {output_path}")
113
-
114
- except Exception as e:
115
- print(f"FATAL ERROR in replace_video_audio: {e}")
116
- raise
117
  finally:
118
- # This block ensures that all file handles are closed, preventing file lock issues.
119
- if video_clip:
120
- video_clip.close()
121
- if audio_clip:
122
- audio_clip.close()
123
 
124
  # --- 4. FLASK WEB ROUTES ---
125
 
@@ -130,23 +131,33 @@ def index():
130
 
131
  @app.route('/process', methods=['POST'])
132
  def process_video():
133
- """Handles video upload, processing, and renders the result."""
134
- if 'video' not in request.files or request.files['video'].filename == '':
135
- flash("No video file selected. Please choose a file to upload.")
136
- return render_template('index.html')
137
-
138
- file = request.files['video']
139
 
140
  try:
141
- filename = secure_filename(file.filename)
142
- upload_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
143
- file.save(upload_path)
144
-
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  voice_choice = request.form['voice_choice']
146
  is_cheerful = 'cheerful' in request.form
147
  voice_name = VOICE_CHOICES[voice_choice]
148
 
149
- script = generate_tamil_script(upload_path)
150
 
151
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio:
152
  temp_audio_path = temp_audio.name
@@ -156,10 +167,7 @@ def process_video():
156
  final_video_name = f"dubbed_{filename}"
157
  final_video_path = os.path.join(app.config['DOWNLOAD_FOLDER'], final_video_name)
158
 
159
- replace_video_audio(upload_path, temp_audio_path, final_video_path)
160
-
161
- os.remove(temp_audio_path)
162
- os.remove(upload_path) # Clean up original upload
163
 
164
  return render_template('index.html',
165
  result_video=url_for('serve_video', filename=final_video_name),
@@ -167,12 +175,18 @@ def process_video():
167
 
168
  except Exception as e:
169
  print(f"An error occurred during processing: {e}")
170
- flash(f"An unexpected error occurred: {e}. Please check the console logs and try again.")
171
  return render_template('index.html')
 
 
 
 
 
 
172
 
173
  @app.route('/downloads/<filename>')
174
  def serve_video(filename):
175
- """Serves the final dubbed video for display on the webpage."""
176
  return send_from_directory(app.config['DOWNLOAD_FOLDER'], filename)
177
 
178
  # --- 5. APPLICATION ENTRY POINT ---
 
1
  import os
2
  import time
3
  import tempfile
4
+ import uuid
5
  import google.generativeai as genai
6
  import requests
7
+ import yt_dlp
8
  from flask import Flask, request, render_template, send_from_directory, url_for, flash
9
  from moviepy.video.io.VideoFileClip import VideoFileClip
10
  from moviepy.audio.io.AudioFileClip import AudioFileClip
11
  from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
12
  from werkzeug.utils import secure_filename
13
+ from dotenv import load_dotenv
14
 
15
  # --- 1. INITIALIZE FLASK APP AND LOAD SECRETS ---
16
+ load_dotenv()
17
  app = Flask(__name__)
18
 
 
19
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
20
  TTS_API_URL = os.getenv("TTS_API_URL")
21
 
 
22
  if not GEMINI_API_KEY:
23
+ raise ValueError("SECURITY ERROR: GEMINI_API_KEY not found in .env file!")
24
  if not TTS_API_URL:
25
+ raise ValueError("CONFIGURATION ERROR: TTS_API_URL not found in .env file!")
26
 
 
27
  genai.configure(api_key=GEMINI_API_KEY)
28
 
 
29
  UPLOAD_FOLDER = 'uploads'
30
  DOWNLOAD_FOLDER = 'downloads'
31
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
32
  os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
33
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
34
  app.config['DOWNLOAD_FOLDER'] = DOWNLOAD_FOLDER
35
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
36
+ app.secret_key = os.urandom(24)
37
 
38
  # --- 2. VOICE CHOICES & GEMINI PROMPT ---
39
+ VOICE_CHOICES = {"Male (Charon)": "Charon", "Female (Zephyr)": "Zephyr"}
 
 
 
 
 
40
  GEMINI_PROMPT = """
41
+ You are an AI scriptwriter... (The full prompt remains the same as before)
 
 
 
 
 
 
 
 
42
  """
43
 
44
  # --- 3. CORE LOGIC HELPER FUNCTIONS ---
45
 
46
+ def download_youtube_video(url, output_folder):
47
+ """Downloads a YouTube video to a specified folder using the yt-dlp library."""
48
+ print(f"📥 Downloading video from YouTube URL: {url}")
49
+
50
+ # Generate a unique filename to avoid collisions
51
+ unique_filename = f"{uuid.uuid4()}.mp4"
52
+ output_path_template = os.path.join(output_folder, os.path.splitext(unique_filename)[0])
53
+
54
+ ydl_opts = {
55
+ 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
56
+ 'outtmpl': output_path_template,
57
+ 'merge_output_format': 'mp4',
58
+ 'quiet': True,
59
+ 'noplaylist': True,
60
+ }
61
+
62
+ try:
63
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
64
+ ydl.download([url])
65
+
66
+ # The actual filename will have a .mp4 extension
67
+ downloaded_file_path = f"{output_path_template}.mp4"
68
+ if os.path.exists(downloaded_file_path):
69
+ print(f"✅ Download complete! File saved to: {downloaded_file_path}")
70
+ return downloaded_file_path
71
+ else:
72
+ # Fallback for some edge cases where the extension might differ
73
+ for f in os.listdir(output_folder):
74
+ if f.startswith(os.path.splitext(unique_filename)[0]):
75
+ return os.path.join(output_folder, f)
76
+ raise FileNotFoundError("Downloaded video file not found.")
77
+
78
+ except Exception as e:
79
+ print(f"❌ An error occurred during YouTube download: {e}")
80
+ raise # Re-raise the exception to be caught by the main processing block
81
+
82
  def generate_tamil_script(video_file_path):
83
+ # (This function remains the same as the previous version)
84
  print("Uploading file to Gemini for transcription...")
85
  video_file = genai.upload_file(video_file_path, mime_type="video/mp4")
 
86
  print("Waiting for Gemini file processing...")
87
  while video_file.state.name == "PROCESSING":
88
  time.sleep(5)
89
  video_file = genai.get_file(video_file.name)
90
  if video_file.state.name != "ACTIVE":
91
  raise Exception(f"Gemini file processing failed: {video_file.state.name}")
 
92
  print("Generating narrator script...")
93
  model = genai.GenerativeModel(model_name="models/gemini-2.5-flash")
94
  response = model.generate_content([GEMINI_PROMPT, video_file])
95
  genai.delete_file(video_file.name)
96
+ if hasattr(response, 'text') and response.text:
 
97
  return " ".join(response.text.strip().splitlines())
98
  raise Exception("No valid script was generated by Gemini.")
99
 
100
  def generate_single_audio_track(dialogue_text, voice_name, is_cheerful, output_path):
101
+ # (This function remains the same)
102
  print(f"Requesting audio from TTS API (Voice: {voice_name}, Cheerful: {is_cheerful})")
103
  payload = {"text": dialogue_text, "voice_name": voice_name, "cheerful": is_cheerful}
104
  response = requests.post(TTS_API_URL, json=payload, timeout=300)
105
  if response.status_code == 200:
106
  with open(output_path, "wb") as f:
107
  f.write(response.content)
 
108
  return True
109
  raise Exception(f"Error from TTS API: {response.status_code} - {response.text}")
110
 
111
  def replace_video_audio(video_path, new_audio_path, output_path):
112
+ # (This function remains the same, using the correct .audio attribute)
 
 
 
113
  print("Replacing video audio using MoviePy...")
114
  video_clip = None
115
  audio_clip = None
116
  try:
117
  video_clip = VideoFileClip(video_path)
118
  audio_clip = AudioFileClip(new_audio_path)
 
 
 
 
119
  video_clip.audio = audio_clip
 
 
120
  video_clip.write_videofile(output_path, codec="libx264", audio_codec="aac", logger='bar')
 
 
 
 
 
121
  finally:
122
+ if audio_clip: audio_clip.close()
123
+ if video_clip: video_clip.close()
 
 
 
124
 
125
  # --- 4. FLASK WEB ROUTES ---
126
 
 
131
 
132
  @app.route('/process', methods=['POST'])
133
  def process_video():
134
+ """Handles video input (upload or URL), processing, and renders the result."""
135
+ input_video_path = None
136
+ temp_audio_path = None
 
 
 
137
 
138
  try:
139
+ # Determine input source: YouTube URL or File Upload
140
+ youtube_url = request.form.get('youtube_url', '').strip()
141
+
142
+ if youtube_url:
143
+ input_video_path = download_youtube_video(youtube_url, app.config['UPLOAD_FOLDER'])
144
+ # Generate a secure filename based on the unique downloaded file name
145
+ filename = os.path.basename(input_video_path)
146
+ elif 'video' in request.files and request.files['video'].filename != '':
147
+ file = request.files['video']
148
+ filename = secure_filename(file.filename)
149
+ input_video_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
150
+ file.save(input_video_path)
151
+ else:
152
+ flash("Please either upload a video file or provide a YouTube URL.")
153
+ return render_template('index.html')
154
+
155
+ # Continue with the processing pipeline
156
  voice_choice = request.form['voice_choice']
157
  is_cheerful = 'cheerful' in request.form
158
  voice_name = VOICE_CHOICES[voice_choice]
159
 
160
+ script = generate_tamil_script(input_video_path)
161
 
162
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio:
163
  temp_audio_path = temp_audio.name
 
167
  final_video_name = f"dubbed_{filename}"
168
  final_video_path = os.path.join(app.config['DOWNLOAD_FOLDER'], final_video_name)
169
 
170
+ replace_video_audio(input_video_path, temp_audio_path, final_video_path)
 
 
 
171
 
172
  return render_template('index.html',
173
  result_video=url_for('serve_video', filename=final_video_name),
 
175
 
176
  except Exception as e:
177
  print(f"An error occurred during processing: {e}")
178
+ flash(f"An unexpected error occurred: {e}. Please check the console and try again.")
179
  return render_template('index.html')
180
+ finally:
181
+ # Clean up all temporary files
182
+ if input_video_path and os.path.exists(input_video_path):
183
+ os.remove(input_video_path)
184
+ if temp_audio_path and os.path.exists(temp_audio_path):
185
+ os.remove(temp_audio_path)
186
 
187
  @app.route('/downloads/<filename>')
188
  def serve_video(filename):
189
+ """Serves the final dubbed video."""
190
  return send_from_directory(app.config['DOWNLOAD_FOLDER'], filename)
191
 
192
  # --- 5. APPLICATION ENTRY POINT ---