Nipun commited on
Commit
75bb141
·
1 Parent(s): aec7879

Switch to client-side Google Drive Picker API - individual user auth

Browse files
Files changed (2) hide show
  1. app.py +185 -497
  2. app_client_drive.py +463 -0
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import gradio as gr
2
  import subprocess
3
  import os
@@ -6,20 +8,13 @@ import shutil
6
  import logging
7
  import time
8
  from pathlib import Path
9
- try:
10
- from native_drive_picker import GoogleDrivePickerManager, get_native_picker_instructions, GOOGLE_DRIVE_AVAILABLE
11
- except ImportError:
12
- GOOGLE_DRIVE_AVAILABLE = False
13
- GoogleDrivePickerManager = None
14
- def get_native_picker_instructions():
15
- return "Google Drive integration not available in this environment."
16
 
17
  # Set up logging
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
  logger = logging.getLogger(__name__)
20
 
21
  def process_video_trim(video_file, start_time, end_time):
22
- """Process video trimming using the trim-convert.sh script"""
23
  logger.info(f"🎬 Starting trim process: file={video_file}, start={start_time}, end={end_time}")
24
 
25
  if not video_file or start_time is None or end_time is None:
@@ -28,7 +23,6 @@ def process_video_trim(video_file, start_time, end_time):
28
  return None, None, None, error_msg
29
 
30
  try:
31
- # start_time and end_time are now numbers (seconds) from sliders
32
  start_seconds = float(start_time)
33
  end_seconds = float(end_time)
34
 
@@ -39,7 +33,6 @@ def process_video_trim(video_file, start_time, end_time):
39
  logger.error(f"❌ {error_msg}")
40
  return None, None, None, error_msg
41
 
42
- # Check if input file exists
43
  if not os.path.exists(video_file):
44
  error_msg = f"Input video file not found: {video_file}"
45
  logger.error(f"❌ {error_msg}")
@@ -49,26 +42,14 @@ def process_video_trim(video_file, start_time, end_time):
49
  temp_dir = tempfile.mkdtemp()
50
  logger.info(f"📁 Created temp directory: {temp_dir}")
51
 
52
- input_path = video_file
53
-
54
  # Get the base filename without extension
55
- base_name = Path(input_path).stem
56
- output_prefix = os.path.join(temp_dir, f"{base_name}_trimmed")
57
-
58
- # The script will create these files based on the prefix
59
- output_video = f"{output_prefix}.mp4"
60
- output_audio = f"{output_prefix}.aac"
61
 
62
  logger.info(f"📤 Output files will be: video={output_video}, audio={output_audio}")
63
 
64
- # Check if trim-convert.sh script exists
65
- script_path = "./trim-convert.sh"
66
- if not os.path.exists(script_path):
67
- error_msg = f"trim-convert.sh script not found at: {script_path}"
68
- logger.error(f"❌ {error_msg}")
69
- return None, None, None, error_msg
70
-
71
- # Convert seconds to HH:MM:SS format for the script
72
  def seconds_to_time(seconds):
