Chaitanya-aitf commited on
Commit
926f850
Β·
verified Β·
1 Parent(s): 8f917de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -221
app.py CHANGED
@@ -1,15 +1,7 @@
1
  """
2
- ShortSmith v2 - Gradio Application
3
 
4
  Hugging Face Space interface for video highlight extraction.
5
-
6
- Features:
7
- - Video upload and processing
8
- - Domain selection for optimized scoring
9
- - Reference image for person-specific filtering
10
- - Custom prompt/instructions
11
- - Progress tracking
12
- - Output clip gallery with download
13
  """
14
 
15
  import os
@@ -17,7 +9,6 @@ import sys
17
  import tempfile
18
  import shutil
19
  from pathlib import Path
20
- from typing import Optional, List, Tuple, Generator
21
  import time
22
 
23
  import gradio as gr
@@ -25,62 +16,14 @@ import gradio as gr
25
  # Add project root to path
26
  sys.path.insert(0, str(Path(__file__).parent))
27
 
 
28
  from utils.logger import setup_logging, get_logger
29
- from utils.helpers import format_duration, validate_video_file, validate_image_file
30
- from config import get_config, set_config, AppConfig
31
- from scoring.domain_presets import list_domains, Domain
32
- from pipeline.orchestrator import PipelineOrchestrator, PipelineResult, PipelineProgress
33
-
34
- # Initialize logging
35
  setup_logging(log_level="INFO", log_to_console=True)
36
  logger = get_logger("app")
37
 
38
- # Global state
39
- _current_pipeline: Optional[PipelineOrchestrator] = None
40
- _progress_state: dict = {"stage": "", "progress": 0, "message": "Ready"}
41
-
42
-
43
- def update_progress(progress: PipelineProgress) -> None:
44
- """Update global progress state."""
45
- global _progress_state
46
- _progress_state = {
47
- "stage": progress.stage.value,
48
- "progress": progress.progress,
49
- "message": progress.message,
50
- "elapsed": progress.elapsed_time,
51
- "remaining": progress.estimated_remaining,
52
- }
53
-
54
-
55
- def process_video(
56
- video_file,
57
- api_key,
58
- domain,
59
- num_clips,
60
- clip_duration,
61
- reference_image,
62
- custom_prompt,
63
- progress=None,
64
- ):
65
- """
66
- Main processing function for Gradio interface.
67
-
68
- Args:
69
- video_file: Path to uploaded video
70
- api_key: API key (for future use)
71
- domain: Content domain selection
72
- num_clips: Number of clips to extract
73
- clip_duration: Target clip duration
74
- reference_image: Reference image for person filtering
75
- custom_prompt: Custom instructions
76
-
77
- Returns:
78
- Tuple of (clip_paths, status_message, log_output)
79
- """
80
- global _current_pipeline, _progress_state
81
-
82
- # Reset progress
83
- _progress_state = {"stage": "starting", "progress": 0, "message": "Starting..."}
84
 
85
  log_messages = []
86
 
