vidhi0405 commited on
Commit
1223036
Β·
1 Parent(s): 4d1fa0f
Files changed (3) hide show
  1. README.md +12 -12
  2. app.py +32 -226
  3. test_api.py +8 -39
README.md CHANGED
@@ -21,15 +21,12 @@ This is a FastAPI service that uses HuggingFace's proven segment-based classific
21
  - **Dual Criteria Generation**: Creates two different highlight criteria sets and selects the most selective one
22
  - **SmolVLM2-256M-Video-Instruct**: Faster processing with specialized video understanding
23
  - **Visual Effects**: Optional fade transitions between segments for professional-quality output
24
- - **REST API**: Upload videos and download processed highlights with job tracking
25
- - **Background Processing**: Non-blocking video processing with real-time status updates
26
 
27
  ## πŸ”— API Endpoints
28
 
29
- - `POST /upload-video` - Upload video for processing
30
- - `GET /job-status/{job_id}` - Check processing status
31
- - `GET /download/{filename}` - Download generated highlights
32
- - `GET /docs` - Interactive API documentation
33
 
34
  ## πŸ“± Usage
35
 
@@ -42,13 +39,16 @@ curl -X POST \
42
  -F "model_name=HuggingFaceTB/SmolVLM2-256M-Video-Instruct" \
43
  -F "with_effects=true" \
44
  https://your-space-url.hf.space/upload-video
 
45
 
46
- # Check processing status
47
- curl https://your-space-url.hf.space/job-status/YOUR_JOB_ID
48
-
49
- # Download highlights and analysis
50
- curl -O https://your-space-url.hf.space/download/HIGHLIGHTS.mp4
51
- curl -O https://your-space-url.hf.space/download/ANALYSIS.json
 
 
52
  ```
53
 
54
  ### Via Android App
 
21
  - **Dual Criteria Generation**: Creates two different highlight criteria sets and selects the most selective one
22
  - **SmolVLM2-256M-Video-Instruct**: Faster processing with specialized video understanding
23
  - **Visual Effects**: Optional fade transitions between segments for professional-quality output
24
+ - **REST API**: Upload videos and get generated video description + analysis file path
 
25
 
26
  ## πŸ”— API Endpoints
27
 
28
+ - `POST /upload-video` - Upload video and receive analysis response
29
+ - `GET /health` - Health check
 
 
30
 
31
  ## πŸ“± Usage
32
 
 
39
  -F "model_name=HuggingFaceTB/SmolVLM2-256M-Video-Instruct" \
40
  -F "with_effects=true" \
41
  https://your-space-url.hf.space/upload-video
42
+ ```
43
 
44
+ Example response:
45
+ ```json
46
+ {
47
+ "success": true,
48
+ "message": "Video description generated successfully",
49
+ "video_description": "A concise description of the uploaded video...",
50
+ "analysis_file": "/tmp/outputs/<uuid>_analysis.json"
51
+ }
52
  ```
53
 
54
  ### Via Android App