73
  hours = int(seconds // 3600)
74
  minutes = int((seconds % 3600) // 60)
@@ -77,134 +58,69 @@ def process_video_trim(video_file, start_time, end_time):
77
 
78
  start_time_str = seconds_to_time(start_seconds)
79
  end_time_str = seconds_to_time(end_seconds)
80
-
81
- logger.info(f"🕒 Converted times: start={start_time_str}, end={end_time_str}")
82
-
83
- # Call the trim-convert.sh script with proper format
84
- cmd = [
85
- "bash", script_path,
86
- "-s", start_time_str,
87
- "-e", end_time_str,
88
- "-o", output_prefix,
89
- input_path
 
 
90
  ]
91
 
92
- logger.info(f"🚀 Running command: {' '.join(cmd)}")
93
-
94
- result = subprocess.run(cmd, capture_output=True, text=True, cwd='.')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- logger.info(f"📋 Command finished with return code: {result.returncode}")
97
- logger.info(f"📤 STDOUT: {result.stdout}")
98
- if result.stderr:
99
- logger.warning(f"⚠️ STDERR: {result.stderr}")
100
 
101
- if result.returncode == 0:
102
- # Check if files were created
103
- video_exists = os.path.exists(output_video)
104
- audio_exists = os.path.exists(output_audio)
105
-
106
- logger.info(f"📁 File check: video_exists={video_exists}, audio_exists={audio_exists}")
107
-
108
- if video_exists and audio_exists:
109
- video_size = os.path.getsize(output_video)
110
- audio_size = os.path.getsize(output_audio)
111
- logger.info(f"📊 File sizes: video={video_size} bytes, audio={audio_size} bytes")
112
-
113
- # Check if video file is valid and convert for better web compatibility
114
- try:
115
- test_duration = get_video_duration(output_video)
116
- logger.info(f"✅ Output video duration: {test_duration} seconds")
117
- if test_duration == 0:
118
- logger.warning("⚠️ Output video duration is 0, may have encoding issues")
119
-
120
- # Check if trimmed video is web-compatible, if not, convert only the headers
121
- display_video = output_video # Start with original
122
-
123
- # Quick check if video might have compatibility issues
124
- try:
125
- # Test if ffprobe can read the file properly
126
- probe_cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", output_video]
127
- probe_result = subprocess.run(probe_cmd, capture_output=True, text=True)
128
-
129
- if probe_result.returncode == 0:
130
- import json
131
- probe_data = json.loads(probe_result.stdout)
132
- format_info = probe_data.get('format', {})
133
-
134
- # Check if it needs web optimization
135
- needs_conversion = False
136
-
137
- # If the file has issues or isn't web-optimized, do a quick fix
138
- if needs_conversion or True: # Always do quick web optimization for now
139
- web_video_path = os.path.join(temp_dir, f"{base_name}_web.mp4")
140
-
141
- # Quick web compatibility fix - just fix headers and ensure proper format
142
- web_convert_cmd = [
143
- "ffmpeg", "-y", "-i", output_video,
144
- "-c", "copy", # Copy streams (fast)
145
- "-movflags", "+faststart", # Optimize for web
146
- "-f", "mp4", # Ensure MP4 format
147
- web_video_path
148
- ]
149
-
150
- logger.info(f"🌐 Quick web optimization (stream copy)...")
151
- web_result = subprocess.run(web_convert_cmd, capture_output=True, text=True)
152
-
153
- if web_result.returncode == 0 and os.path.exists(web_video_path):
154
- web_size = os.path.getsize(web_video_path)
155
- logger.info(f"✅ Web-optimized video: {web_video_path} ({web_size} bytes)")
156
- display_video = web_video_path
157
-
158
- # Verify the optimized video
159
- web_duration = get_video_duration(web_video_path)
160
- logger.info(f"🎬 Optimized video duration: {web_duration} seconds")
161
- else:
162
- logger.warning(f"⚠️ Quick optimization failed: {web_result.stderr}")
163
- logger.info("Using original trimmed video")
164
- else:
165
- logger.warning("⚠️ Could not analyze trimmed video, using as-is")
166
-
167
- except Exception as e:
168
- logger.warning(f"⚠️ Video analysis failed: {e}, using original")
169
-
170
- except Exception as e:
171
- logger.warning(f"⚠️ Could not verify output video: {e}")
172
- display_video = output_video
173
-
174
- # Create MP3 version for audio player (better browser compatibility)
175
- timestamp = str(int(time.time() * 1000))
176
- temp_audio_dir = os.path.dirname(output_audio)
177
- audio_player_file = os.path.join(temp_audio_dir, f"player_audio_{timestamp}.mp3")
178
-
179
- # Convert AAC to MP3 for better browser support
180
- convert_cmd = [
181
- "ffmpeg", "-y", "-i", output_audio,
182
  "-codec:a", "libmp3lame", "-b:a", "128k",
183
- audio_player_file
184
  ]
185
 
186
- logger.info(f"🔄 Converting audio for player: {' '.join(convert_cmd)}")
187
- convert_result = subprocess.run(convert_cmd, capture_output=True, text=True)
188
-
189
- if convert_result.returncode == 0 and os.path.exists(audio_player_file):
190
- logger.info(f"🎵 Created MP3 audio player file: {audio_player_file}")
191
- logger.info(f"📊 Audio player file size: {os.path.getsize(audio_player_file)} bytes")
192
- else:
193
- logger.warning(f"⚠️ MP3 conversion failed, using original AAC file")
194
- audio_player_file = output_audio
195
 
196
  success_msg = f"✅ Successfully trimmed video from {start_seconds:.1f}s to {end_seconds:.1f}s"
197
-
198
- # No automatic upload - will be done manually after trimming
199
-
200
  logger.info(success_msg)
201
- return display_video, audio_player_file, output_audio, success_msg, output_video, output_audio
202
  else:
203
- error_msg = f"❌ Output files not created.\n\nScript STDOUT:\n{result.stdout}\n\nScript STDERR:\n{result.stderr}\n\nExpected files:\nVideo: {output_video}\nAudio: {output_audio}"
204
- logger.error(error_msg)
205
- return None, None, None, error_msg, None, None
206
  else:
207
- error_msg = f"❌ trim-convert.sh failed with return code {result.returncode}\n\nCommand run:\n{' '.join(cmd)}\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
208
  logger.error(error_msg)
209
  return None, None, None, error_msg
210
 
@@ -221,7 +137,6 @@ def get_video_duration(video_file):
221
  try:
222
  logger.info(f"📺 Getting duration for: {video_file}")
223
 
224
- # Use ffprobe to get video duration
225
  cmd = [
226
  "ffprobe", "-v", "quiet", "-print_format", "json",
227
  "-show_format", "-show_streams", video_file
@@ -268,134 +183,119 @@ def get_video_info(video_file):
268
  logger.warning(f"⚠️ {info}")
269
  return info, 100, 0, 100
270
 
271
- # Native Google Drive picker functions
272
- def open_file_picker(drive_manager):
273
- """Open Google Drive for file selection (full access)"""
274
- if not drive_manager or not drive_manager.is_available():
275
- return "❌ Google Drive not available"
276
-
277
- instructions = drive_manager.open_drive_picker("file")
278
- return instructions
279
 
280
- def open_folder_picker(drive_manager):
281
- """Open Google Drive for folder selection"""
282
- if not drive_manager or not drive_manager.is_available():
283
- return "❌ Google Drive not available"
284
-
285
- instructions = drive_manager.open_drive_picker("folder")
286
- return instructions
287
 
288
- def download_from_drive_url(drive_manager, drive_url, custom_filename=""):
289
- """Download video from Google Drive URL"""
290
- if not drive_manager or not drive_manager.is_available():
291
- return None, "❌ Google Drive not available"
292
-
293
- if not drive_url or not drive_url.strip():
294
- return None, "⚠️ Please paste a Google Drive link"
295
 
296
- filename = custom_filename.strip() if custom_filename.strip() else None
297
- return drive_manager.download_file_from_url(drive_url, filename)
 
 
 
298
 
299
- def download_from_google_drive(file_id, file_display, drive_manager):
300
- """Download selected file from Google Drive"""
301
- if not file_id or not drive_manager or not drive_manager.is_available():
302
- return None, "❌ No file selected or Google Drive unavailable"
303
-
304
- try:
305
- # Extract filename from display string
306
- filename = file_display.split(' (')[0] if file_display else f"video_{file_id}.mp4"
307
-
308
- logger.info(f"📥 Downloading {filename} from Google Drive...")
309
- local_path = drive_manager.download_file(file_id, filename)
310
-
311
- if local_path and os.path.exists(local_path):
312
- return local_path, f"✅ Downloaded: {filename}"
313
- else:
314
- return None, "❌ Download failed"
315
- except Exception as e:
316
- logger.error(f"Error downloading from Google Drive: {e}")
317
- return None, f"❌ Download error: {str(e)}"
318
 
319
- # Initialize Google Drive manager
320
- try:
321
- if GOOGLE_DRIVE_AVAILABLE and GoogleDrivePickerManager:
322
- # Check if running on HF Space and use secrets
323
- oauth_json = os.getenv('OAUTH_CREDENTIALS_JSON')
324
- oauth_token = os.getenv('OAUTH_TOKEN_PICKLE')
325
- logger.info(f"🔍 Checking for OAuth secrets... Credentials: {oauth_json is not None}, Token: {oauth_token is not None}")
326
-
327
- if oauth_json:
328
- logger.info(f"📝 OAuth credentials length: {len(oauth_json)} characters")
329
- # Write the credentials to a temporary file
330
- with open('oauth_credentials.json', 'w') as f:
331
- f.write(oauth_json)
332
- logger.info("✅ OAuth credentials loaded from HF secret and written to file")
333
-
334
- # Verify file was created
335
- if os.path.exists('oauth_credentials.json'):
336
- file_size = os.path.getsize('oauth_credentials.json')
337
- logger.info(f"✅ oauth_credentials.json created successfully ({file_size} bytes)")
338
- else:
339
- logger.error("❌ Failed to create oauth_credentials.json file")
340
- else:
341
- logger.info("ℹ️ No OAuth credentials secret found - checking for local file")
342
- if os.path.exists('oauth_credentials.json'):
343
- logger.info("✅ Using local oauth_credentials.json file")
344
- else:
345
- logger.warning("⚠️ No OAuth credentials available (neither secret nor local file)")
346
-
347
- if oauth_token:
348
- import base64
349
- logger.info(f"📝 OAuth token length: {len(oauth_token)} characters")
350
- # Smart detection: try base64 decode, fallback to raw
351
- try:
352
- try:
353
- # Try base64 decode first (for HF Spaces secrets)
354
- token_data = base64.b64decode(oauth_token, validate=True)
355
- logger.info("✅ Detected and decoded base64-encoded token")
356
- except Exception:
357
- # If base64 fails, treat as raw binary (shouldn't happen in env vars)
358
- logger.info("ℹ️ Not base64, treating as raw token data")
359
- token_data = oauth_token.encode('latin1')
360
-
361
- with open('oauth_token.pickle', 'wb') as f:
362
- f.write(token_data)
363
- logger.info("✅ OAuth token written to file")
364
-
365
- # Verify file was created
366
- if os.path.exists('oauth_token.pickle'):
367
- file_size = os.path.getsize('oauth_token.pickle')
368
- logger.info(f"✅ oauth_token.pickle created successfully ({file_size} bytes)")
369
- else:
370
- logger.error("❌ Failed to create oauth_token.pickle file")
371
- except Exception as e:
372
- logger.error(f"❌ Failed to process OAuth token: {e}")
373
- logger.info("💡 Tip: Encode your token with: base64 -i oauth_token.pickle")
374
- else:
375
- logger.info("ℹ️ No OAuth token secret found - checking for local file")
376
- if os.path.exists('oauth_token.pickle'):
377
- logger.info("✅ Using local oauth_token.pickle file")
378
- else:
379
- logger.warning("⚠️ No OAuth token available (neither secret nor local file)")
380
 
381
- # Set environment variable to disable browser for HF Spaces only if we don't have a token
382
- if oauth_json and not oauth_token:
383
- os.environ['GOOGLE_DRIVE_HEADLESS'] = 'true'
384
- logger.info("🌐 Set headless mode for HF Spaces (no token available)")
385
- elif oauth_token:
386
- logger.info("🎉 Using pre-authenticated token - browser not needed!")
387
 
388
- drive_manager = GoogleDrivePickerManager()
389
- drive_available = drive_manager.is_available()
390
- else:
391
- drive_manager = None
392
- drive_available = False
393
- except Exception as e:
394
- logger.warning(f"Google Drive initialization failed: {e}")
395
- drive_manager = None
396
- drive_available = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- # Create the Gradio interface with custom CSS and JS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  custom_css = """
400
  .video-container video {
401
  width: 100%;
@@ -404,97 +304,24 @@ custom_css = """
404
  .slider-container {
405
  margin: 10px 0;
406
  }
407
- .drive-section {
408
- border: 1px solid #e0e0e0;
409
- padding: 15px;
410
- border-radius: 8px;
411
- margin: 10px 0;
412
- }
413
- """
414
-
415
- custom_js = """
416
- function seekVideo(slider_value, video_id) {
417
- const video = document.querySelector('#' + video_id + ' video');
418
- if (video && !isNaN(slider_value)) {
419
- video.currentTime = slider_value;
420
- }
421
- return slider_value;
422
- }
423
  """
424
 
425
  with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_css) as demo:
426
  gr.Markdown("""
427
- # 🎬 Video Trimmer Demo
428
- Upload an MP4 video, set trim points, and generate trimmed video + audio files.
 
 
429
  """)
430
 
431
- # Native Google Drive picker section
432
- if drive_available:
433
- user_email = drive_manager.get_user_info() if drive_manager else "Unknown"
434
- with gr.Group():
435
- gr.Markdown("### 🔗 Google Drive Integration (Native Picker)")
436
- gr.Markdown(f"**👤 Signed in as:** {user_email}")
437
-
438
- # Video picker section
439
- with gr.Row():
440
- with gr.Column(scale=2):
441
- gr.Markdown("#### 📁 Load Any File from Google Drive")
442
-
443
- open_picker_btn = gr.Button(
444
- "🌍 Browse Your Entire Google Drive",
445
- variant="primary",
446
- size="lg"
447
- )
448
-
449
- picker_instructions = gr.Textbox(
450
- label="📝 Instructions",
451
- value="Click the button above to open your full Google Drive - browse any folder!",
452
- interactive=False,
453
- lines=6
454
- )
455
-
456
- drive_url_input = gr.Textbox(
457
- label="🔗 Paste Any Google Drive File Link",
458
- placeholder="https://drive.google.com/file/d/FILE_ID/view...",
459
- info="Works with any file type - videos, docs, etc. from any folder"
460
- )
461
-
462
- custom_filename_input = gr.Textbox(
463
- label="🏷️ Custom Filename (Optional)",
464
- placeholder="my_video.mp4"
465
- )
466
-
467
- download_from_url_btn = gr.Button(
468
- "📥 Download Video from Link",
469
- variant="secondary"
470
- )
471
-
472
- with gr.Column(scale=1):
473
- drive_status = gr.Textbox(
474
- label="📊 Status",
475
- value="✅ Ready to pick from Google Drive",
476
- interactive=False
477
- )
478
-
479
- # Simplified note
480
- gr.Markdown("🚀 **Upload to Google Drive will be available after video trimming.**")
481
- else:
482
- with gr.Group():
483
- gr.Markdown("### 🔗 Google Drive Integration")
484
- if not GOOGLE_DRIVE_AVAILABLE:
485
- gr.Markdown("**⚠️ Google Drive libraries not installed.**")
486
- gr.Markdown("Install with: `pip install google-api-python-client google-auth google-auth-oauthlib`")
487
- else:
488
- gr.Markdown("**⚠️ Setup needed:** Create oauth_credentials.json file")
489
-
490
- with gr.Accordion("📋 Setup Instructions", open=False):
491
- gr.Markdown(get_native_picker_instructions())
492
 
493
  with gr.Row():
494
  with gr.Column(scale=2):
495
  # Video upload and display
496
  video_input = gr.File(
497
- label="📁 Upload MP4 Video",
498
  file_types=[".mp4", ".mov", ".avi", ".mkv"],
499
  type="filepath"
500
  )
@@ -518,14 +345,14 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
518
  gr.Markdown("**🎯 Drag sliders to set trim points:**")
519
 
520
  with gr.Group():
521
- gr.Markdown("**🎯 Scrub to find start point:**")
522
  start_slider = gr.Slider(
523
  minimum=0,
524
  maximum=100,
525
  value=0,
526
  step=0.1,
527
- label="⏯️ Start Time (scrub video)",
528
- info="Drag to seek video and set start position",
529
  elem_classes=["slider-container"]
530
  )
531
 
@@ -537,14 +364,14 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
537
  )
538
 
539
  with gr.Group():
540
- gr.Markdown("**🎯 Scrub to find end point:**")
541
  end_slider = gr.Slider(
542
  minimum=0,
543
  maximum=100,
544
  value=100,
545
  step=0.1,
546
- label="⏹️ End Time (scrub video)",
547
- info="Drag to seek video and set end position",
548
  elem_classes=["slider-container"]
549
  )
550
 
@@ -561,9 +388,6 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
561
  size="lg"
562
  )
563
 
564
- # Note about manual upload
565
- gr.Markdown("📝 **Note:** Upload options will appear after trimming is complete.")
566
-
567
  status_msg = gr.Textbox(
568
  label="📝 Status",
569
  interactive=False,
@@ -592,48 +416,6 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
592
  show_label=True
593
  )
594
 
595
- # Post-processing upload section (appears after trimming)
596
- if drive_available:
597
- with gr.Group(visible=False) as post_upload_section:
598
- gr.Markdown("### 🚀 Upload Trimmed Files to Google Drive")
599
-
600
- with gr.Row():
601
- with gr.Column(scale=2):
602
- post_open_folder_btn = gr.Button(
603
- "🌍 Choose Google Drive Upload Folder",
604
- variant="primary"
605
- )
606
-
607
- post_folder_instructions = gr.Textbox(
608
- label="📝 Folder Instructions",
609
- value="Click button above to choose where to upload your trimmed files",
610
- interactive=False,
611
- lines=4
612
- )
613
-
614
- post_upload_folder_url = gr.Textbox(
615
- label="📁 Upload Folder Link",
616
- placeholder="https://drive.google.com/drive/folders/FOLDER_ID...",
617
- info="Leave empty to upload to My Drive root"
618
- )
619
-
620
- post_upload_btn = gr.Button(
621
- "📤 Upload Files to Google Drive",
622
- variant="secondary",
623
- size="lg"
624
- )
625
-
626
- with gr.Column(scale=1):
627
- post_upload_status = gr.Textbox(
628
- label="📊 Upload Status",
629
- value="Ready to upload",
630
- interactive=False
631
- )
632
-
633
- # Hidden state to store file paths for post-upload
634
- trimmed_video_path = gr.State(None)
635
- trimmed_audio_path = gr.State(None)
636
-
637
  # Event handlers
638
  def update_video_and_sliders(video_file):
639
  info, duration, start_val, end_val = get_video_info(video_file)
@@ -658,118 +440,24 @@ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_cs
658
  outputs=[video_player, video_info, start_slider, end_slider, start_time_display, end_time_display]
659
  )
