Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| import gradio as gr | |
| import subprocess | |
| import os | |
| import tempfile | |
| import shutil | |
| import logging | |
| import time | |
| from pathlib import Path | |
| # Set up logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| def process_video_trim(video_file, start_time, end_time): | |
| """Process video trimming using ffmpeg directly""" | |
| logger.info(f"🎬 Starting trim process: file={video_file}, start={start_time}, end={end_time}") | |
| if not video_file or start_time is None or end_time is None: | |
| error_msg = "Please provide video file and both start/end times" | |
| logger.error(f"❌ {error_msg}") | |
| return None, None, None, error_msg | |
| try: | |
| start_seconds = float(start_time) | |
| end_seconds = float(end_time) | |
| logger.info(f"📊 Parsed times: start={start_seconds}s, end={end_seconds}s") | |
| if start_seconds >= end_seconds: | |
| error_msg = "Start time must be less than end time" | |
| logger.error(f"❌ {error_msg}") | |
| return None, None, None, error_msg | |
| if not os.path.exists(video_file): | |
| error_msg = f"Input video file not found: {video_file}" | |
| logger.error(f"❌ {error_msg}") | |
| return None, None, None, error_msg | |
| # Create temporary directory for output | |
| temp_dir = tempfile.mkdtemp() | |
| logger.info(f"📁 Created temp directory: {temp_dir}") | |
| # Get the base filename without extension | |
| base_name = Path(video_file).stem | |
| output_video = os.path.join(temp_dir, f"{base_name}_trimmed.mp4") | |
| output_audio = os.path.join(temp_dir, f"{base_name}_trimmed.aac") | |
| logger.info(f"📤 Output files will be: video={output_video}, audio={output_audio}") | |
| # Convert seconds to HH:MM:SS format | |
| def seconds_to_time(seconds): | |
| hours = int(seconds // 3600) | |
| minutes = int((seconds % 3600) // 60) | |
| secs = seconds % 60 | |
| return f"{hours:02d}:{minutes:02d}:{secs:06.3f}" | |
| start_time_str = seconds_to_time(start_seconds) | |
| end_time_str = seconds_to_time(end_seconds) | |
| duration = end_seconds - start_seconds | |
| logger.info(f"🕒 Converted times: start={start_time_str}, duration={duration}s") | |
| # Trim video using ffmpeg | |
| video_cmd = [ | |
| "ffmpeg", "-y", "-i", video_file, | |
| "-ss", start_time_str, | |
| "-t", str(duration), | |
| "-c", "copy", | |
| "-avoid_negative_ts", "make_zero", | |
| output_video | |
| ] | |
| logger.info(f"🚀 Running video command: {' '.join(video_cmd)}") | |
| video_result = subprocess.run(video_cmd, capture_output=True, text=True) | |
| if video_result.returncode != 0: | |
| logger.warning("Stream copy failed, trying with re-encoding...") | |
| # Fallback to re-encoding | |
| video_cmd = [ | |
| "ffmpeg", "-y", "-i", video_file, | |
| "-ss", start_time_str, | |
| "-t", str(duration), | |
| "-c:v", "libx264", "-preset", "fast", "-crf", "23", | |
| "-c:a", "aac", "-b:a", "128k", | |
| output_video | |
| ] | |
| video_result = subprocess.run(video_cmd, capture_output=True, text=True) | |
| # Extract audio | |
| audio_cmd = [ | |
| "ffmpeg", "-y", "-i", video_file, | |
| "-ss", start_time_str, | |
| "-t", str(duration), | |
| "-vn", "-acodec", "aac", "-b:a", "128k", | |
| output_audio | |
| ] | |
| logger.info(f"🎵 Running audio command: {' '.join(audio_cmd)}") | |
| audio_result = subprocess.run(audio_cmd, capture_output=True, text=True) | |
| if video_result.returncode == 0 and audio_result.returncode == 0: | |
| if os.path.exists(output_video) and os.path.exists(output_audio): | |
| # Create MP3 version for better browser compatibility | |
| audio_mp3 = os.path.join(temp_dir, f"{base_name}_trimmed.mp3") | |
| mp3_cmd = [ | |
| "ffmpeg", "-y", "-i", output_audio, | |
| "-codec:a", "libmp3lame", "-b:a", "128k", | |
| audio_mp3 | |
| ] | |
| mp3_result = subprocess.run(mp3_cmd, capture_output=True, text=True) | |
| audio_player_file = audio_mp3 if mp3_result.returncode == 0 else output_audio | |
| success_msg = f"✅ Successfully trimmed video from {start_seconds:.1f}s to {end_seconds:.1f}s" | |
| logger.info(success_msg) | |
| return output_video, audio_player_file, output_audio, success_msg | |
| else: | |
| error_msg = "❌ Output files not created" | |
| return None, None, None, error_msg | |
| else: | |
| error_msg = f"❌ FFmpeg failed. Video: {video_result.stderr}, Audio: {audio_result.stderr}" | |
| logger.error(error_msg) | |
| return None, None, None, error_msg | |
| except Exception as e: | |
| error_msg = f"❌ Unexpected error: {str(e)}" | |
| logger.exception(error_msg) | |
| return None, None, None, error_msg | |
| def get_video_duration(video_file): | |
| """Get video duration in seconds""" | |
| if not video_file: | |
| return 0 | |
| try: | |
| logger.info(f"📺 Getting duration for: {video_file}") | |
| cmd = [ | |
| "ffprobe", "-v", "quiet", "-print_format", "json", | |
| "-show_format", "-show_streams", video_file | |
| ] | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode == 0: | |
| import json | |
| data = json.loads(result.stdout) | |
| duration = float(data['format']['duration']) | |
| logger.info(f"⏱️ Video duration: {duration} seconds") | |
| return duration | |
| else: | |
| logger.warning(f"⚠️ Could not get duration: {result.stderr}") | |
| return 0 | |
| except Exception as e: | |
| logger.exception(f"❌ Error getting video duration: {e}") | |
| return 0 | |
| def format_time(seconds): | |
| """Format seconds to mm:ss""" | |
| if seconds is None: | |
| return "0:00" | |
| minutes = int(seconds // 60) | |
| secs = int(seconds % 60) | |
| return f"{minutes}:{secs:02d}" | |
| def get_video_info(video_file): | |
| """Get video duration and basic info""" | |
| if not video_file: | |
| return "No video uploaded", 0, 0, 0 | |
| logger.info(f"📹 Processing video upload: {video_file}") | |
| duration = get_video_duration(video_file) | |
| if duration > 0: | |
| minutes = int(duration // 60) | |
| seconds = int(duration % 60) | |
| info = f"📹 Video loaded! Duration: {minutes}:{seconds:02d} ({duration:.1f}s)" | |
| logger.info(f"✅ {info}") | |
| return info, duration, 0, duration | |
| else: | |
| info = "📹 Video loaded! (Could not determine duration)" | |
| logger.warning(f"⚠️ {info}") | |
| return info, 100, 0, 100 | |
| # Client-side Google Drive integration | |
| google_drive_js = """ | |
| <script src="https://apis.google.com/js/api.js" async defer></script> | |
| <script> | |
| // Google Drive Picker API - make functions global | |
| window.pickerApiLoaded = false; | |
| window.oauthToken = null; | |
| // Ensure functions are globally available | |
| window.authenticateGoogleDrive = function() { | |
| console.log('🔐 Authenticate button clicked!'); | |
| console.log('🔍 Checking GAPI status...', { | |
| gapiExists: typeof gapi !== 'undefined', | |
| auth2Loaded: typeof gapi !== 'undefined' && gapi.auth2, | |
| hasAuthInstance: typeof gapi !== 'undefined' && gapi.auth2 && gapi.auth2.getAuthInstance | |
| }); | |
| try { | |
| if (typeof gapi === 'undefined') { | |
| throw new Error('Google API (gapi) not loaded yet'); | |
| } | |
| if (!gapi.auth2) { | |
| throw new Error('Google Auth2 API not loaded yet'); | |
| } | |
| const authInstance = gapi.auth2.getAuthInstance(); | |
| console.log('🔍 Auth instance:', authInstance); | |
| if (!authInstance) { | |
| throw new Error('Auth instance not available - APIs may still be loading'); | |
| } | |
| authInstance.signIn({ | |
| scope: 'https://www.googleapis.com/auth/drive.file' | |
| }).then(() => { | |
| window.oauthToken = authInstance.currentUser.get().getAuthResponse().access_token; | |
| console.log('✅ Google Drive authenticated!'); | |
| document.getElementById('drive-status').innerText = '✅ Authenticated with Google Drive - ready to browse!'; | |
| }).catch(error => { | |
| console.error('❌ Authentication failed:', error); | |
| document.getElementById('drive-status').innerText = '❌ Authentication failed: ' + error.message; | |
| }); | |
| } catch (error) { | |
| console.error('❌ Auth error:', error); | |
| document.getElementById('drive-status').innerText = '❌ Error: ' + error.message + ' - Please wait for APIs to load'; | |
| } | |
| }; | |
| window.openDrivePicker = function() { | |
| console.log('📁 Drive picker button clicked!'); | |
| console.log('🔍 Picker status:', { | |
| pickerApiLoaded: window.pickerApiLoaded, | |
| hasToken: !!window.oauthToken, | |
| gapiExists: typeof gapi !== 'undefined', | |
| googlePickerExists: typeof google !== 'undefined' && google.picker | |
| }); | |
| if (!window.pickerApiLoaded) { | |
| document.getElementById('drive-status').innerText = '❌ Picker API not loaded yet - please wait and try again'; | |
| return; | |
| } | |
| if (!window.oauthToken) { | |
| document.getElementById('drive-status').innerText = '❌ Please authenticate first'; | |
| return; | |
| } | |
| try { | |
| const picker = new google.picker.PickerBuilder() | |
| .addView(google.picker.ViewId.DOCS_VIDEOS) | |
| .setOAuthToken(window.oauthToken) | |
| .setDeveloperKey('AIzaSyAOYIFpJLIFUSmNGuj1-LdtIG6X2UWJY-I') | |
| .setCallback(window.pickerCallback) | |
| .build(); | |
| picker.setVisible(true); | |
| console.log('✅ Picker opened'); | |
| } catch (error) { | |
| console.error('❌ Picker error:', error); | |
| document.getElementById('drive-status').innerText = '❌ Picker error: ' + error.message; | |
| } | |
| }; | |
| // Initialize the APIs when gapi loads | |
| function initializeGoogleDrive() { | |
| console.log('🚀 Initializing Google Drive...'); | |
| if (typeof gapi === 'undefined') { | |
| console.log('⏳ GAPI not loaded yet, retrying in 1 second...'); | |
| setTimeout(initializeGoogleDrive, 1000); | |
| return; | |
| } | |
| gapi.load('auth2:picker', onAuthApiLoad); | |
| } | |
| function onAuthApiLoad() { | |
| console.log('✅ Auth API loaded'); | |
| // Load picker API | |
| gapi.load('picker', onPickerApiLoad); | |
| // Initialize OAuth2 | |
| console.log('🔄 Initializing OAuth2...'); | |
| gapi.auth2.init({ | |
| client_id: '178332609596-f0fnduvmrk0bfiojmrltb4or7msqtcoc.apps.googleusercontent.com' | |
| }).then(() => { | |
| console.log('✅ OAuth2 initialized successfully'); | |
| document.getElementById('drive-status').innerText = '✅ Google APIs loaded - click Authenticate to continue'; | |
| }).catch(error => { | |
| console.error('❌ OAuth2 init failed:', error); | |
| document.getElementById('drive-status').innerText = '❌ OAuth2 initialization failed: ' + error.message; | |
| }); | |
| } | |
| function onPickerApiLoad() { | |
| console.log('✅ Picker API loaded'); | |
| window.pickerApiLoaded = true; | |
| } | |
| // Authenticate user | |
| function authenticateGoogleDrive() { | |
| console.log('🔐 Starting authentication...'); | |
| try { | |
| const authInstance = gapi.auth2.getAuthInstance(); | |
| if (!authInstance) { | |
| throw new Error('Auth instance not available'); | |
| } | |
| authInstance.signIn({ | |
| scope: 'https://www.googleapis.com/auth/drive.file' | |
| }).then(() => { | |
| oauthToken = authInstance.currentUser.get().getAuthResponse().access_token; | |
| console.log('✅ Google Drive authenticated!'); | |
| document.getElementById('drive-status').innerText = '✅ Authenticated with Google Drive - ready to browse!'; | |
| }).catch(error => { | |
| console.error('❌ Authentication failed:', error); | |
| document.getElementById('drive-status').innerText = '❌ Authentication failed: ' + error.message; | |
| }); | |
| } catch (error) { | |
| console.error('❌ Auth error:', error); | |
| document.getElementById('drive-status').innerText = '❌ Error: ' + error.message; | |
| } | |
| } | |
| // Open Google Drive Picker | |
| function openDrivePicker() { | |
| console.log('📁 Opening Drive picker...', { pickerApiLoaded, oauthToken: !!oauthToken }); | |
| if (!pickerApiLoaded) { | |
| document.getElementById('drive-status').innerText = '❌ Picker API not loaded yet'; | |
| return; | |
| } | |
| if (!oauthToken) { | |
| document.getElementById('drive-status').innerText = '❌ Please authenticate first'; | |
| return; | |
| } | |
| try { | |
| const picker = new google.picker.PickerBuilder() | |
| .addView(google.picker.ViewId.DOCS_VIDEOS) | |
| .setOAuthToken(oauthToken) | |
| .setDeveloperKey('AIzaSyAOYIFpJLIFUSmNGuj1-LdtIG6X2UWJY-I') | |
| .setCallback(pickerCallback) | |
| .build(); | |
| picker.setVisible(true); | |
| console.log('✅ Picker opened'); | |
| } catch (error) { | |
| console.error('❌ Picker error:', error); | |
| document.getElementById('drive-status').innerText = '❌ Picker error: ' + error.message; | |
| } | |
| } | |
| // Handle file selection | |
| window.pickerCallback = function(data) { | |
| if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) { | |
| const file = data[google.picker.Response.DOCUMENTS][0]; | |
| const fileId = file[google.picker.Document.ID]; | |
| const fileName = file[google.picker.Document.NAME]; | |
| console.log('Selected file:', fileName, fileId); | |
| // Download the file | |
| downloadFromDrive(fileId, fileName); | |
| } | |
| }; | |
| // Download file from Google Drive | |
| function downloadFromDrive(fileId, fileName) { | |
| const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`; | |
| fetch(url, { | |
| headers: { | |
| 'Authorization': `Bearer ${oauthToken}` | |
| } | |
| }) | |
| .then(response => response.blob()) | |
| .then(blob => { | |
| // Create a file object and trigger upload to Gradio | |
| const file = new File([blob], fileName, { type: blob.type }); | |
| // This would need to integrate with Gradio's file input | |
| console.log('Downloaded file:', file); | |
| document.getElementById('drive-status').innerText = `✅ Downloaded: ${fileName}`; | |
| }) | |
| .catch(error => { | |
| console.error('Download failed:', error); | |
| document.getElementById('drive-status').innerText = `❌ Download failed: ${error.message}`; | |
| }); | |
| } | |
| // Initialize when page loads - multiple attempts to ensure it works | |
| window.addEventListener('load', function() { | |
| console.log('🚀 Page loaded, starting Google Drive initialization...'); | |
| // Attach event listeners to buttons | |
| const authBtn = document.getElementById('auth-btn'); | |
| const pickerBtn = document.getElementById('picker-btn'); | |
| if (authBtn) { | |
| authBtn.addEventListener('click', window.authenticateGoogleDrive); | |
| console.log('✅ Auth button listener attached'); | |
| } | |
| if (pickerBtn) { | |
| pickerBtn.addEventListener('click', window.openDrivePicker); | |
| console.log('✅ Picker button listener attached'); | |
| } | |
| initializeGoogleDrive(); | |
| }); | |
| // Also try after a delay in case GAPI loads slowly | |
| setTimeout(function() { | |
| console.log('🔄 Delayed initialization attempt...'); | |
| initializeGoogleDrive(); | |
| // Try attaching listeners again in case they weren't ready | |
| const authBtn = document.getElementById('auth-btn'); | |
| const pickerBtn = document.getElementById('picker-btn'); | |
| if (authBtn && !authBtn.onclick) { | |
| authBtn.addEventListener('click', window.authenticateGoogleDrive); | |
| console.log('✅ Delayed: Auth button listener attached'); | |
| } | |
| if (pickerBtn && !pickerBtn.onclick) { | |
| pickerBtn.addEventListener('click', window.openDrivePicker); | |
| console.log('✅ Delayed: Picker button listener attached'); | |
| } | |
| }, 2000); | |
| </script> | |
| <div id="google-drive-section" style="border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 8px;"> | |
| <h3>🔗 Google Drive Integration (Client-Side)</h3> | |
| <p><strong>Each user authenticates with their own Google account</strong></p> | |
| <button id="auth-btn" style="background: #4285f4; color: white; padding: 10px 20px; border: none; border-radius: 5px; margin: 5px;"> | |
| 🔐 Authenticate with Google Drive | |
| </button> | |
| <button id="picker-btn" style="background: #34a853; color: white; padding: 10px 20px; border: none; border-radius: 5px; margin: 5px;"> | |
| 📁 Browse Google Drive | |
| </button> | |
| <div id="drive-status" style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 5px;"> | |
| 🔄 Loading Google APIs... Check console (F12) for debug logs | |
| </div> | |
| </div> | |
| """ | |
| # Create the Gradio interface | |
| custom_css = """ | |
| .video-container video { | |
| width: 100%; | |
| max-height: 400px; | |
| } | |
| .slider-container { | |
| margin: 10px 0; | |
| } | |
| """ | |
| with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_css) as demo: | |
| gr.Markdown(""" | |
| # 🎬 Video Trimmer Tool (Client-Side Google Drive) v2.1 DEBUG | |
| Upload a video file, set trim points using the sliders, and get both trimmed video and extracted audio files. | |
| **NEW: Individual Google Drive Authentication** - Each user connects their own Google account! | |
| **No more shared credentials!** 🎉 | |
| **DEBUG MODE: Check browser console for detailed logs** 🔍 | |
| """) | |
| # Add Google Drive integration | |
| gr.HTML(google_drive_js) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # Video upload and display | |
| video_input = gr.File( | |
| label="📁 Upload Video File (or use Google Drive above)", | |
| file_types=[".mp4", ".mov", ".avi", ".mkv"], | |
| type="filepath" | |
| ) | |
| video_player = gr.Video( | |
| label="🎥 Video Player", | |
| show_label=True, | |
| elem_id="main_video_player", | |
| elem_classes=["video-container"] | |
| ) | |
| video_info = gr.Textbox( | |
| label="📊 Video Info", | |
| interactive=False, | |
| value="Upload a video to see information" | |
| ) | |
| with gr.Column(scale=1): | |
| # Trim controls | |
| gr.Markdown("### ✂️ Trim Settings") | |
| gr.Markdown("**🎯 Drag sliders to set trim points:**") | |
| with gr.Group(): | |
| gr.Markdown("**🎯 Start point:**") | |
| start_slider = gr.Slider( | |
| minimum=0, | |
| maximum=100, | |
| value=0, | |
| step=0.1, | |
| label="⏯️ Start Time", | |
| info="Drag to set start position", | |
| elem_classes=["slider-container"] | |
| ) | |
| start_time_display = gr.Textbox( | |
| label="⏯️ Start Time", | |
| value="0:00", | |
| interactive=False, | |
| info="Current start time" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("**🎯 End point:**") | |
| end_slider = gr.Slider( | |
| minimum=0, | |
| maximum=100, | |
| value=100, | |
| step=0.1, | |
| label="⏹️ End Time", | |
| info="Drag to set end position", | |
| elem_classes=["slider-container"] | |
| ) | |
| end_time_display = gr.Textbox( | |
| label="⏹️ End Time", | |
| value="1:40", | |
| interactive=False, | |
| info="Current end time" | |
| ) | |
| trim_btn = gr.Button( | |
| "✂️ Trim Video", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| status_msg = gr.Textbox( | |
| label="📝 Status", | |
| interactive=False, | |
| value="Ready to trim..." | |
| ) | |
| # Output section | |
| gr.Markdown("### 📤 Output Files") | |
| with gr.Row(): | |
| with gr.Column(): | |
| output_video = gr.Video( | |
| label="🎬 Trimmed Video", | |
| show_label=True | |
| ) | |
| with gr.Column(): | |
| output_audio_player = gr.Audio( | |
| label="🎵 Play Extracted Audio", | |
| show_label=True, | |
| type="filepath" | |
| ) | |
| output_audio_download = gr.File( | |
| label="💾 Download Audio (AAC)", | |
| show_label=True | |
| ) | |
| # Event handlers | |
| def update_video_and_sliders(video_file): | |
| info, duration, start_val, end_val = get_video_info(video_file) | |
| return ( | |
| video_file, # video_player | |
| info, # video_info | |
| gr.Slider(minimum=0, maximum=duration, value=0, step=0.1), # start_slider | |
| gr.Slider(minimum=0, maximum=duration, value=duration, step=0.1), # end_slider | |
| "0:00", # start_time_display | |
| format_time(duration) # end_time_display | |
| ) | |
| def update_start_display(start_val): | |
| return format_time(start_val) | |
| def update_end_display(end_val): | |
| return format_time(end_val) | |
| video_input.change( | |
| fn=update_video_and_sliders, | |
| inputs=[video_input], | |
| outputs=[video_player, video_info, start_slider, end_slider, start_time_display, end_time_display] | |
| ) | |
| start_slider.change( | |
| fn=update_start_display, | |
| inputs=[start_slider], | |
| outputs=[start_time_display] | |
| ) | |
| end_slider.change( | |
| fn=update_end_display, | |
| inputs=[end_slider], | |
| outputs=[end_time_display] | |
| ) | |
| # Trim button handler | |
| trim_btn.click( | |
| fn=process_video_trim, | |
| inputs=[video_input, start_slider, end_slider], | |
| outputs=[output_video, output_audio_player, output_audio_download, status_msg] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |