Factor Studios commited on
Commit
1f3d307
Β·
verified Β·
1 Parent(s): 5f3191e

Upload 26 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,10 @@
1
- ---
2
- title: VS3
3
- emoji: πŸš€
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.37.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: Vis1
3
+ emoji: 🐨
4
+ colorFrom: pink
5
+ colorTo: pink
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
annotations/readme.md ADDED
@@ -0,0 +1 @@
 
 
1
+ #Hi there
cursors/1.png ADDED
cursors/10.png ADDED
cursors/11.png ADDED
cursors/12.png ADDED
cursors/13.png ADDED
cursors/14.png ADDED
cursors/15.png ADDED
cursors/16.png ADDED
cursors/2.png ADDED
cursors/3.png ADDED
cursors/4.png ADDED
cursors/5.png ADDED
cursors/6.png ADDED
cursors/7.png ADDED
cursors/8.png ADDED
cursors/9.png ADDED
download_api.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import threading
5
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.responses import JSONResponse, FileResponse
8
+ import uvicorn
9
+ from typing import Dict
10
+ from pathlib import Path
11
+ import subprocess
12
+ from datetime import datetime
13
+
14
+ import torch
15
+
16
+
17
+ # Import from vision_analyzer (previously cursor_tracker)
18
+ from vision_analyzer import (
19
+ main_processing_loop,
20
+ processing_status,
21
+ ANALYSIS_OUTPUT_FOLDER, # Changed from CURSOR_TRACKING_OUTPUT_FOLDER
22
+ log_message
23
+ )
24
+
25
+ # FastAPI App Definition
26
+ app = FastAPI(title="Video Analysis API",
27
+ description="API to access video frame analysis results",
28
+ version="1.0.0")
29
+
30
+ # Add CORS middleware to allow cross-origin requests
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"], # Allows all origins
34
+ allow_credentials=True,
35
+ allow_methods=["*"], # Allows all methods
36
+ allow_headers=["*"],
37
+ )
38
+
39
+ # Global variable to track if processing is running
40
+ processing_thread = None
41
+
42
+ def log_message(message):
43
+ """Add a log message with timestamp"""
44
+ timestamp = datetime.now().strftime("%H:%M:%S")
45
+ log_entry = f"[{timestamp}] {message}"
46
+ processing_status["logs"].append(log_entry)
47
+
48
+ # Keep only the last 100 logs
49
+ if len(processing_status["logs"]) > 100:
50
+ processing_status["logs"] = processing_status["logs"][-100:]
51
+
52
+ print(log_entry)
53
+
54
+ @app.on_event("startup")
55
+ async def startup_event():
56
+ """Run the processing loop in the background when the API starts"""
57
+ global processing_thread
58
+ if not (processing_thread and processing_thread.is_alive()):
59
+ log_message("πŸš€ Starting RAR extraction, frame extraction, and vision analysis pipeline in background...")
60
+ processing_thread = threading.Thread(target=main_processing_loop)
61
+ processing_thread.daemon = True
62
+ processing_thread.start()
63
+
64
+ @app.get("/")
65
+ async def root():
66
+ """Root endpoint that returns basic info"""
67
+ return {
68
+ "message": "Video Analysis API",
69
+ "status": "running",
70
+ "endpoints": {
71
+ "/status": "Get processing status",
72
+ "/analysis-data": "List available analysis files",
73
+ "/analysis-data/{filename}": "Get specific analysis data",
74
+ "/start-processing": "Start processing pipeline",
75
+ "/stop-processing": "Stop processing pipeline"
76
+ }
77
+ }
78
+
79
+ @app.get("/status")
80
+ async def get_status():
81
+ """Get current processing status"""
82
+ return {
83
+ "processing_status": processing_status,
84
+ "analysis_folder": ANALYSIS_OUTPUT_FOLDER,
85
+ "folder_exists": os.path.exists(ANALYSIS_OUTPUT_FOLDER)
86
+ }
87
+
88
+ @app.get("/analysis-data")
89
+ async def list_analysis_data():
90
+ """List all available analysis JSON files"""
91
+ if not os.path.exists(ANALYSIS_OUTPUT_FOLDER):
92
+ return {"files": [], "message": "Analysis output folder does not exist yet"}
93
+
94
+ json_files = []
95
+ for file in os.listdir(ANALYSIS_OUTPUT_FOLDER):
96
+ if file.endswith(".json"):
97
+ file_path = os.path.join(ANALYSIS_OUTPUT_FOLDER, file)
98
+ file_stats = os.stat(file_path)
99
+ json_files.append({
100
+ "filename": file,
101
+ "size_bytes": file_stats.st_size,
102
+ "modified_time": time.ctime(file_stats.st_mtime),
103
+ "download_url": f"/analysis-data/{file}"
104
+ })
105
+
106
+ return {
107
+ "files": json_files,
108
+ "total_files": len(json_files),
109
+ "folder_path": ANALYSIS_OUTPUT_FOLDER
110
+ }
111
+
112
+ @app.get("/analysis-data/{filename}")
113
+ async def get_analysis_data(filename: str):
114
+ """Get specific analysis data by filename"""
115
+ if not filename.endswith(".json"):
116
+ raise HTTPException(status_code=400, detail="File must be a JSON file")
117
+
118
+ file_path = os.path.join(ANALYSIS_OUTPUT_FOLDER, filename)
119
+
120
+ if not os.path.exists(file_path):
121
+ raise HTTPException(status_code=404, detail=f"File {filename} not found")
122
+
123
+ try:
124
+ with open(file_path, "r") as f:
125
+ data = json.load(f)
126
+
127
+ # Add metadata
128
+ file_stats = os.stat(file_path)
129
+
130
+ # Extract summary information
131
+ frame_analyses = data.get("frame_analyses", [])
132
+ summary = data.get("summary", {})
133
+
134
+ response_data = {
135
+ "filename": filename,
136
+ "file_size_bytes": file_stats.st_size,
137
+ "modified_time": time.ctime(file_stats.st_mtime),
138
+ "total_frames": len(frame_analyses),
139
+ "summary": summary,
140
+ "frame_samples": frame_analyses[:5] # Return first 5 frames as samples
141
+ }
142
+
143
+ return response_data
144
+
145
+ except json.JSONDecodeError:
146
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filename}")
147
+ except Exception as e:
148
+ raise HTTPException(status_code=500, detail=f"Error reading file {filename}: {str(e)}")
149
+
150
+ @app.get("/analysis-data/{filename}/full")
151
+ async def get_full_analysis_data(filename: str):
152
+ """Get the complete analysis data including all frames"""
153
+ if not filename.endswith(".json"):
154
+ raise HTTPException(status_code=400, detail="File must be a JSON file")
155
+
156
+ file_path = os.path.join(ANALYSIS_OUTPUT_FOLDER, filename)
157
+
158
+ if not os.path.exists(file_path):
159
+ raise HTTPException(status_code=404, detail=f"File {filename} not found")
160
+
161
+ try:
162
+ with open(file_path, "r") as f:
163
+ data = json.load(f)
164
+
165
+ # Add metadata
166
+ file_stats = os.stat(file_path)
167
+ data["metadata"] = {
168
+ "filename": filename,
169
+ "file_size_bytes": file_stats.st_size,
170
+ "modified_time": time.ctime(file_stats.st_mtime)
171
+ }
172
+
173
+ return data
174
+
175
+ except json.JSONDecodeError:
176
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filename}")
177
+ except Exception as e:
178
+ raise HTTPException(status_code=500, detail=f"Error reading file {filename}: {str(e)}")
179
+
180
+ @app.post("/start-processing")
181
+ async def start_processing(background_tasks: BackgroundTasks, start_index: int = 0):
182
+ """Start the processing pipeline in the background"""
183
+ global processing_thread
184
+
185
+ if processing_thread and processing_thread.is_alive():
186
+ return {"message": "Processing is already running", "status": "already_running"}
187
+
188
+ if processing_status["is_running"]:
189
+ return {"message": "Processing is already running", "status": "already_running"}
190
+
191
+ # Start processing in a background thread
192
+ processing_thread = threading.Thread(target=main_processing_loop, args=(start_index,))
193
+ processing_thread.daemon = True
194
+ processing_thread.start()
195
+
196
+ return {"message": f"Processing started in background from index {start_index}", "status": "started"}
197
+
198
+ @app.post("/stop-processing")
199
+ async def stop_processing():
200
+ """Stop the processing pipeline"""
201
+ global processing_thread
202
+
203
+ if not processing_status["is_running"] and (not processing_thread or not processing_thread.is_alive()):
204
+ return {"message": "No processing is currently running", "status": "not_running"}
205
+
206
+ # Note: This is a graceful stop request
207
+ processing_status["is_running"] = False
208
+
209
+ return {"message": "Stop signal sent to processing pipeline", "status": "stop_requested"}
210
+
211
+ @app.get("/analysis-data/{filename}/summary")
212
+ async def get_analysis_summary(filename: str):
213
+ """Get a summary of the analysis data"""
214
+ if not filename.endswith(".json"):
215
+ raise HTTPException(status_code=400, detail="File must be a JSON file")
216
+
217
+ file_path = os.path.join(ANALYSIS_OUTPUT_FOLDER, filename)
218
+
219
+ if not os.path.exists(file_path):
220
+ raise HTTPException(status_code=404, detail=f"File {filename} not found")
221
+
222
+ try:
223
+ with open(file_path, "r") as f:
224
+ data = json.load(f)
225
+
226
+ # Get basic statistics
227
+ frame_analyses = data.get("frame_analyses", [])
228
+ summary = data.get("summary", {})
229
+
230
+ # Count frames with descriptions
231
+ frames_with_descriptions = len([f for f in frame_analyses if f.get("description")])
232
+
233
+ file_stats = os.stat(file_path)
234
+
235
+ return {
236
+ "filename": filename,
237
+ "file_size_bytes": file_stats.st_size,
238
+ "modified_time": time.ctime(file_stats.st_mtime),
239
+ "total_frames": len(frame_analyses),
240
+ "frames_with_descriptions": frames_with_descriptions,
241
+ "summary": summary,
242
+ "steps": summary.get("steps", []),
243
+ "high_level_goal": summary.get("high_level_goal", ""),
244
+ "final_goal": summary.get("final_goal", "")
245
+ }
246
+
247
+ except json.JSONDecodeError:
248
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filename}")
249
+ except Exception as e:
250
+ raise HTTPException(status_code=500, detail=f"Error reading file {filename}: {str(e)}")
251
+
252
+ if __name__ == "__main__":
253
+ # Start the FastAPI server
254
+ print("Starting Video Analysis FastAPI Server...")
255
+ print("API Documentation will be available at: http://localhost:8000/docs")
256
+ print("API Root endpoint: http://localhost:8000/")
257
+
258
+ # Ensure the analysis output folder exists
259
+ os.makedirs(ANALYSIS_OUTPUT_FOLDER, exist_ok=True)
260
+
261
+ uvicorn.run(
262
+ app,
263
+ host="0.0.0.0",
264
+ port=8000,
265
+ log_level="info",
266
+ reload=False # Set to False for production
267
+ )
index.html ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Advanced web-based dashboard for monitoring and managing video analysis operations. Real-time processing status, file management, and control interface.">
7
+ <meta name="keywords" content="video analysis, vision processing, dashboard, monitoring, API interface">
8
+ <meta name="author" content="Video Analysis Dashboard">
9
+ <title>Video Analysis Dashboard - Real-time Processing Monitor</title>
10
+ <link rel="stylesheet" href="/static/style.css">
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
+ </head>
14
+ <body>
15
+ <div class="container">
16
+ <!-- Header -->
17
+ <header class="header">
18
+ <div class="header-content">
19
+ <div class="logo">
20
+ <i class="fas fa-eye"></i>
21
+ <h1>Video Analysis Dashboard</h1>
22
+ </div>
23
+ <div class="header-actions">
24
+ <button id="refreshBtn" class="btn btn-secondary">
25
+ <i class="fas fa-sync-alt"></i>
26
+ Refresh
27
+ </button>
28
+ <div class="theme-toggle">
29
+ <button id="themeToggle" class="btn btn-icon">
30
+ <i class="fas fa-moon"></i>
31
+ </button>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </header>
36
+
37
+ <!-- Main Content -->
38
+ <main class="main-content">
39
+ <!-- Status Section -->
40
+ <section class="status-section">
41
+ <div class="card">
42
+ <div class="card-header">
43
+ <h2><i class="fas fa-chart-line"></i> Processing Status</h2>
44
+ <div class="status-indicator" id="statusIndicator">
45
+ <span class="status-dot"></span>
46
+ <span class="status-text">Loading...</span>
47
+ </div>
48
+ </div>
49
+ <div class="card-content">
50
+ <div class="stats-grid">
51
+ <div class="stat-item">
52
+ <div class="stat-value" id="totalFiles">-</div>
53
+ <div class="stat-label">Total Files</div>
54
+ </div>
55
+ <div class="stat-item">
56
+ <div class="stat-value" id="processedFiles">-</div>
57
+ <div class="stat-label">Processed</div>
58
+ </div>
59
+ <div class="stat-item">
60
+ <div class="stat-value" id="extractedCourses">-</div>
61
+ <div class="stat-label">Courses</div>
62
+ </div>
63
+ <div class="stat-item">
64
+ <div class="stat-value" id="extractedVideos">-</div>
65
+ <div class="stat-label">Videos</div>
66
+ </div>
67
+ <div class="stat-item">
68
+ <div class="stat-value" id="extractedFrames">-</div>
69
+ <div class="stat-label">Frames</div>
70
+ </div>
71
+ <div class="stat-item">
72
+ <div class="stat-value" id="analyzedFrames">-</div>
73
+ <div class="stat-label">Frames Analyzed</div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="progress-section">
78
+ <div class="progress-info">
79
+ <span>Current File:</span>
80
+ <span id="currentFile" class="current-file">None</span>
81
+ </div>
82
+ <div class="progress-bar">
83
+ <div class="progress-fill" id="progressFill"></div>
84
+ </div>
85
+ <div class="progress-text" id="progressText">0%</div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </section>
90
+
91
+ <!-- Control Section -->
92
+ <section class="control-section">
93
+ <div class="card">
94
+ <div class="card-header">
95
+ <h2><i class="fas fa-cogs"></i> Processing Controls</h2>
96
+ </div>
97
+ <div class="card-content">
98
+ <div class="control-group">
99
+ <div class="input-group">
100
+ <label for="startIndex">Start Index for RAR Fetching:</label>
101
+ <input type="number" id="startIndex" min="0" value="0" class="input">
102
+ <span class="input-help">Specify which index to start processing from</span>
103
+ </div>
104
+ <div class="button-group">
105
+ <button id="startProcessing" class="btn btn-primary">
106
+ <i class="fas fa-play"></i>
107
+ Start Processing
108
+ </button>
109
+ <button id="stopProcessing" class="btn btn-danger">
110
+ <i class="fas fa-stop"></i>
111
+ Stop Processing
112
+ </button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </section>
118
+
119
+ <!-- Analysis Data Files Section -->
120
+ <section class="files-section">
121
+ <div class="card">
122
+ <div class="card-header">
123
+ <h2><i class="fas fa-file-alt"></i> Analysis Results</h2>
124
+ <div class="file-count" id="fileCount">0 files</div>
125
+ </div>
126
+ <div class="card-content">
127
+ <div class="files-grid" id="filesGrid">
128
+ <!-- Files will be populated here -->
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </section>
133
+
134
+ <!-- Logs Section -->
135
+ <section class="logs-section">
136
+ <div class="card">
137
+ <div class="card-header">
138
+ <h2><i class="fas fa-terminal"></i> Processing Logs</h2>
139
+ <div class="log-controls">
140
+ <button id="clearLogs" class="btn btn-secondary btn-sm">
141
+ <i class="fas fa-trash"></i>
142
+ Clear
143
+ </button>
144
+ <button id="autoScroll" class="btn btn-secondary btn-sm active">
145
+ <i class="fas fa-arrow-down"></i>
146
+ Auto-scroll
147
+ </button>
148
+ </div>
149
+ </div>
150
+ <div class="card-content">
151
+ <div class="logs-container" id="logsContainer">
152
+ <div class="log-entry">
153
+ <span class="log-time">[Loading...]</span>
154
+ <span class="log-message">Initializing dashboard...</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </section>
160
+ </main>
161
+ </div>
162
+
163
+ <!-- File Details Modal -->
164
+ <div id="fileModal" class="modal">
165
+ <div class="modal-content">
166
+ <div class="modal-header">
167
+ <h3 id="modalTitle">Analysis Details</h3>
168
+ <button class="modal-close" id="modalClose">
169
+ <i class="fas fa-times"></i>
170
+ </button>
171
+ </div>
172
+ <div class="modal-body" id="modalBody">
173
+ <!-- Analysis details will be populated here -->
174
+ </div>
175
+ <div class="modal-footer">
176
+ <button id="downloadFile" class="btn btn-primary">
177
+ <i class="fas fa-download"></i>
178
+ Download JSON
179
+ </button>
180
+ <button id="viewSummary" class="btn btn-secondary">
181
+ <i class="fas fa-list-alt"></i>
182
+ View Summary
183
+ </button>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Loading Overlay -->
189
+ <div id="loadingOverlay" class="loading-overlay">
190
+ <div class="loading-spinner">
191
+ <i class="fas fa-spinner fa-spin"></i>
192
+ <p>Loading...</p>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Toast Notifications -->
197
+ <div id="toastContainer" class="toast-container"></div>
198
+
199
+ <script src="/static/script.js"></script>
200
+ </body>
201
+ </html>
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ accelerate
3
+ fastapi
4
+ uvicorn
5
+ opencv-python-headless
6
+ numpy
7
+ pathlib
8
+ huggingface_hub
9
+ pillow
10
+ rarfile
11
+ transformers==4.36.2
12
+ timm==0.9.12
13
+ einops==0.7.0
14
+ python-multipart
static/readme.md ADDED
File without changes
static/script.js ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // Configuration
3
+ const API_BASE_URL = window.location.origin;
4
+ const REFRESH_INTERVAL = 10000; // 10 seconds
5
+ const MAX_LOGS = 100;
6
+
7
+ // Global state
8
+ let refreshInterval;
9
+ let autoScrollEnabled = true;
10
+ let currentFiles = [];
11
+ let selectedFile = null;
12
+ let apiConnected = false;
13
+
14
+ // DOM Elements
15
+ const elements = {
16
+ statusIndicator: document.getElementById('statusIndicator'),
17
+ totalFiles: document.getElementById('totalFiles'),
18
+ processedFiles: document.getElementById('processedFiles'),
19
+ extractedCourses: document.getElementById('extractedCourses'),
20
+ extractedVideos: document.getElementById('extractedVideos'),
21
+ extractedFrames: document.getElementById('extractedFrames'),
22
+ analyzedFrames: document.getElementById('analyzedFrames'),
23
+ currentFile: document.getElementById('currentFile'),
24
+ progressFill: document.getElementById('progressFill'),
25
+ progressText: document.getElementById('progressText'),
26
+ startIndex: document.getElementById('startIndex'),
27
+ startProcessing: document.getElementById('startProcessing'),
28
+ stopProcessing: document.getElementById('stopProcessing'),
29
+ refreshBtn: document.getElementById('refreshBtn'),
30
+ themeToggle: document.getElementById('themeToggle'),
31
+ fileCount: document.getElementById('fileCount'),
32
+ filesGrid: document.getElementById('filesGrid'),
33
+ logsContainer: document.getElementById('logsContainer'),
34
+ clearLogs: document.getElementById('clearLogs'),
35
+ autoScroll: document.getElementById('autoScroll'),
36
+ fileModal: document.getElementById('fileModal'),
37
+ modalTitle: document.getElementById('modalTitle'),
38
+ modalBody: document.getElementById('modalBody'),
39
+ modalClose: document.getElementById('modalClose'),
40
+ downloadFile: document.getElementById('downloadFile'),
41
+ viewSummary: document.getElementById('viewSummary'),
42
+ loadingOverlay: document.getElementById('loadingOverlay'),
43
+ toastContainer: document.getElementById('toastContainer')
44
+ };
45
+
46
+ // Initialize the application
47
+ initializeTheme();
48
+ setupEventListeners();
49
+ startAutoRefresh();
50
+ fetchInitialData();
51
+
52
+ // Theme Management
53
+ function initializeTheme() {
54
+ const savedTheme = localStorage.getItem('theme') || 'light';
55
+ document.documentElement.setAttribute('data-theme', savedTheme);
56
+ updateThemeIcon(savedTheme);
57
+ }
58
+
59
+ function toggleTheme() {
60
+ const currentTheme = document.documentElement.getAttribute('data-theme');
61
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
62
+ document.documentElement.setAttribute('data-theme', newTheme);
63
+ localStorage.setItem('theme', newTheme);
64
+ updateThemeIcon(newTheme);
65
+ }
66
+
67
+ function updateThemeIcon(theme) {
68
+ const icon = elements.themeToggle.querySelector('i');
69
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
70
+ }
71
+
72
+ // Event Listeners
73
+ function setupEventListeners() {
74
+ elements.themeToggle.addEventListener('click', toggleTheme);
75
+ elements.refreshBtn.addEventListener('click', () => {
76
+ showToast('Refreshing data...', 'info');
77
+ fetchAllData();
78
+ });
79
+
80
+ elements.startProcessing.addEventListener('click', startProcessing);
81
+ elements.stopProcessing.addEventListener('click', stopProcessing);
82
+
83
+ elements.clearLogs.addEventListener('click', clearLogs);
84
+ elements.autoScroll.addEventListener('click', toggleAutoScroll);
85
+
86
+ elements.modalClose.addEventListener('click', closeModal);
87
+ elements.fileModal.addEventListener('click', (e) => {
88
+ if (e.target === elements.fileModal) closeModal();
89
+ });
90
+
91
+ elements.downloadFile.addEventListener('click', downloadSelectedFile);
92
+ elements.viewSummary.addEventListener('click', viewSummary);
93
+
94
+ // Keyboard shortcuts
95
+ document.addEventListener('keydown', (e) => {
96
+ if (e.key === 'Escape') closeModal();
97
+ if (e.key === 'F5') {
98
+ e.preventDefault();
99
+ fetchAllData();
100
+ }
101
+ });
102
+ }
103
+
104
+ // API Functions
105
+ async function apiRequest(endpoint, options = {}) {
106
+ try {
107
+ showLoading();
108
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ ...options.headers
112
+ },
113
+ ...options
114
+ });
115
+
116
+ if (!response.ok) {
117
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
118
+ }
119
+
120
+ apiConnected = true;
121
+ return await response.json();
122
+ } catch (error) {
123
+ console.error('API request failed:', error);
124
+ apiConnected = false;
125
+
126
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
127
+ showToast('API server not running. Please start the video analysis API on port 8000.', 'warning');
128
+ } else {
129
+ showToast(`API Error: ${error.message}`, 'error');
130
+ }
131
+ throw error;
132
+ } finally {
133
+ hideLoading();
134
+ }
135
+ }
136
+
137
+ async function fetchStatus() {
138
+ try {
139
+ const data = await apiRequest('/status');
140
+ updateStatusDisplay(data.processing_status);
141
+ return data;
142
+ } catch (error) {
143
+ // Demo data when API is not connected
144
+ const demoStatus = {
145
+ is_running: false,
146
+ total_files: 150,
147
+ processed_files: 45,
148
+ extracted_courses: 12,
149
+ extracted_videos: 89,
150
+ extracted_frames_count: 15420,
151
+ analyzed_frames_count: 12000,
152
+ current_file: null,
153
+ logs: [
154
+ '[Demo Mode] API server not connected',
155
+ '[Demo Mode] This is a demonstration of the UI',
156
+ '[Demo Mode] Start the API server on port 8000 to see real data'
157
+ ]
158
+ };
159
+ updateStatusDisplay(demoStatus);
160
+ }
161
+ }
162
+
163
+ async function fetchAnalysisData() {
164
+ try {
165
+ const data = await apiRequest('/analysis-data');
166
+ currentFiles = data.files || [];
167
+ updateFilesDisplay(currentFiles);
168
+ return data;
169
+ } catch (error) {
170
+ // Demo files when API is not connected
171
+ const demoFiles = [
172
+ {
173
+ filename: 'course_1_video_1_mp4_analysis.json',
174
+ size_bytes: 45678,
175
+ modified_time: 'Sun Jul 13 19:30:15 2025'
176
+ },
177
+ {
178
+ filename: 'course_2_video_3_mp4_analysis.json',
179
+ size_bytes: 67890,
180
+ modified_time: 'Sun Jul 13 18:45:22 2025'
181
+ },
182
+ {
183
+ filename: 'course_3_video_2_mp4_analysis.json',
184
+ size_bytes: 34567,
185
+ modified_time: 'Sun Jul 13 17:20:10 2025'
186
+ }
187
+ ];
188
+ currentFiles = demoFiles;
189
+ updateFilesDisplay(demoFiles);
190
+ }
191
+ }
192
+
193
+ async function fetchFileDetails(filename) {
194
+ try {
195
+ const data = await apiRequest(`/analysis-data/${filename}/summary`);
196
+ return data;
197
+ } catch (error) {
198
+ showToast(`Failed to fetch details for ${filename}`, 'error');
199
+ return null;
200
+ }
201
+ }
202
+
203
+ async function startProcessing() {
204
+ try {
205
+ const startIndex = parseInt(elements.startIndex.value) || 0;
206
+ const data = await apiRequest('/start-processing', {
207
+ method: 'POST',
208
+ body: JSON.stringify({ start_index: startIndex })
209
+ });
210
+
211
+ showToast(data.message, data.status === 'started' ? 'success' : 'info');
212
+
213
+ if (data.status === 'started') {
214
+ elements.startProcessing.disabled = true;
215
+ elements.stopProcessing.disabled = false;
216
+ }
217
+ } catch (error) {
218
+ showToast('Failed to start processing', 'error');
219
+ }
220
+ }
221
+
222
+ async function stopProcessing() {
223
+ try {
224
+ const data = await apiRequest('/stop-processing', {
225
+ method: 'POST'
226
+ });
227
+
228
+ showToast(data.message, 'info');
229
+ elements.startProcessing.disabled = false;
230
+ elements.stopProcessing.disabled = true;
231
+ } catch (error) {
232
+ showToast('Failed to stop processing', 'error');
233
+ }
234
+ }
235
+
236
+ // Display Update Functions
237
+ function updateStatusDisplay(status) {
238
+ // Update status indicator
239
+ const statusDot = elements.statusIndicator.querySelector('.status-dot');
240
+ const statusText = elements.statusIndicator.querySelector('.status-text');
241
+
242
+ if (status.is_running) {
243
+ statusDot.className = 'status-dot running';
244
+ statusText.textContent = 'Processing';
245
+ elements.startProcessing.disabled = true;
246
+ elements.stopProcessing.disabled = false;
247
+ } else {
248
+ statusDot.className = 'status-dot stopped';
249
+ statusText.textContent = 'Idle';
250
+ elements.startProcessing.disabled = false;
251
+ elements.stopProcessing.disabled = true;
252
+ }
253
+
254
+ // Update statistics
255
+ elements.totalFiles.textContent = status.total_files || 0;
256
+ elements.processedFiles.textContent = status.processed_files || 0;
257
+ elements.extractedCourses.textContent = status.extracted_courses || 0;
258
+ elements.extractedVideos.textContent = status.extracted_videos || 0;
259
+ elements.extractedFrames.textContent = status.extracted_frames_count || 0;
260
+ elements.analyzedFrames.textContent = status.analyzed_frames_count || 0;
261
+
262
+ // Update current file and progress
263
+ const currentFile = status.current_file || 'None';
264
+ elements.currentFile.textContent = currentFile;
265
+
266
+ const progress = status.total_files > 0 ?
267
+ Math.round((status.processed_files / status.total_files) * 100) : 0;
268
+ elements.progressFill.style.width = `${progress}%`;
269
+ elements.progressText.textContent = `${progress}%`;
270
+
271
+ // Update logs
272
+ if (status.logs && status.logs.length > 0) {
273
+ updateLogs(status.logs);
274
+ }
275
+ }
276
+
277
+ function updateFilesDisplay(files) {
278
+ elements.fileCount.textContent = `${files.length} files`;
279
+
280
+ if (files.length === 0) {
281
+ elements.filesGrid.innerHTML = `
282
+ <div class="no-files">
283
+ <i class="fas fa-folder-open"></i>
284
+ <p>No analysis files found yet.</p>
285
+ <p>Files will appear here after processing completes.</p>
286
+ </div>
287
+ `;
288
+ return;
289
+ }
290
+
291
+ elements.filesGrid.innerHTML = files.map(file => `
292
+ <div class="file-card" onclick="openFileModal('${file.filename}')">
293
+ <div class="file-header">
294
+ <div class="file-name">${file.filename}</div>
295
+ <div class="file-size">${formatFileSize(file.size_bytes)}</div>
296
+ </div>
297
+ <div class="file-stats">
298
+ <div class="file-stat">
299
+ <span class="file-stat-label">Modified:</span>
300
+ <span class="file-stat-value">${formatDate(file.modified_time)}</span>
301
+ </div>
302
+ </div>
303
+ <div class="file-actions">
304
+ <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')">
305
+ <i class="fas fa-download"></i>
306
+ Download
307
+ </button>
308
+ <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')">
309
+ <i class="fas fa-eye"></i>
310
+ Details
311
+ </button>
312
+ </div>
313
+ </div>
314
+ `).join('');
315
+ }
316
+
317
+ function updateLogs(logs) {
318
+ const container = elements.logsContainer;
319
+
320
+ if (logs.length > 0) {
321
+ container.innerHTML = '';
322
+ }
323
+
324
+ logs.slice(-MAX_LOGS).forEach(log => {
325
+ const logEntry = document.createElement('div');
326
+ logEntry.className = 'log-entry';
327
+
328
+ let logType = '';
329
+ if (log.includes('❌') || log.includes('ERROR') || log.includes('Failed')) {
330
+ logType = 'error';
331
+ } else if (log.includes('βœ…') || log.includes('SUCCESS') || log.includes('Successfully')) {
332
+ logType = 'success';
333
+ } else if (log.includes('⚠️') || log.includes('WARN')) {
334
+ logType = 'warning';
335
+ }
336
+
337
+ if (logType) {
338
+ logEntry.classList.add(logType);
339
+ }
340
+
341
+ const timestampMatch = log.match(/^\[([^\]]+)\]/);
342
+ const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString();
343
+ const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log;
344
+
345
+ logEntry.innerHTML = `
346
+ <span class="log-time">[${timestamp}]</span>
347
+ <span class="log-message">${escapeHtml(message)}</span>
348
+ `;
349
+
350
+ container.appendChild(logEntry);
351
+ });
352
+
353
+ if (autoScrollEnabled) {
354
+ container.scrollTop = container.scrollHeight;
355
+ }
356
+ }
357
+
358
+ // Modal Functions
359
+ async function openFileModal(filename) {
360
+ selectedFile = filename;
361
+ elements.modalTitle.textContent = `Analysis Details: ${filename}`;
362
+
363
+ showModal();
364
+
365
+ const details = await fetchFileDetails(filename);
366
+ if (details) {
367
+ elements.modalBody.innerHTML = `
368
+ <div class="file-details">
369
+ <div class="detail-section">
370
+ <h4>File Information</h4>
371
+ <div class="detail-grid">
372
+ <div class="detail-item">
373
+ <span class="detail-label">File Size:</span>
374
+ <span class="detail-value">${formatFileSize(details.file_size_bytes)}</span>
375
+ </div>
376
+ <div class="detail-item">
377
+ <span class="detail-label">Modified:</span>
378
+ <span class="detail-value">${details.modified_time}</span>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <div class="detail-section">
384
+ <h4>Frame Statistics</h4>
385
+ <div class="detail-grid">
386
+ <div class="detail-item">
387
+ <span class="detail-label">Total Frames:</span>
388
+ <span class="detail-value">${details.total_frames}</span>
389
+ </div>
390
+ <div class="detail-item">
391
+ <span class="detail-label">Frames Analyzed:</span>
392
+ <span class="detail-value">${details.frames_with_descriptions}</span>
393
+ </div>
394
+ </div>
395
+ </div>
396
+
397
+ <div class="detail-section">
398
+ <h4>Analysis Summary</h4>
399
+ <div class="summary-content">
400
+ <p><strong>High Level Goal:</strong> ${details.high_level_goal || 'Not available'}</p>
401
+ <p><strong>Final Goal:</strong> ${details.final_goal || 'Not available'}</p>
402
+
403
+ <h5>Key Steps:</h5>
404
+ <ul class="steps-list">
405
+ ${details.steps.map(step => `
406
+ <li>
407
+ <strong>${step.action}:</strong> ${step.description}
408
+ </li>
409
+ `).join('')}
410
+ </ul>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ <style>
416
+ .file-details { font-size: 0.875rem; }
417
+ .detail-section { margin-bottom: 1.5rem; }
418
+ .detail-section h4 {
419
+ margin-bottom: 0.75rem;
420
+ color: var(--accent-primary);
421
+ font-weight: 600;
422
+ }
423
+ .detail-grid {
424
+ display: grid;
425
+ grid-template-columns: 1fr 1fr;
426
+ gap: 0.5rem;
427
+ }
428
+ .detail-item {
429
+ display: flex;
430
+ justify-content: space-between;
431
+ padding: 0.5rem;
432
+ background: var(--bg-secondary);
433
+ border-radius: var(--radius);
434
+ }
435
+ .detail-label { color: var(--text-secondary); }
436
+ .detail-value { font-weight: 500; }
437
+ .summary-content { padding: 0.5rem; }
438
+ .steps-list {
439
+ margin-top: 0.5rem;
440
+ padding-left: 1.5rem;
441
+ }
442
+ .steps-list li {
443
+ margin-bottom: 0.5rem;
444
+ line-height: 1.4;
445
+ }
446
+ </style>
447
+ `;
448
+ } else {
449
+ elements.modalBody.innerHTML = '<p>Failed to load analysis details.</p>';
450
+ }
451
+ }
452
+
453
+ function showModal() {
454
+ elements.fileModal.classList.add('show');
455
+ document.body.style.overflow = 'hidden';
456
+ }
457
+
458
+ function closeModal() {
459
+ elements.fileModal.classList.remove('show');
460
+ document.body.style.overflow = '';
461
+ selectedFile = null;
462
+ }
463
+
464
+ // File Operations
465
+ async function downloadFile(filename) {
466
+ try {
467
+ const response = await fetch(`${API_BASE_URL}/analysis-data/${filename}`);
468
+ if (!response.ok) throw new Error('Download failed');
469
+
470
+ const blob = await response.blob();
471
+ const url = window.URL.createObjectURL(blob);
472
+ const a = document.createElement('a');
473
+ a.href = url;
474
+ a.download = filename;
475
+ document.body.appendChild(a);
476
+ a.click();
477
+ document.body.removeChild(a);
478
+ window.URL.revokeObjectURL(url);
479
+
480
+ showToast(`Downloaded ${filename}`, 'success');
481
+ } catch (error) {
482
+ showToast(`Failed to download ${filename}`, 'error');
483
+ }
484
+ }
485
+
486
+ function downloadSelectedFile() {
487
+ if (selectedFile) {
488
+ downloadFile(selectedFile);
489
+ }
490
+ }
491
+
492
+ function viewSummary() {
493
+ if (selectedFile) {
494
+ window.open(`${API_BASE_URL}/analysis-data/${selectedFile}/summary`, '_blank');
495
+ }
496
+ }
497
+
498
+ // Utility Functions
499
+ function formatFileSize(bytes) {
500
+ if (bytes === 0) return '0 B';
501
+ const k = 1024;
502
+ const sizes = ['B', 'KB', 'MB', 'GB'];
503
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
504
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
505
+ }
506
+
507
+ function formatDate(dateString) {
508
+ try {
509
+ return new Date(dateString).toLocaleDateString();
510
+ } catch {
511
+ return dateString;
512
+ }
513
+ }
514
+
515
+ function escapeHtml(text) {
516
+ const div = document.createElement('div');
517
+ div.textContent = text;
518
+ return div.innerHTML;
519
+ }
520
+
521
+ // Log Management
522
+ function clearLogs() {
523
+ elements.logsContainer.innerHTML = '<div class="log-entry"><span class="log-time">[' +
524
+ new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>';
525
+ showToast('Logs cleared', 'info');
526
+ }
527
+
528
+ function toggleAutoScroll() {
529
+ autoScrollEnabled = !autoScrollEnabled;
530
+ elements.autoScroll.classList.toggle('active', autoScrollEnabled);
531
+
532
+ if (autoScrollEnabled) {
533
+ elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight;
534
+ }
535
+ }
536
+
537
+ // Loading and Toast Functions
538
+ function showLoading() {
539
+ elements.loadingOverlay.classList.add('show');
540
+ }
541
+
542
+ function hideLoading() {
543
+ elements.loadingOverlay.classList.remove('show');
544
+ }
545
+
546
+ function showToast(message, type = 'info', duration = 5000) {
547
+ const toast = document.createElement('div');
548
+ toast.className = `toast ${type}`;
549
+
550
+ const icons = {
551
+ success: 'fas fa-check-circle',
552
+ error: 'fas fa-exclamation-circle',
553
+ warning: 'fas fa-exclamation-triangle',
554
+ info: 'fas fa-info-circle'
555
+ };
556
+
557
+ toast.innerHTML = `
558
+ <i class="toast-icon ${icons[type]}"></i>
559
+ <div class="toast-content">
560
+ <div class="toast-message">${escapeHtml(message)}</div>
561
+ </div>
562
+ <button class="toast-close">
563
+ <i class="fas fa-times"></i>
564
+ </button>
565
+ `;
566
+
567
+ const closeBtn = toast.querySelector('.toast-close');
568
+ closeBtn.addEventListener('click', () => removeToast(toast));
569
+
570
+ elements.toastContainer.appendChild(toast);
571
+
572
+ setTimeout(() => removeToast(toast), duration);
573
+ }
574
+
575
+ function removeToast(toast) {
576
+ if (toast && toast.parentNode) {
577
+ toast.style.animation = 'slideInRight 0.3s ease reverse';
578
+ setTimeout(() => {
579
+ if (toast.parentNode) {
580
+ toast.parentNode.removeChild(toast);
581
+ }
582
+ }, 300);
583
+ }
584
+ }
585
+
586
+ // Auto-refresh Management
587
+ function startAutoRefresh() {
588
+ fetchAllData();
589
+ refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL);
590
+ }
591
+
592
+ function stopAutoRefresh() {
593
+ if (refreshInterval) {
594
+ clearInterval(refreshInterval);
595
+ refreshInterval = null;
596
+ }
597
+ }
598
+
599
+ async function fetchInitialData() {
600
+ await fetchAllData();
601
+ }
602
+
603
+ async function fetchAllData() {
604
+ try {
605
+ await Promise.all([
606
+ fetchStatus(),
607
+ fetchAnalysisData()
608
+ ]);
609
+ } catch (error) {
610
+ console.error('Failed to fetch data:', error);
611
+ }
612
+ }
613
+
614
+ // Make functions available globally for HTML onclick handlers
615
+ window.openFileModal = openFileModal;
616
+ window.downloadFile = downloadFile;
617
+
618
+ // Cleanup on page unload
619
+ window.addEventListener('beforeunload', () => {
620
+ stopAutoRefresh();
621
+ });
622
+ });
static/style.css ADDED
@@ -0,0 +1,770 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and Base Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :root {
9
+ /* Light theme colors */
10
+ --bg-primary: #ffffff;
11
+ --bg-secondary: #f8fafc;
12
+ --bg-tertiary: #f1f5f9;
13
+ --text-primary: #1e293b;
14
+ --text-secondary: #64748b;
15
+ --text-muted: #94a3b8;
16
+ --border-color: #e2e8f0;
17
+ --accent-primary: #3b82f6;
18
+ --accent-secondary: #8b5cf6;
19
+ --success: #10b981;
20
+ --warning: #f59e0b;
21
+ --danger: #ef4444;
22
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
23
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
24
+ --radius: 8px;
25
+ --radius-lg: 12px;
26
+ --transition: all 0.2s ease-in-out;
27
+ }
28
+
29
+ [data-theme="dark"] {
30
+ --bg-primary: #0f172a;
31
+ --bg-secondary: #1e293b;
32
+ --bg-tertiary: #334155;
33
+ --text-primary: #f8fafc;
34
+ --text-secondary: #cbd5e1;
35
+ --text-muted: #64748b;
36
+ --border-color: #334155;
37
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
38
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
39
+ }
40
+
41
+ body {
42
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
43
+ background: var(--bg-secondary);
44
+ color: var(--text-primary);
45
+ line-height: 1.6;
46
+ transition: var(--transition);
47
+ }
48
+
49
+ /* Container */
50
+ .container {
51
+ max-width: 1400px;
52
+ margin: 0 auto;
53
+ padding: 0 1rem;
54
+ }
55
+
56
+ /* Header */
57
+ .header {
58
+ background: var(--bg-primary);
59
+ border-bottom: 1px solid var(--border-color);
60
+ padding: 1rem 0;
61
+ position: sticky;
62
+ top: 0;
63
+ z-index: 100;
64
+ backdrop-filter: blur(10px);
65
+ }
66
+
67
+ .header-content {
68
+ display: flex;
69
+ justify-content: space-between;
70
+ align-items: center;
71
+ }
72
+
73
+ .logo {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.75rem;
77
+ }
78
+
79
+ .logo i {
80
+ font-size: 1.5rem;
81
+ color: var(--accent-primary);
82
+ }
83
+
84
+ .logo h1 {
85
+ font-size: 1.5rem;
86
+ font-weight: 600;
87
+ color: var(--text-primary);
88
+ }
89
+
90
+ .header-actions {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 1rem;
94
+ }
95
+
96
+ /* Main Content */
97
+ .main-content {
98
+ padding: 2rem 0;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 2rem;
102
+ }
103
+
104
+ /* Cards */
105
+ .card {
106
+ background: var(--bg-primary);
107
+ border: 1px solid var(--border-color);
108
+ border-radius: var(--radius-lg);
109
+ box-shadow: var(--shadow);
110
+ overflow: hidden;
111
+ transition: var(--transition);
112
+ }
113
+
114
+ .card:hover {
115
+ box-shadow: var(--shadow-lg);
116
+ }
117
+
118
+ .card-header {
119
+ padding: 1.5rem;
120
+ border-bottom: 1px solid var(--border-color);
121
+ display: flex;
122
+ justify-content: space-between;
123
+ align-items: center;
124
+ }
125
+
126
+ .card-header h2 {
127
+ font-size: 1.25rem;
128
+ font-weight: 600;
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 0.5rem;
132
+ }
133
+
134
+ .card-header i {
135
+ color: var(--accent-primary);
136
+ }
137
+
138
+ .card-content {
139
+ padding: 1.5rem;
140
+ }
141
+
142
+ /* Status Section */
143
+ .status-indicator {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 0.5rem;
147
+ font-size: 0.875rem;
148
+ font-weight: 500;
149
+ }
150
+
151
+ .status-dot {
152
+ width: 8px;
153
+ height: 8px;
154
+ border-radius: 50%;
155
+ background: var(--text-muted);
156
+ animation: pulse 2s infinite;
157
+ }
158
+
159
+ .status-dot.running {
160
+ background: var(--success);
161
+ }
162
+
163
+ .status-dot.stopped {
164
+ background: var(--danger);
165
+ }
166
+
167
+ .status-dot.idle {
168
+ background: var(--warning);
169
+ }
170
+
171
+ @keyframes pulse {
172
+ 0%, 100% { opacity: 1; }
173
+ 50% { opacity: 0.5; }
174
+ }
175
+
176
+ .stats-grid {
177
+ display: grid;
178
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
179
+ gap: 1.5rem;
180
+ margin-bottom: 2rem;
181
+ }
182
+
183
+ .stat-item {
184
+ text-align: center;
185
+ padding: 1rem;
186
+ background: var(--bg-secondary);
187
+ border-radius: var(--radius);
188
+ transition: var(--transition);
189
+ }
190
+
191
+ .stat-item:hover {
192
+ transform: translateY(-2px);
193
+ box-shadow: var(--shadow);
194
+ }
195
+
196
+ .stat-value {
197
+ font-size: 2rem;
198
+ font-weight: 700;
199
+ color: var(--accent-primary);
200
+ margin-bottom: 0.25rem;
201
+ }
202
+
203
+ .stat-label {
204
+ font-size: 0.875rem;
205
+ color: var(--text-secondary);
206
+ font-weight: 500;
207
+ }
208
+
209
+ .progress-section {
210
+ background: var(--bg-secondary);
211
+ padding: 1.5rem;
212
+ border-radius: var(--radius);
213
+ }
214
+
215
+ .progress-info {
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ margin-bottom: 1rem;
220
+ font-size: 0.875rem;
221
+ }
222
+
223
+ .current-file {
224
+ font-weight: 600;
225
+ color: var(--accent-primary);
226
+ max-width: 300px;
227
+ overflow: hidden;
228
+ text-overflow: ellipsis;
229
+ white-space: nowrap;
230
+ }
231
+
232
+ .progress-bar {
233
+ height: 8px;
234
+ background: var(--bg-tertiary);
235
+ border-radius: 4px;
236
+ overflow: hidden;
237
+ margin-bottom: 0.5rem;
238
+ }
239
+
240
+ .progress-fill {
241
+ height: 100%;
242
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
243
+ border-radius: 4px;
244
+ transition: width 0.3s ease;
245
+ width: 0%;
246
+ }
247
+
248
+ .progress-text {
249
+ text-align: center;
250
+ font-size: 0.875rem;
251
+ font-weight: 600;
252
+ color: var(--text-secondary);
253
+ }
254
+
255
+ /* Control Section */
256
+ .control-group {
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: 1.5rem;
260
+ }
261
+
262
+ .input-group {
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 0.5rem;
266
+ }
267
+
268
+ .input-group label {
269
+ font-weight: 500;
270
+ color: var(--text-primary);
271
+ }
272
+
273
+ .input {
274
+ padding: 0.75rem;
275
+ border: 1px solid var(--border-color);
276
+ border-radius: var(--radius);
277
+ background: var(--bg-primary);
278
+ color: var(--text-primary);
279
+ font-size: 0.875rem;
280
+ transition: var(--transition);
281
+ max-width: 200px;
282
+ }
283
+
284
+ .input:focus {
285
+ outline: none;
286
+ border-color: var(--accent-primary);
287
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
288
+ }
289
+
290
+ .input-help {
291
+ font-size: 0.75rem;
292
+ color: var(--text-muted);
293
+ }
294
+
295
+ .button-group {
296
+ display: flex;
297
+ gap: 1rem;
298
+ flex-wrap: wrap;
299
+ }
300
+
301
+ /* Buttons */
302
+ .btn {
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: 0.5rem;
306
+ padding: 0.75rem 1.5rem;
307
+ border: none;
308
+ border-radius: var(--radius);
309
+ font-size: 0.875rem;
310
+ font-weight: 500;
311
+ cursor: pointer;
312
+ transition: var(--transition);
313
+ text-decoration: none;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ .btn:disabled {
318
+ opacity: 0.5;
319
+ cursor: not-allowed;
320
+ }
321
+
322
+ .btn-primary {
323
+ background: var(--accent-primary);
324
+ color: white;
325
+ }
326
+
327
+ .btn-primary:hover:not(:disabled) {
328
+ background: #2563eb;
329
+ transform: translateY(-1px);
330
+ box-shadow: var(--shadow);
331
+ }
332
+
333
+ .btn-secondary {
334
+ background: var(--bg-secondary);
335
+ color: var(--text-primary);
336
+ border: 1px solid var(--border-color);
337
+ }
338
+
339
+ .btn-secondary:hover:not(:disabled) {
340
+ background: var(--bg-tertiary);
341
+ transform: translateY(-1px);
342
+ }
343
+
344
+ .btn-danger {
345
+ background: var(--danger);
346
+ color: white;
347
+ }
348
+
349
+ .btn-danger:hover:not(:disabled) {
350
+ background: #dc2626;
351
+ transform: translateY(-1px);
352
+ box-shadow: var(--shadow);
353
+ }
354
+
355
+ .btn-sm {
356
+ padding: 0.5rem 1rem;
357
+ font-size: 0.75rem;
358
+ }
359
+
360
+ .btn-icon {
361
+ padding: 0.75rem;
362
+ width: auto;
363
+ height: auto;
364
+ }
365
+
366
+ .btn.active {
367
+ background: var(--accent-primary);
368
+ color: white;
369
+ }
370
+
371
+ /* Files Section */
372
+ .file-count {
373
+ font-size: 0.875rem;
374
+ color: var(--text-secondary);
375
+ font-weight: 500;
376
+ }
377
+
378
+ .files-grid {
379
+ display: grid;
380
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
381
+ gap: 1rem;
382
+ }
383
+
384
+ .file-card {
385
+ background: var(--bg-secondary);
386
+ border: 1px solid var(--border-color);
387
+ border-radius: var(--radius);
388
+ padding: 1rem;
389
+ transition: var(--transition);
390
+ cursor: pointer;
391
+ }
392
+
393
+ .file-card:hover {
394
+ transform: translateY(-2px);
395
+ box-shadow: var(--shadow);
396
+ border-color: var(--accent-primary);
397
+ }
398
+
399
+ .file-header {
400
+ display: flex;
401
+ justify-content: space-between;
402
+ align-items: flex-start;
403
+ margin-bottom: 0.75rem;
404
+ }
405
+
406
+ .file-name {
407
+ font-weight: 600;
408
+ color: var(--text-primary);
409
+ font-size: 0.875rem;
410
+ word-break: break-word;
411
+ flex: 1;
412
+ margin-right: 0.5rem;
413
+ }
414
+
415
+ .file-size {
416
+ font-size: 0.75rem;
417
+ color: var(--text-muted);
418
+ white-space: nowrap;
419
+ }
420
+
421
+ .file-stats {
422
+ display: grid;
423
+ grid-template-columns: 1fr 1fr;
424
+ gap: 0.5rem;
425
+ margin-bottom: 0.75rem;
426
+ }
427
+
428
+ .file-stat {
429
+ display: flex;
430
+ justify-content: space-between;
431
+ font-size: 0.75rem;
432
+ }
433
+
434
+ .file-stat-label {
435
+ color: var(--text-secondary);
436
+ }
437
+
438
+ .file-stat-value {
439
+ color: var(--text-primary);
440
+ font-weight: 500;
441
+ }
442
+
443
+ .file-actions {
444
+ display: flex;
445
+ gap: 0.5rem;
446
+ }
447
+
448
+ .file-actions .btn {
449
+ padding: 0.5rem 0.75rem;
450
+ font-size: 0.75rem;
451
+ }
452
+
453
+ /* Logs Section */
454
+ .log-controls {
455
+ display: flex;
456
+ gap: 0.5rem;
457
+ }
458
+
459
+ .logs-container {
460
+ background: var(--bg-secondary);
461
+ border: 1px solid var(--border-color);
462
+ border-radius: var(--radius);
463
+ height: 300px;
464
+ overflow-y: auto;
465
+ padding: 1rem;
466
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
467
+ font-size: 0.75rem;
468
+ line-height: 1.4;
469
+ }
470
+
471
+ .log-entry {
472
+ margin-bottom: 0.5rem;
473
+ display: flex;
474
+ gap: 0.5rem;
475
+ }
476
+
477
+ .log-time {
478
+ color: var(--text-muted);
479
+ white-space: nowrap;
480
+ }
481
+
482
+ .log-message {
483
+ color: var(--text-primary);
484
+ word-break: break-word;
485
+ }
486
+
487
+ .log-entry.error .log-message {
488
+ color: var(--danger);
489
+ }
490
+
491
+ .log-entry.success .log-message {
492
+ color: var(--success);
493
+ }
494
+
495
+ .log-entry.warning .log-message {
496
+ color: var(--warning);
497
+ }
498
+
499
+ /* Modal */
500
+ .modal {
501
+ display: none;
502
+ position: fixed;
503
+ top: 0;
504
+ left: 0;
505
+ width: 100%;
506
+ height: 100%;
507
+ background: rgba(0, 0, 0, 0.5);
508
+ z-index: 1000;
509
+ backdrop-filter: blur(4px);
510
+ }
511
+
512
+ .modal.show {
513
+ display: flex;
514
+ align-items: center;
515
+ justify-content: center;
516
+ animation: fadeIn 0.2s ease;
517
+ }
518
+
519
+ .modal-content {
520
+ background: var(--bg-primary);
521
+ border-radius: var(--radius-lg);
522
+ box-shadow: var(--shadow-lg);
523
+ max-width: 600px;
524
+ width: 90%;
525
+ max-height: 80vh;
526
+ overflow: hidden;
527
+ animation: slideUp 0.2s ease;
528
+ }
529
+
530
+ .modal-header {
531
+ padding: 1.5rem;
532
+ border-bottom: 1px solid var(--border-color);
533
+ display: flex;
534
+ justify-content: space-between;
535
+ align-items: center;
536
+ }
537
+
538
+ .modal-header h3 {
539
+ font-size: 1.25rem;
540
+ font-weight: 600;
541
+ }
542
+
543
+ .modal-close {
544
+ background: none;
545
+ border: none;
546
+ font-size: 1.25rem;
547
+ cursor: pointer;
548
+ color: var(--text-muted);
549
+ padding: 0.25rem;
550
+ border-radius: var(--radius);
551
+ transition: var(--transition);
552
+ }
553
+
554
+ .modal-close:hover {
555
+ background: var(--bg-secondary);
556
+ color: var(--text-primary);
557
+ }
558
+
559
+ .modal-body {
560
+ padding: 1.5rem;
561
+ max-height: 400px;
562
+ overflow-y: auto;
563
+ }
564
+
565
+ .modal-footer {
566
+ padding: 1.5rem;
567
+ border-top: 1px solid var(--border-color);
568
+ display: flex;
569
+ gap: 1rem;
570
+ justify-content: flex-end;
571
+ }
572
+
573
+ /* Loading Overlay */
574
+ .loading-overlay {
575
+ display: none;
576
+ position: fixed;
577
+ top: 0;
578
+ left: 0;
579
+ width: 100%;
580
+ height: 100%;
581
+ background: rgba(0, 0, 0, 0.3);
582
+ z-index: 2000;
583
+ backdrop-filter: blur(2px);
584
+ }
585
+
586
+ .loading-overlay.show {
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: center;
590
+ }
591
+
592
+ .loading-spinner {
593
+ background: var(--bg-primary);
594
+ padding: 2rem;
595
+ border-radius: var(--radius-lg);
596
+ text-align: center;
597
+ box-shadow: var(--shadow-lg);
598
+ }
599
+
600
+ .loading-spinner i {
601
+ font-size: 2rem;
602
+ color: var(--accent-primary);
603
+ margin-bottom: 1rem;
604
+ }
605
+
606
+ .loading-spinner p {
607
+ color: var(--text-secondary);
608
+ font-weight: 500;
609
+ }
610
+
611
+ /* Toast Notifications */
612
+ .toast-container {
613
+ position: fixed;
614
+ top: 1rem;
615
+ right: 1rem;
616
+ z-index: 3000;
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 0.5rem;
620
+ }
621
+
622
+ .toast {
623
+ background: var(--bg-primary);
624
+ border: 1px solid var(--border-color);
625
+ border-radius: var(--radius);
626
+ padding: 1rem;
627
+ box-shadow: var(--shadow-lg);
628
+ min-width: 300px;
629
+ animation: slideInRight 0.3s ease;
630
+ display: flex;
631
+ align-items: center;
632
+ gap: 0.75rem;
633
+ }
634
+
635
+ .toast.success {
636
+ border-left: 4px solid var(--success);
637
+ }
638
+
639
+ .toast.error {
640
+ border-left: 4px solid var(--danger);
641
+ }
642
+
643
+ .toast.warning {
644
+ border-left: 4px solid var(--warning);
645
+ }
646
+
647
+ .toast.info {
648
+ border-left: 4px solid var(--accent-primary);
649
+ }
650
+
651
+ .toast-icon {
652
+ font-size: 1.25rem;
653
+ }
654
+
655
+ .toast.success .toast-icon {
656
+ color: var(--success);
657
+ }
658
+
659
+ .toast.error .toast-icon {
660
+ color: var(--danger);
661
+ }
662
+
663
+ .toast.warning .toast-icon {
664
+ color: var(--warning);
665
+ }
666
+
667
+ .toast.info .toast-icon {
668
+ color: var(--accent-primary);
669
+ }
670
+
671
+ .toast-content {
672
+ flex: 1;
673
+ }
674
+
675
+ .toast-title {
676
+ font-weight: 600;
677
+ margin-bottom: 0.25rem;
678
+ }
679
+
680
+ .toast-message {
681
+ font-size: 0.875rem;
682
+ color: var(--text-secondary);
683
+ }
684
+
685
+ .toast-close {
686
+ background: none;
687
+ border: none;
688
+ color: var(--text-muted);
689
+ cursor: pointer;
690
+ padding: 0.25rem;
691
+ border-radius: var(--radius);
692
+ transition: var(--transition);
693
+ }
694
+
695
+ .toast-close:hover {
696
+ background: var(--bg-secondary);
697
+ color: var(--text-primary);
698
+ }
699
+
700
+ /* Animations */
701
+ @keyframes fadeIn {
702
+ from { opacity: 0; }
703
+ to { opacity: 1; }
704
+ }
705
+
706
+ @keyframes slideUp {
707
+ from { transform: translateY(20px); opacity: 0; }
708
+ to { transform: translateY(0); opacity: 1; }
709
+ }
710
+
711
+ @keyframes slideInRight {
712
+ from { transform: translateX(100%); opacity: 0; }
713
+ to { transform: translateX(0); opacity: 1; }
714
+ }
715
+
716
+ /* Responsive Design */
717
+ @media (max-width: 768px) {
718
+ .container {
719
+ padding: 0 0.5rem;
720
+ }
721
+
722
+ .header-content {
723
+ flex-direction: column;
724
+ gap: 1rem;
725
+ align-items: flex-start;
726
+ }
727
+
728
+ .stats-grid {
729
+ grid-template-columns: repeat(2, 1fr);
730
+ }
731
+
732
+ .button-group {
733
+ flex-direction: column;
734
+ }
735
+
736
+ .files-grid {
737
+ grid-template-columns: 1fr;
738
+ }
739
+
740
+ .modal-content {
741
+ width: 95%;
742
+ margin: 1rem;
743
+ }
744
+
745
+ .toast {
746
+ min-width: 280px;
747
+ }
748
+
749
+ .toast-container {
750
+ left: 1rem;
751
+ right: 1rem;
752
+ }
753
+ }
754
+
755
+ @media (max-width: 480px) {
756
+ .stats-grid {
757
+ grid-template-columns: 1fr;
758
+ }
759
+
760
+ .progress-info {
761
+ flex-direction: column;
762
+ align-items: flex-start;
763
+ gap: 0.5rem;
764
+ }
765
+
766
+ .current-file {
767
+ max-width: 100%;
768
+ }
769
+ }
770
+
vision_analyzer.py ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import requests
4
+ import subprocess
5
+ import shutil
6
+ import time
7
+ import re
8
+ import threading
9
+ from typing import Dict, List, Set, Optional
10
+ from huggingface_hub import HfApi, list_repo_files
11
+ from fastapi import FastAPI, File, UploadFile, Form
12
+ from fastapi.responses import JSONResponse
13
+ from pathlib import Path
14
+ import smtplib
15
+ from email.message import EmailMessage
16
+ import tempfile
17
+ import rarfile
18
+ import zipfile
19
+ import cv2
20
+ import numpy as np
21
+ from PIL import Image
22
+ import torch
23
+ from transformers import AutoProcessor, AutoModelForCausalLM
24
+
25
+ # Initialize FastAPI
26
+ app = FastAPI()
27
+
28
+ # ==== CONFIGURATION ====
29
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
30
+ SOURCE_REPO_ID = os.getenv("SOURCE_REPO", "Fred808/BG1")
31
+
32
+ # Path Configuration
33
+ DOWNLOAD_FOLDER = "downloads"
34
+ EXTRACT_FOLDER = "extracted"
35
+ FRAMES_OUTPUT_FOLDER = "extracted_frames"
36
+ ANALYSIS_OUTPUT_FOLDER = "analysis_results"
37
+
38
+ os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
39
+ os.makedirs(EXTRACT_FOLDER, exist_ok=True)
40
+ os.makedirs(FRAMES_OUTPUT_FOLDER, exist_ok=True)
41
+ os.makedirs(ANALYSIS_OUTPUT_FOLDER, exist_ok=True)
42
+
43
+ # State Files
44
+ DOWNLOAD_STATE_FILE = "download_progress.json"
45
+ PROCESS_STATE_FILE = "process_progress.json"
46
+ FAILED_FILES_LOG = "failed_files.log"
47
+
48
+ # Processing Parameters
49
+ CHUNK_SIZE = 1
50
+ PROCESSING_DELAY = 2
51
+ MAX_RETRIES = 3
52
+ MIN_FREE_SPACE_GB = 2 # Minimum free space in GB before processing
53
+
54
+ # Frame Extraction Parameters
55
+ DEFAULT_FPS = 3 # Default frames per second for extraction
56
+
57
+ # Initialize HF API
58
+ hf_api = HfApi(token=HF_TOKEN)
59
+
60
+ # Global State
61
+ processing_status = {
62
+ "is_running": False,
63
+ "current_file": None,
64
+ "total_files": 0,
65
+ "processed_files": 0,
66
+ "failed_files": 0,
67
+ "extracted_courses": 0,
68
+ "extracted_videos": 0,
69
+ "extracted_frames_count": 0,
70
+ "analyzed_frames_count": 0,
71
+ "last_update": None,
72
+ "logs": []
73
+ }
74
+
75
+ import torch
76
+ import subprocess
77
+ import sys
78
+
79
+
80
+
81
+ device = "cpu" # Explicitly ensure CPU usage
82
+
83
+ try:
84
+ # Load the model, forcing the 'eager' (CPU-compatible) attention implementation
85
+ vision_language_model_large = AutoModelForCausalLM.from_pretrained(
86
+ "microsoft/Florence-2-Base",
87
+ trust_remote_code=True
88
+ ).to(device).eval()
89
+ vision_language_processor_large = AutoProcessor.from_pretrained(
90
+ "microsoft/Florence-2-Base",
91
+ trust_remote_code=True
92
+ )
93
+ print("Florence-2 large model and processor loaded successfully on CPU using eager attention.")
94
+ except Exception as e:
95
+ print(f"Error loading Florence-2 model on CPU: {e}")
96
+ print("Please ensure you have enough RAM and a compatible PyTorch version.")
97
+ vision_language_model_large = None
98
+ vision_language_processor_large = None
99
+
100
+
101
+ def log_message(message: str):
102
+ """Log messages with timestamp"""
103
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
104
+ log_entry = f"[{timestamp}] {message}"
105
+ print(log_entry)
106
+ processing_status["logs"].append(log_entry)
107
+ processing_status["last_update"] = timestamp
108
+ if len(processing_status["logs"]) > 100:
109
+ processing_status["logs"] = processing_status["logs"][-100:]
110
+
111
+ def log_failed_file(filename: str, error: str):
112
+ """Log failed files to persistent file"""
113
+ with open(FAILED_FILES_LOG, "a") as f:
114
+ f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {filename}: {error}\n")
115
+
116
+ def get_disk_usage(path: str) -> Dict[str, float]:
117
+ """Get disk usage statistics in GB"""
118
+ statvfs = os.statvfs(path)
119
+ total = statvfs.f_frsize * statvfs.f_blocks / (1024**3)
120
+ free = statvfs.f_frsize * statvfs.f_bavail / (1024**3)
121
+ used = total - free
122
+ return {"total": total, "free": free, "used": used}
123
+
124
+ def check_disk_space(path: str = ".") -> bool:
125
+ """Check if there's enough disk space"""
126
+ disk_info = get_disk_usage(path)
127
+ if disk_info["free"] < MIN_FREE_SPACE_GB:
128
+ log_message(f'⚠️ Low disk space: {disk_info["free"]:.2f}GB free, {disk_info["used"]:.2f}GB used')
129
+ return False
130
+ return True
131
+
132
+ def cleanup_temp_files():
133
+ """Clean up temporary files to free space"""
134
+ log_message("🧹 Cleaning up temporary files...")
135
+
136
+ # Clean old downloads (keep only current processing file)
137
+ current_file = processing_status.get("current_file")
138
+ for file in os.listdir(DOWNLOAD_FOLDER):
139
+ if file != current_file and file.endswith((".rar", ".zip")):
140
+ try:
141
+ os.remove(os.path.join(DOWNLOAD_FOLDER, file))
142
+ log_message(f"πŸ—‘οΈ Removed old download: {file}")
143
+ except:
144
+ pass
145
+
146
+ def load_json_state(file_path: str, default_value):
147
+ """Load state from JSON file"""
148
+ if os.path.exists(file_path):
149
+ try:
150
+ with open(file_path, "r") as f:
151
+ return json.load(f)
152
+ except json.JSONDecodeError:
153
+ log_message(f"⚠️ Corrupted state file: {file_path}")
154
+ return default_value
155
+
156
+ def save_json_state(file_path: str, data):
157
+ """Save state to JSON file"""
158
+ with open(file_path, "w") as f:
159
+ json.dump(data, f, indent=2)
160
+
161
+ def download_with_retry(url: str, dest_path: str, max_retries: int = 3) -> bool:
162
+ """Download file with retry logic and disk space checking"""
163
+ if not check_disk_space():
164
+ cleanup_temp_files()
165
+ if not check_disk_space():
166
+ log_message("❌ Insufficient disk space even after cleanup")
167
+ return False
168
+
169
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
170
+ for attempt in range(max_retries):
171
+ try:
172
+ with requests.get(url, headers=headers, stream=True) as r:
173
+ r.raise_for_status()
174
+
175
+ # Check content length if available
176
+ content_length = r.headers.get("content-length")
177
+ if content_length:
178
+ size_gb = int(content_length) / (1024**3)
179
+ disk_info = get_disk_usage(".")
180
+ if size_gb > disk_info["free"] - 0.5: # Leave 0.5GB buffer
181
+ log_message(f'❌ File too large: {size_gb:.2f}GB, only {disk_info["free"]:.2f}GB free')
182
+ return False
183
+
184
+ with open(dest_path, "wb") as f:
185
+ for chunk in r.iter_content(chunk_size=8192):
186
+ f.write(chunk)
187
+ return True
188
+ except Exception as e:
189
+ if attempt < max_retries - 1:
190
+ time.sleep(2 ** attempt)
191
+ continue
192
+ log_message(f"❌ Download failed after {max_retries} attempts: {e}")
193
+ return False
194
+ return False
195
+
196
+ def is_multipart_rar(filename: str) -> bool:
197
+ """Check if this is a multi-part RAR file"""
198
+ return ".part" in filename.lower() and filename.lower().endswith(".rar")
199
+
200
+ def get_rar_part_base(filename: str) -> str:
201
+ """Get the base name for multi-part RAR files"""
202
+ if ".part" in filename.lower():
203
+ return filename.split(".part")[0]
204
+ return filename.replace(".rar", "")
205
+
206
+ def extract_with_retry(rar_path: str, output_dir: str, max_retries: int = 2) -> bool:
207
+ """Extract RAR with retry and recovery, handling multi-part archives"""
208
+ filename = os.path.basename(rar_path)
209
+
210
+ # For multi-part RARs, we need the first part
211
+ if is_multipart_rar(filename):
212
+ base_name = get_rar_part_base(filename)
213
+ first_part = f"{base_name}.part01.rar"
214
+ first_part_path = os.path.join(os.path.dirname(rar_path), first_part)
215
+
216
+ if not os.path.exists(first_part_path):
217
+ log_message(f"⚠️ Multi-part RAR detected but first part not found: {first_part}")
218
+ return False
219
+
220
+ rar_path = first_part_path
221
+ log_message(f"πŸ“¦ Processing multi-part RAR starting with: {first_part}")
222
+
223
+ for attempt in range(max_retries):
224
+ try:
225
+ # Test RAR first
226
+ test_cmd = ["unrar", "t", rar_path]
227
+ test_result = subprocess.run(test_cmd, capture_output=True, text=True)
228
+ if test_result.returncode != 0:
229
+ log_message(f"⚠️ RAR test failed: {test_result.stderr}")
230
+ if attempt == max_retries - 1:
231
+ return False
232
+ continue
233
+
234
+ # Extract RAR
235
+ cmd = ["unrar", "x", "-o+", rar_path, output_dir]
236
+ if attempt > 0: # Try recovery on subsequent attempts
237
+ cmd.insert(2, "-kb")
238
+
239
+ result = subprocess.run(cmd, capture_output=True, text=True)
240
+ if result.returncode == 0:
241
+ log_message(f"βœ… Successfully extracted: {os.path.basename(rar_path)}")
242
+ return True
243
+ else:
244
+ error_msg = result.stderr or result.stdout
245
+ log_message(f"⚠️ Extraction attempt {attempt + 1} failed: {error_msg}")
246
+
247
+ if "checksum error" in error_msg.lower() or "CRC failed" in error_msg:
248
+ log_message(f"⚠️ Data corruption detected, attempt {attempt + 1}")
249
+ elif result.returncode == 10:
250
+ log_message(f"⚠️ No files to extract (exit code 10)")
251
+ return False
252
+ elif result.returncode == 1:
253
+ log_message(f"⚠️ Non-fatal error (exit code 1)")
254
+
255
+ except Exception as e:
256
+ log_message(f"❌ Extraction exception: {str(e)}")
257
+ if attempt == max_retries - 1:
258
+ return False
259
+ time.sleep(1)
260
+
261
+ return False
262
+
263
+ def ensure_dir(path):
264
+ os.makedirs(path, exist_ok=True)
265
+
266
+ def extract_frames(video_path, output_dir, fps=DEFAULT_FPS):
267
+ """Extract frames from video at the specified frames per second (fps)."""
268
+ log_message(f"[INFO] Extracting frames from {video_path} to {output_dir} at {fps} fps...")
269
+ ensure_dir(output_dir)
270
+ cap = cv2.VideoCapture(str(video_path))
271
+ if not cap.isOpened():
272
+ log_message(f"[ERROR] Failed to open video file: {video_path}")
273
+ return 0
274
+ video_fps = cap.get(cv2.CAP_PROP_FPS)
275
+ if not video_fps or video_fps <= 0:
276
+ video_fps = 30 # fallback if FPS is not available
277
+ log_message(f"[WARN] Using fallback FPS: {video_fps}")
278
+ frame_interval = int(round(video_fps / fps))
279
+ frame_idx = 0
280
+ saved_idx = 1
281
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
282
+ log_message(f"[DEBUG] Total frames in video: {total_frames}")
283
+ while cap.isOpened():
284
+ ret, frame = cap.read()
285
+ if not ret:
286
+ break
287
+ if frame_idx % frame_interval == 0:
288
+ frame_name = f"{saved_idx:04d}.png"
289
+ cv2.imwrite(str(Path(output_dir) / frame_name), frame)
290
+ saved_idx += 1
291
+ frame_idx += 1
292
+ cap.release()
293
+ log_message(f"Extracted {saved_idx-1} frames from {video_path} to {output_dir}")
294
+ return saved_idx - 1
295
+
296
+ def analyze_frame_with_florence2(image_path: str, prompt: str = "<CAPTION>") -> Dict:
297
+ """Analyze a single frame using Florence-2 vision model."""
298
+ if not vision_language_model_large or not vision_language_processor_large:
299
+ return {
300
+ "image": os.path.basename(image_path),
301
+ "description": "[ERROR] Vision model not loaded."
302
+ }
303
+
304
+ image = Image.open(image_path).convert("RGB")
305
+ inputs = vision_language_processor_large(images=image, text=prompt, return_tensors="pt").to(device)
306
+
307
+ with torch.no_grad():
308
+ generated_ids = vision_language_model_large.generate(
309
+ input_ids=inputs["input_ids"],
310
+ pixel_values=inputs["pixel_values"],
311
+ max_new_tokens=512,
312
+ do_sample=False,
313
+ num_beams=3
314
+ )
315
+
316
+ generated_text = vision_language_processor_large.batch_decode(generated_ids, skip_special_tokens=False)[0]
317
+ description = vision_language_processor_large.post_process_generation(
318
+ generated_text,
319
+ task="<CAPTION>",
320
+ image_size=(image.width, image.height)
321
+ )["<CAPTION>"]
322
+
323
+ return {
324
+ "image": os.path.basename(image_path),
325
+ "description": description
326
+ }
327
+
328
+ def summarize_activities(frame_analyses: List[Dict]) -> Dict:
329
+ """Summarize activities from frame analyses."""
330
+ return {
331
+ # "steps": [
332
+ # {
333
+ # "action": "Open Blender software",
334
+ # "description": "User launches Blender 3D modeling application on their computer"
335
+ # },
336
+ # {
337
+ # "action": "Create 3D object",
338
+ # "description": "User works with a default cube object in the 3D viewport"
339
+ # },
340
+ # {
341
+ # "action": "Manipulate 3D model",
342
+ # "description": "User rotates and transforms the cube using mouse interactions"
343
+ # },
344
+ # {
345
+ # "action": "Navigate interface",
346
+ # "description": "User explores different tools and panels in the Blender interface"
347
+ # }
348
+ # ],
349
+ # "high_level_goal": "Learning basic 3D modeling operations in Blender software",
350
+ # "creative_actions": "3D object manipulation, interface navigation, basic modeling workflow",
351
+ # "objects": ["computer", "monitor", "mouse", "keyboard", "Blender software", "3D cube", "desktop interface"],
352
+ # "final_goal": "Introduction to Blender 3D modeling fundamentals and basic object manipulation"
353
+ }
354
+
355
+ def analyze_frames(frames_dir: str, output_json_path: str, prompt: Optional[str] = None) -> int:
356
+ """Analyze all frames in directory using Florence-2 model."""
357
+ log_message(f"[INFO] Analyzing frames in {frames_dir}...")
358
+ frames_dir = Path(frames_dir).resolve()
359
+ output_json_path = Path(output_json_path).resolve()
360
+ ensure_dir(frames_dir)
361
+ ensure_dir(output_json_path.parent)
362
+
363
+ frame_analyses = []
364
+ analyzed_count = 0
365
+
366
+ for frame_file in sorted(frames_dir.glob("*.png")):
367
+ analysis = analyze_frame_with_florence2(str(frame_file), prompt)
368
+ frame_analyses.append(analysis)
369
+ analyzed_count += 1
370
+
371
+ # Generate summary
372
+ summary = summarize_activities(frame_analyses)
373
+
374
+ # Save results
375
+ results = {
376
+ "frame_analyses": frame_analyses,
377
+ "summary": summary
378
+ }
379
+
380
+ try:
381
+ with open(output_json_path, "w") as f:
382
+ json.dump(results, f, indent=2)
383
+ log_message(f"[SUCCESS] Analysis results saved to {output_json_path}")
384
+ except Exception as e:
385
+ log_message(f"[ERROR] Failed to write output JSON: {e}")
386
+
387
+ return analyzed_count
388
+
389
+
390
+ def process_rar_file(rar_path: str) -> bool:
391
+ """Process a single RAR file - extract, then process videos for frames and vision analysis"""
392
+ filename = os.path.basename(rar_path)
393
+ processing_status["current_file"] = filename
394
+
395
+ # Handle multi-part RAR naming
396
+ if is_multipart_rar(filename):
397
+ course_name = get_rar_part_base(filename)
398
+ else:
399
+ course_name = filename.replace(".rar", "")
400
+
401
+ extract_dir = os.path.join(EXTRACT_FOLDER, course_name)
402
+
403
+ try:
404
+ log_message(f"πŸ”„ Processing: {filename}")
405
+
406
+ # Clean up any existing directory
407
+ if os.path.exists(extract_dir):
408
+ shutil.rmtree(extract_dir, ignore_errors=True)
409
+
410
+ # Extract RAR
411
+ os.makedirs(extract_dir, exist_ok=True)
412
+ if not extract_with_retry(rar_path, extract_dir):
413
+ raise Exception("RAR extraction failed")
414
+
415
+ # Count extracted files
416
+ file_count = 0
417
+ video_files_found = []
418
+ for root, dirs, files in os.walk(extract_dir):
419
+ for file in files:
420
+ file_count += 1
421
+ if file.lower().endswith((".mp4", ".avi", ".mov", ".mkv")):
422
+ video_files_found.append(os.path.join(root, file))
423
+
424
+ processing_status["extracted_courses"] += 1
425
+ log_message(f"βœ… Successfully extracted '{course_name}' ({file_count} files, {len(video_files_found)} videos)")
426
+
427
+ # Process video files for frame extraction and vision analysis
428
+ for video_path in video_files_found:
429
+ video_filename = Path(video_path).name
430
+ # Create a unique output directory for frames for each video
431
+ frames_output_dir = os.path.join(FRAMES_OUTPUT_FOLDER, f"{course_name}_{video_filename.replace('.', '_')}_frames")
432
+ ensure_dir(frames_output_dir)
433
+
434
+ extracted_frames_count = extract_frames(video_path, frames_output_dir, fps=DEFAULT_FPS)
435
+ processing_status["extracted_frames_count"] += extracted_frames_count
436
+ if extracted_frames_count > 0:
437
+ processing_status["extracted_videos"] += 1
438
+ log_message(f"[INFO] Extracted {extracted_frames_count} frames from {video_filename}")
439
+
440
+ # Perform vision analysis on the extracted frames
441
+ analysis_output_json = os.path.join(ANALYSIS_OUTPUT_FOLDER, f"{course_name}_{video_filename.replace('.', '_')}_analysis.json")
442
+ analyzed_frames = analyze_frames(frames_output_dir, analysis_output_json)
443
+ processing_status["analyzed_frames_count"] += analyzed_frames
444
+ log_message(f"[INFO] Analyzed {analyzed_frames} frames from {video_filename}")
445
+ else:
446
+ log_message(f"[WARN] No frames extracted from {video_filename}")
447
+
448
+ return True
449
+
450
+ except Exception as e:
451
+ error_msg = str(e)
452
+ log_message(f"❌ Processing failed: {error_msg}")
453
+ log_failed_file(filename, error_msg)
454
+ return False
455
+
456
+ finally:
457
+ processing_status["current_file"] = None
458
+
459
+ def main_processing_loop(start_index: int = 0):
460
+ """Main processing workflow - extraction, frame extraction, and vision analysis"""
461
+ processing_status["is_running"] = True
462
+
463
+ try:
464
+ # Load state
465
+ processed_rars = load_json_state(PROCESS_STATE_FILE, {"processed_rars": []})["processed_rars"]
466
+ download_state = load_json_state(DOWNLOAD_STATE_FILE, {"next_download_index": 0})
467
+
468
+ # Use start_index if provided, otherwise use the saved state
469
+ next_index = start_index if start_index > 0 else download_state["next_download_index"]
470
+
471
+ log_message(f"πŸ“Š Starting from index {next_index}")
472
+ log_message(f"πŸ“Š Previously processed: {len(processed_rars)} files")
473
+
474
+ # Get file list
475
+ try:
476
+ files = list(hf_api.list_repo_files(repo_id=SOURCE_REPO_ID, repo_type="dataset"))
477
+ rar_files = sorted([f for f in files if f.endswith(".rar")])
478
+
479
+ processing_status["total_files"] = len(rar_files)
480
+ log_message(f"πŸ“ Found {len(rar_files)} RAR files in repository")
481
+
482
+ if next_index >= len(rar_files):
483
+ log_message("βœ… All files have been processed!")
484
+ return
485
+
486
+ except Exception as e:
487
+ log_message(f"❌ Failed to get file list: {str(e)}")
488
+ return
489
+
490
+ # Process only one file per run
491
+ if next_index < len(rar_files):
492
+ rar_file = rar_files[next_index]
493
+ filename = os.path.basename(rar_file)
494
+
495
+ if filename in processed_rars:
496
+ log_message(f"⏭️ Skipping already processed: {filename}")
497
+ processing_status["processed_files"] += 1
498
+ # Move to next file
499
+ next_index += 1
500
+ save_json_state(DOWNLOAD_STATE_FILE, {"next_download_index": next_index})
501
+ log_message(f"πŸ“Š Moving to next file. Progress: {next_index}/{len(rar_files)}")
502
+ return
503
+
504
+ log_message(f"πŸ“₯ Downloading: {filename}")
505
+ dest_path = os.path.join(DOWNLOAD_FOLDER, filename)
506
+
507
+ # Download file
508
+ download_url = f"https://huggingface.co/datasets/{SOURCE_REPO_ID}/resolve/main/{rar_file}"
509
+ if download_with_retry(download_url, dest_path):
510
+ # Process file
511
+ if process_rar_file(dest_path):
512
+ processed_rars.append(filename)
513
+ save_json_state(PROCESS_STATE_FILE, {"processed_rars": processed_rars})
514
+ log_message(f"βœ… Successfully processed: {filename}")
515
+ processing_status["processed_files"] += 1
516
+ else:
517
+ log_message(f"❌ Failed to process: {filename}")
518
+ processing_status["failed_files"] += 1
519
+
520
+ # Clean up downloaded file
521
+ try:
522
+ os.remove(dest_path)
523
+ log_message(f"πŸ—‘οΈ Cleaned up download: {filename}")
524
+ except:
525
+ pass
526
+ else:
527
+ log_message(f"❌ Failed to download: {filename}")
528
+ processing_status["failed_files"] += 1
529
+
530
+ # Update download state for next run
531
+ next_index += 1
532
+ save_json_state(DOWNLOAD_STATE_FILE, {"next_download_index": next_index})
533
+
534
+ # Status update
535
+ log_message(f"πŸ“Š Progress: {next_index}/{len(rar_files)} files processed")
536
+ log_message(f'πŸ“Š Extracted: {processing_status["extracted_courses"]} courses')
537
+ log_message(f'πŸ“Š Videos Processed: {processing_status["extracted_videos"]}')
538
+ log_message(f'πŸ“Š Frames Extracted: {processing_status["extracted_frames_count"]}')
539
+ log_message(f'πŸ“Š Frames Analyzed: {processing_status["analyzed_frames_count"]}')
540
+ log_message(f'πŸ“Š Failed: {processing_status["failed_files"]} files')
541
+
542
+ if next_index < len(rar_files):
543
+ log_message(f"πŸ”„ Run the script again to process the next file: {os.path.basename(rar_files[next_index])}")
544
+ else:
545
+ log_message("πŸŽ‰ All files have been processed!")
546
+ else:
547
+ log_message("βœ… All files have been processed!")
548
+
549
+ log_message("πŸŽ‰ Processing complete!")
550
+ log_message(f'πŸ“Š Final stats: {processing_status["extracted_courses"]} courses extracted, {processing_status["extracted_videos"]} videos processed, {processing_status["extracted_frames_count"]} frames extracted, {processing_status["analyzed_frames_count"]} frames analyzed')
551
+
552
+ except KeyboardInterrupt:
553
+ log_message("⏹️ Processing interrupted by user")
554
+ except Exception as e:
555
+ log_message(f"❌ Fatal error: {str(e)}")
556
+ finally:
557
+ processing_status["is_running"] = False
558
+ cleanup_temp_files()
559
+
560
+ # FastAPI Endpoints
561
+ @app.post("/analyze-video")
562
+ async def analyze_video_endpoint(
563
+ file: UploadFile = File(...),
564
+ fps: int = Form(DEFAULT_FPS),
565
+ prompt: Optional[str] = Form(None)
566
+ ):
567
+ """Analyze a single video file and return frame-by-frame analysis."""
568
+ if not file.filename.lower().endswith((".mp4", ".avi", ".mov", ".mkv")):
569
+ return JSONResponse(status_code=400, content={
570
+ "error": "File type not allowed",
571
+ "allowed_types": [".mp4", ".avi", ".mov", ".mkv"]
572
+ })
573
+
574
+ with tempfile.TemporaryDirectory() as temp_dir:
575
+ temp_dir_path = Path(temp_dir)
576
+ file_path = temp_dir_path / file.filename
577
+
578
+ with open(file_path, "wb") as buffer:
579
+ shutil.copyfileobj(file.file, buffer)
580
+
581
+ frames_dir = temp_dir_path / "frames"
582
+ frame_count = extract_frames(file_path, frames_dir, fps)
583
+
584
+ frame_analyses = []
585
+ for frame_file in sorted(frames_dir.glob("*.png")):
586
+ analysis = analyze_frame_with_florence2(str(frame_file), prompt)
587
+ frame_analyses.append(analysis)
588
+
589
+ summary = summarize_activities(frame_analyses)
590
+
591
+ return JSONResponse(content={
592
+ "video_filename": file.filename,
593
+ "frame_count": frame_count,
594
+ "fps": fps,
595
+ "frame_analyses": frame_analyses,
596
+ "summary": summary
597
+ })
598
+
599
+ @app.post("/analyze-archive")
600
+ async def analyze_archive_endpoint(
601
+ file: UploadFile = File(...),
602
+ fps: int = Form(DEFAULT_FPS),
603
+ prompt: Optional[str] = Form(None)
604
+ ):
605
+ """Analyze videos from RAR/ZIP archive and return frame-by-frame analysis."""
606
+ if not file.filename.lower().endswith((".rar", ".zip")):
607
+ return JSONResponse(status_code=400, content={
608
+ "error": "File type not allowed",
609
+ "allowed_types": [".rar", ".zip"]
610
+ })
611
+
612
+ with tempfile.TemporaryDirectory() as temp_dir:
613
+ temp_dir_path = Path(temp_dir)
614
+ file_path = temp_dir_path / file.filename
615
+
616
+ with open(file_path, "wb") as buffer:
617
+ shutil.copyfileobj(file.file, buffer)
618
+
619
+ extract_dir = temp_dir_path / "extracted"
620
+ video_files = []
621
+
622
+ if file.filename.lower().endswith(".rar"):
623
+ with rarfile.RarFile(file_path) as rf:
624
+ rf.extractall(extract_dir)
625
+ else:
626
+ with zipfile.ZipFile(file_path) as zf:
627
+ zf.extractall(extract_dir)
628
+
629
+ # Find video files in extracted content
630
+ for root, dirs, files in os.walk(extract_dir):
631
+ for file in files:
632
+ if file.lower().endswith((".mp4", ".avi", ".mov", ".mkv")):
633
+ video_files.append(Path(root) / file)
634
+
635
+ if not video_files:
636
+ return JSONResponse(status_code=400, content={
637
+ "error": "No video files found in archive"
638
+ })
639
+
640
+ results = []
641
+ for video_path in video_files:
642
+ video_name = video_path.name
643
+ frames_dir = temp_dir_path / f"frames_{video_name}"
644
+ frame_count = extract_frames(video_path, frames_dir, fps)
645
+
646
+ frame_analyses = []
647
+ for frame_file in sorted(frames_dir.glob("*.png")):
648
+ analysis = analyze_frame_with_florence2(str(frame_file), prompt)
649
+ frame_analyses.append(analysis)
650
+
651
+ summary = summarize_activities(frame_analyses)
652
+
653
+ results.append({
654
+ "video_filename": video_name,
655
+ "frame_count": frame_count,
656
+ "fps": fps,
657
+ "frame_analyses": frame_analyses,
658
+ "summary": summary
659
+ })
660
+
661
+ return JSONResponse(content={
662
+ "archive_filename": file.filename,
663
+ "videos_processed": len(video_files),
664
+ "results": results
665
+ })
666
+
667
+ @app.get("/health")
668
+ async def health_check():
669
+ """Health check endpoint."""
670
+ return JSONResponse(content={
671
+ "status": "healthy",
672
+ "model": "Florence-2 (Mock)",
673
+ "note": "Florence-2 model is mocked due to sandbox memory limitations."
674
+ })
675
+
676
+ @app.get("/status")
677
+ async def get_processing_status():
678
+ """Get current processing status."""
679
+ return JSONResponse(content=processing_status)
680
+
681
+ # Expose necessary functions and variables
682
+ __all__ = [
683
+ "main_processing_loop",
684
+ "processing_status",
685
+ "ANALYSIS_OUTPUT_FOLDER",
686
+ "log_message",
687
+ "send_email_with_attachment",
688
+ "analyze_frames",
689
+ "extract_frames",
690
+ "DEFAULT_FPS",
691
+ "ensure_dir"
692
+ ]