660
 
661
- def update_start_and_seek(start_val):
662
- return format_time(start_val)
663
-
664
- def update_end_and_seek(end_val):
665
- return format_time(end_val)
666
-
667
  start_slider.change(
668
- fn=update_start_and_seek,
669
  inputs=[start_slider],
670
  outputs=[start_time_display]
671
  )
672
 
673
  end_slider.change(
674
- fn=update_end_and_seek,
675
  inputs=[end_slider],
676
  outputs=[end_time_display]
677
  )
678
 
679
- # Google Drive native picker event handlers
680
- if drive_available:
681
- # Open file picker (full Google Drive access)
682
- open_picker_btn.click(
683
- fn=lambda: open_file_picker(drive_manager),
684
- outputs=[picker_instructions]
685
- )
686
-
687
- # Download from URL
688
- download_from_url_btn.click(
689
- fn=lambda url, filename: download_from_drive_url(drive_manager, url, filename),
690
- inputs=[drive_url_input, custom_filename_input],
691
- outputs=[video_input, drive_status]
692
- ).then(
693
- fn=update_video_and_sliders,
694
- inputs=[video_input],
695
- outputs=[video_player, video_info, start_slider, end_slider, start_time_display, end_time_display]
696
- )
697
-
698
- # No pre-upload handlers needed
699
-
700
- # Post-upload event handlers
701
- post_open_folder_btn.click(
702
- fn=lambda: open_folder_picker(drive_manager),
703
- outputs=[post_folder_instructions]
704
- )
705
-
706
- def post_upload_files(video_path, audio_path, folder_url):
707
- if not video_path or not audio_path:
708
- return "❌ No files to upload"
709
-
710
- try:
711
- folder_url_clean = folder_url.strip() if folder_url and folder_url.strip() else None
712
-
713
- video_success, video_result = drive_manager.upload_file_to_folder(video_path, folder_url_clean)
714
- audio_success, audio_result = drive_manager.upload_file_to_folder(audio_path, folder_url_clean)
715
-
716
- if video_success and audio_success:
717
- return f"✅ Files uploaded successfully:\n• {video_result}\n• {audio_result}"
718
- elif video_success:
719
- return f"✅ {video_result}\n❌ Audio upload failed: {audio_result}"
720
- elif audio_success:
721
- return f"✅ {audio_result}\n❌ Video upload failed: {video_result}"
722
- else:
723
- return f"❌ Upload failed:\n• Video: {video_result}\n• Audio: {audio_result}"
724
-
725
- except Exception as e:
726
- return f"❌ Upload error: {str(e)}"
727
-
728
- post_upload_btn.click(
729
- fn=post_upload_files,
730
- inputs=[trimmed_video_path, trimmed_audio_path, post_upload_folder_url],
731
- outputs=[post_upload_status]
732
- )
733
-
734
- # Trim button handler with Google Drive upload support
735
- if drive_available:
736
- # Simplified trim function that shows upload section after completion
737
- def trim_and_show_upload(video_file, start_time, end_time):
738
- result = process_video_trim(video_file, start_time, end_time)
739
- display_video, audio_player, audio_download, status, orig_video, orig_audio = result
740
-
741
- # Show post-upload section if trimming was successful
742
- show_upload = orig_video is not None and orig_audio is not None
743
-
744
- return (
745
- display_video, audio_player, audio_download, status, # Original outputs
746
- orig_video, orig_audio, # Store paths for post-upload
747
- gr.Group(visible=show_upload) # Show/hide upload section
748
- )
749
-
750
- trim_btn.click(
751
- fn=trim_and_show_upload,
752
- inputs=[video_input, start_slider, end_slider],
753
- outputs=[output_video, output_audio_player, output_audio_download, status_msg,
754
- trimmed_video_path, trimmed_audio_path, post_upload_section]
755
- )
756
- else:
757
- # No Google Drive available - simple trim only
758
- def simple_trim(video_file, start_time, end_time):
759
- result = process_video_trim(video_file, start_time, end_time)
760
- return result[:4] # Return only the first 4 outputs
761
-
762
- trim_btn.click(
763
- fn=simple_trim,
764
- inputs=[video_input, start_slider, end_slider],
765
- outputs=[output_video, output_audio_player, output_audio_download, status_msg]
766
- )
767
 
