Maaz1 commited on
Commit
952467c
·
0 Parent(s):

new upload

Browse files
.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Required API key for AssemblyAI
2
+ ASSEMBLYAI_API_KEY=your_api_key_here
3
+
4
+ # Optional configuration
5
+ DEBUG=False
6
+ OUTPUT_DIR=outputs
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .env
2
+ venv/
README.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Video Translator 🌐
2
+
3
+ A complete video translation system that converts videos into multiple languages by translating both subtitles and audio.
4
+
5
+ ## Features
6
+
7
+ - 🎬 Video to text transcription using AssemblyAI
8
+ - 🔤 Translation of transcripts to multiple languages
9
+ - 🔊 Text-to-speech generation in target languages
10
+ - 📝 Subtitle generation and embedding
11
+ - 🎞️ Final video with translated audio and subtitles
12
+
13
+ ## Supported Languages
14
+
15
+ - English
16
+ - Spanish
17
+ - French
18
+ - German
19
+ - Japanese
20
+ - Hindi
21
+ - And more...
22
+
23
+ ## Installation
24
+
25
+ 1. Clone this repository:
26
+ ```bash
27
+ git clone https://github.com/yourusername/video-translator.git
28
+ cd video-translator
29
+ ```
30
+
31
+ 2. Install dependencies:
32
+ ```bash
33
+ pip install -r requirements.txt
34
+ ```
35
+
36
+ 3. Install FFmpeg:
37
+ - On Ubuntu/Debian: `sudo apt-get install ffmpeg`
38
+ - On macOS (with Homebrew): `brew install ffmpeg`
39
+ - On Windows: Download from [FFmpeg website](https://ffmpeg.org/download.html)
40
+
41
+ 4. Set up your API key:
42
+ - Copy `.env.example` to `.env`
43
+ - Add your AssemblyAI API key to the `.env` file
44
+
45
+ ## Usage
46
+
47
+ 1. Run the app:
48
+ ```bash
49
+ python app.py
50
+ ```
51
+
52
+ 2. Open the provided URL in your browser
53
+ 3. Upload a video file
54
+ 4. Select source and target languages
55
+ 5. Click "Translate" and wait for processing
56
+
57
+ ## Deployment on Hugging Face Spaces
58
+
59
+ This project is configured for easy deployment to [Hugging Face Spaces](https://huggingface.co/spaces). To deploy:
60
+
61
+ 1. Fork this repository
62
+ 2. Create a new Space on Hugging Face
63
+ 3. Connect your GitHub repository
64
+ 4. Set the required environment variables (ASSEMBLYAI_API_KEY)
65
+ 5. Deploy!
66
+
67
+ ## Project Structure
68
+
69
+ ```
70
+ video-translator/
71
+ ├── app.py # Main Gradio app entry point
72
+ ├── config.py # Configuration and constants
73
+ ├── src/ # Source code
74
+ │ ├── audio/ # Audio processing
75
+ │ ├── video/ # Video processing
76
+ │ ├── subtitles/ # Subtitle handling
77
+ │ └── utils/ # Utilities and helpers
78
+ └── outputs/ # Output directory
79
+ ```
80
+
81
+ ## Environment Variables
82
+
83
+ - `ASSEMBLYAI_API_KEY`: API key for AssemblyAI (required)
84
+ - `DEBUG`: Set to "True" for debug logging (optional)
85
+ - `OUTPUT_DIR`: Custom output directory path (optional)
86
+
87
+ ## License
88
+
89
+ MIT License
__pycache__/config.cpython-311.pyc ADDED
Binary file (1.95 kB). View file
 
app.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main application entry point for the Video Translator.
3
+ """
4
+ import os
5
+ import tempfile
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ import gradio as gr
10
+ from tqdm import tqdm
11
+
12
+ from src.utils.logger import get_logger
13
+ from src.audio.extractor import extract_audio, get_video_duration
14
+ from src.subtitles.transcriber import generate_subtitles
15
+ from src.subtitles.translator import translate_subtitles
16
+ from src.audio.generator import generate_translated_audio
17
+ from src.video.processor import combine_video_audio_subtitles
18
+ from config import LANGUAGES, OUTPUT_DIR, MAX_VIDEO_DURATION, MAX_UPLOAD_SIZE
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ def process_video(video_file, source_lang, target_langs, progress=gr.Progress()):
23
+ """
24
+ Process video file and generate translated versions.
25
+
26
+ Args:
27
+ video_file (str): Path to the uploaded video file
28
+ source_lang (str): Source language name
29
+ target_langs (list): List of target language names
30
+ progress (gr.Progress): Gradio progress tracker
31
+
32
+ Returns:
33
+ list: List of paths to translated videos
34
+ """
35
+ try:
36
+ # Convert language names to codes
37
+ source_lang_code = LANGUAGES[source_lang]
38
+ target_lang_codes = [LANGUAGES[lang] for lang in target_langs]
39
+
40
+ # Create temporary copy of uploaded file
41
+ temp_dir = Path(tempfile.mkdtemp(prefix="video_processing_", dir=OUTPUT_DIR / "temp"))
42
+ video_path = temp_dir / "input_video.mp4"
43
+ shutil.copy2(video_file, video_path)
44
+
45
+ logger.info(f"Processing video: {video_path}")
46
+ logger.info(f"Source language: {source_lang} ({source_lang_code})")
47
+ logger.info(f"Target languages: {', '.join(target_langs)} ({', '.join(target_lang_codes)})")
48
+
49
+ # Check video duration
50
+ progress(0.05, "Checking video duration...")
51
+ duration = get_video_duration(video_path)
52
+ if duration > MAX_VIDEO_DURATION:
53
+ raise ValueError(f"Video is too long ({duration:.1f} seconds). Maximum allowed duration is {MAX_VIDEO_DURATION} seconds.")
54
+
55
+ # Extract audio
56
+ progress(0.1, "Extracting audio...")
57
+ audio_path = extract_audio(video_path)
58
+
59
+ # Generate subtitles
60
+ progress(0.2, "Generating subtitles...")
61
+ srt_path = generate_subtitles(audio_path, source_lang_code)
62
+
63
+ # Translate subtitles
64
+ progress(0.3, "Translating subtitles...")
65
+ translated_srt_paths = translate_subtitles(srt_path, target_lang_codes)
66
+
67
+ # Generate translated audio
68
+ translated_audio_paths = {}
69
+ for i, (lang_code, srt_path) in enumerate(translated_srt_paths.items()):
70
+ progress_val = 0.3 + (0.4 * (i / len(translated_srt_paths)))
71
+ progress(progress_val, f"Generating {[k for k, v in LANGUAGES.items() if v == lang_code][0]} audio...")
72
+ audio_path = generate_translated_audio(srt_path, lang_code, duration)
73
+ translated_audio_paths[lang_code] = audio_path
74
+
75
+ # Combine video, audio, and subtitles
76
+ output_videos = []
77
+ for i, (lang_code, audio_path) in enumerate(translated_audio_paths.items()):
78
+ progress_val = 0.7 + (0.25 * (i / len(translated_audio_paths)))
79
+ lang_name = [k for k, v in LANGUAGES.items() if v == lang_code][0]
80
+ progress(progress_val, f"Creating {lang_name} video...")
81
+
82
+ srt_path = translated_srt_paths[lang_code]
83
+ output_path = combine_video_audio_subtitles(video_path, audio_path, srt_path)
84
+ output_videos.append(output_path)
85
+
86
+ # Clean up
87
+ try:
88
+ shutil.rmtree(temp_dir)
89
+ except:
90
+ logger.warning(f"Failed to clean up temp directory: {temp_dir}")
91
+
92
+ progress(1.0, "Translation complete!")
93
+ return output_videos
94
+
95
+ except Exception as e:
96
+ logger.error(f"Video processing failed: {str(e)}", exc_info=True)
97
+ raise gr.Error(f"Video processing failed: {str(e)}")
98
+
99
+ def create_app():
100
+ """
101
+ Create and configure the Gradio application.
102
+
103
+ Returns:
104
+ gr.Blocks: Configured Gradio application
105
+ """
106
+ with gr.Blocks(title="Video Translator") as app:
107
+ gr.Markdown("# 🌐 Video Translator")
108
+ gr.Markdown("Upload a video and translate it to different languages with subtitles!")
109
+
110
+ with gr.Row():
111
+ with gr.Column(scale=1):
112
+ video_input = gr.Video(label="Upload Video")
113
+ source_lang = gr.Dropdown(
114
+ choices=sorted(list(LANGUAGES.keys())),
115
+ value="English",
116
+ label="Source Language"
117
+ )
118
+ target_langs = gr.CheckboxGroup(
119
+ choices=[lang for lang in sorted(list(LANGUAGES.keys())) if lang != "English"],
120
+ value=["Spanish", "French"],
121
+ label="Target Languages"
122
+ )
123
+ translate_btn = gr.Button("Translate Video", variant="primary")
124
+
125
+ with gr.Column(scale=2):
126
+ output_gallery = gr.Gallery(
127
+ label="Translated Videos",
128
+ columns=2,
129
+ object_fit="contain",
130
+ height="auto"
131
+ )
132
+
133
+ translate_btn.click(
134
+ fn=process_video,
135
+ inputs=[video_input, source_lang, target_langs],
136
+ outputs=output_gallery
137
+ )
138
+
139
+ gr.Markdown("""
140
+ ## How it works
141
+
142
+ 1. Upload a video (max 10 minutes)
143
+ 2. Select the source language of your video
144
+ 3. Choose the target languages you want to translate to
145
+ 4. Click "Translate Video" and wait for processing
146
+ 5. Download your translated videos!
147
+
148
+ ## Features
149
+
150
+ - Automatic speech recognition using AssemblyAI
151
+ - Translation to multiple languages
152
+ - Generated speech in target languages
153
+ - Embedded subtitles
154
+ """)
155
+
156
+ return app
157
+
158
+ if __name__ == "__main__":
159
+ app = create_app()
160
+ app.launch(share=True, enable_queue=True)
161
+ logger.info("Starting Video Translator application...")
config.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for the video translator application.
3
+ """
4
+ import os
5
+ from pathlib import Path
6
+ from dotenv import load_dotenv
7
+
8
+ # Load environment variables from .env file
9
+ load_dotenv()
10
+
11
+ # Base directory
12
+ BASE_DIR = Path(__file__).resolve().parent
13
+
14
+ # API Keys
15
+ ASSEMBLYAI_API_KEY = os.getenv("ASSEMBLYAI_API_KEY")
16
+ if not ASSEMBLYAI_API_KEY:
17
+ raise ValueError("ASSEMBLYAI_API_KEY is not set in environment variables or .env file")
18
+
19
+ # Output directory
20
+ OUTPUT_DIR = Path(os.getenv("OUTPUT_DIR", BASE_DIR / "outputs"))
21
+ OUTPUT_DIR.mkdir(exist_ok=True)
22
+
23
+ # Temp directory for processing
24
+ TEMP_DIR = OUTPUT_DIR / "temp"
25
+ TEMP_DIR.mkdir(exist_ok=True)
26
+
27
+ # Debug mode
28
+ DEBUG = os.getenv("DEBUG", "False").lower() == "true"
29
+
30
+ # Supported languages
31
+ LANGUAGES = {
32
+ "English": "en",
33
+ "Spanish": "es",
34
+ "French": "fr",
35
+ "German": "de",
36
+ "Japanese": "ja",
37
+ "Hindi": "hi",
38
+ "Chinese (Simplified)": "zh-CN",
39
+ "Russian": "ru",
40
+ "Italian": "it",
41
+ "Portuguese": "pt",
42
+ "Arabic": "ar",
43
+ "Korean": "ko"
44
+ }
45
+
46
+ # TTS voice mapping for different languages
47
+ TTS_VOICES = {
48
+ "en": "en-US",
49
+ "es": "es-ES",
50
+ "fr": "fr-FR",
51
+ "de": "de-DE",
52
+ "ja": "ja-JP",
53
+ "hi": "hi-IN",
54
+ "zh-CN": "zh-CN",
55
+ "ru": "ru-RU",
56
+ "it": "it-IT",
57
+ "pt": "pt-BR",
58
+ "ar": "ar",
59
+ "ko": "ko"
60
+ }
61
+
62
+ # FFmpeg configurations
63
+ FFMPEG_AUDIO_PARAMS = {
64
+ "format": "wav",
65
+ "codec": "pcm_s16le",
66
+ "sample_rate": 44100,
67
+ "channels": 2
68
+ }
69
+
70
+ # Application settings
71
+ MAX_VIDEO_DURATION = 600 # in seconds (10 minutes)
72
+ MAX_UPLOAD_SIZE = 500 * 1024 * 1024 # 500 MB
73
+ SUBTITLE_FONT_SIZE = 24
74
+ MAX_RETRY_ATTEMPTS = 3
dir_struct.txt ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ video-translator/
2
+ ├── .gitignore
3
+ ├── README.md
4
+ ├── LICENSE
5
+ ├── requirements.txt
6
+ ├── app.py # Main Gradio app entry point
7
+ ├── config.py # Configuration and constants
8
+ ├── .env.example # Example environment variables
9
+ ├── src/
10
+ │ ├── __init__.py
11
+ │ ├── audio/
12
+ │ │ ├── __init__.py
13
+ │ │ ├── extractor.py # Audio extraction from video
14
+ │ │ └── generator.py # TTS generation
15
+ │ ├── video/
16
+ │ │ ├── __init__.py
17
+ │ │ └── processor.py # Video processing functions
18
+ │ ├── subtitles/
19
+ │ │ ├── __init__.py
20
+ │ │ ├── transcriber.py # Subtitle generation
21
+ │ │ └── translator.py # Subtitle translation
22
+ │ └── utils/
23
+ │ ├── __init__.py
24
+ │ └── logger.py # Logging configuration
25
+ ├── tests/
26
+ │ ├── __init__.py
27
+ │ ├── test_audio.py
28
+ │ ├── test_subtitles.py
29
+ │ └── test_video.py
30
+ └── outputs/ # Output directory (generated)
31
+ └── .gitkeep
outputs/logs/app_2025-04-09.log ADDED
File without changes
outputs/logs/error_2025-04-09.log ADDED
File without changes
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ gradio==3.50.2
3
+ python-dotenv==1.0.0
4
+ tqdm==4.66.1
5
+
6
+ # Video and audio processing
7
+ ffmpeg-python==0.2.0
8
+ moviepy==1.0.3
9
+ pydub==0.25.1
10
+
11
+ # Speech recognition and text-to-speech
12
+ assemblyai==0.15.1
13
+ gTTS==2.3.2
14
+
15
+ # Translation and subtitle handling
16
+ deep-translator==1.9.2
17
+ pysrt==1.1.2
18
+
19
+ # Utility packages
20
+ loguru==0.7.2
src/audio/__pycache__/extractor.cpython-311.pyc ADDED
Binary file (6.44 kB). View file
 
src/audio/__pycache__/generator.cpython-311.pyc ADDED
Binary file (8.65 kB). View file
 
src/audio/extractor.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio extraction utilities for the video translator application.
3
+ """
4
+ import os
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from src.utils.logger import get_logger
9
+ from config import OUTPUT_DIR, FFMPEG_AUDIO_PARAMS
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ def extract_audio(video_path):
14
+ """
15
+ Extract audio from video file using ffmpeg.
16
+
17
+ Args:
18
+ video_path (str): Path to the input video file
19
+
20
+ Returns:
21
+ Path: Path to the extracted audio file
22
+
23
+ Raises:
24
+ Exception: If audio extraction fails
25
+ """
26
+ try:
27
+ video_path = Path(video_path)
28
+ logger.info(f"Extracting audio from video: {video_path}")
29
+
30
+ # Create output filename based on input filename
31
+ video_name = video_path.stem
32
+ audio_path = OUTPUT_DIR / f"{video_name}_audio.{FFMPEG_AUDIO_PARAMS['format']}"
33
+
34
+ # Use ffmpeg to extract audio
35
+ cmd = [
36
+ 'ffmpeg',
37
+ '-i', str(video_path),
38
+ '-vn', # No video
39
+ '-acodec', FFMPEG_AUDIO_PARAMS['codec'],
40
+ '-ar', str(FFMPEG_AUDIO_PARAMS['sample_rate']),
41
+ '-ac', str(FFMPEG_AUDIO_PARAMS['channels']),
42
+ '-y', # Overwrite output file
43
+ str(audio_path)
44
+ ]
45
+
46
+ logger.debug(f"Running command: {' '.join(cmd)}")
47
+ process = subprocess.run(cmd, capture_output=True, text=True)
48
+
49
+ if process.returncode != 0:
50
+ error_message = f"Audio extraction failed: {process.stderr}"
51
+ logger.error(error_message)
52
+ raise Exception(error_message)
53
+
54
+ logger.info(f"Audio extraction successful: {audio_path}")
55
+ return audio_path
56
+ except Exception as e:
57
+ logger.error(f"Audio extraction failed: {str(e)}", exc_info=True)
58
+ raise Exception(f"Audio extraction failed: {str(e)}")
59
+
60
+ def get_video_duration(video_path):
61
+ """
62
+ Get the duration of a video file in seconds.
63
+
64
+ Args:
65
+ video_path (str): Path to the video file
66
+
67
+ Returns:
68
+ float: Duration in seconds
69
+
70
+ Raises:
71
+ Exception: If duration extraction fails
72
+ """
73
+ try:
74
+ video_path = Path(video_path)
75
+ logger.info(f"Getting duration for video: {video_path}")
76
+
77
+ cmd = [
78
+ 'ffprobe',
79
+ '-v', 'error',
80
+ '-show_entries', 'format=duration',
81
+ '-of', 'default=noprint_wrappers=1:nokey=1',
82
+ str(video_path)
83
+ ]
84
+
85
+ process = subprocess.run(cmd, capture_output=True, text=True)
86
+
87
+ if process.returncode != 0 or not process.stdout.strip():
88
+ error_message = f"Failed to get video duration: {process.stderr}"
89
+ logger.error(error_message)
90
+ raise Exception(error_message)
91
+
92
+ duration = float(process.stdout.strip())
93
+ logger.info(f"Video duration: {duration} seconds")
94
+ return duration
95
+ except Exception as e:
96
+ logger.error(f"Failed to get video duration: {str(e)}", exc_info=True)
97
+ raise Exception(f"Failed to get video duration: {str(e)}")
98
+
99
+ def create_silent_audio(duration, output_path=None):
100
+ """
101
+ Create a silent audio file with the specified duration.
102
+
103
+ Args:
104
+ duration (float): Duration in seconds
105
+ output_path (str, optional): Path to save the silent audio file
106
+
107
+ Returns:
108
+ Path: Path to the silent audio file
109
+
110
+ Raises:
111
+ Exception: If silent audio creation fails
112
+ """
113
+ try:
114
+ if output_path is None:
115
+ output_path = OUTPUT_DIR / f"silent_{int(duration)}s.wav"
116
+ else:
117
+ output_path = Path(output_path)
118
+
119
+ logger.info(f"Creating silent audio track of {duration} seconds")
120
+
121
+ cmd = [
122
+ 'ffmpeg',
123
+ '-f', 'lavfi',
124
+ '-i', f'anullsrc=r={FFMPEG_AUDIO_PARAMS["sample_rate"]}:cl=stereo',
125
+ '-t', str(duration),
126
+ '-q:a', '0',
127
+ '-y',
128
+ str(output_path)
129
+ ]
130
+
131
+ logger.debug(f"Running command: {' '.join(cmd)}")
132
+ process = subprocess.run(cmd, capture_output=True, text=True)
133
+
134
+ if process.returncode != 0:
135
+ error_message = f"Silent audio creation failed: {process.stderr}"
136
+ logger.error(error_message)
137
+ raise Exception(error_message)
138
+
139
+ logger.info(f"Silent audio created: {output_path}")
140
+ return output_path
141
+ except Exception as e:
142
+ logger.error(f"Failed to create silent audio: {str(e)}", exc_info=True)
143
+ raise Exception(f"Failed to create silent audio: {str(e)}")
src/audio/generator.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Text-to-speech audio generation for translated subtitles.
3
+ """
4
+ import os
5
+ import time
6
+ import shutil
7
+ import tempfile
8
+ from pathlib import Path
9
+ from tqdm import tqdm
10
+ import subprocess
11
+
12
+ from gtts import gTTS
13
+ import pysrt
14
+
15
+ from src.utils.logger import get_logger
16
+ from src.audio.extractor import create_silent_audio
17
+ from config import OUTPUT_DIR, TTS_VOICES, MAX_RETRY_ATTEMPTS
18
+
19
+ logger = get_logger(__name__)
20
+
21
+ def generate_translated_audio(srt_path, target_lang, video_duration=180):
22
+ """
23
+ Generate translated audio using text-to-speech for each subtitle.
24
+
25
+ Args:
26
+ srt_path (str): Path to the SRT subtitle file
27
+ target_lang (str): Target language code (e.g., 'en', 'es')
28
+ video_duration (float): Duration of the original video in seconds
29
+
30
+ Returns:
31
+ Path: Path to the translated audio file
32
+
33
+ Raises:
34
+ Exception: If audio generation fails
35
+ """
36
+ try:
37
+ srt_path = Path(srt_path)
38
+ logger.info(f"Generating translated audio for {target_lang} from {srt_path}")
39
+
40
+ # Load subtitles
41
+ subs = pysrt.open(srt_path, encoding="utf-8")
42
+ logger.info(f"Loaded {len(subs)} subtitles from SRT file")
43
+
44
+ # Create temporary directory for audio chunks
45
+ temp_dir = Path(tempfile.mkdtemp(prefix=f"audio_{target_lang}_", dir=OUTPUT_DIR / "temp"))
46
+ logger.debug(f"Created temporary directory: {temp_dir}")
47
+
48
+ # Generate TTS for each subtitle
49
+ audio_files = []
50
+ timings = []
51
+
52
+ logger.info(f"Generating speech for {len(subs)} subtitles in {target_lang}")
53
+ for i, sub in enumerate(tqdm(subs, desc=f"Generating {target_lang} speech")):
54
+ text = sub.text.strip()
55
+ if not text:
56
+ continue
57
+
58
+ # Get timing information
59
+ start_time = (sub.start.hours * 3600 +
60
+ sub.start.minutes * 60 +
61
+ sub.start.seconds +
62
+ sub.start.milliseconds / 1000)
63
+
64
+ end_time = (sub.end.hours * 3600 +
65
+ sub.end.minutes * 60 +
66
+ sub.end.seconds +
67
+ sub.end.milliseconds / 1000)
68
+
69
+ duration = end_time - start_time
70
+
71
+ # Generate TTS audio
72
+ tts_lang = TTS_VOICES.get(target_lang, target_lang)
73
+ audio_file = temp_dir / f"chunk_{i:04d}.mp3"
74
+
75
+ # Add a retry mechanism
76
+ retry_count = 0
77
+ while retry_count < MAX_RETRY_ATTEMPTS:
78
+ try:
79
+ # For certain languages, use slower speed which might improve reliability
80
+ slow_option = target_lang in ["hi", "ja", "zh-CN", "ar"]
81
+ tts = gTTS(text=text, lang=target_lang, slow=slow_option)
82
+ tts.save(str(audio_file))
83
+
84
+ if audio_file.exists() and audio_file.stat().st_size > 0:
85
+ break
86
+ else:
87
+ raise Exception("Generated audio file is empty")
88
+
89
+ except Exception as e:
90
+ retry_count += 1
91
+ logger.warning(f"TTS attempt {retry_count} failed for {target_lang}: {str(e)}")
92
+ time.sleep(1) # Wait before retrying
93
+
94
+ # If still failing after retries, try with shorter text
95
+ if retry_count == MAX_RETRY_ATTEMPTS - 1 and len(text) > 100:
96
+ logger.warning(f"Trying with shortened text for {target_lang}")
97
+ shortened_text = text[:100] + "..."
98
+ tts = gTTS(text=shortened_text, lang=target_lang, slow=True)
99
+ tts.save(str(audio_file))
100
+
101
+ if audio_file.exists() and audio_file.stat().st_size > 0:
102
+ audio_files.append(audio_file)
103
+ timings.append((start_time, end_time, duration, audio_file))
104
+ else:
105
+ logger.warning(f"Failed to generate audio for subtitle {i}")
106
+
107
+ # Check if we generated any audio files
108
+ if not audio_files:
109
+ logger.warning(f"No audio files were generated for {target_lang}")
110
+ # Create a silent audio file as fallback
111
+ silent_audio = OUTPUT_DIR / f"translated_audio_{target_lang}.wav"
112
+ create_silent_audio(video_duration, silent_audio)
113
+ return silent_audio
114
+
115
+ # Create a silent audio track as base
116
+ silence_file = temp_dir / "silence.wav"
117
+ create_silent_audio(video_duration, silence_file)
118
+
119
+ # Create filter complex for audio mixing
120
+ filter_complex = []
121
+ input_count = 1 # Starting with 1 because 0 is the silence track
122
+
123
+ # Start with silent track
124
+ filter_parts = ["[0:a]"]
125
+
126
+ # Add each audio segment
127
+ for start_time, end_time, duration, audio_file in timings:
128
+ delay_ms = int(start_time * 1000)
129
+ filter_parts.append(f"[{input_count}:a]adelay={delay_ms}|{delay_ms}")
130
+ input_count += 1
131
+
132
+ # Mix all audio tracks
133
+ filter_parts.append(f"amix=inputs={input_count}:dropout_transition=0:normalize=0[aout]")
134
+ filter_complex = ";".join(filter_parts)
135
+
136
+ # Build the ffmpeg command
137
+ cmd = ['ffmpeg', '-y']
138
+
139
+ # Add silent base track
140
+ cmd.extend(['-i', str(silence_file)])
141
+
142
+ # Add all audio chunks
143
+ for audio_file in audio_files:
144
+ cmd.extend(['-i', str(audio_file)])
145
+
146
+ # Add filter complex and output
147
+ output_audio = OUTPUT_DIR / f"translated_audio_{target_lang}.wav"
148
+ cmd.extend([
149
+ '-filter_complex', filter_complex,
150
+ '-map', '[aout]',
151
+ output_audio
152
+ ])
153
+
154
+ # Run the command
155
+ logger.info(f"Combining {len(audio_files)} audio segments")
156
+ logger.debug(f"Running command: {' '.join(cmd)}")
157
+ process = subprocess.run(cmd, capture_output=True, text=True)
158
+
159
+ if process.returncode != 0:
160
+ logger.error(f"Audio combination failed: {process.stderr}")
161
+ # Create a fallback silent audio
162
+ silent_audio = OUTPUT_DIR / f"translated_audio_{target_lang}.wav"
163
+ create_silent_audio(video_duration, silent_audio)
164
+ output_audio = silent_audio
165
+
166
+ # Clean up temporary files
167
+ try:
168
+ shutil.rmtree(temp_dir)
169
+ logger.debug(f"Cleaned up temporary directory: {temp_dir}")
170
+ except Exception as e:
171
+ logger.warning(f"Failed to clean up temp directory: {str(e)}")
172
+
173
+ logger.info(f"Successfully created translated audio: {output_audio}")
174
+ return output_audio
175
+ except Exception as e:
176
+ logger.error(f"Audio translation failed: {str(e)}", exc_info=True)
177
+
178
+ # Create an emergency fallback silent audio
179
+ try:
180
+ silent_audio = OUTPUT_DIR / f"translated_audio_{target_lang}.wav"
181
+ create_silent_audio(video_duration, silent_audio)
182
+ return silent_audio
183
+ except:
184
+ raise Exception(f"Audio translation failed: {str(e)}")
src/subtitles/__pycache__/transcriber.cpython-311.pyc ADDED
Binary file (3.08 kB). View file
 
src/subtitles/__pycache__/translator.cpython-311.pyc ADDED
Binary file (4.17 kB). View file
 
src/subtitles/transcriber.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Speech-to-text transcription for subtitle generation.
3
+ """
4
+ import os
5
+ from pathlib import Path
6
+ import assemblyai as aai
7
+
8
+ from src.utils.logger import get_logger
9
+ from config import ASSEMBLYAI_API_KEY, OUTPUT_DIR
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ # Configure AssemblyAI
14
+ aai.settings.api_key = ASSEMBLYAI_API_KEY
15
+
16
+ def generate_subtitles(audio_path, language_code="en"):
17
+ """
18
+ Generate subtitles using AssemblyAI's speech recognition.
19
+
20
+ Args:
21
+ audio_path (str): Path to the audio file
22
+ language_code (str): Language code for transcription
23
+
24
+ Returns:
25
+ Path: Path to the generated SRT subtitle file
26
+
27
+ Raises:
28
+ Exception: If subtitle generation fails
29
+ """
30
+ try:
31
+ audio_path = Path(audio_path)
32
+ logger.info(f"Transcribing audio with AssemblyAI: {audio_path}")
33
+
34
+ # Create output filename
35
+ audio_name = audio_path.stem
36
+ srt_path = OUTPUT_DIR / f"{audio_name}_subtitles.srt"
37
+
38
+ # Configure transcription options
39
+ config = aai.TranscriptionConfig(
40
+ language_code=language_code,
41
+ punctuate=True,
42
+ format_text=True
43
+ )
44
+
45
+ # Transcribe audio
46
+ transcriber = aai.Transcriber()
47
+ transcript = transcriber.transcribe(str(audio_path), config=config)
48
+
49
+ if not transcript or not hasattr(transcript, 'export_subtitles_srt'):
50
+ error_message = "Transcription failed or returned invalid result"
51
+ logger.error(error_message)
52
+ raise Exception(error_message)
53
+
54
+ # Export as SRT
55
+ logger.info(f"Saving subtitles to: {srt_path}")
56
+ with open(srt_path, "w", encoding="utf-8") as f:
57
+ f.write(transcript.export_subtitles_srt())
58
+
59
+ logger.info(f"Subtitle generation successful: {srt_path}")
60
+ return srt_path
61
+ except Exception as e:
62
+ logger.error(f"Subtitle generation failed: {str(e)}", exc_info=True)
63
+ raise Exception(f"Subtitle generation failed: {str(e)}")
src/subtitles/translator.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Translation of subtitles into target languages.
3
+ """
4
+ import os
5
+ from pathlib import Path
6
+ import time
7
+ from tqdm import tqdm
8
+ import pysrt
9
+ from deep_translator import GoogleTranslator
10
+
11
+ from src.utils.logger import get_logger
12
+ from config import OUTPUT_DIR, MAX_RETRY_ATTEMPTS
13
+
14
+ logger = get_logger(__name__)
15
+
16
+ def translate_subtitles(srt_path, target_langs):
17
+ """
18
+ Translate subtitles to target languages.
19
+
20
+ Args:
21
+ srt_path (str): Path to the SRT subtitle file
22
+ target_langs (list): List of target language codes
23
+
24
+ Returns:
25
+ dict: Dictionary mapping language codes to translated SRT file paths
26
+
27
+ Raises:
28
+ Exception: If translation fails
29
+ """
30
+ try:
31
+ srt_path = Path(srt_path)
32
+ logger.info(f"Loading subtitles from: {srt_path}")
33
+
34
+ # Load subtitles
35
+ subs = pysrt.open(srt_path, encoding="utf-8")
36
+ logger.info(f"Loaded {len(subs)} subtitles from SRT file")
37
+
38
+ results = {}
39
+
40
+ for lang_code in target_langs:
41
+ logger.info(f"Translating to language code: {lang_code}")
42
+ translated_subs = subs[:] # Create a copy
43
+ translator = GoogleTranslator(source="auto", target=lang_code)
44
+
45
+ # Translate each subtitle with progress bar
46
+ for i, sub in enumerate(tqdm(translated_subs, desc=f"Translating to {lang_code}")):
47
+ retry_count = 0
48
+ original_text = sub.text
49
+
50
+ while retry_count < MAX_RETRY_ATTEMPTS:
51
+ try:
52
+ sub.text = translator.translate(original_text)
53
+ break
54
+ except Exception as e:
55
+ retry_count += 1
56
+ logger.warning(f"Translation attempt {retry_count} failed: {str(e)}")
57
+ time.sleep(1) # Delay between retries
58
+
59
+ # If final retry, preserve original text
60
+ if retry_count == MAX_RETRY_ATTEMPTS:
61
+ logger.warning(f"Failed to translate subtitle after {MAX_RETRY_ATTEMPTS} attempts")
62
+ sub.text = original_text
63
+
64
+ # Log progress periodically
65
+ if (i + 1) % 20 == 0 or i == len(translated_subs) - 1:
66
+ logger.info(f"Translated {i+1}/{len(translated_subs)} subtitles to {lang_code}")
67
+
68
+ # Save translated subtitles
69
+ output_path = OUTPUT_DIR / f"subtitles_{lang_code}.srt"
70
+ logger.info(f"Saving translated subtitles to: {output_path}")
71
+ translated_subs.save(str(output_path), encoding='utf-8')
72
+ results[lang_code] = output_path
73
+
74
+ logger.info(f"Successfully translated subtitles to {len(results)} languages")
75
+ return results
76
+ except Exception as e:
77
+ logger.error(f"Translation failed: {str(e)}", exc_info=True)
78
+ raise Exception(f"Translation failed: {str(e)}")
src/utils/__pycache__/logger.cpython-311.pyc ADDED
Binary file (1.76 kB). View file
 
src/utils/logger.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Centralized logging configuration for the application.
3
+ """
4
+ import sys
5
+ import os
6
+ from loguru import logger
7
+ from pathlib import Path
8
+
9
+ from config import DEBUG, OUTPUT_DIR
10
+
11
+ # Create logs directory
12
+ LOGS_DIR = OUTPUT_DIR / "logs"
13
+ LOGS_DIR.mkdir(exist_ok=True)
14
+
15
+ # Configure logger
16
+ logger.remove() # Remove default handler
17
+
18
+ # Add console handler
19
+ log_level = "DEBUG" if DEBUG else "INFO"
20
+ logger.add(
21
+ sys.stderr,
22
+ format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
23
+ level=log_level
24
+ )
25
+
26
+ # Add file handler for errors
27
+ logger.add(
28
+ LOGS_DIR / "error_{time:YYYY-MM-DD}.log",
29
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
30
+ level="ERROR",
31
+ rotation="1 day",
32
+ retention="7 days"
33
+ )
34
+
35
+ # Add file handler for all logs
36
+ logger.add(
37
+ LOGS_DIR / "app_{time:YYYY-MM-DD}.log",
38
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
39
+ level=log_level,
40
+ rotation="1 day",
41
+ retention="3 days"
42
+ )
43
+
44
+ # Export the configured logger
45
+ def get_logger(name):
46
+ """
47
+ Get a logger instance with the specified name.
48
+
49
+ Args:
50
+ name (str): Name of the logger, typically __name__
51
+
52
+ Returns:
53
+ logger: Configured logger instance
54
+ """
55
+ return logger.bind(name=name)
src/video/__pycache__/processor.cpython-311.pyc ADDED
Binary file (11.2 kB). View file
 
src/video/processor.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video processing utilities for combining video, audio, and subtitles.
3
+ """
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+ import tempfile
9
+
10
+ from src.utils.logger import get_logger
11
+ from config import OUTPUT_DIR, SUBTITLE_FONT_SIZE
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ def combine_video_audio_subtitles(video_path, audio_path, srt_path, output_path=None):
16
+ """
17
+ Combine video with translated audio and subtitles.
18
+
19
+ Args:
20
+ video_path (str): Path to the video file
21
+ audio_path (str): Path to the translated audio file
22
+ srt_path (str): Path to the subtitle file
23
+ output_path (str, optional): Path for the output video
24
+
25
+ Returns:
26
+ Path: Path to the output video
27
+
28
+ Raises:
29
+ Exception: If combining fails
30
+ """
31
+ try:
32
+ video_path = Path(video_path)
33
+ audio_path = Path(audio_path)
34
+ srt_path = Path(srt_path)
35
+
36
+ # Generate output path if not provided
37
+ if output_path is None:
38
+ lang_code = srt_path.stem.split('_')[-1]
39
+ output_path = OUTPUT_DIR / f"{video_path.stem}_translated_{lang_code}.mp4"
40
+ else:
41
+ output_path = Path(output_path)
42
+
43
+ logger.info(f"Combining video, audio, and subtitles")
44
+
45
+ # Verify that all input files exist
46
+ if not video_path.exists():
47
+ raise FileNotFoundError(f"Video file does not exist: {video_path}")
48
+ if not audio_path.exists():
49
+ raise FileNotFoundError(f"Audio file does not exist: {audio_path}")
50
+ if not srt_path.exists():
51
+ raise FileNotFoundError(f"Subtitle file does not exist: {srt_path}")
52
+
53
+ logger.info(f"Input files verified: Video: {video_path.stat().st_size} bytes, "
54
+ f"Audio: {audio_path.stat().st_size} bytes, "
55
+ f"Subtitles: {srt_path.stat().st_size} bytes")
56
+
57
+ # Try different methods to combine
58
+ methods = [
59
+ combine_method_subtitles_filter,
60
+ combine_method_with_temp,
61
+ combine_method_no_subtitles
62
+ ]
63
+
64
+ success = False
65
+ error_messages = []
66
+
67
+ for i, method in enumerate(methods):
68
+ try:
69
+ logger.info(f"Trying combination method {i+1}/{len(methods)}")
70
+ result = method(video_path, audio_path, srt_path, output_path)
71
+ if result and Path(result).exists() and Path(result).stat().st_size > 0:
72
+ success = True
73
+ output_path = result
74
+ logger.info(f"Combination method {i+1} succeeded")
75
+ break
76
+ else:
77
+ error_messages.append(f"Method {i+1} failed: Result file not valid")
78
+ except Exception as e:
79
+ error_message = f"Method {i+1} failed: {str(e)}"
80
+ logger.warning(error_message)
81
+ error_messages.append(error_message)
82
+
83
+ if not success:
84
+ error_message = f"All combination methods failed: {'; '.join(error_messages)}"
85
+ logger.error(error_message)
86
+ raise Exception(error_message)
87
+
88
+ logger.info(f"Successfully combined video, audio, and subtitles: {output_path}")
89
+ return output_path
90
+ except Exception as e:
91
+ logger.error(f"Combining failed: {str(e)}", exc_info=True)
92
+ raise Exception(f"Combining failed: {str(e)}")
93
+
94
+ def combine_method_subtitles_filter(video_path, audio_path, srt_path, output_path):
95
+ """
96
+ Combine video, audio, and subtitles using ffmpeg with subtitle filter.
97
+
98
+ Args:
99
+ video_path (Path): Path to the video file
100
+ audio_path (Path): Path to the translated audio file
101
+ srt_path (Path): Path to the subtitle file
102
+ output_path (Path): Path for the output video
103
+
104
+ Returns:
105
+ Path: Path to the output video
106
+ """
107
+ logger.info(f"Using subtitles filter method")
108
+
109
+ # Use ffmpeg to combine video, audio, and subtitles
110
+ cmd = [
111
+ 'ffmpeg',
112
+ '-i', str(video_path), # Video input
113
+ '-i', str(audio_path), # Audio input
114
+ '-vf', f"subtitles={str(srt_path)}:force_style='FontSize={SUBTITLE_FONT_SIZE}'", # Subtitle filter
115
+ '-map', '0:v', # Map video from first input
116
+ '-map', '1:a', # Map audio from second input
117
+ '-c:v', 'libx264', # Video codec
118
+ '-c:a', 'aac', # Audio codec
119
+ '-strict', 'experimental',
120
+ '-b:a', '192k', # Audio bitrate
121
+ '-y', # Overwrite output
122
+ str(output_path)
123
+ ]
124
+
125
+ logger.debug(f"Running command: {' '.join(cmd)}")
126
+ process = subprocess.run(cmd, capture_output=True, text=True)
127
+
128
+ if process.returncode != 0:
129
+ error_message = f"FFmpeg subtitles filter method failed: {process.stderr}"
130
+ logger.error(error_message)
131
+ raise Exception(error_message)
132
+
133
+ return output_path
134
+
135
+ def combine_method_with_temp(video_path, audio_path, srt_path, output_path):
136
+ """
137
+ Combine video, audio, and subtitles using temporary files.
138
+
139
+ Args:
140
+ video_path (Path): Path to the video file
141
+ audio_path (Path): Path to the translated audio file
142
+ srt_path (Path): Path to the subtitle file
143
+ output_path (Path): Path for the output video
144
+
145
+ Returns:
146
+ Path: Path to the output video
147
+ """
148
+ logger.info(f"Using temporary file method")
149
+
150
+ # Create temporary directory
151
+ temp_dir = Path(tempfile.mkdtemp(prefix="video_combine_", dir=OUTPUT_DIR / "temp"))
152
+ try:
153
+ # Step 1: Combine video with audio
154
+ temp_video_audio = temp_dir / "video_with_audio.mp4"
155
+ cmd1 = [
156
+ 'ffmpeg',
157
+ '-i', str(video_path),
158
+ '-i', str(audio_path),
159
+ '-c:v', 'copy',
160
+ '-c:a', 'aac',
161
+ '-strict', 'experimental',
162
+ '-map', '0:v',
163
+ '-map', '1:a',
164
+ '-y',
165
+ str(temp_video_audio)
166
+ ]
167
+
168
+ logger.debug(f"Running command (step 1): {' '.join(cmd1)}")
169
+ process1 = subprocess.run(cmd1, capture_output=True, text=True)
170
+
171
+ if process1.returncode != 0:
172
+ error_message = f"Step 1 failed: {process1.stderr}"
173
+ logger.error(error_message)
174
+ raise Exception(error_message)
175
+
176
+ # Step 2: Add subtitles to the combined video
177
+ cmd2 = [
178
+ 'ffmpeg',
179
+ '-i', str(temp_video_audio),
180
+ '-vf', f"subtitles={str(srt_path)}:force_style='FontSize={SUBTITLE_FONT_SIZE}'",
181
+ '-c:a', 'copy',
182
+ '-y',
183
+ str(output_path)
184
+ ]
185
+
186
+ logger.debug(f"Running command (step 2): {' '.join(cmd2)}")
187
+ process2 = subprocess.run(cmd2, capture_output=True, text=True)
188
+
189
+ if process2.returncode != 0:
190
+ error_message = f"Step 2 failed: {process2.stderr}"
191
+ logger.error(error_message)
192
+ raise Exception(error_message)
193
+
194
+ return output_path
195
+ finally:
196
+ # Clean up temporary directory
197
+ try:
198
+ shutil.rmtree(temp_dir)
199
+ logger.debug(f"Cleaned up temporary directory: {temp_dir}")
200
+ except Exception as e:
201
+ logger.warning(f"Failed to clean up temp directory: {str(e)}")
202
+
203
+ def combine_method_no_subtitles(video_path, audio_path, srt_path, output_path):
204
+ """
205
+ Fallback method: Combine only video and audio without subtitles.
206
+
207
+ Args:
208
+ video_path (Path): Path to the video file
209
+ audio_path (Path): Path to the translated audio file
210
+ srt_path (Path): Path to the subtitle file (unused in this method)
211
+ output_path (Path): Path for the output video
212
+
213
+ Returns:
214
+ Path: Path to the output video
215
+ """
216
+ logger.info(f"Using fallback method (no subtitles)")
217
+
218
+ # Just combine video and audio as fallback
219
+ cmd = [
220
+ 'ffmpeg',
221
+ '-i', str(video_path),
222
+ '-i', str(audio_path),
223
+ '-c:v', 'copy',
224
+ '-c:a', 'aac',
225
+ '-strict', 'experimental',
226
+ '-map', '0:v',
227
+ '-map', '1:a',
228
+ '-y',
229
+ str(output_path)
230
+ ]
231
+
232
+ logger.debug(f"Running command: {' '.join(cmd)}")
233
+ process = subprocess.run(cmd, capture_output=True, text=True)
234
+
235
+ if process.returncode != 0:
236
+ error_message = f"Fallback method failed: {process.stderr}"
237
+ logger.error(error_message)
238
+ raise Exception(error_message)
239
+
240
+ logger.warning("Video was combined without subtitles")
241
+ return output_path