#!/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 = """

🔗 Google Drive Integration (Client-Side)

Each user authenticates with their own Google account

🔄 Loading Google APIs... Check console (F12) for debug logs
""" # 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()