video-toolkit / app.py
Nipun's picture
Force HF Spaces rebuild - add DEBUG MODE v2.1
968923a
#!/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()