Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import time | |
| import threading | |
| from fastapi import FastAPI, HTTPException, BackgroundTasks | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse, FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| import uvicorn | |
| from typing import Dict | |
| from pathlib import Path | |
| import subprocess | |
| from datetime import datetime | |
| import torch | |
| # Import from vision_analyzer (previously cursor_tracker) | |
| from vision_analyzer import ( | |
| main_processing_loop, | |
| processing_status, | |
| log_message, | |
| FRAMES_OUTPUT_FOLDER # Add this import for frames directory | |
| ) | |
| # FastAPI App Definition | |
| app = FastAPI(title="Video Analysis API", | |
| description="API to access video frame analysis results and extracted images", | |
| version="1.0.0") | |
| # Add CORS middleware to allow cross-origin requests | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # Allows all origins | |
| allow_credentials=True, | |
| allow_methods=["*"], # Allows all methods | |
| allow_headers=["*"], | |
| ) | |
| # Global variable to track if processing is running | |
| processing_thread = None | |
| def log_message(message): | |
| """Add a log message with timestamp""" | |
| timestamp = datetime.now().strftime("%H:%M:%S") | |
| log_entry = f"[{timestamp}] {message}" | |
| processing_status["logs"].append(log_entry) | |
| # Keep only the last 100 logs | |
| if len(processing_status["logs"]) > 100: | |
| processing_status["logs"] = processing_status["logs"][-100:] | |
| print(log_entry) | |
| async def startup_event(): | |
| """Run the processing loop in the background when the API starts""" | |
| global processing_thread | |
| if not (processing_thread and processing_thread.is_alive()): | |
| log_message("🚀 Starting RAR extraction, frame extraction, and vision analysis pipeline in background...") | |
| processing_thread = threading.Thread(target=main_processing_loop) | |
| processing_thread.daemon = True | |
| processing_thread.start() | |
| async def root(): | |
| """Root endpoint that returns basic info""" | |
| return { | |
| "message": "Video Analysis API", | |
| "status": "running", | |
| "endpoints": { | |
| "/status": "Get processing status", | |
| "/courses": "List all available course folders", | |
| "/images/{course_folder}": "List images in a course folder", | |
| "/images/{course_folder}/{frame_filename}": "Get specific frame image", | |
| "/start-processing": "Start processing pipeline", | |
| "/stop-processing": "Stop processing pipeline" | |
| } | |
| } | |
| async def get_status(): | |
| """Get current processing status""" | |
| return { | |
| "processing_status": processing_status, | |
| "frames_folder": FRAMES_OUTPUT_FOLDER, | |
| "frames_folder_exists": os.path.exists(FRAMES_OUTPUT_FOLDER) | |
| } | |
| # ===== NEW IMAGE SERVING ENDPOINTS ===== | |
| async def get_frame_image(course_folder: str, frame_filename: str): | |
| """ | |
| Serve extracted frame images from course folders | |
| Args: | |
| course_folder: The course folder name (e.g., "course1_video1_mp4_frames") | |
| frame_filename: The frame file name (e.g., "0001.png") | |
| """ | |
| # Construct the full path to the image | |
| image_path = os.path.join(FRAMES_OUTPUT_FOLDER, course_folder, frame_filename) | |
| # Check if file exists | |
| if not os.path.exists(image_path): | |
| raise HTTPException(status_code=404, detail=f"Image not found: {course_folder}/{frame_filename}") | |
| # Verify it's an image file | |
| if not frame_filename.lower().endswith(('.png', '.jpg', '.jpeg')): | |
| raise HTTPException(status_code=400, detail="File must be an image (PNG, JPG, JPEG)") | |
| # Return the image file | |
| return FileResponse(image_path) | |
| async def list_course_images(course_folder: str): | |
| """ | |
| List all available images in a specific course folder | |
| Args: | |
| course_folder: The course folder name | |
| """ | |
| folder_path = os.path.join(FRAMES_OUTPUT_FOLDER, course_folder) | |
| if not os.path.exists(folder_path): | |
| raise HTTPException(status_code=404, detail=f"Course folder not found: {course_folder}") | |
| # Get all image files | |
| image_files = [] | |
| for file in os.listdir(folder_path): | |
| if file.lower().endswith(('.png', '.jpg', '.jpeg')): | |
| file_path = os.path.join(folder_path, file) | |
| file_stats = os.stat(file_path) | |
| image_files.append({ | |
| "filename": file, | |
| "size_bytes": file_stats.st_size, | |
| "modified_time": time.ctime(file_stats.st_mtime), | |
| "url": f"/images/{course_folder}/{file}" | |
| }) | |
| return { | |
| "course_folder": course_folder, | |
| "total_images": len(image_files), | |
| "images": image_files | |
| } | |
| async def list_all_courses(): | |
| """ | |
| List all available course folders with their image counts | |
| """ | |
| if not os.path.exists(FRAMES_OUTPUT_FOLDER): | |
| return {"courses": [], "message": "Frames output folder does not exist yet"} | |
| courses = [] | |
| for folder in os.listdir(FRAMES_OUTPUT_FOLDER): | |
| folder_path = os.path.join(FRAMES_OUTPUT_FOLDER, folder) | |
| if os.path.isdir(folder_path): | |
| # Count image files | |
| image_count = len([f for f in os.listdir(folder_path) | |
| if f.lower().endswith(('.png', '.jpg', '.jpeg'))]) | |
| courses.append({ | |
| "course_folder": folder, | |
| "image_count": image_count, | |
| "images_url": f"/images/{folder}", | |
| "sample_image_url": f"/images/{folder}/0001.png" if image_count > 0 else None | |
| }) | |
| return { | |
| "total_courses": len(courses), | |
| "courses": courses | |
| } | |
| if __name__ == "__main__": | |
| # Start the FastAPI server | |
| print("Starting Video Analysis FastAPI Server...") | |
| print("API Documentation will be available at: http://localhost:8000/docs") | |
| print("API Root endpoint: http://localhost:8000/") | |
| # Ensure the analysis output folder exists | |
| os.makedirs(FRAMES_OUTPUT_FOLDER, exist_ok=True) | |
| uvicorn.run( | |
| app, | |
| host="0.0.0.0", | |
| port=8000, | |
| log_level="info", | |
| reload=False # Set to False for production | |
| ) | |