Spaces:
Sleeping
Sleeping
| import os | |
| import subprocess | |
| from flask import Flask, request, redirect, url_for, send_from_directory, flash, render_template_string | |
| from werkzeug.utils import secure_filename | |
| from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip | |
| from moviepy.video.tools.subtitles import SubtitlesClip | |
| # --- Configuration --- | |
| UPLOAD_FOLDER = 'uploads' | |
| ALLOWED_EXTENSIONS = {'mp4', 'mkv', 'avi', 'srt'} | |
| app = Flask(__name__) | |
| app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER | |
| app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 # 1 GB max upload size | |
| app.secret_key = 'super secret key' # Needed for flashing messages | |
| # --- HTML Template --- | |
| # The entire web interface is defined in this string. | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Web Subtitle Tool</title> | |
| <style> | |
| body { font-family: 'Helvetica', sans-serif; background-color: #2c3e50; color: #ecf0f1; margin: 0; padding: 40px; display: flex; justify-content: center; align-items: center; min-height: 100vh; } | |
| .container { background-color: #34495e; padding: 30px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); width: 100%; max-width: 700px; } | |
| h1 { color: #ecf0f1; text-align: center; margin-bottom: 10px; } | |
| p { color: #bdc3c7; text-align: center; margin-bottom: 30px; } | |
| form { display: flex; flex-direction: column; gap: 20px; } | |
| .form-group { display: flex; flex-direction: column; } | |
| label { margin-bottom: 8px; font-weight: bold; color: #bdc3c7; } | |
| input[type="file"] { background-color: #2c3e50; color: #ecf0f1; border: 2px dashed #4a627a; border-radius: 5px; padding: 15px; cursor: pointer; transition: background-color 0.3s; } | |
| input[type="file"]:hover { background-color: #4a627a; } | |
| .button-group { display: flex; justify-content: center; gap: 20px; margin-top: 10px; } | |
| button { color: white; font-weight: bold; font-size: 16px; border: none; border-radius: 5px; padding: 15px 30px; cursor: pointer; transition: background-color 0.3s; } | |
| .btn-extract { background-color: #27ae60; } | |
| .btn-extract:hover { background-color: #2ecc71; } | |
| .btn-add { background-color: #e67e22; } | |
| .btn-add:hover { background-color: #f39c12; } | |
| .flash-messages { list-style: none; padding: 0; margin-top: 20px; } | |
| .flash-messages li { padding: 15px; border-radius: 5px; margin-bottom: 10px; text-align: center; } | |
| .flash-error { background-color: #c0392b; color: white; } | |
| .flash-success { background-color: #27ae60; color: white; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Subtitle Extractor & Adder</h1> | |
| <p>Upload a video, and optionally an SRT file, to begin.</p> | |
| {% with messages = get_flashed_messages(with_categories=true) %} | |
| {% if messages %} | |
| <ul class="flash-messages"> | |
| {% for category, message in messages %} | |
| <li class="flash-{{ category }}">{{ message }}</li> | |
| {% endfor %} | |
| </ul> | |
| {% endif %} | |
| {% endwith %} | |
| <form method="post" enctype="multipart/form-data"> | |
| <div class="form-group"> | |
| <label for="video_file">1. Select Video File (.mp4, .mkv, .avi)</label> | |
| <input type="file" name="video_file" id="video_file" required> | |
| </div> | |
| <div class="form-group"> | |
| <label for="srt_file">2. Select Subtitle File (for adding subtitles)</label> | |
| <input type="file" name="srt_file" id="srt_file"> | |
| </div> | |
| <div class="button-group"> | |
| <button type="submit" name="action" value="extract" class="btn-extract">Extract Subtitles</button> | |
| <button type="submit" name="action" value="add" class="btn-add">Add Subtitles</button> | |
| </div> | |
| </form> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| def allowed_file(filename): | |
| return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS | |
| # --- Core Subtitle Functions (Adapted for Web) --- | |
| def extract_subtitles_web(video_path): | |
| """ Extracts subtitles and returns the path to the resulting SRT file. """ | |
| try: | |
| output_filename = os.path.basename(os.path.splitext(video_path)[0]) + ".srt" | |
| output_srt_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename) | |
| command = f'ffmpeg -y -i "{video_path}" -map 0:s:0 -c:s srt "{output_srt_path}"' | |
| # Use subprocess for better error handling | |
| result = subprocess.run(command, shell=True, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| if "Subtitle stream" not in result.stderr: | |
| raise Exception("No subtitle stream found in the video file.") | |
| else: | |
| raise Exception(f"FFmpeg command failed. Error: {result.stderr}") | |
| return output_filename | |
| except Exception as e: | |
| # To see the error in the Flask console | |
| print(f"Extraction error: {e}") | |
| return str(e) | |
| def add_subtitles_web(video_path, srt_path): | |
| """ Adds subtitles and returns the path to the new video file. """ | |
| try: | |
| video = VideoFileClip(video_path) | |
| generator = lambda txt: TextClip(txt, font='Arial', fontsize=24, color='white', stroke_color='black', stroke_width=1) | |
| subtitles = SubtitlesClip(srt_path, generator) | |
| final_video = CompositeVideoClip([video, subtitles.set_position(('center', 'bottom'))]) | |
| output_filename = os.path.basename(os.path.splitext(video_path)[0]) + "_subtitled.mp4" | |
| output_video_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename) | |
| final_video.write_videofile(output_video_path, codec='libx264', audio_codec='aac') | |
| return output_filename | |
| except Exception as e: | |
| print(f"Add subtitle error: {e}") | |
| return str(e) | |
| # --- Flask Routes --- | |
| def upload_file(): | |
| if request.method == 'POST': | |
| # Check if files are part of the request | |
| if 'video_file' not in request.files: | |
| flash('No video file part in the request.', 'error') | |
| return redirect(request.url) | |
| video_file = request.files['video_file'] | |
| if video_file.filename == '': | |
| flash('No video file selected.', 'error') | |
| return redirect(request.url) | |
| if video_file and allowed_file(video_file.filename): | |
| video_filename = secure_filename(video_file.filename) | |
| video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_filename) | |
| video_file.save(video_path) | |
| action = request.form.get('action') | |
| if action == 'extract': | |
| flash('Extracting subtitles... please wait.', 'success') | |
| result_file = extract_subtitles_web(video_path) | |
| if result_file and not "Error" in result_file and not "failed" in result_file and not "found" in result_file: | |
| return redirect(url_for('download_file', filename=result_file)) | |
| else: | |
| flash(f'Extraction Failed: {result_file}', 'error') | |
| return redirect(request.url) | |
| elif action == 'add': | |
| if 'srt_file' not in request.files or request.files['srt_file'].filename == '': | |
| flash('You must select an SRT subtitle file to add.', 'error') | |
| return redirect(request.url) | |
| srt_file = request.files['srt_file'] | |
| if srt_file and allowed_file(srt_file.filename): | |
| srt_filename = secure_filename(srt_file.filename) | |
| srt_path = os.path.join(app.config['UPLOAD_FOLDER'], srt_filename) | |
| srt_file.save(srt_path) | |
| flash('Adding subtitles... this can take a very long time.', 'success') | |
| result_file = add_subtitles_web(video_path, srt_path) | |
| if result_file and not "Error" in result_file: | |
| return redirect(url_for('download_file', filename=result_file)) | |
| else: | |
| flash(f'Adding Subtitles Failed: {result_file}', 'error') | |
| return redirect(request.url) | |
| else: | |
| flash('Invalid subtitle file type.', 'error') | |
| return redirect(request.url) | |
| return render_template_string(HTML_TEMPLATE) | |
| def download_file(filename): | |
| return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True) | |
| # --- Main execution block --- | |
| if __name__ == '__main__': | |
| # Create the upload folder if it doesn't exist | |
| if not os.path.exists(UPLOAD_FOLDER): | |
| os.makedirs(UPLOAD_FOLDER) | |
| # Check for FFmpeg - this is not needed for Hugging Face deployment | |
| # as we handle it in packages.txt, but good for local testing. | |
| try: | |
| subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True) | |
| print("FFmpeg found. Starting server...") | |
| # When running on Hugging Face, they control the host and port. | |
| # For local running, we use the old command. | |
| # The 'app.run' command is often not needed in production servers. | |
| # However, for simple Hugging Face deployment, this works. | |
| app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860))) | |
| except (subprocess.CalledProcessError, FileNotFoundError): | |
| print("\n--- FATAL ERROR ---") | |
| print("FFmpeg could not be found. Please install it and ensure it's in your system's PATH.") | |
| print("You can download it from ffmpeg.org.") | |
| input("Press Enter to exit.") | |