768
  if __name__ == "__main__":
769
- demo.launch(
770
- server_name="0.0.0.0",
771
- server_port=None, # Auto-find available port
772
- share=False,
773
- show_error=True,
774
- debug=True
775
- )
 
1
+ #!/usr/bin/env python3
2
+
3
  import gradio as gr
4
  import subprocess
5
  import os
 
8
  import logging
9
  import time
10
  from pathlib import Path
 
 
 
 
 
 
 
11
 
12
  # Set up logging
13
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
  logger = logging.getLogger(__name__)
15
 
16
  def process_video_trim(video_file, start_time, end_time):
17
+ """Process video trimming using ffmpeg directly"""
18
  logger.info(f"🎬 Starting trim process: file={video_file}, start={start_time}, end={end_time}")
19
 
20
  if not video_file or start_time is None or end_time is None:
 
23
  return None, None, None, error_msg
24
 
25
  try:
 
26
  start_seconds = float(start_time)
27
  end_seconds = float(end_time)
28
 
 
33
  logger.error(f"❌ {error_msg}")
34
  return None, None, None, error_msg
35
 
 
36
  if not os.path.exists(video_file):
37
  error_msg = f"Input video file not found: {video_file}"
38
  logger.error(f"❌ {error_msg}")
 
42
  temp_dir = tempfile.mkdtemp()
43
  logger.info(f"📁 Created temp directory: {temp_dir}")
44
 
 
 
45
  # Get the base filename without extension
46
+ base_name = Path(video_file).stem
47
+ output_video = os.path.join(temp_dir, f"{base_name}_trimmed.mp4")
48
+ output_audio = os.path.join(temp_dir, f"{base_name}_trimmed.aac")
 
 
 
49
 
50
  logger.info(f"📤 Output files will be: video={output_video}, audio={output_audio}")
51
 
52
+ # Convert seconds to HH:MM:SS format
 
 
 
 
 
 
 
