Nipun commited on
Commit
aec7879
·
1 Parent(s): 7402c26

Backup: server-side OAuth version before client-side experiment

Browse files
Files changed (1) hide show
  1. app_server_oauth.py +775 -0
app_server_oauth.py ADDED
@@ -0,0 +1,775 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import subprocess
3
+ import os
4
+ import tempfile
5
+ 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:
26
+ error_msg = "Please provide video file and both start/end times"
27
+ logger.error(f"❌ {error_msg}")
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
+
35
+ logger.info(f"📊 Parsed times: start={start_seconds}s, end={end_seconds}s")
36
+
37
+ if start_seconds >= end_seconds:
38
+ error_msg = "Start time must be less than 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}")
46
+ return None, None, None, error_msg
47
+
48
+ # Create temporary directory for output
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)
75
+ secs = seconds % 60
76
+ return f"{hours:02d}:{minutes:02d}:{secs:06.3f}"
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
+
211
+ except Exception as e:
212
+ error_msg = f"❌ Unexpected error: {str(e)}"
213
+ logger.exception(error_msg)
214
+ return None, None, None, error_msg
215
+
216
+ def get_video_duration(video_file):
217
+ """Get video duration in seconds"""
218
+ if not video_file:
219
+ return 0
220
+
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
228
+ ]
229
+ result = subprocess.run(cmd, capture_output=True, text=True)
230
+
231
+ if result.returncode == 0:
232
+ import json
233
+ data = json.loads(result.stdout)
234
+ duration = float(data['format']['duration'])
235
+ logger.info(f"⏱️ Video duration: {duration} seconds")
236
+ return duration
237
+ else:
238
+ logger.warning(f"⚠️ Could not get duration: {result.stderr}")
239
+ return 0
240
+ except Exception as e:
241
+ logger.exception(f"❌ Error getting video duration: {e}")
242
+ return 0
243
+
244
+ def format_time(seconds):
245
+ """Format seconds to mm:ss"""
246
+ if seconds is None:
247
+ return "0:00"
248
+ minutes = int(seconds // 60)
249
+ secs = int(seconds % 60)
250
+ return f"{minutes}:{secs:02d}"
251
+
252
+ def get_video_info(video_file):
253
+ """Get video duration and basic info"""
254
+ if not video_file:
255
+ return "No video uploaded", 0, 0, 0
256
+
257
+ logger.info(f"📹 Processing video upload: {video_file}")
258
+
259
+ duration = get_video_duration(video_file)
260
+ if duration > 0:
261
+ minutes = int(duration // 60)
262
+ seconds = int(duration % 60)
263
+ info = f"📹 Video loaded! Duration: {minutes}:{seconds:02d} ({duration:.1f}s)"
264
+ logger.info(f"✅ {info}")
265
+ return info, duration, 0, duration
266
+ else:
267
+ info = "📹 Video loaded! (Could not determine duration)"
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%;
402
+ max-height: 400px;
403
+ }
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
+ )
501
+
502
+ video_player = gr.Video(
503
+ label="🎥 Video Player",
504
+ show_label=True,
505
+ elem_id="main_video_player",
506
+ elem_classes=["video-container"]
507
+ )
508
+
509
+ video_info = gr.Textbox(
510
+ label="📊 Video Info",
511
+ interactive=False,
512
+ value="Upload a video to see information"
513
+ )
514
+
515
+ with gr.Column(scale=1):
516
+ # Trim controls
517
+ gr.Markdown("### ✂️ Trim Settings")
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
+
532
+ start_time_display = gr.Textbox(
533
+ label="⏯️ Start Time",
534
+ value="0:00",
535
+ interactive=False,
536
+ info="Current start time"
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
+
551
+ end_time_display = gr.Textbox(
552
+ label="⏹️ End Time",
553
+ value="1:40",
554
+ interactive=False,
555
+ info="Current end time"
556
+ )
557
+
558
+ trim_btn = gr.Button(
559
+ "✂️ Trim Video",
560
+ variant="primary",
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,
570
+ value="Ready to trim..."
571
+ )
572
+
573
+ # Output section
574
+ gr.Markdown("### 📤 Output Files")
575
+
576
+ with gr.Row():
577
+ with gr.Column():
578
+ output_video = gr.Video(
579
+ label="🎬 Trimmed Video",
580
+ show_label=True
581
+ )
582
+
583
+ with gr.Column():
584
+ output_audio_player = gr.Audio(
585
+ label="🎵 Play Extracted Audio",
586
+ show_label=True,
587
+ type="filepath"
588
+ )
589
+
590
+ output_audio_download = gr.File(
591
+ label="💾 Download Audio (AAC)",
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)
640
+ return (
641
+ video_file, # video_player
642
+ info, # video_info
643
+ gr.Slider(minimum=0, maximum=duration, value=0, step=0.1), # start_slider
644
+ gr.Slider(minimum=0, maximum=duration, value=duration, step=0.1), # end_slider
645
+ "0:00", # start_time_display
646
+ format_time(duration) # end_time_display
647
+ )
648
+
649
+ def update_start_display(start_val):
650
+ return format_time(start_val)
651
+
652
+ def update_end_display(end_val):
653
+ return format_time(end_val)
654
+
655
+ video_input.change(
656
+ fn=update_video_and_sliders,
657
+ inputs=[video_input],
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
+ )