|
|
|
|
|
""" |
|
|
FastAPI Wrapper for Audio-Enhanced Video Highlights |
|
|
Converts your SmolVLM2 + Whisper system into a web API for Android apps |
|
|
""" |
|
|
|
|
|
from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks |
|
|
from fastapi.responses import FileResponse, JSONResponse |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from pydantic import BaseModel |
|
|
import os |
|
|
import sys |
|
|
import tempfile |
|
|
import uuid |
|
|
import json |
|
|
import asyncio |
|
|
from pathlib import Path |
|
|
from typing import Optional |
|
|
import logging |
|
|
|
|
|
|
|
|
sys.path.append(str(Path(__file__).parent / "src")) |
|
|
|
|
|
try: |
|
|
from audio_enhanced_highlights_final import AudioVisualAnalyzer, extract_frames_at_intervals, save_frame_at_time, create_highlights_video |
|
|
except ImportError: |
|
|
print("❌ Cannot import audio_enhanced_highlights_final.py") |
|
|
sys.exit(1) |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="SmolVLM2 Video Highlights API", |
|
|
description="Generate intelligent video highlights using SmolVLM2 + Whisper", |
|
|
version="1.0.0" |
|
|
) |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
class AnalysisRequest(BaseModel): |
|
|
interval: float = 20.0 |
|
|
min_score: float = 6.5 |
|
|
max_highlights: int = 3 |
|
|
whisper_model: str = "base" |
|
|
timeout: int = 35 |
|
|
|
|
|
class AnalysisResponse(BaseModel): |
|
|
job_id: str |
|
|
status: str |
|
|
message: str |
|
|
|
|
|
class JobStatus(BaseModel): |
|
|
job_id: str |
|
|
status: str |
|
|
progress: int |
|
|
message: str |
|
|
highlights_url: Optional[str] = None |
|
|
analysis_url: Optional[str] = None |
|
|
|
|
|
|
|
|
active_jobs = {} |
|
|
completed_jobs = {} |
|
|
|
|
|
|
|
|
os.makedirs("outputs", exist_ok=True) |
|
|
os.makedirs("temp", exist_ok=True) |
|
|
|
|
|
@app.get("/") |
|
|
async def root(): |
|
|
return { |
|
|
"message": "SmolVLM2 Video Highlights API", |
|
|
"version": "1.0.0", |
|
|
"endpoints": { |
|
|
"upload": "/upload-video", |
|
|
"status": "/job-status/{job_id}", |
|
|
"download": "/download/{filename}" |
|
|
} |
|
|
} |
|
|
|
|
|
@app.post("/upload-video", response_model=AnalysisResponse) |
|
|
async def upload_video( |
|
|
background_tasks: BackgroundTasks, |
|
|
video: UploadFile = File(...), |
|
|
interval: float = 20.0, |
|
|
min_score: float = 6.5, |
|
|
max_highlights: int = 3, |
|
|
whisper_model: str = "base", |
|
|
timeout: int = 35 |
|
|
): |
|
|
""" |
|
|
Upload a video and start processing highlights |
|
|
""" |
|
|
|
|
|
if not video.filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')): |
|
|
raise HTTPException(status_code=400, detail="Only video files are supported") |
|
|
|
|
|
|
|
|
job_id = str(uuid.uuid4()) |
|
|
|
|
|
try: |
|
|
|
|
|
temp_video_path = f"temp/{job_id}_{video.filename}" |
|
|
with open(temp_video_path, "wb") as f: |
|
|
content = await video.read() |
|
|
f.write(content) |
|
|
|
|
|
|
|
|
active_jobs[job_id] = { |
|
|
"status": "processing", |
|
|
"progress": 0, |
|
|
"message": "Video uploaded, starting analysis...", |
|
|
"video_path": temp_video_path, |
|
|
"settings": { |
|
|
"interval": interval, |
|
|
"min_score": min_score, |
|
|
"max_highlights": max_highlights, |
|
|
"whisper_model": whisper_model, |
|
|
"timeout": timeout |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
background_tasks.add_task( |
|
|
process_video_highlights, |
|
|
job_id, |
|
|
temp_video_path, |
|
|
interval, |
|
|
min_score, |
|
|
max_highlights, |
|
|
whisper_model, |
|
|
timeout |
|
|
) |
|
|
|
|
|
return AnalysisResponse( |
|
|
job_id=job_id, |
|
|
status="processing", |
|
|
message="Video uploaded successfully. Processing started." |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Upload failed: {e}") |
|
|
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") |
|
|
|
|
|
@app.get("/job-status/{job_id}", response_model=JobStatus) |
|
|
async def get_job_status(job_id: str): |
|
|
""" |
|
|
Get the status of a processing job |
|
|
""" |
|
|
|
|
|
if job_id in active_jobs: |
|
|
job = active_jobs[job_id] |
|
|
return JobStatus( |
|
|
job_id=job_id, |
|
|
status=job["status"], |
|
|
progress=job["progress"], |
|
|
message=job["message"] |
|
|
) |
|
|
|
|
|
|
|
|
if job_id in completed_jobs: |
|
|
job = completed_jobs[job_id] |
|
|
return JobStatus( |
|
|
job_id=job_id, |
|
|
status=job["status"], |
|
|
progress=100, |
|
|
message=job["message"], |
|
|
highlights_url=job.get("highlights_url"), |
|
|
analysis_url=job.get("analysis_url") |
|
|
) |
|
|
|
|
|
raise HTTPException(status_code=404, detail="Job not found") |
|
|
|
|
|
@app.get("/download/{filename}") |
|
|
async def download_file(filename: str): |
|
|
""" |
|
|
Download generated files |
|
|
""" |
|
|
file_path = f"outputs/{filename}" |
|
|
if not os.path.exists(file_path): |
|
|
raise HTTPException(status_code=404, detail="File not found") |
|
|
|
|
|
return FileResponse( |
|
|
file_path, |
|
|
media_type='application/octet-stream', |
|
|
filename=filename |
|
|
) |
|
|
|
|
|
async def process_video_highlights( |
|
|
job_id: str, |
|
|
video_path: str, |
|
|
interval: float, |
|
|
min_score: float, |
|
|
max_highlights: int, |
|
|
whisper_model: str, |
|
|
timeout: int |
|
|
): |
|
|
""" |
|
|
Background task to process video highlights |
|
|
""" |
|
|
try: |
|
|
|
|
|
active_jobs[job_id]["progress"] = 10 |
|
|
active_jobs[job_id]["message"] = "Initializing AI models..." |
|
|
|
|
|
|
|
|
analyzer = AudioVisualAnalyzer( |
|
|
whisper_model_size=whisper_model, |
|
|
timeout_seconds=timeout |
|
|
) |
|
|
|
|
|
active_jobs[job_id]["progress"] = 20 |
|
|
active_jobs[job_id]["message"] = "Extracting video segments..." |
|
|
|
|
|
|
|
|
segments = extract_frames_at_intervals(video_path, interval) |
|
|
total_segments = len(segments) |
|
|
|
|
|
active_jobs[job_id]["progress"] = 30 |
|
|
active_jobs[job_id]["message"] = f"Analyzing {total_segments} segments..." |
|
|
|
|
|
|
|
|
analyzed_segments = [] |
|
|
temp_frame_path = f"temp/{job_id}_frame.jpg" |
|
|
|
|
|
for i, segment in enumerate(segments): |
|
|
|
|
|
progress = 30 + int((i / total_segments) * 50) |
|
|
active_jobs[job_id]["progress"] = progress |
|
|
active_jobs[job_id]["message"] = f"Analyzing segment {i+1}/{total_segments}" |
|
|
|
|
|
|
|
|
if save_frame_at_time(video_path, segment['start_time'], temp_frame_path): |
|
|
|
|
|
analysis = analyzer.analyze_segment(video_path, segment, temp_frame_path) |
|
|
analyzed_segments.append(analysis) |
|
|
|
|
|
|
|
|
try: |
|
|
os.unlink(temp_frame_path) |
|
|
except: |
|
|
pass |
|
|
|
|
|
active_jobs[job_id]["progress"] = 85 |
|
|
active_jobs[job_id]["message"] = "Selecting best highlights..." |
|
|
|
|
|
|
|
|
analyzed_segments.sort(key=lambda x: x['combined_score'], reverse=True) |
|
|
selected_segments = [s for s in analyzed_segments if s['combined_score'] >= min_score] |
|
|
selected_segments = selected_segments[:max_highlights] |
|
|
|
|
|
if not selected_segments: |
|
|
raise Exception(f"No segments met minimum score of {min_score}") |
|
|
|
|
|
active_jobs[job_id]["progress"] = 90 |
|
|
active_jobs[job_id]["message"] = f"Creating highlights video with {len(selected_segments)} segments..." |
|
|
|
|
|
|
|
|
highlights_filename = f"{job_id}_highlights.mp4" |
|
|
analysis_filename = f"{job_id}_analysis.json" |
|
|
highlights_path = f"outputs/{highlights_filename}" |
|
|
analysis_path = f"outputs/{analysis_filename}" |
|
|
|
|
|
|
|
|
success = create_highlights_video(video_path, selected_segments, highlights_path) |
|
|
|
|
|
if not success: |
|
|
raise Exception("Failed to create highlights video") |
|
|
|
|
|
|
|
|
analysis_data = { |
|
|
'job_id': job_id, |
|
|
'input_video': video_path, |
|
|
'output_video': highlights_path, |
|
|
'settings': { |
|
|
'interval': interval, |
|
|
'min_score': min_score, |
|
|
'max_highlights': max_highlights, |
|
|
'whisper_model': whisper_model, |
|
|
'timeout': timeout |
|
|
}, |
|
|
'segments': analyzed_segments, |
|
|
'selected_segments': selected_segments, |
|
|
'summary': { |
|
|
'total_segments': len(analyzed_segments), |
|
|
'selected_segments': len(selected_segments), |
|
|
'processing_time': "Completed successfully" |
|
|
} |
|
|
} |
|
|
|
|
|
with open(analysis_path, 'w') as f: |
|
|
json.dump(analysis_data, f, indent=2) |
|
|
|
|
|
|
|
|
completed_jobs[job_id] = { |
|
|
"status": "completed", |
|
|
"message": f"Successfully created highlights with {len(selected_segments)} segments", |
|
|
"highlights_url": f"/download/{highlights_filename}", |
|
|
"analysis_url": f"/download/{analysis_filename}", |
|
|
"summary": analysis_data['summary'] |
|
|
} |
|
|
|
|
|
|
|
|
del active_jobs[job_id] |
|
|
|
|
|
|
|
|
try: |
|
|
os.unlink(video_path) |
|
|
except: |
|
|
pass |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Processing failed for job {job_id}: {e}") |
|
|
|
|
|
|
|
|
completed_jobs[job_id] = { |
|
|
"status": "failed", |
|
|
"message": f"Processing failed: {str(e)}", |
|
|
"highlights_url": None, |
|
|
"analysis_url": None |
|
|
} |
|
|
|
|
|
|
|
|
if job_id in active_jobs: |
|
|
del active_jobs[job_id] |
|
|
|
|
|
|
|
|
try: |
|
|
os.unlink(video_path) |
|
|
except: |
|
|
pass |
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
uvicorn.run(app, host="0.0.0.0", port=8000) |
|
|
|