53
  def seconds_to_time(seconds):
54
  hours = int(seconds // 3600)
55
  minutes = int((seconds % 3600) // 60)
 
58
 
59
  start_time_str = seconds_to_time(start_seconds)
60
  end_time_str = seconds_to_time(end_seconds)
61
+ duration = end_seconds - start_seconds
62
+
63
+ logger.info(f"🕒 Converted times: start={start_time_str}, duration={duration}s")
64
+
65
+ # Trim video using ffmpeg
66
+ video_cmd = [
67
+ "ffmpeg", "-y", "-i", video_file,
68
+ "-ss", start_time_str,
69
+ "-t", str(duration),
70
+ "-c", "copy",
71
+ "-avoid_negative_ts", "make_zero",
72
+ output_video
73
  ]
74
 
75
+ logger.info(f"🚀 Running video command: {' '.join(video_cmd)}")
76
+ video_result = subprocess.run(video_cmd, capture_output=True, text=True)
77
+
78
+ if video_result.returncode != 0:
79
+ logger.warning("Stream copy failed, trying with re-encoding...")
80
+ # Fallback to re-encoding
81
+ video_cmd = [
82
+ "ffmpeg", "-y", "-i", video_file,
83
+ "-ss", start_time_str,
84
+ "-t", str(duration),
85
+ "-c:v", "libx264", "-preset", "fast", "-crf", "23",
86
+ "-c:a", "aac", "-b:a", "128k",
87
+ output_video
88
+ ]
89
+ video_result = subprocess.run(video_cmd, capture_output=True, text=True)
90
+
91
+ # Extract audio
92
+ audio_cmd = [
93
+ "ffmpeg", "-y", "-i", video_file,
94
+ "-ss", start_time_str,
95
+ "-t", str(duration),
96
+ "-vn", "-acodec", "aac", "-b:a", "128k",
97
+ output_audio
98
+ ]
99
 
100
+ logger.info(f"🎵 Running audio command: {' '.join(audio_cmd)}")
101
+ audio_result = subprocess.run(audio_cmd, capture_output=True, text=True)
 
 
102
 
103
+ if video_result.returncode == 0 and audio_result.returncode == 0:
104
+ if os.path.exists(output_video) and os.path.exists(output_audio):
105
+ # Create MP3 version for better browser compatibility
106
+ audio_mp3 = os.path.join(temp_dir, f"{base_name}_trimmed.mp3")
107
+ mp3_cmd = [
108
+ "ffmpeg", "-y", "-i", output_audio,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  "-codec:a", "libmp3lame", "-b:a", "128k",
110
+ audio_mp3
111
  ]
112
 
113
+ mp3_result = subprocess.run(mp3_cmd, capture_output=True, text=True)
114
+ audio_player_file = audio_mp3 if mp3_result.returncode == 0 else output_audio
 
 
 
 
 
 
 
115
 
116
  success_msg = f"✅ Successfully trimmed video from {start_seconds:.1f}s to {end_seconds:.1f}s"
 
 
 
117
  logger.info(success_msg)
118
+ return output_video, audio_player_file, output_audio, success_msg
119
  else:
120
+ error_msg = "❌ Output files not created"
121
+ return None, None, None, error_msg
 
122
  else:
123
+ error_msg = f"❌ FFmpeg failed. Video: {video_result.stderr}, Audio: {audio_result.stderr}"
124
  logger.error(error_msg)
125
  return None, None, None, error_msg
126
 
 
137
  try:
138
  logger.info(f"📺 Getting duration for: {video_file}")
139
 
 
140
  cmd = [
141
  "ffprobe", "-v", "quiet", "-print_format", "json",
142
  "-show_format", "-show_streams", video_file
 
183
  logger.warning(f"⚠️ {info}")
184
  return info, 100, 0, 100
185
 
186
+ # Client-side Google Drive integration
187
+ google_drive_js = """
188
+ <script src="https://apis.google.com/js/api.js"></script>
189
+ <script src="https://accounts.google.com/gsi/client"></script>
190
+ <script>
191
+ // Google Drive Picker API
192
+ let pickerApiLoaded = false;
193
+ let oauthToken;
194
 
195
+ // Initialize the APIs
196
+ function initializeGoogleDrive() {
197
+ gapi.load('auth2:picker', onAuthApiLoad);
198
+ }
 
 
 
199
 
200
+ function onAuthApiLoad() {
201
+ window.gapi.load('picker', onPickerApiLoad);
 
 
 
 
 
202
 
203
+ // Initialize OAuth2
204
+ gapi.auth2.init({
205
+ client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com' // You'll need to add your client ID
206
+ });
207
+ }
208
 
209
+ function onPickerApiLoad() {
210
+ pickerApiLoaded = true;
211
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
+ // Authenticate user
214
+ function authenticateGoogleDrive() {
215
+ const authInstance = gapi.auth2.getAuthInstance();
216
+ authInstance.signIn().then(() => {
217
+ oauthToken = authInstance.currentUser.get().getAuthResponse().access_token;
218
+ console.log('Google Drive authenticated!');
219
+ document.getElementById('drive-status').innerText = '✅ Authenticated with Google Drive';
220
+ });
221
+ }
222
+
223
+ // Open Google Drive Picker
224
+ function openDrivePicker() {
225
+ if (pickerApiLoaded && oauthToken) {
226
+ const picker = new google.picker.PickerBuilder()
227
+ .addView(google.picker.ViewId.DOCS_VIDEOS)
228
+ .setOAuthToken(oauthToken)
229
+ .setDeveloperKey('YOUR_API_KEY') // You'll need to add your API key
230
+ .setCallback(pickerCallback)
231
+ .build();
232
+ picker.setVisible(true);
233
+ } else {
234
+ alert('Please authenticate with Google Drive first');
235
+ }
236
+ }
237
+
238
+ // Handle file selection
239
+ function pickerCallback(data) {
240
+ if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
241
+ const file = data[google.picker.Response.DOCUMENTS][0];
242
+ const fileId = file[google.picker.Document.ID];
243
+ const fileName = file[google.picker.Document.NAME];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ console.log('Selected file:', fileName, fileId);
 
 
 
 
 
246
 
247
+ // Download the file
248
+ downloadFromDrive(fileId, fileName);
249
+ }
250
+ }
251
+
252
+ // Download file from Google Drive
253
+ function downloadFromDrive(fileId, fileName) {
254
+ const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`;
255
+
256
+ fetch(url, {
257
+ headers: {
258
+ 'Authorization': `Bearer ${oauthToken}`
259
+ }
260
+ })
261
+ .then(response => response.blob())
262
+ .then(blob => {
263
+ // Create a file object and trigger upload to Gradio
264
+ const file = new File([blob], fileName, { type: blob.type });
265
+
266
+ // This would need to integrate with Gradio's file input
267
+ console.log('Downloaded file:', file);
268
+ document.getElementById('drive-status').innerText = `✅ Downloaded: ${fileName}`;
269
+ })
270
+ .catch(error => {
271
+ console.error('Download failed:', error);
272
+ document.getElementById('drive-status').innerText = `❌ Download failed: ${error.message}`;
273
+ });
274
+ }
275
 
276
+ // Initialize when page loads
277
+ window.addEventListener('load', initializeGoogleDrive);
278
+ </script>
279
+
280
+ <div id="google-drive-section" style="border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 8px;">
281
+ <h3>🔗 Google Drive Integration (Client-Side)</h3>
282
+ <p><strong>Each user authenticates with their own Google account</strong></p>
283
+
284
+ <button onclick="authenticateGoogleDrive()" style="background: #4285f4; color: white; padding: 10px 20px; border: none; border-radius: 5px; margin: 5px;">
285
+ 🔐 Authenticate with Google Drive
286
+ </button>
287
+
288
+ <button onclick="openDrivePicker()" style="background: #34a853; color: white; padding: 10px 20px; border: none; border-radius: 5px; margin: 5px;">
289
+ 📁 Browse Google Drive
290
+ </button>
291
+
292
+ <div id="drive-status" style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 5px;">
293
+ ⚠️ Click "Authenticate" to connect your Google Drive
294
+ </div>
295
+ </div>
296
+ """
297
+
298
+ # Create the Gradio interface
299
  custom_css = """
300
  .video-container video {
301
  width: 100%;
 
304
  .slider-container {
305
  margin: 10px 0;
306
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  """
308
 
309
  with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_css) as demo:
310
  gr.Markdown("""
311
+ # 🎬 Video Trimmer Tool (Client-Side Google Drive)
312
+ Upload a video file, set trim points using the sliders, and get both trimmed video and extracted audio files.
313
+
314
+ **NEW: Individual Google Drive Authentication** - Each user connects their own Google account!
315
  """)
316
 
317
+ # Add Google Drive integration
318
+ gr.HTML(google_drive_js)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
  with gr.Row():
321
  with gr.Column(scale=2):
322
  # Video upload and display
323
  video_input = gr.File(
324
+ label="📁 Upload Video File (or use Google Drive above)",
325
  file_types=[".mp4", ".mov", ".avi", ".mkv"],
326
  type="filepath"
327
  )
 
345
  gr.Markdown("**🎯 Drag sliders to set trim points:**")
346
 
347
  with gr.Group():
348
+ gr.Markdown("**🎯 Start point:**")
349
  start_slider = gr.Slider(
350
  minimum=0,
351
  maximum=100,
352
  value=0,
353
  step=0.1,
354
+ label="⏯️ Start Time",
355
+ info="Drag to set start position",
356
  elem_classes=["slider-container"]
357
  )
358
 
 
364
  )
365
 
366
  with gr.Group():
367
+ gr.Markdown("**🎯 End point:**")
368
  end_slider = gr.Slider(
369
  minimum=0,
370
  maximum=100,
371
  value=100,
372
  step=0.1,
373
+ label="⏹️ End Time",
374
+ info="Drag to set end position",
375
  elem_classes=["slider-container"]
376
  )
377
 
 
388
  size="lg"
389
  )
