Spaces:
Running
Running
Commit
·
952467c
0
Parent(s):
new upload
Browse files- .env.example +6 -0
- .gitignore +2 -0
- README.md +89 -0
- __pycache__/config.cpython-311.pyc +0 -0
- app.py +161 -0
- config.py +74 -0
- dir_struct.txt +31 -0
- outputs/logs/app_2025-04-09.log +0 -0
- outputs/logs/error_2025-04-09.log +0 -0
- requirements.txt +20 -0
- src/audio/__pycache__/extractor.cpython-311.pyc +0 -0
- src/audio/__pycache__/generator.cpython-311.pyc +0 -0
- src/audio/extractor.py +143 -0
- src/audio/generator.py +184 -0
- src/subtitles/__pycache__/transcriber.cpython-311.pyc +0 -0
- src/subtitles/__pycache__/translator.cpython-311.pyc +0 -0
- src/subtitles/transcriber.py +63 -0
- src/subtitles/translator.py +78 -0
- src/utils/__pycache__/logger.cpython-311.pyc +0 -0
- src/utils/logger.py +55 -0
- src/video/__pycache__/processor.cpython-311.pyc +0 -0
- src/video/processor.py +241 -0
.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
|