app.py CHANGED
@@ -5,7 +5,6 @@ Updated with the latest segment-based approach for better accuracy
5
  """
6
 
7
  import os
8
- import tempfile
9
 
10
  # Set cache directories to writable locations for HuggingFace Spaces
11
  # Use /tmp which is guaranteed to be writable in containers
@@ -20,17 +19,13 @@ os.environ['XDG_CACHE_HOME'] = os.path.join("/tmp", ".cache")
20
  os.environ['HUGGINGFACE_HUB_CACHE'] = CACHE_DIR
21
  os.environ['TOKENIZERS_PARALLELISM'] = 'false'
22
 
23
- from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks
24
- from fastapi.responses import FileResponse, JSONResponse
25
  from fastapi.middleware.cors import CORSMiddleware
26
  from pydantic import BaseModel
27
  import sys
28
  import uuid
29
  import json
30
- import asyncio
31
  from pathlib import Path
32
- from typing import Optional
33
- import logging
34
 
35
  # Add src directory to path for imports
36
  sys.path.append(str(Path(__file__).parent / "src"))
@@ -41,15 +36,14 @@ except ImportError:
41
  print("❌ Cannot import huggingface_exact_approach.py")
42
  sys.exit(1)
43
 
44
- # Configure logging
45
- logging.basicConfig(level=logging.INFO)
46
- logger = logging.getLogger(__name__)
47
-
48
  # FastAPI app
49
  app = FastAPI(
50
  title="SmolVLM2 Optimized HuggingFace Video Highlights API",
51
  description="Generate intelligent video highlights using SmolVLM2 segment-based approach",
52
- version="2.0.0"
 
 
 
53
  )
54
 
55
  # Enable CORS for web apps
@@ -61,31 +55,11 @@ app.add_middleware(
61
  allow_headers=["*"],
62
  )
63
 
64
- # Request/Response models
65
- class AnalysisRequest(BaseModel):
66
- segment_length: float = 5.0
67
- model_name: str = "HuggingFaceTB/SmolVLM2-256M-Video-Instruct"
68
- with_effects: bool = True
69
-
70
  class AnalysisResponse(BaseModel):
71
- job_id: str
72
- status: str
73
  message: str
74
-
75
- class JobStatus(BaseModel):
76
- job_id: str
77
- status: str # "processing", "completed", "failed"
78
- progress: int # 0-100
79
- message: str
80
- highlights_url: Optional[str] = None
81
- analysis_url: Optional[str] = None
82
- total_segments: Optional[int] = None
83
- selected_segments: Optional[int] = None
84
- compression_ratio: Optional[float] = None
85
-
86
- # Global storage for jobs (in production, use Redis/database)
87
- active_jobs = {}
88
- completed_jobs = {}
89
 
90
  # Create output directories with proper permissions
91
  TEMP_DIR = os.path.join("/tmp", "temp")
@@ -95,100 +69,13 @@ OUTPUTS_DIR = os.path.join("/tmp", "outputs")
95
  os.makedirs(OUTPUTS_DIR, mode=0o755, exist_ok=True)
96
  os.makedirs(TEMP_DIR, mode=0o755, exist_ok=True)
97
 
98
- @app.get("/")
99
- async def read_root():
100
- """Welcome message with API information"""
101
- return {
102
- "message": "SmolVLM2 Optimized HuggingFace Video Highlights API",
103
- "version": "3.0.0",
104
- "approach": "Optimized HuggingFace exact approach with STRICT prompting",
105
- "model": "SmolVLM2-256M-Video-Instruct (faster processing)",
106
- "improvements": [
107
- "STRICT system prompting for selectivity",
108
- "Structured YES/NO user prompts",
109
- "Temperature 0.3 for consistent decisions",
110
- "Enhanced response processing with fallbacks"
111
- ],
112
- "endpoints": {
113
- "upload": "POST /upload-video",
114
- "status": "GET /job-status/{job_id}",
115
- "download": "GET /download/{filename}",
116
- "docs": "GET /docs"
117
- }
118
- }
119
-
120
  @app.get("/health")
121
  async def health_check():
122
  """Health check endpoint"""
123
  return {"status": "healthy", "model": "SmolVLM2-256M-Video-Instruct"}
124
 
125
- async def process_video_background(job_id: str, video_path: str, output_path: str,
126
- segment_length: float, model_name: str, with_effects: bool):
127
- """Background task to process video"""
128
- try:
129
- # Update job status
130
- active_jobs[job_id]["status"] = "processing"
131
- active_jobs[job_id]["progress"] = 10
132
- active_jobs[job_id]["message"] = "Initializing AI model..."
133
-
134
- # Initialize detector
135
- detector = VideoHighlightDetector(model_path=model_name)
136
-
137
- active_jobs[job_id]["progress"] = 20
138
- active_jobs[job_id]["message"] = "Analyzing video content..."
139
-
140
- # Process video
141
- results = detector.process_video(
142
- video_path=video_path,
143
- output_path=output_path,
144
- segment_length=segment_length,
145
- with_effects=with_effects
146
- )
147
-
148
- if "error" in results:
149
- # Failed
150
- active_jobs[job_id]["status"] = "failed"
151
- active_jobs[job_id]["message"] = results["error"]
152
- active_jobs[job_id]["progress"] = 0
153
- else:
154
- # Success - move to completed jobs
155
- output_filename = os.path.basename(output_path)
156
- analysis_filename = output_filename.replace('.mp4', '_analysis.json')
157
- analysis_path = os.path.join(OUTPUTS_DIR, analysis_filename)
158
-
159
- # Save analysis
160
- with open(analysis_path, 'w') as f:
161
- json.dump(results, f, indent=2)
162
-
163
- completed_jobs[job_id] = {
164
- "job_id": job_id,
165
- "status": "completed",
166
- "progress": 100,
167
- "message": f"Created highlights with {results['selected_segments']} segments",
168
- "highlights_url": f"/download/{output_filename}",
169
- "analysis_url": f"/download/{analysis_filename}",
170
- "total_segments": results["total_segments"],
171
- "selected_segments": results["selected_segments"],
172
- "compression_ratio": results["compression_ratio"]
173
- }
174
-
175
- # Remove from active jobs
176
- if job_id in active_jobs:
177
- del active_jobs[job_id]
178
-
179
- except Exception as e:
180
- logger.error(f"Error processing video {job_id}: {str(e)}")
181
- active_jobs[job_id]["status"] = "failed"
182
- active_jobs[job_id]["message"] = f"Processing error: {str(e)}"
183
- active_jobs[job_id]["progress"] = 0
184
- finally:
185
- # Clean up temp video file
186
- if os.path.exists(video_path):
187
- os.unlink(video_path)
188
-
189
  @app.post("/upload-video", response_model=AnalysisResponse)
190
  async def upload_video(
191
- background_tasks: BackgroundTasks,
192
  video: UploadFile = File(...),
193
  segment_length: float = 5.0,
194
  model_name: str = "HuggingFaceTB/SmolVLM2-256M-Video-Instruct",
@@ -207,126 +94,45 @@ async def upload_video(
207
  if not video.content_type.startswith('video/'):
208
  raise HTTPException(status_code=400, detail="File must be a video")
209
 
210
- # Generate unique job ID
211
- job_id = str(uuid.uuid4())
212
-
213
  # Save uploaded video to temp file
 
214
  temp_video_path = os.path.join(TEMP_DIR, f"{job_id}_input.mp4")
215
  output_path = os.path.join(OUTPUTS_DIR, f"{job_id}_highlights.mp4")
 
216
 
217
  try:
218
  # Save uploaded file
219
  with open(temp_video_path, "wb") as buffer:
220
  content = await video.read()
221
  buffer.write(content)
222
-
223
- # Initialize job tracking
224
- active_jobs[job_id] = {
225
- "job_id": job_id,
226
- "status": "queued",
227
- "progress": 5,
228
- "message": "Video uploaded, queued for processing",
229
- "highlights_url": None,
230
- "analysis_url": None
231
- }
232
-
233
- # Start background processing
234
- background_tasks.add_task(
235
- process_video_background,
236
- job_id, temp_video_path, output_path,
237
- segment_length, model_name, with_effects
238
  )
239
-
 
 
 
 
 
 
240
  return AnalysisResponse(
241
- job_id=job_id,
242
- status="queued",
243
- message="Video uploaded successfully. Processing started."
 
244
  )
245
-
246
  except Exception as e:
247
- # Clean up on error
 
 
 
248
  if os.path.exists(temp_video_path):
249
  os.unlink(temp_video_path)
250
- raise HTTPException(status_code=500, detail=f"Failed to process upload: {str(e)}")
251
-
252
- @app.get("/job-status/{job_id}", response_model=JobStatus)
253
- async def get_job_status(job_id: str):
254
- """Get processing status for a job"""
255
-
256
- # Check completed jobs first
257
- if job_id in completed_jobs:
258
- return JobStatus(**completed_jobs[job_id])
259
-
260
- # Check active jobs
261
- if job_id in active_jobs:
262
- return JobStatus(**active_jobs[job_id])
263
-
264
- # Job not found
265
- raise HTTPException(status_code=404, detail="Job not found")
266
-
267
- @app.get("/download/{filename}")
268
- async def download_file(filename: str):
269
- """Download generated highlights or analysis file"""
270
- file_path = os.path.join(OUTPUTS_DIR, filename)
271
-
272
- if not os.path.exists(file_path):
273
- raise HTTPException(status_code=404, detail="File not found")
274
-
275
- # Determine media type
276
- if filename.endswith('.mp4'):
277
- media_type = 'video/mp4'
278
- elif filename.endswith('.json'):
279
- media_type = 'application/json'
280
- else:
281
- media_type = 'application/octet-stream'
282
-
283
- return FileResponse(
284
- path=file_path,
285
- media_type=media_type,
286
- filename=filename
287
- )
288
-
289
- @app.get("/jobs")
290
- async def list_jobs():
291
- """List all jobs (for debugging)"""
292
- return {
293
- "active_jobs": len(active_jobs),
294
- "completed_jobs": len(completed_jobs),
295
- "active": list(active_jobs.keys()),
296
- "completed": list(completed_jobs.keys())
297
- }
298
-
299
- @app.delete("/cleanup")
300
- async def cleanup_old_jobs():
301
- """Clean up old completed jobs and files"""
302
- cleaned_jobs = 0
303
- cleaned_files = 0
304
-
305
- # Keep only last 10 completed jobs
306
- if len(completed_jobs) > 10:
307
- jobs_to_remove = list(completed_jobs.keys())[:-10]
308
- for job_id in jobs_to_remove:
309
- del completed_jobs[job_id]
310
- cleaned_jobs += 1
311
-
312
- # Clean up old files (keep only files from last 20 jobs)
313
- all_jobs = list(active_jobs.keys()) + list(completed_jobs.keys())
314
-
315
- try:
316
- for filename in os.listdir(OUTPUTS_DIR):
317
- file_job_id = filename.split('_')[0]
318
- if file_job_id not in all_jobs:
319
- file_path = os.path.join(OUTPUTS_DIR, filename)
320
- os.unlink(file_path)
321
- cleaned_files += 1
322
- except Exception as e:
323
- logger.error(f"Error during cleanup: {e}")
324
-
325
- return {
326
- "message": "Cleanup completed",
327
- "cleaned_jobs": cleaned_jobs,
328
- "cleaned_files": cleaned_files
329
- }
330
 
331
  if __name__ == "__main__":
332
  import uvicorn
 
5
  """