390
 
 
 
 
391
  status_msg = gr.Textbox(
392
  label="📝 Status",
393
  interactive=False,
 
416
  show_label=True
417
  )
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  # Event handlers
420
  def update_video_and_sliders(video_file):
421
  info, duration, start_val, end_val = get_video_info(video_file)
 
440
  outputs=[video_player, video_info, start_slider, end_slider, start_time_display, end_time_display]
441
  )
442
 
 
 
 
 
 
 
443
  start_slider.change(
444
+ fn=update_start_display,
445
  inputs=[start_slider],
446
  outputs=[start_time_display]
447
  )
448
 
449
  end_slider.change(
450
+ fn=update_end_display,
451
  inputs=[end_slider],
452
  outputs=[end_time_display]
453
  )
454
 
455
+ # Trim button handler
456
+ trim_btn.click(
457
+ fn=process_video_trim,
458
+ inputs=[video_input, start_slider, end_slider],
459
+ outputs=[output_video, output_audio_player, output_audio_download, status_msg]
460
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
  if __name__ == "__main__":
463
+ demo.launch()
 
 
 
 
 
 
app_client_drive.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import gradio as gr
4
+ import subprocess
5
+ import os
6
+ import tempfile
7
+ import shutil
8
+ import logging
9
+ import time
10
+ from pathlib import Path
11
+
12
+ # Set up logging
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+ def process_video_trim(video_file, start_time, end_time):
17
+ """Process video trimming using ffmpeg directly"""
18
+ logger.info(f"🎬 Starting trim process: file={video_file}, start={start_time}, end={end_time}")
19
+
20
+ if not video_file or start_time is None or end_time is None:
21
+ error_msg = "Please provide video file and both start/end times"
22
+ logger.error(f"❌ {error_msg}")
23
+ return None, None, None, error_msg
24
+
25
+ try:
26
+ start_seconds = float(start_time)
27
+ end_seconds = float(end_time)
28
+
29
+ logger.info(f"📊 Parsed times: start={start_seconds}s, end={end_seconds}s")
30
+
31
+ if start_seconds >= end_seconds:
32
+ error_msg = "Start time must be less than end time"
33
+ logger.error(f"❌ {error_msg}")
34
+ return None, None, None, error_msg
35
+
36
+ if not os.path.exists(video_file):
37
+ error_msg = f"Input video file not found: {video_file}"
38
+ logger.error(f"❌ {error_msg}")
39
+ return None, None, None, error_msg
40
+
41
+ # Create temporary directory for output
42
+ temp_dir = tempfile.mkdtemp()
43
+ logger.info(f"📁 Created temp directory: {temp_dir}")
44
+
45
+ # Get the base filename without extension
46
+ base_name = Path(video_file).stem
47
+ output_video = os.path.join(temp_dir, f"{base_name}_trimmed.mp4")
48
+ output_audio = os.path.join(temp_dir, f"{base_name}_trimmed.aac")
49
+
50
+ logger.info(f"📤 Output files will be: video={output_video}, audio={output_audio}")
51
+
52
+ # Convert seconds to HH:MM:SS format
53
+ def seconds_to_time(seconds):
54
+ hours = int(seconds // 3600)
55
+ minutes = int((seconds % 3600) // 60)
56
+ secs = seconds % 60
57
+ return f"{hours:02d}:{minutes:02d}:{secs:06.3f}"
58
+
59
+ start_time_str = seconds_to_time(start_seconds)
60
+ end_time_str = seconds_to_time(end_seconds)
61
+ duration = end_seconds - start_seconds
62
+
63
+ logger.info(f"🕒 Converted times: start={start_time_str}, duration={duration}s")
64
+
65
+ # Trim video using ffmpeg
66
+ video_cmd = [
67
+ "ffmpeg", "-y", "-i", video_file,
68
+ "-ss", start_time_str,
69
+ "-t", str(duration),
70
+ "-c", "copy",
71
+ "-avoid_negative_ts", "make_zero",
72
+ output_video
73
+ ]
74
+
75
+ logger.info(f"🚀 Running video command: {' '.join(video_cmd)}")
76
+ video_result = subprocess.run(video_cmd, capture_output=True, text=True)
77
+
78
+ if video_result.returncode != 0:
79
+ logger.warning("Stream copy failed, trying with re-encoding...")
80
+ # Fallback to re-encoding
81
+ video_cmd = [
82
+ "ffmpeg", "-y", "-i", video_file,
83
+ "-ss", start_time_str,
84
+ "-t", str(duration),
85
+ "-c:v", "libx264", "-preset", "fast", "-crf", "23",
86
+ "-c:a", "aac", "-b:a", "128k",
87
+ output_video
88
+ ]
89
+ video_result = subprocess.run(video_cmd, capture_output=True, text=True)
90
+
91
+ # Extract audio
92
+ audio_cmd = [
93
+ "ffmpeg", "-y", "-i", video_file,
94
+ "-ss", start_time_str,
95
+ "-t", str(duration),
96
+ "-vn", "-acodec", "aac", "-b:a", "128k",
97
+ output_audio
98
+ ]
99
+
100
+ logger.info(f"🎵 Running audio command: {' '.join(audio_cmd)}")
101
+ audio_result = subprocess.run(audio_cmd, capture_output=True, text=True)
102
+
103
+ if video_result.returncode == 0 and audio_result.returncode == 0:
104
+ if os.path.exists(output_video) and os.path.exists(output_audio):
105
+ # Create MP3 version for better browser compatibility
106
+ audio_mp3 = os.path.join(temp_dir, f"{base_name}_trimmed.mp3")
107
+ mp3_cmd = [
108
+ "ffmpeg", "-y", "-i", output_audio,
109
+ "-codec:a", "libmp3lame", "-b:a", "128k",
110
+ audio_mp3
111
+ ]
112
+
113
+ mp3_result = subprocess.run(mp3_cmd, capture_output=True, text=True)
114
+ audio_player_file = audio_mp3 if mp3_result.returncode == 0 else output_audio
115
+
116
+ success_msg = f"✅ Successfully trimmed video from {start_seconds:.1f}s to {end_seconds:.1f}s"
117
+ logger.info(success_msg)
118
+ return output_video, audio_player_file, output_audio, success_msg
119
+ else:
120
+ error_msg = "❌ Output files not created"
121
+ return None, None, None, error_msg
122
+ else:
123
+ error_msg = f"❌ FFmpeg failed. Video: {video_result.stderr}, Audio: {audio_result.stderr}"
124
+ logger.error(error_msg)
125
+ return None, None, None, error_msg
126
+
127
+ except Exception as e:
128
+ error_msg = f"❌ Unexpected error: {str(e)}"
129
+ logger.exception(error_msg)
130
+ return None, None, None, error_msg
131
+
132
+ def get_video_duration(video_file):
133
+ """Get video duration in seconds"""
134
+ if not video_file:
135
+ return 0
136
+
137
+ try:
138
+ logger.info(f"📺 Getting duration for: {video_file}")
139
+
140
+ cmd = [
141
+ "ffprobe", "-v", "quiet", "-print_format", "json",
142
+ "-show_format", "-show_streams", video_file
143
+ ]
144
+ result = subprocess.run(cmd, capture_output=True, text=True)
145
+
146
+ if result.returncode == 0:
147
+ import json
148
+ data = json.loads(result.stdout)
149
+ duration = float(data['format']['duration'])
150
+ logger.info(f"⏱️ Video duration: {duration} seconds")
151
+ return duration
152
+ else:
153
+ logger.warning(f"⚠️ Could not get duration: {result.stderr}")
154
+ return 0
155
+ except Exception as e:
156
+ logger.exception(f"❌ Error getting video duration: {e}")
157
+ return 0
158
+
159
+ def format_time(seconds):
160
+ """Format seconds to mm:ss"""
161
+ if seconds is None:
162
+ return "0:00"
163
+ minutes = int(seconds // 60)
164
+ secs = int(seconds % 60)
165
+ return f"{minutes}:{secs:02d}"
166
+
167
+ def get_video_info(video_file):
168
+ """Get video duration and basic info"""
169
+ if not video_file:
170
+ return "No video uploaded", 0, 0, 0
171
+
172
+ logger.info(f"📹 Processing video upload: {video_file}")
173
+
174
+ duration = get_video_duration(video_file)
175
+ if duration > 0:
176
+ minutes = int(duration // 60)
177
+ seconds = int(duration % 60)
178
+ info = f"📹 Video loaded! Duration: {minutes}:{seconds:02d} ({duration:.1f}s)"
179
+ logger.info(f"✅ {info}")
180
+ return info, duration, 0, duration
181
+ else:
182
+ info = "📹 Video loaded! (Could not determine duration)"
183
+ logger.warning(f"⚠️ {info}")
184
+ return info, 100, 0, 100
185
+
186
+ # Client-side Google Drive integration
187
+ google_drive_js = """
188
+ <script src="https://apis.google.com/js/api.js"></script>
189
+ <script src="https://accounts.google.com/gsi/client"></script>
190
+ <script>
191
+ // Google Drive Picker API
192
+ let pickerApiLoaded = false;
193
+ let oauthToken;
194
+
195
+ // Initialize the APIs
196
+ function initializeGoogleDrive() {
197
+ gapi.load('auth2:picker', onAuthApiLoad);
198
+ }
199
+
200
+ function onAuthApiLoad() {
201
+ window.gapi.load('picker', onPickerApiLoad);
202
+
203
+ // Initialize OAuth2
204
+ gapi.auth2.init({
205
+ client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com' // You'll need to add your client ID
206
+ });
207
+ }
208
+
209
+ function onPickerApiLoad() {
210
+ pickerApiLoaded = true;
211
+ }
212
+
213
+ // Authenticate user
214
+ function authenticateGoogleDrive() {
215
+ const authInstance = gapi.auth2.getAuthInstance();
216
+ authInstance.signIn().then(() => {
217
+ oauthToken = authInstance.currentUser.get().getAuthResponse().access_token;
218
+ console.log('Google Drive authenticated!');
219
+ document.getElementById('drive-status').innerText = '✅ Authenticated with Google Drive';
220
+ });
221
+ }
222
+
223
+ // Open Google Drive Picker
224
+ function openDrivePicker() {
225
+ if (pickerApiLoaded && oauthToken) {
226
+ const picker = new google.picker.PickerBuilder()
227
+ .addView(google.picker.ViewId.DOCS_VIDEOS)
228
+ .setOAuthToken(oauthToken)
229
+ .setDeveloperKey('YOUR_API_KEY') // You'll need to add your API key
230
+ .setCallback(pickerCallback)
231
+ .build();
232
+ picker.setVisible(true);
233
+ } else {
234
+ alert('Please authenticate with Google Drive first');
235
+ }
236
+ }
237
+
238
+ // Handle file selection
239
+ function pickerCallback(data) {
240
+ if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
241
+ const file = data[google.picker.Response.DOCUMENTS][0];
242
+ const fileId = file[google.picker.Document.ID];
243
+ const fileName = file[google.picker.Document.NAME];
244
+
245
+ console.log('Selected file:', fileName, fileId);
246
+
247
+ // Download the file
248
+ downloadFromDrive(fileId, fileName);
249
+ }
250
+ }
251
+
252
+ // Download file from Google Drive
253
+ function downloadFromDrive(fileId, fileName) {
254
+ const url = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`;
255
+
256
+ fetch(url, {
257
+ headers: {
258
+ 'Authorization': `Bearer ${oauthToken}`
259
+ }
260
+ })
261
+ .then(response => response.blob())
262
+ .then(blob => {
263
+ // Create a file object and trigger upload to Gradio
264
+ const file = new File([blob], fileName, { type: blob.type });
265
+
266
+ // This would need to integrate with Gradio's file input
267
+ console.log('Downloaded file:', file);
268
+ document.getElementById('drive-status').innerText = `✅ Downloaded: ${fileName}`;
269
+ })
270
+ .catch(error => {
271
+ console.error('Download failed:', error);
272
+ document.getElementById('drive-status').innerText = `��� Download failed: ${error.message}`;
273
+ });
274
+ }
275
+
276
+ // Initialize when page loads
277
+ window.addEventListener('load', initializeGoogleDrive);
278
+ </script>
279
+
280
+ <div id="google-drive-section" style="border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 8px;">
281
+ <h3>🔗 Google Drive Integration (Client-Side)</h3>
282
+ <p><strong>Each user authenticates with their own Google account</strong></p>
283
+
284
+ <button onclick="authenticateGoogleDrive()" style="background: #4285f4; color: white; padding: 10px 20px; border: none; border-radius: 5px; margin: 5px;">
285
+ 🔐 Authenticate with Google Drive
286
+ </button>
287
+
288
+ <button onclick="openDrivePicker()" style="background: #34a853; color: white; padding: 10px 20px; border: none; border-radius: 5px; margin: 5px;">
289
+ 📁 Browse Google Drive
290
+ </button>
291
+
292
+ <div id="drive-status" style="margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 5px;">
293
+ ⚠️ Click "Authenticate" to connect your Google Drive
294
+ </div>
295
+ </div>
296
+ """
297
+
298
+ # Create the Gradio interface
299
+ custom_css = """
300
+ .video-container video {
301
+ width: 100%;
302
+ max-height: 400px;
303
+ }
304
+ .slider-container {
305
+ margin: 10px 0;
306
+ }
307
+ """
308
+
309
+ with gr.Blocks(title="Video Trimmer Tool", theme=gr.themes.Soft(), css=custom_css) as demo:
310
+ gr.Markdown("""
311
+ # 🎬 Video Trimmer Tool (Client-Side Google Drive)
312
+ Upload a video file, set trim points using the sliders, and get both trimmed video and extracted audio files.
313
+
314
+ **NEW: Individual Google Drive Authentication** - Each user connects their own Google account!
315
+ """)
316
+
317
+ # Add Google Drive integration
318
+ gr.HTML(google_drive_js)
319
+
320
+ with gr.Row():
321
+ with gr.Column(scale=2):
322
+ # Video upload and display
323
+ video_input = gr.File(
324
+ label="📁 Upload Video File (or use Google Drive above)",
325
+ file_types=[".mp4", ".mov", ".avi", ".mkv"],
326
+ type="filepath"
327
+ )
328
+
329
+ video_player = gr.Video(
330
+ label="🎥 Video Player",
331
+ show_label=True,
332
+ elem_id="main_video_player",
333
+ elem_classes=["video-container"]
334
+ )
335
+
336
+ video_info = gr.Textbox(
337
+ label="📊 Video Info",
338
+ interactive=False,
339
+ value="Upload a video to see information"
340
+ )
341
+
342
+ with gr.Column(scale=1):
343
+ # Trim controls
344
+ gr.Markdown("### ✂️ Trim Settings")
345
+ gr.Markdown("**🎯 Drag sliders to set trim points:**")
346
+
347
+ with gr.Group():
348
+ gr.Markdown("**🎯 Start point:**")
349
+ start_slider = gr.Slider(
350
+ minimum=0,
351
+ maximum=100,
352
+ value=0,
353
+ step=0.1,
354
+ label="⏯️ Start Time",
355
+ info="Drag to set start position",
356
+ elem_classes=["slider-container"]
357
+ )
358
+
359
+ start_time_display = gr.Textbox(
360
+ label="⏯️ Start Time",
361
+ value="0:00",
362
+ interactive=False,
363
+ info="Current start time"
364
+ )
365
+
366
+ with gr.Group():
367
+ gr.Markdown("**🎯 End point:**")
368
+ end_slider = gr.Slider(
369
+ minimum=0,
370
+ maximum=100,
371
+ value=100,
372
+ step=0.1,
373
+ label="⏹️ End Time",
374
+ info="Drag to set end position",
375
+ elem_classes=["slider-container"]
376
+ )
377
+
378
+ end_time_display = gr.Textbox(
379
+ label="⏹️ End Time",
380
+ value="1:40",
381
+ interactive=False,
382
+ info="Current end time"
383
+ )
384
+
385
+ trim_btn = gr.Button(
386
+ "✂️ Trim Video",
387
+ variant="primary",
388
+ size="lg"
389
+ )
390
+
391
+ status_msg = gr.Textbox(
392
+ label="📝 Status",
393
+ interactive=False,
394
+ value="Ready to trim..."
395
+ )
396
+
397
+ # Output section
398
+ gr.Markdown("### 📤 Output Files")
399
+
400
+ with gr.Row():
401
+ with gr.Column():
402
+ output_video = gr.Video(
403
+ label="🎬 Trimmed Video",
404
+ show_label=True
405
+ )
406
+
407
+ with gr.Column():
408
+ output_audio_player = gr.Audio(
409
+ label="🎵 Play Extracted Audio",
410
+ show_label=True,
411
+ type="filepath"
412
+ )
413
+
414
+ output_audio_download = gr.File(
415
+ label="💾 Download Audio (AAC)",
416
+ show_label=True
417
+ )
418
+
419
+ # Event handlers
420
+ def update_video_and_sliders(video_file):
421
+ info, duration, start_val, end_val = get_video_info(video_file)
422
+ return (
423
+ video_file, # video_player
424
+ info, # video_info
425
+ gr.Slider(minimum=0, maximum=duration, value=0, step=0.1), # start_slider
426
+ gr.Slider(minimum=0, maximum=duration, value=duration, step=0.1), # end_slider
427
+ "0:00", # start_time_display
428
+ format_time(duration) # end_time_display
429
+ )
430
+
431
+ def update_start_display(start_val):
432
+ return format_time(start_val)
433
+
434
+ def update_end_display(end_val):
435
+ return format_time(end_val)
436
+
437
+ video_input.change(
438
+ fn=update_video_and_sliders,
439
+ inputs=[video_input],
440
+ outputs=[video_player, video_info, start_slider, end_slider, start_time_display, end_time_display]
441
+ )
442
+
443
+ start_slider.change(
444
+ fn=update_start_display,
445
+ inputs=[start_slider],
446
+ outputs=[start_time_display]
447
+ )
448
+
449
+ end_slider.change(
450
+ fn=update_end_display,
451
+ inputs=[end_slider],
452
+ outputs=[end_time_display]
453
+ )
454
+
455
+ # Trim button handler
456
+ trim_btn.click(
457
+ fn=process_video_trim,
458
+ inputs=[video_input, start_slider, end_slider],
459
+ outputs=[output_video, output_audio_player, output_audio_download, status_msg]
460
+ )
461
+
462
+ if __name__ == "__main__":
463
+ demo.launch()