@@ -88,27 +31,25 @@ def process_video(
88
  log_messages.append(f"[{time.strftime('%H:%M:%S')}] {msg}")
89
  logger.info(msg)
90
 
91
- def update_prog(val, desc=""):
92
- if progress is not None:
93
- try:
94
- progress(val, desc=desc)
95
- except:
96
- pass
97
-
98
  try:
99
  # Validate inputs
100
  log("Validating inputs...")
101
- update_prog(0.02, "Validating inputs...")
102
 
103
  if not video_file:
104
  return [], "❌ Error: No video file provided", "\n".join(log_messages)
105
 
 
 
 
 
 
 
 
106
  validation = validate_video_file(video_file)
107
  if not validation.is_valid:
108
  return [], f"❌ Error: {validation.error_message}", "\n".join(log_messages)
109
 
110
- video_path = Path(video_file)
111
- log(f"Video: {video_path.name} ({validation.file_size / (1024*1024):.1f} MB)")
112
 
113
  # Validate reference image if provided
114
  ref_path = None
@@ -120,7 +61,7 @@ def process_video(
120
  else:
121
  log(f"Warning: Invalid reference image - {ref_validation.error_message}")
122
 
123
- # Map domain string to enum
124
  domain_map = {
125
  "Sports": "sports",
126
  "Vlogs": "vlogs",
@@ -130,246 +71,135 @@ def process_video(
130
  "General": "general",
131
  }
132
  domain_value = domain_map.get(domain, "general")
133
- log(f"Domain: {domain} -> {domain_value}")
134
 
135
  # Create output directory
136
  output_dir = Path(tempfile.mkdtemp(prefix="shortsmith_output_"))
137
  log(f"Output directory: {output_dir}")
138
 
139
- # Initialize pipeline with progress callback
140
- def progress_callback(p):
141
- update_progress(p)
142
- update_prog(p.progress, p.message)
143
-
144
  log("Initializing pipeline...")
145
- update_prog(0.05, "Initializing pipeline...")
146
-
147
- _current_pipeline = PipelineOrchestrator(
148
- progress_callback=progress_callback
149
- )
150
 
151
  # Process video
152
- log(f"Processing video: {num_clips} clips @ {clip_duration}s each")
153
 
154
- result = _current_pipeline.process(
155
  video_path=video_path,
156
  num_clips=int(num_clips),
157
  clip_duration=float(clip_duration),
158
  domain=domain_value,
159
  reference_image=ref_path,
160
- custom_prompt=custom_prompt if custom_prompt.strip() else None,
161
- api_key=api_key if api_key.strip() else None,
162
  )
163
 
164
  # Handle result
165
  if result.success:
166
  log(f"βœ… Processing complete in {result.processing_time:.1f}s")
167
 
168
- # Copy clips to output directory
169
  clip_paths = []
170
  for i, clip in enumerate(result.clips):
171
  if clip.clip_path.exists():
172
- # Copy to output dir
173
  output_path = output_dir / f"highlight_{i+1}.mp4"
174
  shutil.copy2(clip.clip_path, output_path)
175
  clip_paths.append(str(output_path))
176
- log(f" Clip {i+1}: {format_duration(clip.start_time)} - {format_duration(clip.end_time)} "
177
- f"(score: {clip.hype_score:.2f})")
178
 
179
- status = (
180
- f"βœ… Successfully extracted {len(clip_paths)} highlight clips!\n"
181
- f"Processing time: {result.processing_time:.1f}s\n"
182
- f"Video duration: {format_duration(result.metadata.duration) if result.metadata else 'N/A'}"
183
- )
184
-
185
- # Cleanup pipeline temp files (but keep output)
186
- if result.temp_dir and result.temp_dir != output_dir:
187
- _current_pipeline.cleanup()
188
 
 
189
  return clip_paths, status, "\n".join(log_messages)
190
-
191
  else:
192
  log(f"❌ Processing failed: {result.error_message}")
193
- _current_pipeline.cleanup()
194
  return [], f"❌ Error: {result.error_message}", "\n".join(log_messages)
195
 
196
  except Exception as e:
197
  error_msg = f"Unexpected error: {str(e)}"
198
  log(f"❌ {error_msg}")
199
  logger.exception("Pipeline error")
200
-
201
- if _current_pipeline:
202
- try:
203
- _current_pipeline.cleanup()
204
- except:
205
- pass
206
-
207
  return [], f"❌ {error_msg}", "\n".join(log_messages)
208
 
209
 
210
- def create_interface() -> gr.Blocks:
211
  """Create the Gradio interface."""
212
 
213
- # Get available domains
214
  domains = ["Sports", "Vlogs", "Music Videos", "Podcasts", "Gaming", "General"]
215
 
216
- # Custom CSS
217
- css = """
218
- .container { max-width: 1200px; margin: auto; }
219
- .output-video { max-height: 300px; }
220
- .status-box { font-family: monospace; }
221
- """
222
-
223
  with gr.Blocks(
224
- title="ShortSmith v2 - AI Video Highlight Extractor",
225
- css=css,
226
  theme=gr.themes.Soft(),
227
  ) as demo:
228
  gr.Markdown("""
229
  # 🎬 ShortSmith v2
230
  ### AI-Powered Video Highlight Extractor
231
 
232
- Upload a video and let AI extract the most engaging highlight clips automatically.
233
-
234
- **Features:**
235
- - 🎯 Domain-optimized hype detection (Sports, Music, Vlogs, etc.)
236
- - πŸ‘€ Person-specific filtering (optional)
237
- - 🎡 Audio + Visual + Motion analysis
238
- - ⚑ Fast hierarchical processing
239
  """)
240
 
241
  with gr.Row():
242
- # Left column: Inputs
243
  with gr.Column(scale=1):
244
  gr.Markdown("### πŸ“€ Input")
245
 
246
- video_input = gr.Video(
247
- label="Upload Video",
248
- sources=["upload"],
249
- )
250
 
251
  with gr.Accordion("βš™οΈ Settings", open=True):
252
  domain_dropdown = gr.Dropdown(
253
  choices=domains,
254
  value="General",
255
  label="Content Domain",
256
- info="Select the type of content for optimized detection",
257
  )
258
 
259
  with gr.Row():
260
  num_clips_slider = gr.Slider(
261
- minimum=1,
262
- maximum=10,
263
- value=3,
264
- step=1,
265
  label="Number of Clips",
266
  )
267
-
268
  duration_slider = gr.Slider(
269
- minimum=5,
270
- maximum=30,
271
- value=15,
272
- step=1,
273
  label="Clip Duration (seconds)",
274
  )
275
 
276
  with gr.Accordion("πŸ‘€ Person Filtering (Optional)", open=False):
277
- gr.Markdown(
278
- "Upload a reference image to extract only clips featuring a specific person."
279
- )
280
- reference_image = gr.Image(
281
- label="Reference Image",
282
- type="filepath",
283
- )
284
 
285
  with gr.Accordion("πŸ“ Custom Instructions (Optional)", open=False):
286
  custom_prompt = gr.Textbox(
287
  label="Additional Instructions",
288
- placeholder="E.g., 'Focus on crowd reactions' or 'Prioritize close-up shots'",
289
- lines=3,
290
  )
291
 
292
  with gr.Accordion("πŸ”‘ API Key (Optional)", open=False):
293
  api_key_input = gr.Textbox(
294
  label="API Key",
295
  type="password",
296
- placeholder="For future external service integrations",
297
  )
298
 
299
- process_btn = gr.Button(
300
- "πŸš€ Extract Highlights",
301
- variant="primary",
302
- size="lg",
303
- )
304
 
305
- # Right column: Outputs
306
  with gr.Column(scale=1):
307
  gr.Markdown("### πŸ“₯ Output")
308
 
309
- status_output = gr.Textbox(
310
- label="Status",
311
- lines=3,
312
- interactive=False,
313
- elem_classes=["status-box"],
314
- )
315
-
316
- clip_gallery = gr.Gallery(
317
- label="Extracted Clips",
318
- columns=3,
319
- height=400,
320
- object_fit="contain",
321
- )
322
-
323
- # Download all clips
324
- download_btn = gr.DownloadButton(
325
- label="πŸ“¦ Download All Clips",
326
- visible=False,
327
- )
328
 
329
  with gr.Accordion("πŸ“‹ Processing Log", open=False):
330
- log_output = gr.Textbox(
331
- label="Log",
332
- lines=10,
333
- interactive=False,
334
- elem_classes=["status-box"],
335
- )
336
 
337
- # Footer
338
- gr.Markdown("""
339
- ---
340
- **ShortSmith v2** | Powered by Qwen2-VL, InsightFace, and Librosa
341
-
342
- [GitHub](https://github.com/your-repo) | [Documentation](https://your-docs.com)
343
- """)
344
 
345
- # Event handlers
346
- def on_process(video, api_key, domain, num_clips, duration, ref_img, prompt, progress=gr.Progress()):
347
- """Handle process button click."""
348
- try:
349
- clips, status, logs = process_video(
350
- video, api_key, domain, num_clips, duration, ref_img, prompt, progress
351
- )
352
-
353
- # Convert clip paths to gallery format
354
- gallery_items = []
355
- for clip_path in clips:
356
- gallery_items.append((clip_path, f"Clip {len(gallery_items)+1}"))
357
-
358
- return status, gallery_items, logs
359
- except Exception as e:
360
- return f"❌ Error: {str(e)}", [], f"Error: {str(e)}"
361
 
362
  process_btn.click(
363
  fn=on_process,
364
- inputs=[
365
- video_input,
366
- api_key_input,
367
- domain_dropdown,
368
- num_clips_slider,
369
- duration_slider,
370
- reference_image,
371
- custom_prompt,
372
- ],
373
  outputs=[status_output, clip_gallery, log_output],
374
  )
375
 
@@ -380,16 +210,11 @@ def main():
380
  """Main entry point."""
381
  logger.info("Starting ShortSmith v2 Gradio interface...")
382
 
383
- # Create and launch interface
384
  demo = create_interface()
385
-
386
- # Launch settings for Hugging Face Spaces
387
  demo.queue()
388
  demo.launch(
389
  server_name="0.0.0.0",
390
  server_port=7860,
391
- share=False,
392
- show_error=True,
393
  )
394
 
395
 
 
1
  """
2
+ ShortSmith v2 - Gradio Application (Simplified)
3
 
4
  Hugging Face Space interface for video highlight extraction.
 
 
 
 
 
 
 
 
5
  """
6
 
7
  import os
 
9
  import tempfile
10
  import shutil
11
  from pathlib import Path
 
12
  import time
13
 
14
  import gradio as gr
 
16
  # Add project root to path
17
  sys.path.insert(0, str(Path(__file__).parent))
18
 
19
+ # Initialize logging first
20
  from utils.logger import setup_logging, get_logger
 
 
 
 
 
 
21
  setup_logging(log_level="INFO", log_to_console=True)
22
  logger = get_logger("app")
23
 
24
+
25
+ def process_video(video_file, api_key, domain, num_clips, clip_duration, reference_image, custom_prompt):
26
+ """Main processing function."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  log_messages = []
29
 
 
31
  log_messages.append(f"[{time.strftime('%H:%M:%S')}] {msg}")
32
  logger.info(msg)
33
 
 
 
 
 
 
 
 
34
  try:
35
  # Validate inputs
36
  log("Validating inputs...")
 
37
 
38
  if not video_file:
39
  return [], "❌ Error: No video file provided", "\n".join(log_messages)
40
 
41
+ video_path = Path(video_file)
42
+ log(f"Video: {video_path.name}")
43
+
44
+ # Import here to avoid schema issues at startup
45
+ from utils.helpers import validate_video_file, validate_image_file, format_duration
46
+ from pipeline.orchestrator import PipelineOrchestrator
47
+
48
  validation = validate_video_file(video_file)
49
  if not validation.is_valid:
50
  return [], f"❌ Error: {validation.error_message}", "\n".join(log_messages)
51
 
52
+ log(f"Video size: {validation.file_size / (1024*1024):.1f} MB")
 
53
 
54
  # Validate reference image if provided
55
  ref_path = None
 
61
  else:
62
  log(f"Warning: Invalid reference image - {ref_validation.error_message}")
63
 
64
+ # Map domain string
65
  domain_map = {
66
  "Sports": "sports",
67
  "Vlogs": "vlogs",
 
71
  "General": "general",
72
  }
73
  domain_value = domain_map.get(domain, "general")
74
+ log(f"Domain: {domain_value}")
75
 
76
  # Create output directory
77
  output_dir = Path(tempfile.mkdtemp(prefix="shortsmith_output_"))
78
  log(f"Output directory: {output_dir}")
79
 
80
+ # Initialize pipeline
 
 
 
 
81
  log("Initializing pipeline...")
82
+ pipeline = PipelineOrchestrator()
 
 
 
 
83
 
84
  # Process video
85
+ log(f"Processing: {num_clips} clips @ {clip_duration}s each")
86
 
87
+ result = pipeline.process(
88
  video_path=video_path,
89
  num_clips=int(num_clips),
90
  clip_duration=float(clip_duration),
91
  domain=domain_value,
92
  reference_image=ref_path,
93
+ custom_prompt=custom_prompt if custom_prompt and custom_prompt.strip() else None,
94
+ api_key=api_key if api_key and api_key.strip() else None,
95
  )
96
 
97
  # Handle result
98
  if result.success:
99
  log(f"βœ… Processing complete in {result.processing_time:.1f}s")
100
 
 
101
  clip_paths = []
102
  for i, clip in enumerate(result.clips):
103
  if clip.clip_path.exists():
 
104
  output_path = output_dir / f"highlight_{i+1}.mp4"
105
  shutil.copy2(clip.clip_path, output_path)
106
  clip_paths.append(str(output_path))
107
+ log(f" Clip {i+1}: {format_duration(clip.start_time)} - {format_duration(clip.end_time)} (score: {clip.hype_score:.2f})")
 
108
 
109
+ status = f"βœ… Successfully extracted {len(clip_paths)} highlight clips!\nProcessing time: {result.processing_time:.1f}s"
 
 
 
 
 
 
 
 
110
 
111
+ pipeline.cleanup()
112
  return clip_paths, status, "\n".join(log_messages)
 
113
  else:
114
  log(f"❌ Processing failed: {result.error_message}")
115
+ pipeline.cleanup()
116
  return [], f"❌ Error: {result.error_message}", "\n".join(log_messages)
117
 
118
  except Exception as e:
119
  error_msg = f"Unexpected error: {str(e)}"
120
  log(f"❌ {error_msg}")
121
  logger.exception("Pipeline error")
 
 
 
 
 
 
 
122
  return [], f"❌ {error_msg}", "\n".join(log_messages)
123
 
124
 
125
+ def create_interface():
126
  """Create the Gradio interface."""
127
 
 
128
  domains = ["Sports", "Vlogs", "Music Videos", "Podcasts", "Gaming", "General"]
129
 
 
 
 
 
 
 
 
130
  with gr.Blocks(
131
+ title="ShortSmith v2",
 
132
  theme=gr.themes.Soft(),
133
  ) as demo:
134
  gr.Markdown("""
135
  # 🎬 ShortSmith v2
136
  ### AI-Powered Video Highlight Extractor
137
 
138
+ Upload a video and extract the most engaging highlight clips automatically.
 
 
 
 
 
 
139
  """)
140
 
141
  with gr.Row():
 
142
  with gr.Column(scale=1):
143
  gr.Markdown("### πŸ“€ Input")
144
 
145
+ video_input = gr.Video(label="Upload Video")
 
 
 
146
 
147
  with gr.Accordion("βš™οΈ Settings", open=True):
148
  domain_dropdown = gr.Dropdown(
149
  choices=domains,
150
  value="General",
151
  label="Content Domain",
 
152
  )
153
 
154
  with gr.Row():
155
  num_clips_slider = gr.Slider(
156
+ minimum=1, maximum=10, value=3, step=1,
 
 
 
157
  label="Number of Clips",
158
  )
 
159
  duration_slider = gr.Slider(
160
+ minimum=5, maximum=30, value=15, step=1,
 
 
 
161
  label="Clip Duration (seconds)",
162
  )
163
 
164
  with gr.Accordion("πŸ‘€ Person Filtering (Optional)", open=False):
165
+ reference_image = gr.Image(label="Reference Image", type="filepath")
 
 
 
 
 
 
166
 
167
  with gr.Accordion("πŸ“ Custom Instructions (Optional)", open=False):
168
  custom_prompt = gr.Textbox(
169
  label="Additional Instructions",
170
+ placeholder="E.g., 'Focus on crowd reactions'",
171
+ lines=2,
172
  )
173
 
174
  with gr.Accordion("πŸ”‘ API Key (Optional)", open=False):
175
  api_key_input = gr.Textbox(
176
  label="API Key",
177
  type="password",
178
+ placeholder="For future integrations",
179
  )
180
 
181
+ process_btn = gr.Button("πŸš€ Extract Highlights", variant="primary", size="lg")
 
 
 
 
182
 
 
183
  with gr.Column(scale=1):
184
  gr.Markdown("### πŸ“₯ Output")
185
 
186
+ status_output = gr.Textbox(label="Status", lines=3, interactive=False)
187
+ clip_gallery = gr.Gallery(label="Extracted Clips", columns=3, height=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  with gr.Accordion("πŸ“‹ Processing Log", open=False):
190
+ log_output = gr.Textbox(label="Log", lines=10, interactive=False)
 
 
 
 
 
191
 
192
+ gr.Markdown("---\n**ShortSmith v2** | Powered by Qwen2-VL, InsightFace, and Librosa")
 
 
 
 
 
 
193
 
194
+ # Event handler
195
+ def on_process(video, api_key, domain, num_clips, duration, ref_img, prompt):
196
+ clips, status, logs = process_video(video, api_key, domain, num_clips, duration, ref_img, prompt)
197
+ gallery_items = [(clip, f"Clip {i+1}") for i, clip in enumerate(clips)]
198
+ return status, gallery_items, logs
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  process_btn.click(
201
  fn=on_process,
202
+ inputs=[video_input, api_key_input, domain_dropdown, num_clips_slider, duration_slider, reference_image, custom_prompt],
 
 
 
 
 
 
 
 
203
  outputs=[status_output, clip_gallery, log_output],
204
  )
205
 
 
210
  """Main entry point."""
211
  logger.info("Starting ShortSmith v2 Gradio interface...")
212
 
 
213
  demo = create_interface()
 
 
214
  demo.queue()
215
  demo.launch(
216
  server_name="0.0.0.0",
217
  server_port=7860,
 
 
218
  )
219
 
220