6
 
7
  import os
 
8
 
9
  # Set cache directories to writable locations for HuggingFace Spaces
10
  # Use /tmp which is guaranteed to be writable in containers
 
19
  os.environ['HUGGINGFACE_HUB_CACHE'] = CACHE_DIR
20
  os.environ['TOKENIZERS_PARALLELISM'] = 'false'
21
 
22
+ from fastapi import FastAPI, UploadFile, File, HTTPException
 
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from pydantic import BaseModel
25
  import sys
26
  import uuid
27
  import json
 
28
  from pathlib import Path
 
 
29
 
30
  # Add src directory to path for imports
31
  sys.path.append(str(Path(__file__).parent / "src"))
 
36
  print("❌ Cannot import huggingface_exact_approach.py")
37
  sys.exit(1)
38
 
 
 
 
 
39
  # FastAPI app
40
  app = FastAPI(
41
  title="SmolVLM2 Optimized HuggingFace Video Highlights API",
42
  description="Generate intelligent video highlights using SmolVLM2 segment-based approach",
43
+ version="2.0.0",
44
+ openapi_url=None,
45
+ docs_url=None,
46
+ redoc_url=None
47
  )
48
 
49
  # Enable CORS for web apps
 
55
  allow_headers=["*"],
56
  )
57
 
 
 
 
 
 
 
58
  class AnalysisResponse(BaseModel):
59
+ success: bool
 
60
  message: str
61
+ video_description: str
62
+ analysis_file: str
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  # Create output directories with proper permissions
65
  TEMP_DIR = os.path.join("/tmp", "temp")
 
69
  os.makedirs(OUTPUTS_DIR, mode=0o755, exist_ok=True)
70
  os.makedirs(TEMP_DIR, mode=0o755, exist_ok=True)
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  @app.get("/health")
73
  async def health_check():
74
  """Health check endpoint"""
75
  return {"status": "healthy", "model": "SmolVLM2-256M-Video-Instruct"}
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  @app.post("/upload-video", response_model=AnalysisResponse)
78
  async def upload_video(
 
79
  video: UploadFile = File(...),
80
  segment_length: float = 5.0,
81
  model_name: str = "HuggingFaceTB/SmolVLM2-256M-Video-Instruct",
 
94
  if not video.content_type.startswith('video/'):
95
  raise HTTPException(status_code=400, detail="File must be a video")
96
 
 
 
 
97
  # Save uploaded video to temp file
98
+ job_id = str(uuid.uuid4())
99
  temp_video_path = os.path.join(TEMP_DIR, f"{job_id}_input.mp4")
100
  output_path = os.path.join(OUTPUTS_DIR, f"{job_id}_highlights.mp4")
101
+ analysis_path = os.path.join(OUTPUTS_DIR, f"{job_id}_analysis.json")
102
 
103
  try:
104
  # Save uploaded file
105
  with open(temp_video_path, "wb") as buffer:
106
  content = await video.read()
107
  buffer.write(content)
108
+
109
+ detector = VideoHighlightDetector(model_path=model_name)
110
+ results = detector.process_video(
111
+ video_path=temp_video_path,
112
+ output_path=output_path,
113
+ segment_length=segment_length,
114
+ with_effects=with_effects
 
 
 
 
 
 
 
 
 
115
  )
116
+
117
+ if "error" in results:
118
+ raise HTTPException(status_code=500, detail=results["error"])
119
+
120
+ with open(analysis_path, 'w') as f:
121
+ json.dump(results, f, indent=2)
122
+
123
  return AnalysisResponse(
124
+ success=True,
125
+ message="Video description generated successfully",
126
+ video_description=results.get("video_description", ""),
127
+ analysis_file=analysis_path
128
  )
 
129
  except Exception as e:
130
+ if isinstance(e, HTTPException):
131
+ raise e
132
+ raise HTTPException(status_code=500, detail=f"Failed to process upload: {str(e)}")
133
+ finally:
134
  if os.path.exists(temp_video_path):
135
  os.unlink(temp_video_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  if __name__ == "__main__":
138
  import uvicorn
test_api.py CHANGED
@@ -1,11 +1,7 @@
1
  #!/usr/bin/env python3
2
- """
3
- Test script for the HuggingFace Segment-Based Video Highlights API
4
- """
5
 
6
  import requests
7
- import time
8
- import json
9
  from pathlib import Path
10
 
11
  # API configuration
@@ -13,7 +9,7 @@ API_BASE = "http://localhost:7860" # Change to your deployed URL
13
  TEST_VIDEO = "../test_video/test.mp4" # Adjust path as needed
14
 
15
  def test_api():
16
- """Test the complete API workflow"""
17
  print("πŸ§ͺ Testing HuggingFace Segment-Based Video Highlights API")
18
 
19
  # Check if test video exists
@@ -42,39 +38,12 @@ def test_api():
42
  print(f"❌ Upload failed: {response.status_code} - {response.text}")
43
  return
44
 
45
- job_data = response.json()
46
- job_id = job_data['job_id']
47
- print(f"βœ… Video uploaded successfully! Job ID: {job_id}")
48
-
49
- # 3. Monitor job status
50
- print("\n3️⃣ Monitoring job progress...")
51
- while True:
52
- response = requests.get(f"{API_BASE}/job-status/{job_id}")
53
- if response.status_code != 200:
54
- print(f"❌ Status check failed: {response.status_code}")
55
- break
56
-
57
- status_data = response.json()
58
- print(f"Status: {status_data['status']} - {status_data['message']} ({status_data['progress']}%)")
59
-
60
- if status_data['status'] == 'completed':
61
- print(f"βœ… Processing completed!")
62
- print(f"πŸ“Ή Highlights URL: {status_data['highlights_url']}")
63
- print(f"πŸ“Š Analysis URL: {status_data['analysis_url']}")
64
- print(f"🎬 Segments: {status_data['selected_segments']}/{status_data['total_segments']}")
65
- print(f"πŸ“ˆ Compression: {status_data['compression_ratio']:.1%}")
66
- break
67
- elif status_data['status'] == 'failed':
68
- print(f"❌ Processing failed: {status_data['message']}")
69
- break
70
-
71
- time.sleep(5) # Wait 5 seconds before checking again
72
-
73
- # 4. Download results (optional)
74
- if status_data['status'] == 'completed':
75
- print("\n4️⃣ Download URLs available:")
76
- print(f"Highlights: {API_BASE}{status_data['highlights_url']}")
77
- print(f"Analysis: {API_BASE}{status_data['analysis_url']}")
78
 
79
  except requests.exceptions.ConnectionError:
80
  print(f"❌ Cannot connect to API at {API_BASE}")
 
1
  #!/usr/bin/env python3
2
+ """Test script for the current synchronous upload-video API."""
 
 
3
 
4
  import requests
 
 
5
  from pathlib import Path
6
 
7
  # API configuration
 
9
  TEST_VIDEO = "../test_video/test.mp4" # Adjust path as needed
10
 
11
  def test_api():
12
+ """Test health + upload-video workflow."""
13
  print("πŸ§ͺ Testing HuggingFace Segment-Based Video Highlights API")
14
 
15
  # Check if test video exists
 
38
  print(f"❌ Upload failed: {response.status_code} - {response.text}")
39
  return
40
 
41
+ result = response.json()
42
+ print("βœ… Upload and processing completed!")
43
+ print(f"Success: {result.get('success')}")
44
+ print(f"Message: {result.get('message')}")
45
+ print(f"Video Description: {result.get('video_description')}")
46
+ print(f"Analysis File: {result.get('analysis_file')}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  except requests.exceptions.ConnectionError:
49
  print(f"❌ Cannot connect to API at {API_BASE}")