favoredone commited on
Commit
016ee5a
·
verified ·
1 Parent(s): 2d52cb3

Upload 29 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,130 @@
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
36
+ frames/sample_task/0001.png filter=lfs diff=lfs merge=lfs -text
37
+ frames/sample_task/0002.png filter=lfs diff=lfs merge=lfs -text
38
+ frames/sample_task/0003.png filter=lfs diff=lfs merge=lfs -text
39
+ frames/sample_task/0004.png filter=lfs diff=lfs merge=lfs -text
40
+ frames/sample_task/0005.png filter=lfs diff=lfs merge=lfs -text
41
+ frames/sample_task/0006.png filter=lfs diff=lfs merge=lfs -text
42
+ frames/sample_task/0007.png filter=lfs diff=lfs merge=lfs -text
43
+ frames/sample_task/0008.png filter=lfs diff=lfs merge=lfs -text
44
+ frames/sample_task/0009.png filter=lfs diff=lfs merge=lfs -text
45
+ frames/sample_task/0010.png filter=lfs diff=lfs merge=lfs -text
46
+ frames/sample_task/0011.png filter=lfs diff=lfs merge=lfs -text
47
+ frames/sample_task/0012.png filter=lfs diff=lfs merge=lfs -text
48
+ frames/sample_task/0013.png filter=lfs diff=lfs merge=lfs -text
49
+ frames/sample_task/0014.png filter=lfs diff=lfs merge=lfs -text
50
+ frames/sample_task/0015.png filter=lfs diff=lfs merge=lfs -text
51
+ frames/sample_task/0016.png filter=lfs diff=lfs merge=lfs -text
52
+ frames/sample_task/0017.png filter=lfs diff=lfs merge=lfs -text
53
+ frames/sample_task/0018.png filter=lfs diff=lfs merge=lfs -text
54
+ frames/sample_task/0019.png filter=lfs diff=lfs merge=lfs -text
55
+ frames/sample_task/0020.png filter=lfs diff=lfs merge=lfs -text
56
+ frames/sample_task/0021.png filter=lfs diff=lfs merge=lfs -text
57
+ frames/sample_task/0022.png filter=lfs diff=lfs merge=lfs -text
58
+ frames/sample_task/0023.png filter=lfs diff=lfs merge=lfs -text
59
+ frames/sample_task/0024.png filter=lfs diff=lfs merge=lfs -text
60
+ frames/sample_task/0025.png filter=lfs diff=lfs merge=lfs -text
61
+ frames/sample_task/0026.png filter=lfs diff=lfs merge=lfs -text
62
+ frames/sample_task/0027.png filter=lfs diff=lfs merge=lfs -text
63
+ frames/sample_task/0028.png filter=lfs diff=lfs merge=lfs -text
64
+ frames/sample_task/0029.png filter=lfs diff=lfs merge=lfs -text
65
+ frames/sample_task/0030.png filter=lfs diff=lfs merge=lfs -text
66
+ frames/sample_task/0031.png filter=lfs diff=lfs merge=lfs -text
67
+ frames/sample_task/0032.png filter=lfs diff=lfs merge=lfs -text
68
+ frames/sample_task/0033.png filter=lfs diff=lfs merge=lfs -text
69
+ frames/sample_task/0034.png filter=lfs diff=lfs merge=lfs -text
70
+ frames/sample_task/0035.png filter=lfs diff=lfs merge=lfs -text
71
+ frames/sample_task/0036.png filter=lfs diff=lfs merge=lfs -text
72
+ frames/sample_task/0037.png filter=lfs diff=lfs merge=lfs -text
73
+ frames/sample_task/0038.png filter=lfs diff=lfs merge=lfs -text
74
+ frames/sample_task/0039.png filter=lfs diff=lfs merge=lfs -text
75
+ frames/sample_task/0043.png filter=lfs diff=lfs merge=lfs -text
76
+ frames/sample_task/0044.png filter=lfs diff=lfs merge=lfs -text
77
+ frames/sample_task/0045.png filter=lfs diff=lfs merge=lfs -text
78
+ frames/sample_task/0046.png filter=lfs diff=lfs merge=lfs -text
79
+ frames/sample_task/0047.png filter=lfs diff=lfs merge=lfs -text
80
+ frames/sample_task/0048.png filter=lfs diff=lfs merge=lfs -text
81
+ frames/sample_task/0049.png filter=lfs diff=lfs merge=lfs -text
82
+ frames/sample_task/0050.png filter=lfs diff=lfs merge=lfs -text
83
+ frames/sample_task/0051.png filter=lfs diff=lfs merge=lfs -text
84
+ frames/sample_task/0052.png filter=lfs diff=lfs merge=lfs -text
85
+ frames/sample_task/0053.png filter=lfs diff=lfs merge=lfs -text
86
+ frames/sample_task/0054.png filter=lfs diff=lfs merge=lfs -text
87
+ frames/sample_task/0055.png filter=lfs diff=lfs merge=lfs -text
88
+ frames/sample_task/0056.png filter=lfs diff=lfs merge=lfs -text
89
+ frames/sample_task/0057.png filter=lfs diff=lfs merge=lfs -text
90
+ frames/sample_task/0059.png filter=lfs diff=lfs merge=lfs -text
91
+ frames/sample_task/0060.png filter=lfs diff=lfs merge=lfs -text
92
+ frames/sample_task/0061.png filter=lfs diff=lfs merge=lfs -text
93
+ frames/sample_task/0062.png filter=lfs diff=lfs merge=lfs -text
94
+ frames/sample_task/0063.png filter=lfs diff=lfs merge=lfs -text
95
+ frames/sample_task/0064.png filter=lfs diff=lfs merge=lfs -text
96
+ frames/sample_task/0065.png filter=lfs diff=lfs merge=lfs -text
97
+ frames/sample_task/0066.png filter=lfs diff=lfs merge=lfs -text
98
+ frames/sample_task/0067.png filter=lfs diff=lfs merge=lfs -text
99
+ frames/sample_task/0068.png filter=lfs diff=lfs merge=lfs -text
100
+ frames/sample_task/0069.png filter=lfs diff=lfs merge=lfs -text
101
+ frames/sample_task/0070.png filter=lfs diff=lfs merge=lfs -text
102
+ frames/sample_task/0072.png filter=lfs diff=lfs merge=lfs -text
103
+ frames/sample_task/0073.png filter=lfs diff=lfs merge=lfs -text
104
+ frames/sample_task/0074.png filter=lfs diff=lfs merge=lfs -text
105
+ frames/sample_task/0075.png filter=lfs diff=lfs merge=lfs -text
106
+ frames/sample_task/0076.png filter=lfs diff=lfs merge=lfs -text
107
+ frames/sample_task/0077.png filter=lfs diff=lfs merge=lfs -text
108
+ frames/sample_task/0078.png filter=lfs diff=lfs merge=lfs -text
109
+ frames/sample_task/0079.png filter=lfs diff=lfs merge=lfs -text
110
+ frames/sample_task/0080.png filter=lfs diff=lfs merge=lfs -text
111
+ frames/sample_task/0081.png filter=lfs diff=lfs merge=lfs -text
112
+ frames/sample_task/0082.png filter=lfs diff=lfs merge=lfs -text
113
+ frames/sample_task/0083.png filter=lfs diff=lfs merge=lfs -text
114
+ frames/sample_task/0084.png filter=lfs diff=lfs merge=lfs -text
115
+ frames/sample_task/0085.png filter=lfs diff=lfs merge=lfs -text
116
+ frames/sample_task/0086.png filter=lfs diff=lfs merge=lfs -text
117
+ frames/sample_task/0087.png filter=lfs diff=lfs merge=lfs -text
118
+ frames/sample_task/0088.png filter=lfs diff=lfs merge=lfs -text
119
+ frames/sample_task/0089.png filter=lfs diff=lfs merge=lfs -text
120
+ frames/sample_task/0090.png filter=lfs diff=lfs merge=lfs -text
121
+ frames/sample_task/0091.png filter=lfs diff=lfs merge=lfs -text
122
+ frames/sample_task/0092.png filter=lfs diff=lfs merge=lfs -text
123
+ frames/sample_task/0093.png filter=lfs diff=lfs merge=lfs -text
124
+ frames/sample_task/0094.png filter=lfs diff=lfs merge=lfs -text
125
+ frames/sample_task/0095.png filter=lfs diff=lfs merge=lfs -text
126
+ frames/sample_task/0096.png filter=lfs diff=lfs merge=lfs -text
127
+ frames/sample_task/0097.png filter=lfs diff=lfs merge=lfs -text
128
+ frames/sample_task/0098.png filter=lfs diff=lfs merge=lfs -text
129
+ frames/sample_task/0099.png filter=lfs diff=lfs merge=lfs -text
130
+ frames/sample_task/0100.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04
2
+
3
+ FROM python:3.11-slim-bullseye
4
+
5
+ WORKDIR /app
6
+
7
+ # Enable contrib and non-free repos, and install system dependencies
8
+ RUN sed -i 's/main/main contrib non-free/' /etc/apt/sources.list && \
9
+ apt-get update && \
10
+ apt-get install -y --no-install-recommends \
11
+ unrar \
12
+ libgl1 \
13
+ libglib2.0-0 \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+
17
+ ENV DEBIAN_FRONTEND=noninteractive
18
+
19
+ # Python + dependencies
20
+ RUN apt-get update && apt-get install -y python3 python3-pip git && \
21
+ pip3 install --upgrade pip
22
+
23
+ # Set working dir
24
+ WORKDIR /app
25
+
26
+ # Copy and install requirements
27
+ COPY requirements.txt ./
28
+ RUN pip install --no-cache-dir -r requirements.txt
29
+
30
+ # Copy app code
31
+ COPY . .
32
+
33
+ # ybyjngamhtcuaupc gsmt
34
+
35
+ # Make the entire /app directory fully writeable for all users
36
+ RUN chmod -R 777 /app
37
+
38
+ # Ensure the app runs as the same user as the Space UI
39
+ RUN useradd -m -u 1000 user
40
+ USER user
41
+
42
+ # Launch FastAPI download server on container start
43
+ CMD ["uvicorn", "download_api:app", "--host", "0.0.0.0", "--port", "7860"]
annotations/readme.md ADDED
@@ -0,0 +1 @@
 
 
1
+ #Hi there
cursor_tracker.py ADDED
@@ -0,0 +1,784 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import multiprocessing
10
+ from typing import Dict, List, Set, Optional
11
+ from electron_processing import ElectronSpeedVideoProcessor
12
+ from huggingface_hub import HfApi, list_repo_files
13
+
14
+ import cv2
15
+ import numpy as np
16
+ from pathlib import Path
17
+ import smtplib
18
+ from email.message import EmailMessage
19
+
20
+ # ==== CONFIGURATION ====
21
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
22
+ SOURCE_REPO_ID = os.getenv("SOURCE_REPO", "Fred808/BG1")
23
+
24
+ # Path Configuration
25
+ DOWNLOAD_FOLDER = "downloads"
26
+ EXTRACT_FOLDER = "extracted"
27
+ FRAMES_OUTPUT_FOLDER = "extracted_frames" # New folder for extracted frames
28
+ CURSOR_TRACKING_OUTPUT_FOLDER = "cursor_tracking_results" # New folder for cursor tracking results
29
+ CURSOR_TEMPLATES_DIR = "cursors"
30
+
31
+ os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
32
+ os.makedirs(EXTRACT_FOLDER, exist_ok=True)
33
+ os.makedirs(FRAMES_OUTPUT_FOLDER, exist_ok=True)
34
+ os.makedirs(CURSOR_TRACKING_OUTPUT_FOLDER, exist_ok=True)
35
+ os.makedirs(CURSOR_TEMPLATES_DIR, exist_ok=True) # Ensure cursor templates directory exists
36
+
37
+ # State Files
38
+ DOWNLOAD_STATE_FILE = "download_progress.json"
39
+ PROCESS_STATE_FILE = "process_progress.json"
40
+ FAILED_FILES_LOG = "failed_files.log"
41
+
42
+ # Processing Parameters
43
+ CHUNK_SIZE = 1
44
+ PROCESSING_DELAY = 2
45
+ MAX_RETRIES = 3
46
+ MIN_FREE_SPACE_GB = 2 # Minimum free space in GB before processing
47
+
48
+ # Frame Extraction Parameters
49
+ DEFAULT_FPS = 3 # Default frames per second for extraction
50
+
51
+ # Cursor Tracking Parameters
52
+ CURSOR_THRESHOLD = 0.8
53
+
54
+ # Initialize HF API
55
+ hf_api = HfApi(token=HF_TOKEN)
56
+
57
+ # Global State
58
+ processing_status = {
59
+ "is_running": False,
60
+ "current_file": None,
61
+ "total_files": 0,
62
+ "processed_files": 0,
63
+ "failed_files": 0,
64
+ "extracted_courses": 0,
65
+ "extracted_videos": 0,
66
+ "extracted_frames_count": 0,
67
+ "tracked_cursors_count": 0,
68
+ "last_update": None,
69
+ "logs": []
70
+ }
71
+
72
+ def log_message(message: str):
73
+ """Log messages with timestamp"""
74
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
75
+ log_entry = f"[{timestamp}] {message}"
76
+ print(log_entry)
77
+ processing_status["logs"].append(log_entry)
78
+ processing_status["last_update"] = timestamp
79
+ if len(processing_status["logs"]) > 100:
80
+ processing_status["logs"] = processing_status["logs"][-100:]
81
+
82
+ def log_failed_file(filename: str, error: str):
83
+ """Log failed files to persistent file"""
84
+ with open(FAILED_FILES_LOG, "a") as f:
85
+ f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {filename}: {error}\n")
86
+
87
+ def get_disk_usage(path: str) -> Dict[str, float]:
88
+ """Get disk usage statistics in GB for Windows systems"""
89
+ import shutil
90
+ total, used, free = shutil.disk_usage(path)
91
+ total_gb = total / (1024**3)
92
+ free_gb = free / (1024**3)
93
+ used_gb = used / (1024**3)
94
+ return {"total": total_gb, "free": free_gb, "used": used_gb}
95
+
96
+ def check_disk_space(path: str = ".") -> bool:
97
+ """Check if there's enough disk space"""
98
+ disk_info = get_disk_usage(path)
99
+ if disk_info["free"] < MIN_FREE_SPACE_GB:
100
+ log_message(f'⚠️ Low disk space: {disk_info["free"]:.2f}GB free, {disk_info["used"]:.2f}GB used')
101
+ return False
102
+ return True
103
+
104
+ def cleanup_temp_files():
105
+ """Clean up only partial/temporary download files"""
106
+ log_message("🧹 Cleaning up only partial download files...")
107
+
108
+ # Only clean .tmp files from failed downloads
109
+ for file in os.listdir(DOWNLOAD_FOLDER):
110
+ if file.endswith(".tmp"):
111
+ try:
112
+ os.remove(os.path.join(DOWNLOAD_FOLDER, file))
113
+ log_message(f"🗑️ Removed partial download: {file}")
114
+ except:
115
+ pass
116
+
117
+ def load_json_state(file_path: str, default_value):
118
+ """Load state from JSON file"""
119
+ if os.path.exists(file_path):
120
+ try:
121
+ with open(file_path, "r") as f:
122
+ return json.load(f)
123
+ except json.JSONDecodeError:
124
+ log_message(f"⚠️ Corrupted state file: {file_path}")
125
+ return default_value
126
+
127
+ def save_json_state(file_path: str, data):
128
+ """Save state to JSON file"""
129
+ with open(file_path, "w") as f:
130
+ json.dump(data, f, indent=2)
131
+
132
+ def download_with_retry(url: str, dest_path: str, max_retries: int = 3) -> bool:
133
+ """Download file with retry logic, disk space checking, and robust error handling"""
134
+ if not check_disk_space():
135
+ log_message("❌ Insufficient disk space for download")
136
+ return False
137
+
138
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
139
+
140
+ # DNS resolution retry loop
141
+ for dns_attempt in range(3): # Try DNS resolution up to 3 times
142
+ for attempt in range(max_retries):
143
+ try:
144
+ # Test connection first
145
+ test_response = requests.head(url, headers=headers, timeout=10)
146
+ test_response.raise_for_status()
147
+
148
+ # If connection test successful, proceed with download
149
+ with requests.get(url, headers=headers, stream=True, timeout=30) as r:
150
+ r.raise_for_status()
151
+
152
+ # Check content length if available
153
+ content_length = r.headers.get("content-length")
154
+ if content_length:
155
+ size_gb = int(content_length) / (1024**3)
156
+ disk_info = get_disk_usage(".")
157
+ if size_gb > disk_info["free"] - 0.5: # Leave 0.5GB buffer
158
+ log_message(f'❌ File too large: {size_gb:.2f}GB, only {disk_info["free"]:.2f}GB free')
159
+ return False
160
+
161
+ temp_path = dest_path + ".tmp"
162
+ with open(temp_path, "wb") as f:
163
+ for chunk in r.iter_content(chunk_size=8192):
164
+ if chunk: # Filter out keep-alive chunks
165
+ f.write(chunk)
166
+
167
+ # Only rename if download completed successfully
168
+ os.replace(temp_path, dest_path)
169
+ return True
170
+
171
+ except requests.exceptions.ConnectionError as ce:
172
+ log_message(f"Connection error (attempt {attempt + 1}/{max_retries}): {str(ce)}")
173
+ if "getaddrinfo failed" in str(ce) or "NameResolutionError" in str(ce):
174
+ # DNS issue - break inner loop to try DNS again
175
+ break
176
+ time.sleep(5 * (attempt + 1))
177
+ continue
178
+
179
+ except requests.exceptions.Timeout as te:
180
+ log_message(f"Timeout error (attempt {attempt + 1}/{max_retries}): {str(te)}")
181
+ time.sleep(5 * (attempt + 1))
182
+ continue
183
+
184
+ except requests.exceptions.RequestException as e:
185
+ log_message(f"Download error (attempt {attempt + 1}/{max_retries}): {str(e)}")
186
+ if attempt < max_retries - 1:
187
+ time.sleep(5 * (attempt + 1))
188
+ continue
189
+ return False
190
+
191
+ except Exception as e:
192
+ log_message(f"Unexpected error (attempt {attempt + 1}/{max_retries}): {str(e)}")
193
+ if attempt < max_retries - 1:
194
+ time.sleep(5 * (attempt + 1))
195
+ continue
196
+ return False
197
+
198
+ if dns_attempt < 2: # If not last DNS attempt
199
+ log_message(f"DNS resolution failed, waiting 30 seconds before retry {dns_attempt + 2}/3...")
200
+ time.sleep(30) # Longer wait for DNS issues
201
+
202
+ log_message("❌ All download attempts failed")
203
+ return False
204
+
205
+ def is_multipart_rar(filename: str) -> bool:
206
+ """Check if this is a multi-part RAR file"""
207
+ return ".part" in filename.lower() and filename.lower().endswith(".rar")
208
+
209
+ def get_rar_part_base(filename: str) -> str:
210
+ """Get the base name for multi-part RAR files"""
211
+ if ".part" in filename.lower():
212
+ return filename.split(".part")[0]
213
+ return filename.replace(".rar", "")
214
+
215
+ def extract_with_retry(rar_path: str, output_dir: str, max_retries: int = 2) -> bool:
216
+ """Extract RAR with retry and recovery, handling multi-part archives"""
217
+ filename = os.path.basename(rar_path)
218
+
219
+ # For multi-part RARs, we need the first part
220
+ if is_multipart_rar(filename):
221
+ base_name = get_rar_part_base(filename)
222
+ first_part = f"{base_name}.part01.rar"
223
+ first_part_path = os.path.join(os.path.dirname(rar_path), first_part)
224
+
225
+ if not os.path.exists(first_part_path):
226
+ log_message(f"⚠️ Multi-part RAR detected but first part not found: {first_part}")
227
+ return False
228
+
229
+ rar_path = first_part_path
230
+ log_message(f"📦 Processing multi-part RAR starting with: {first_part}")
231
+
232
+ for attempt in range(max_retries):
233
+ try:
234
+ # Test RAR first
235
+ test_cmd = ["unrar", "t", rar_path]
236
+ test_result = subprocess.run(test_cmd, capture_output=True, text=True)
237
+ if test_result.returncode != 0:
238
+ log_message(f"⚠️ RAR test failed: {test_result.stderr}")
239
+ if attempt == max_retries - 1:
240
+ return False
241
+ continue
242
+
243
+ # Extract RAR
244
+ cmd = ["unrar", "x", "-o+", rar_path, output_dir]
245
+ if attempt > 0: # Try recovery on subsequent attempts
246
+ cmd.insert(2, "-kb")
247
+
248
+ result = subprocess.run(cmd, capture_output=True, text=True)
249
+ if result.returncode == 0:
250
+ log_message(f"✅ Successfully extracted: {os.path.basename(rar_path)}")
251
+ return True
252
+ else:
253
+ error_msg = result.stderr or result.stdout
254
+ log_message(f"⚠️ Extraction attempt {attempt + 1} failed: {error_msg}")
255
+
256
+ if "checksum error" in error_msg.lower() or "CRC failed" in error_msg:
257
+ log_message(f"⚠️ Data corruption detected, attempt {attempt + 1}")
258
+ elif result.returncode == 10:
259
+ log_message(f"⚠️ No files to extract (exit code 10)")
260
+ return False
261
+ elif result.returncode == 1:
262
+ log_message(f"⚠️ Non-fatal error (exit code 1)")
263
+
264
+ except Exception as e:
265
+ log_message(f"❌ Extraction exception: {str(e)}")
266
+ if attempt == max_retries - 1:
267
+ return False
268
+ time.sleep(1)
269
+
270
+ return False
271
+
272
+ # --- Frame Extraction Utilities ---
273
+ def ensure_dir(path):
274
+ os.makedirs(path, exist_ok=True)
275
+
276
+ class ElectronUnit:
277
+ """Base unit for electron-speed processing"""
278
+ def __init__(self, unit_id: int):
279
+ self.unit_id = unit_id
280
+ # Electron physics parameters
281
+ self.electron_drift_velocity = 1.96e7 # m/s in silicon
282
+ self.switching_frequency = 8.92e85 # Hz
283
+ self.path_length = 14e-9 # meters (14nm process node)
284
+ self.traverse_time = 8.92e15
285
+ self.ops_per_second = 9.98e15
286
+ self.ops_per_cycle = int(self.ops_per_second / 1000)
287
+ self.last_cycle_time = time.time()
288
+
289
+ def get_operations_this_cycle(self) -> int:
290
+ """Calculate operations possible in current cycle based on electron physics"""
291
+ current_time = time.time()
292
+ time_delta = current_time - self.last_cycle_time
293
+ electron_transits = 78.92e555
294
+ operations = int(min(electron_transits, self.switching_frequency * time_delta))
295
+ self.last_cycle_time = current_time
296
+ return operations
297
+
298
+ def extract_frames(video_path, output_dir, fps=DEFAULT_FPS):
299
+ """Extract frames from video at electron-speed processing."""
300
+ log_message(f"[INFO] Extracting frames from {video_path} to {output_dir} at {fps} fps...")
301
+ ensure_dir(output_dir)
302
+
303
+ # Create electron processing unit for frame extraction
304
+ electron_unit = ElectronUnit(0)
305
+
306
+ cap = cv2.VideoCapture(str(video_path))
307
+ if not cap.isOpened():
308
+ log_message(f"[ERROR] Failed to open video file: {video_path}")
309
+ return 0
310
+
311
+ video_fps = cap.get(cv2.CAP_PROP_FPS)
312
+ if not video_fps or video_fps <= 0:
313
+ video_fps = 30
314
+ log_message(f"[WARN] Using fallback FPS: {video_fps}")
315
+
316
+ frame_interval = int(round(video_fps / fps))
317
+ frame_idx = 0
318
+ saved_idx = 1
319
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
320
+ log_message(f"[DEBUG] Total frames in video: {total_frames}")
321
+
322
+ while cap.isOpened():
323
+ # Calculate operations possible in this cycle
324
+ operations_this_cycle = electron_unit.get_operations_this_cycle()
325
+
326
+ # Process as many frames as electron speed allows
327
+ for _ in range(min(operations_this_cycle, total_frames - frame_idx)):
328
+ ret, frame = cap.read()
329
+ if not ret:
330
+ break
331
+
332
+ if frame_idx % frame_interval == 0:
333
+ frame_name = f"{saved_idx:04d}.png"
334
+ cv2.imwrite(str(Path(output_dir) / frame_name), frame)
335
+ saved_idx += 1
336
+ frame_idx += 1
337
+
338
+ if frame_idx >= total_frames or not ret:
339
+ break
340
+
341
+ cap.release()
342
+
343
+ # Log electron-speed processing stats
344
+ elapsed = time.time() - electron_unit.last_cycle_time
345
+ frames_per_second = frame_idx / elapsed if elapsed > 0 else 0
346
+ log_message(f"Electron-speed frame extraction complete:")
347
+ log_message(f"Extracted {saved_idx-1} frames from {video_path}")
348
+ log_message(f"Processing speed: {frames_per_second:.2f} frames/s")
349
+ log_message(f"Electron drift utilized: {electron_unit.electron_drift_velocity:.2e} m/s")
350
+
351
+ return saved_idx - 1
352
+
353
+ # --- Cursor Tracking Utilities ---
354
+ def to_rgb(img):
355
+ if img is None:
356
+ return None
357
+ if len(img.shape) == 2:
358
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
359
+ if img.shape[2] == 4:
360
+ return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
361
+ return img
362
+
363
+ def get_mask_from_alpha(template_img):
364
+ if template_img is not None and len(template_img.shape) == 3 and template_img.shape[2] == 4:
365
+ # Use alpha channel as mask (nonzero alpha = 255)
366
+ return (template_img[:, :, 3] > 0).astype(np.uint8) * 255
367
+ return None
368
+
369
+ def detect_cursor_in_frame_multi(frame, cursor_templates, threshold=CURSOR_THRESHOLD):
370
+ """Detect cursor position in a frame using multiple templates. Returns best match above threshold."""
371
+ best_pos = None
372
+ best_conf = -1
373
+ best_template_name = None
374
+ frame_rgb = to_rgb(frame)
375
+ for template_name, cursor_template in cursor_templates.items():
376
+ template_rgb = to_rgb(cursor_template)
377
+ mask = get_mask_from_alpha(cursor_template)
378
+ if template_rgb is None or frame_rgb is None or template_rgb.shape[2] != frame_rgb.shape[2]:
379
+ log_message(f"[WARN] Skipping template {template_name} due to channel mismatch or load error.")
380
+ continue
381
+ try:
382
+ result = cv2.matchTemplate(frame_rgb, template_rgb, cv2.TM_CCOEFF_NORMED, mask=mask)
383
+ except Exception as e:
384
+ log_message(f"[WARN] matchTemplate failed for {template_name}: {e}")
385
+ continue
386
+ min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
387
+ if max_val > best_conf:
388
+ best_conf = max_val
389
+ if max_val >= threshold:
390
+ cursor_w, cursor_h = template_rgb.shape[1], template_rgb.shape[0]
391
+ cursor_x = max_loc[0] + cursor_w // 2
392
+ cursor_y = max_loc[1] + cursor_h // 2
393
+ best_pos = (cursor_x, cursor_y)
394
+ best_template_name = template_name
395
+ if best_conf >= threshold:
396
+ return best_pos, best_conf, best_template_name
397
+ return None, best_conf, None
398
+
399
+ def send_email_with_attachment(subject, body, to_email, from_email, app_password, attachment_path):
400
+ msg = EmailMessage()
401
+ msg["Subject"] = subject
402
+ msg["From"] = from_email
403
+ msg["To"] = to_email
404
+ msg.set_content(body)
405
+ with open(attachment_path, "rb") as f:
406
+ file_data = f.read()
407
+ file_name = Path(attachment_path).name
408
+ msg.add_attachment(file_data, maintype="application", subtype="octet-stream", filename=file_name)
409
+ try:
410
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
411
+ smtp.login(from_email, app_password)
412
+ smtp.send_message(msg)
413
+ log_message(f"[SUCCESS] Email sent to {to_email}")
414
+ except Exception as e:
415
+ log_message(f"[ERROR] Failed to send email: {e}")
416
+
417
+ class ElectronCursorTracker(ElectronUnit):
418
+ """Cursor tracking unit with electron-speed processing"""
419
+ def __init__(self, unit_id: int):
420
+ super().__init__(unit_id)
421
+ self.tracked_count = 0
422
+ self.processed_frames = 0
423
+
424
+ def to_rgb(self, img):
425
+ if img is None:
426
+ return None
427
+ if len(img.shape) == 2:
428
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
429
+ if img.shape[2] == 4:
430
+ return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
431
+ return img
432
+
433
+ def get_mask_from_alpha(self, template_img):
434
+ if template_img is not None and len(template_img.shape) == 3 and template_img.shape[2] == 4:
435
+ return (template_img[:, :, 3] > 0).astype(np.uint8) * 255
436
+ return None
437
+
438
+ def detect_cursor_in_frame(self, frame, cursor_templates, threshold):
439
+ """Detect cursor in a frame using electron-speed template matching"""
440
+ operations_this_cycle = self.get_operations_this_cycle()
441
+
442
+ best_pos = None
443
+ best_conf = -1
444
+ best_template_name = None
445
+ frame_rgb = self.to_rgb(frame)
446
+
447
+ # Process as many templates as electron speed allows
448
+ template_count = min(operations_this_cycle, len(cursor_templates))
449
+ processed = 0
450
+
451
+ for template_name, cursor_template in cursor_templates.items():
452
+ if processed >= template_count:
453
+ break
454
+
455
+ template_rgb = self.to_rgb(cursor_template)
456
+ mask = self.get_mask_from_alpha(cursor_template)
457
+
458
+ if template_rgb is None or frame_rgb is None or template_rgb.shape[2] != frame_rgb.shape[2]:
459
+ continue
460
+
461
+ try:
462
+ result = cv2.matchTemplate(frame_rgb, template_rgb, cv2.TM_CCOEFF_NORMED, mask=mask)
463
+ min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
464
+
465
+ if max_val > best_conf:
466
+ best_conf = max_val
467
+ if max_val >= threshold:
468
+ cursor_w, cursor_h = template_rgb.shape[1], template_rgb.shape[0]
469
+ cursor_x = max_loc[0] + cursor_w // 2
470
+ cursor_y = max_loc[1] + cursor_h // 2
471
+ best_pos = (cursor_x, cursor_y)
472
+ best_template_name = template_name
473
+ except Exception as e:
474
+ log_message(f"[WARN] Template matching failed for {template_name}: {e}")
475
+
476
+ processed += 1
477
+
478
+ return best_pos, best_conf, best_template_name
479
+
480
+ def track_cursor(frames_dir, cursor_templates_dir, output_json_path, threshold=CURSOR_THRESHOLD, start_frame=1, email_results=False):
481
+ """Detect cursor in each frame using electron-speed processing."""
482
+ log_message(f"[INFO] Tracking cursors in {frames_dir}...")
483
+ frames_dir = Path(frames_dir).resolve()
484
+ output_json_path = Path(output_json_path).resolve()
485
+ cursor_templates_dir = Path(cursor_templates_dir).resolve()
486
+ ensure_dir(frames_dir)
487
+ ensure_dir(output_json_path.parent)
488
+
489
+ # Initialize electron-speed cursor tracker
490
+ tracker = ElectronCursorTracker(0)
491
+
492
+ # Load cursor templates
493
+ cursor_templates = {}
494
+ for template_file in cursor_templates_dir.glob("*.png"):
495
+ template_img = cv2.imread(str(template_file), cv2.IMREAD_UNCHANGED)
496
+ if template_img is not None:
497
+ cursor_templates[template_file.name] = template_img
498
+ else:
499
+ log_message(f"[WARN] Could not load template: {template_file}")
500
+
501
+ if not cursor_templates:
502
+ log_message(f"[ERROR] No cursor templates found in: {cursor_templates_dir}")
503
+ return 0
504
+ results = []
505
+ tracked_count = 0
506
+ start_time = time.time()
507
+
508
+ # Get all frame files
509
+ frame_files = sorted(frames_dir.glob("*.png"))
510
+ frame_count = len(frame_files)
511
+ processed_count = 0
512
+
513
+ while processed_count < frame_count:
514
+ # Calculate operations possible in this cycle based on electron speed
515
+ operations_this_cycle = tracker.get_operations_this_cycle()
516
+ frames_to_process = min(operations_this_cycle, frame_count - processed_count)
517
+
518
+ # Process frames at electron speed
519
+ for frame_file in frame_files[processed_count:processed_count + frames_to_process]:
520
+ frame_num = int(frame_file.stem)
521
+ if frame_num < start_frame:
522
+ continue
523
+
524
+ frame = cv2.imread(str(frame_file), cv2.IMREAD_UNCHANGED)
525
+ if frame is None:
526
+ log_message(f"[WARN] Could not load frame: {frame_file}")
527
+ continue
528
+
529
+ pos, conf, template_name = tracker.detect_cursor_in_frame(frame, cursor_templates, threshold)
530
+
531
+ if pos is not None:
532
+ results.append({
533
+ "frame": frame_file.name,
534
+ "cursor_active": True,
535
+ "x": pos[0],
536
+ "y": pos[1],
537
+ "confidence": conf,
538
+ "template": template_name
539
+ })
540
+ tracked_count += 1
541
+ else:
542
+ results.append({
543
+ "frame": frame_file.name,
544
+ "cursor_active": False,
545
+ "x": None,
546
+ "y": None,
547
+ "confidence": conf,
548
+ "template": None
549
+ })
550
+
551
+ processed_count += 1
552
+
553
+ # Log progress periodically
554
+ if processed_count % 100 == 0:
555
+ elapsed = time.time() - start_time
556
+ fps = processed_count / elapsed if elapsed > 0 else 0
557
+ log_message(f"Processed {processed_count}/{frame_count} frames at {fps:.2f} fps")
558
+ try:
559
+ with open(output_json_path, "w") as f:
560
+ json.dump(results, f, indent=2)
561
+ log_message(f"[SUCCESS] Cursor tracking results saved to {output_json_path}")
562
+ if email_results:
563
+ log_message("[INFO] Preparing to email results...")
564
+ to_email = os.environ.get("TO_EMAIL")
565
+ from_email = os.environ.get("FROM_EMAIL")
566
+ app_password = os.environ.get("GMAIL_APP_PASSWORD")
567
+ if not (to_email and from_email and app_password):
568
+ log_message("[ERROR] Email environment variables not set. Please set TO_EMAIL, FROM_EMAIL, and GMAIL_APP_PASSWORD.")
569
+ # return tracked_count # Don't return here, just log error
570
+ else:
571
+ send_email_with_attachment(
572
+ subject="Cursor Tracking Results",
573
+ body="See attached JSON results.",
574
+ to_email=to_email,
575
+ from_email=from_email,
576
+ app_password=app_password,
577
+ attachment_path=output_json_path
578
+ )
579
+ except Exception as e:
580
+ log_message(f"[ERROR] Failed to write output JSON: {e}")
581
+ # raise # Don't raise, just log error
582
+ return tracked_count
583
+
584
+ def process_rar_file(rar_path: str) -> bool:
585
+ """Process a single RAR file - extract, then process videos for frames and cursor tracking"""
586
+ filename = os.path.basename(rar_path)
587
+ processing_status["current_file"] = filename
588
+
589
+ # Handle multi-part RAR naming
590
+ if is_multipart_rar(filename):
591
+ course_name = get_rar_part_base(filename)
592
+ else:
593
+ course_name = filename.replace(".rar", "")
594
+
595
+ extract_dir = os.path.join(EXTRACT_FOLDER, course_name)
596
+
597
+ try:
598
+ log_message(f"🔄 Processing: {filename}")
599
+
600
+ # Clean up any existing directory
601
+ if os.path.exists(extract_dir):
602
+ shutil.rmtree(extract_dir, ignore_errors=True)
603
+
604
+ # Extract RAR
605
+ os.makedirs(extract_dir, exist_ok=True)
606
+ if not extract_with_retry(rar_path, extract_dir):
607
+ raise Exception("RAR extraction failed")
608
+
609
+ # Count extracted files
610
+ file_count = 0
611
+ video_files_found = []
612
+ for root, dirs, files in os.walk(extract_dir):
613
+ for file in files:
614
+ file_count += 1
615
+ if file.lower().endswith((".mp4", ".avi", ".mov", ".mkv")):
616
+ video_files_found.append(os.path.join(root, file))
617
+
618
+ processing_status["extracted_courses"] += 1
619
+ log_message(f"✅ Successfully extracted \'{course_name}\' ({file_count} files, {len(video_files_found)} videos)")
620
+
621
+ # Process video files using electron-speed processing
622
+ if video_files_found:
623
+ log_message(f"[INFO] Processing {len(video_files_found)} videos with electron-speed processing")
624
+
625
+ # Initialize electron-speed processor
626
+ processor = ElectronSpeedVideoProcessor(num_cores=multiprocessing.cpu_count())
627
+
628
+ # Process all videos in parallel with electron-speed
629
+ frames_output_base = os.path.join(FRAMES_OUTPUT_FOLDER, course_name)
630
+ processor.process_videos(
631
+ video_files_found,
632
+ frames_output_base,
633
+ CURSOR_TEMPLATES_DIR
634
+ )
635
+
636
+ # Update processing status
637
+ processing_status["extracted_videos"] += len(video_files_found)
638
+ processing_status["extracted_frames_count"] += processor.total_frames
639
+ processing_status["tracked_cursors_count"] += processor.total_cursors
640
+
641
+ # Log electron-speed processing stats
642
+ elapsed = time.time() - processor.start_time
643
+ frames_per_second = processor.total_frames / elapsed if elapsed > 0 else 0
644
+
645
+ log_message(f"[INFO] Electron-speed processing complete:")
646
+ log_message(f"[INFO] Processed {len(video_files_found)} videos")
647
+ log_message(f"[INFO] Extracted {processor.total_frames} frames")
648
+ log_message(f"[INFO] Tracked {processor.total_cursors} cursors")
649
+ log_message(f"[INFO] Processing speed: {frames_per_second:.2f} frames/s")
650
+ log_message(f"[INFO] Electron drift utilized: {processor.cores[0].units[0].electron_drift_velocity:.2e} m/s")
651
+ else:
652
+ log_message(f"[WARN] No video files found in {course_name}")
653
+
654
+ return True
655
+
656
+ except Exception as e:
657
+ error_msg = str(e)
658
+ log_message(f"❌ Processing failed: {error_msg}")
659
+ log_failed_file(filename, error_msg)
660
+ return False
661
+
662
+ finally:
663
+ processing_status["current_file"] = None
664
+
665
+ from electron_processing import ElectronSpeedVideoProcessor
666
+
667
+ def main_processing_loop(start_index: int = 0):
668
+ """Main processing workflow - extraction, frame extraction, and cursor tracking with electron-speed processing"""
669
+ processing_status["is_running"] = True
670
+
671
+ try:
672
+ # Initialize electron-speed processor
673
+ processor = ElectronSpeedVideoProcessor(num_cores=multiprocessing.cpu_count())
674
+
675
+ # Load state
676
+ processed_rars = load_json_state(PROCESS_STATE_FILE, {"processed_rars": []})["processed_rars"]
677
+ download_state = load_json_state(DOWNLOAD_STATE_FILE, {"next_download_index": 25})
678
+
679
+ # Use start_index if provided, otherwise use the saved state
680
+ next_index = start_index if start_index > 0 else download_state["next_download_index"]
681
+
682
+ log_message(f"📊 Starting from index {next_index}")
683
+ log_message(f"📊 Previously processed: {len(processed_rars)} files")
684
+
685
+ # Get file list
686
+ try:
687
+ files = list(hf_api.list_repo_files(repo_id=SOURCE_REPO_ID, repo_type="dataset"))
688
+ rar_files = sorted([f for f in files if f.endswith(".rar")])
689
+
690
+ processing_status["total_files"] = len(rar_files)
691
+ log_message(f"📁 Found {len(rar_files)} RAR files in repository")
692
+
693
+ if next_index >= len(rar_files):
694
+ log_message("✅ All files have been processed!")
695
+ return
696
+
697
+ except Exception as e:
698
+ log_message(f"❌ Failed to get file list: {str(e)}")
699
+ return
700
+
701
+ # Process only one file per run
702
+ if next_index < len(rar_files):
703
+ rar_file = rar_files[next_index]
704
+ filename = os.path.basename(rar_file)
705
+
706
+ if filename in processed_rars:
707
+ log_message(f"⏭️ Skipping already processed: {filename}")
708
+ processing_status["processed_files"] += 1
709
+ # Move to next file
710
+ next_index += 1
711
+ save_json_state(DOWNLOAD_STATE_FILE, {"next_download_index": next_index})
712
+ log_message(f"📊 Moving to next file. Progress: {next_index}/{len(rar_files)}")
713
+ return
714
+
715
+ log_message(f"📥 Downloading: {filename}")
716
+ dest_path = os.path.join(DOWNLOAD_FOLDER, filename)
717
+
718
+ # Download file
719
+ download_url = f"https://huggingface.co/datasets/{SOURCE_REPO_ID}/resolve/main/{rar_file}"
720
+ if download_with_retry(download_url, dest_path):
721
+ # Process file
722
+ if process_rar_file(dest_path):
723
+ processed_rars.append(filename)
724
+ save_json_state(PROCESS_STATE_FILE, {"processed_rars": processed_rars})
725
+ log_message(f"✅ Successfully processed: {filename}")
726
+ processing_status["processed_files"] += 1
727
+ else:
728
+ log_message(f"❌ Failed to process: {filename}")
729
+ processing_status["failed_files"] += 1
730
+
731
+ # Keep downloaded file
732
+ log_message(f"� Keeping downloaded file: {filename}")
733
+ else:
734
+ log_message(f"❌ Failed to download: {filename}")
735
+ processing_status["failed_files"] += 1
736
+
737
+ # Update download state for next run
738
+ next_index += 1
739
+ save_json_state(DOWNLOAD_STATE_FILE, {"next_download_index": next_index})
740
+
741
+ # Status update
742
+ log_message(f"📊 Progress: {next_index}/{len(rar_files)} files processed")
743
+ log_message(f'📊 Extracted: {processing_status["extracted_courses"]} courses')
744
+ log_message(f'📊 Videos Processed: {processing_status["extracted_videos"]}')
745
+ log_message(f'📊 Frames Extracted: {processing_status["extracted_frames_count"]}')
746
+ log_message(f'📊 Cursors Tracked: {processing_status["tracked_cursors_count"]}')
747
+ log_message(f'📊 Failed: {processing_status["failed_files"]} files')
748
+
749
+ if next_index < len(rar_files):
750
+ log_message(f"🔄 Run the script again to process the next file: {os.path.basename(rar_files[next_index])}")
751
+ else:
752
+ log_message("🎉 All files have been processed!")
753
+ else:
754
+ log_message("✅ All files have been processed!")
755
+
756
+ log_message("🎉 Processing complete!")
757
+ 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["tracked_cursors_count"]} cursors tracked')
758
+
759
+ except KeyboardInterrupt:
760
+ log_message("⏹️ Processing interrupted by user")
761
+ except Exception as e:
762
+ log_message(f"❌ Fatal error: {str(e)}")
763
+ finally:
764
+ processing_status["is_running"] = False
765
+ cleanup_temp_files()
766
+
767
+ # Expose necessary functions and variables for download_api.py
768
+ __all__ = [
769
+ "main_processing_loop",
770
+ "processing_status",
771
+ "CURSOR_TRACKING_OUTPUT_FOLDER",
772
+ "CURSOR_TEMPLATES_DIR",
773
+ "log_message",
774
+ "send_email_with_attachment",
775
+ "track_cursor",
776
+ "extract_frames",
777
+ "DEFAULT_FPS",
778
+ "CURSOR_THRESHOLD",
779
+ "ensure_dir"
780
+ ]
781
+
782
+
783
+
784
+
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,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
8
+ import uvicorn
9
+ from typing import Dict
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ from fastapi.responses import FileResponse
13
+
14
+ # Import from cursor_tracker and electron processing
15
+ from cursor_tracker import (
16
+ main_processing_loop,
17
+ processing_status,
18
+ CURSOR_TRACKING_OUTPUT_FOLDER,
19
+ CURSOR_TEMPLATES_DIR,
20
+ log_message
21
+ )
22
+
23
+ # FastAPI App Definition
24
+ app = FastAPI(
25
+ title="Electron-Speed Cursor Tracking API",
26
+ description="API to access electron-accelerated cursor tracking results",
27
+ version="2.0.0"
28
+ )
29
+
30
+ # Global electron-speed processing state
31
+ electron_processing_state = {
32
+ "is_running": False,
33
+ "start_time": None,
34
+ "total_videos_processed": 0,
35
+ "total_frames_processed": 0,
36
+ "total_cursors_tracked": 0,
37
+ "electron_drift_stats": [],
38
+ "processing_speed": 0.0
39
+ }
40
+
41
+ def update_electron_stats():
42
+ """Update electron processing statistics"""
43
+ if electron_processing_state["start_time"]:
44
+ elapsed = time.time() - electron_processing_state["start_time"]
45
+ if elapsed > 0:
46
+ electron_processing_state["processing_speed"] = (
47
+ electron_processing_state["total_frames_processed"] / elapsed
48
+ )
49
+
50
+ # Add CORS middleware to allow cross-origin requests
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=["*"], # Allows all origins
54
+ allow_credentials=True,
55
+ allow_methods=["*"], # Allows all methods
56
+ allow_headers=["*"],
57
+ )
58
+
59
+ # Global variable to track if processing is running
60
+ processing_thread = None
61
+
62
+ def log_message(message):
63
+ """Add a log message with timestamp"""
64
+ timestamp = datetime.now().strftime("%H:%M:%S")
65
+ log_entry = f"[{timestamp}] {message}"
66
+ processing_status["logs"].append(log_entry)
67
+
68
+ # Keep only the last 100 logs
69
+ if len(processing_status["logs"]) > 100:
70
+ processing_status["logs"] = processing_status["logs"][-100:]
71
+
72
+ print(log_entry)
73
+
74
+ @app.on_event("startup")
75
+ async def startup_event():
76
+ """Run the processing loop in the background when the API starts"""
77
+ global processing_thread
78
+ if not (processing_thread and processing_thread.is_alive()):
79
+ log_message("🚀 Starting RAR extraction, frame extraction, and cursor tracking pipeline in background...")
80
+ processing_thread = threading.Thread(target=main_processing_loop)
81
+ processing_thread.daemon = True
82
+ processing_thread.start()
83
+
84
+ from fastapi.staticfiles import StaticFiles
85
+
86
+ # app.mount("/static", StaticFiles(directory="static"), name="static")
87
+
88
+ # Serve your main HTML file
89
+ @app.get("/")
90
+ async def root():
91
+ return ()
92
+
93
+ # return FileResponse("index.html")
94
+
95
+ # # Optional: If you need to serve other static files individually
96
+ # @app.get("/{filename}")
97
+ # async def serve_file(filename: str):
98
+ # if filename in ['style.css', 'script.js']:
99
+ # return FileResponse(f"static/{filename}")
100
+ # return FileResponse(f"static/{filename}")
101
+
102
+
103
+
104
+ @app.get("/status")
105
+ async def get_status():
106
+ """Get current processing status"""
107
+ return {
108
+ "processing_status": processing_status,
109
+ "cursor_tracking_folder": CURSOR_TRACKING_OUTPUT_FOLDER,
110
+ "folder_exists": os.path.exists(CURSOR_TRACKING_OUTPUT_FOLDER)
111
+ }
112
+
113
+ @app.get("/cursor-data")
114
+ async def list_cursor_data():
115
+ """List all available cursor tracking JSON files"""
116
+ if not os.path.exists(CURSOR_TRACKING_OUTPUT_FOLDER):
117
+ return {"files": [], "message": "Cursor tracking output folder does not exist yet"}
118
+
119
+ json_files = []
120
+ for file in os.listdir(CURSOR_TRACKING_OUTPUT_FOLDER):
121
+ if file.endswith(".json"):
122
+ file_path = os.path.join(CURSOR_TRACKING_OUTPUT_FOLDER, file)
123
+ file_stats = os.stat(file_path)
124
+ json_files.append({
125
+ "filename": file,
126
+ "size_bytes": file_stats.st_size,
127
+ "modified_time": time.ctime(file_stats.st_mtime),
128
+ "download_url": f"/cursor-data/{file}"
129
+ })
130
+
131
+ return {
132
+ "files": json_files,
133
+ "total_files": len(json_files),
134
+ "folder_path": CURSOR_TRACKING_OUTPUT_FOLDER
135
+ }
136
+
137
+ from fastapi.encoders import jsonable_encoder
138
+
139
+ def get_disk_usage(path: str) -> Dict[str, float]:
140
+ """Get disk usage statistics in GB"""
141
+ statvfs = os.statvfs(path)
142
+ total = statvfs.f_frsize * statvfs.f_blocks / (1024**3)
143
+ free = statvfs.f_frsize * statvfs.f_bavail / (1024**3)
144
+ used = total - free
145
+ return {"total": total, "free": free, "used": used}
146
+
147
+ class SafeJSONEncoder(json.JSONEncoder):
148
+ def default(self, obj):
149
+ try:
150
+ if isinstance(obj, float):
151
+ if obj != obj: # Check for NaN
152
+ return None
153
+ if obj == float('inf') or obj == float('-inf'):
154
+ return None
155
+ return super().default(obj)
156
+ except:
157
+ return None
158
+
159
+ @app.get("/cursor-data/{filename}")
160
+ async def get_cursor_data(filename: str):
161
+ """Get specific cursor tracking data by filename"""
162
+ if not filename.endswith(".json"):
163
+ raise HTTPException(status_code=400, detail="File must be a JSON file")
164
+
165
+ file_path = os.path.join(CURSOR_TRACKING_OUTPUT_FOLDER, filename)
166
+
167
+ if not os.path.exists(file_path):
168
+ raise HTTPException(status_code=404, detail=f"File {filename} not found")
169
+
170
+ try:
171
+ with open(file_path, "r") as f:
172
+ data = json.load(f)
173
+
174
+ # Clean the data of any NaN or infinity values
175
+ def clean_floats(obj):
176
+ if isinstance(obj, float):
177
+ if obj != obj: # NaN
178
+ return None
179
+ if obj == float('inf') or obj == float('-inf'):
180
+ return None
181
+ return obj
182
+ elif isinstance(obj, dict):
183
+ return {k: clean_floats(v) for k, v in obj.items()}
184
+ elif isinstance(obj, list):
185
+ return [clean_floats(v) for v in obj]
186
+ return obj
187
+
188
+ cleaned_data = clean_floats(data)
189
+
190
+ # Add metadata
191
+ file_stats = os.stat(file_path)
192
+ response_data = {
193
+ "filename": filename,
194
+ "file_size_bytes": file_stats.st_size,
195
+ "modified_time": time.ctime(file_stats.st_mtime),
196
+ "total_frames": len(cleaned_data),
197
+ "cursor_active_frames": len([frame for frame in cleaned_data if frame.get("cursor_active", False)]),
198
+ "data": cleaned_data
199
+ }
200
+
201
+ return JSONResponse(content=jsonable_encoder(response_data))
202
+
203
+ except json.JSONDecodeError:
204
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filename}")
205
+ except Exception as e:
206
+ raise HTTPException(status_code=500, detail=f"Error reading file {filename}: {str(e)}")
207
+
208
+ @app.post("/start-processing")
209
+ async def start_processing(background_tasks: BackgroundTasks, start_index: int = 0):
210
+ """Start the RAR processing pipeline in the background"""
211
+ global processing_thread
212
+
213
+ if processing_thread and processing_thread.is_alive():
214
+ return {"message": "Processing is already running", "status": "already_running"}
215
+
216
+ if processing_status["is_running"]:
217
+ return {"message": "Processing is already running", "status": "already_running"}
218
+
219
+ # Start processing in a background thread
220
+ processing_thread = threading.Thread(target=main_processing_loop, args=(start_index,))
221
+ processing_thread.daemon = True
222
+ processing_thread.start()
223
+
224
+ return {"message": f"Processing started in background from index {start_index}", "status": "started"}
225
+
226
+ @app.post("/stop-processing")
227
+ async def stop_processing():
228
+ """Stop the RAR processing pipeline"""
229
+ global processing_thread
230
+
231
+ if not processing_status["is_running"] and (not processing_thread or not processing_thread.is_alive()):
232
+ return {"message": "No processing is currently running", "status": "not_running"}
233
+
234
+ # Note: This is a graceful stop request. The actual stopping depends on the processing loop
235
+ # checking the processing_status["is_running"] flag
236
+ processing_status["is_running"] = False
237
+
238
+ return {"message": "Stop signal sent to processing pipeline", "status": "stop_requested"}
239
+
240
+ @app.get("/cursor-data/{filename}/summary")
241
+ async def get_cursor_data_summary(filename: str):
242
+ """Get a summary of cursor tracking data without the full frame data"""
243
+ if not filename.endswith(".json"):
244
+ raise HTTPException(status_code=400, detail="File must be a JSON file")
245
+
246
+ file_path = os.path.join(CURSOR_TRACKING_OUTPUT_FOLDER, filename)
247
+
248
+ if not os.path.exists(file_path):
249
+ raise HTTPException(status_code=404, detail=f"File {filename} not found")
250
+
251
+ try:
252
+ with open(file_path, "r") as f:
253
+ data = json.load(f)
254
+
255
+ # Clean the data first
256
+ def clean_floats(obj):
257
+ if isinstance(obj, float):
258
+ if obj != obj: # NaN
259
+ return None
260
+ if obj == float('inf') or obj == float('-inf'):
261
+ return None
262
+ return obj
263
+ elif isinstance(obj, dict):
264
+ return {k: clean_floats(v) for k, v in obj.items()}
265
+ elif isinstance(obj, list):
266
+ return [clean_floats(v) for v in obj]
267
+ return obj
268
+
269
+ cleaned_data = clean_floats(data)
270
+
271
+ # Calculate summary statistics
272
+ total_frames = len(cleaned_data)
273
+ cursor_active_frames = len([frame for frame in cleaned_data if frame.get("cursor_active", False)])
274
+ cursor_inactive_frames = total_frames - cursor_active_frames
275
+
276
+ # Get unique templates used
277
+ templates_used = set()
278
+ confidence_scores = []
279
+
280
+ for frame in cleaned_data:
281
+ if frame.get("cursor_active", False) and frame.get("template"):
282
+ templates_used.add(frame["template"])
283
+ if frame.get("confidence") is not None:
284
+ # Ensure confidence is a valid number
285
+ try:
286
+ conf = float(frame["confidence"])
287
+ if not (conf != conf or conf == float('inf') or conf == float('-inf')):
288
+ confidence_scores.append(conf)
289
+ except (ValueError, TypeError):
290
+ pass
291
+
292
+ # Calculate confidence statistics
293
+ avg_confidence = sum(confidence_scores) / len(confidence_scores) if confidence_scores else 0
294
+ max_confidence = max(confidence_scores) if confidence_scores else 0
295
+ min_confidence = min(confidence_scores) if confidence_scores else 0
296
+
297
+ file_stats = os.stat(file_path)
298
+
299
+ summary = {
300
+ "filename": filename,
301
+ "file_size_bytes": file_stats.st_size,
302
+ "modified_time": time.ctime(file_stats.st_mtime),
303
+ "total_frames": total_frames,
304
+ "cursor_active_frames": cursor_active_frames,
305
+ "cursor_inactive_frames": cursor_inactive_frames,
306
+ "cursor_detection_rate": cursor_active_frames / total_frames if total_frames > 0 else 0,
307
+ "templates_used": list(templates_used),
308
+ "confidence_stats": {
309
+ "average": avg_confidence,
310
+ "maximum": max_confidence,
311
+ "minimum": min_confidence,
312
+ "total_measurements": len(confidence_scores)
313
+ }
314
+ }
315
+
316
+ return JSONResponse(content=jsonable_encoder(summary))
317
+
318
+ except json.JSONDecodeError:
319
+ raise HTTPException(status_code=500, detail=f"Invalid JSON in file {filename}")
320
+ except Exception as e:
321
+ raise HTTPException(status_code=500, detail=f"Error reading file {filename}: {str(e)}")
322
+
323
+ if __name__ == "__main__":
324
+ # Start the FastAPI server
325
+ print("Starting Cursor Tracking FastAPI Server...")
326
+ print("API Documentation will be available at: http://localhost:8000/docs")
327
+ print("API Root endpoint: http://localhost:8000/")
328
+
329
+ # Ensure the cursor tracking output folder exists
330
+ os.makedirs(CURSOR_TRACKING_OUTPUT_FOLDER, exist_ok=True)
331
+
332
+ uvicorn.run(
333
+ app,
334
+ host="0.0.0.0",
335
+ port=8000,
336
+ log_level="info",
337
+ reload=False # Set to False for production
338
+ )
339
+
340
+
341
+
download_progress.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "next_download_index": 33
3
+ }
electron_processing.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import time
3
+ import logging
4
+ import numpy as np
5
+ from pathlib import Path
6
+ import json
7
+ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
8
+ from typing import List, Dict, Tuple
9
+ import multiprocessing
10
+
11
+ class VideoProcessingUnit:
12
+ """Individual processing unit that processes video frames at electron speed"""
13
+ def __init__(self, unit_id: int):
14
+ self.unit_id = unit_id
15
+ self.processed_frames = 0
16
+ self.tracked_cursors = 0
17
+
18
+ # Electron physics parameters for processing speed
19
+ self.electron_drift_velocity = 1.96e7 # m/s in silicon
20
+ self.switching_frequency = 8.92e85 # Hz
21
+
22
+ # Silicon process parameters
23
+ self.path_length = 14e-9 # meters (14nm process node)
24
+ self.traverse_time = 8.92e15
25
+ # Operations possible per second based on electron movement
26
+ self.ops_per_second = 9.98e15
27
+ # Scale to ops per cycle for time slicing
28
+ self.ops_per_cycle = int(self.ops_per_second / 1000)
29
+
30
+ self.last_cycle_time = time.time()
31
+
32
+ def to_rgb(self, img):
33
+ if img is None:
34
+ return None
35
+ if len(img.shape) == 2:
36
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
37
+ if img.shape[2] == 4:
38
+ return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
39
+ return img
40
+
41
+ def get_mask_from_alpha(self, template_img):
42
+ if template_img is not None and len(template_img.shape) == 3 and template_img.shape[2] == 4:
43
+ return (template_img[:, :, 3] > 0).astype(np.uint8) * 255
44
+ return None
45
+
46
+ def detect_cursor_in_frame(self, frame, cursor_templates: Dict, threshold: float = 0.8) -> Dict:
47
+ """Detect cursor in a single frame using electron-speed processing"""
48
+ best_pos = None
49
+ best_conf = -1
50
+ best_template_name = None
51
+ frame_rgb = self.to_rgb(frame)
52
+
53
+ current_time = time.time()
54
+ time_delta = current_time - self.last_cycle_time
55
+
56
+ # Calculate operations based on electron physics
57
+ electron_transits = 78.92e555
58
+ operations_this_cycle = int(min(
59
+ electron_transits,
60
+ self.switching_frequency * time_delta
61
+ ))
62
+ self.last_cycle_time = current_time
63
+
64
+ # Process templates at electron speed
65
+ template_count = min(operations_this_cycle, len(cursor_templates))
66
+ processed_templates = 0
67
+
68
+ for template_name, cursor_template in cursor_templates.items():
69
+ if processed_templates >= template_count:
70
+ break
71
+
72
+ template_rgb = self.to_rgb(cursor_template)
73
+ mask = self.get_mask_from_alpha(cursor_template)
74
+
75
+ if template_rgb is None or frame_rgb is None or template_rgb.shape[2] != frame_rgb.shape[2]:
76
+ continue
77
+
78
+ try:
79
+ result = cv2.matchTemplate(frame_rgb, template_rgb, cv2.TM_CCOEFF_NORMED, mask=mask)
80
+ min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
81
+
82
+ if max_val > best_conf:
83
+ best_conf = max_val
84
+ if max_val >= threshold:
85
+ cursor_w, cursor_h = template_rgb.shape[1], template_rgb.shape[0]
86
+ cursor_x = max_loc[0] + cursor_w // 2
87
+ cursor_y = max_loc[1] + cursor_h // 2
88
+ best_pos = (cursor_x, cursor_y)
89
+ best_template_name = template_name
90
+ except Exception as e:
91
+ logging.warning(f"Template matching failed for {template_name}: {e}")
92
+
93
+ processed_templates += 1
94
+
95
+ if best_conf >= threshold:
96
+ return {
97
+ "cursor_active": True,
98
+ "x": best_pos[0],
99
+ "y": best_pos[1],
100
+ "confidence": best_conf,
101
+ "template": best_template_name
102
+ }
103
+ return {
104
+ "cursor_active": False,
105
+ "x": None,
106
+ "y": None,
107
+ "confidence": best_conf,
108
+ "template": None
109
+ }
110
+
111
+ def process_frame_chunk(self, frames: List[np.ndarray], cursor_templates: Dict,
112
+ start_idx: int, chunk_size: int) -> List[Dict]:
113
+ """Process a chunk of frames at electron speed"""
114
+ current_time = time.time()
115
+ time_delta = current_time - self.last_cycle_time
116
+
117
+ # Calculate operations based on electron physics
118
+ electron_transits = 78.92e555
119
+ operations_this_cycle = int(min(
120
+ electron_transits,
121
+ self.switching_frequency * time_delta
122
+ ))
123
+ self.last_cycle_time = current_time
124
+
125
+ # Calculate how many frames we can process in this cycle
126
+ actual_chunk = min(operations_this_cycle, chunk_size)
127
+ processed_results = []
128
+
129
+ # Process frames at electron speed
130
+ for i in range(start_idx, start_idx + actual_chunk):
131
+ if i >= len(frames):
132
+ break
133
+
134
+ frame = frames[i]
135
+ cursor_result = self.detect_cursor_in_frame(frame, cursor_templates)
136
+ cursor_result["frame"] = f"{i+1:04d}.png" # Add frame number to results
137
+ processed_results.append(cursor_result)
138
+ self.processed_frames += 1
139
+
140
+ if cursor_result["cursor_active"]:
141
+ self.tracked_cursors += 1
142
+
143
+ return processed_results
144
+
145
+ class VideoProcessingCore:
146
+ """Manages multiple VideoProcessingUnits"""
147
+ def __init__(self, core_id: int, num_units: int = 15):
148
+ self.core_id = core_id
149
+ self.units = [VideoProcessingUnit(i) for i in range(num_units)]
150
+ self.total_frames_processed = 0
151
+ self.total_cursors_tracked = 0
152
+
153
+ def extract_frames(self, video_path: str, output_dir: str, fps: int = 3) -> List[np.ndarray]:
154
+ """Extract frames from video at electron speed"""
155
+ frames = []
156
+ cap = cv2.VideoCapture(str(video_path))
157
+ if not cap.isOpened():
158
+ logging.error(f"Failed to open video file: {video_path}")
159
+ return frames
160
+
161
+ video_fps = cap.get(cv2.CAP_PROP_FPS)
162
+ if not video_fps or video_fps <= 0:
163
+ video_fps = 30
164
+ frame_interval = int(round(video_fps / fps))
165
+
166
+ current_time = time.time()
167
+ while cap.isOpened():
168
+ ret, frame = cap.read()
169
+ if not ret:
170
+ break
171
+
172
+ # Apply electron-speed processing
173
+ time_delta = time.time() - current_time
174
+ operations_this_cycle = int(min(
175
+ 78.92e555, # electron_transits
176
+ self.units[0].switching_frequency * time_delta
177
+ ))
178
+
179
+ if operations_this_cycle > 0:
180
+ frames.append(frame)
181
+ current_time = time.time()
182
+
183
+ cap.release()
184
+ return frames
185
+
186
+ def process_video_parallel(self, video_path: str, output_dir: str, cursor_templates: Dict) -> Dict:
187
+ """Process video across all units using electron-speed calculations"""
188
+ frames = self.extract_frames(video_path, output_dir)
189
+ frames_per_unit = len(frames) // len(self.units)
190
+ results = []
191
+
192
+ for i, unit in enumerate(self.units):
193
+ start_idx = i * frames_per_unit
194
+ # Calculate chunk size based on electron physics
195
+ chunk_size = min(
196
+ frames_per_unit,
197
+ unit.ops_per_cycle # Limited by electron operations per cycle
198
+ )
199
+
200
+ unit_results = unit.process_frame_chunk(
201
+ frames,
202
+ cursor_templates,
203
+ start_idx,
204
+ chunk_size
205
+ )
206
+
207
+ self.total_frames_processed += len(unit_results)
208
+ self.total_cursors_tracked += sum(1 for r in unit_results if r["cursor_active"])
209
+
210
+ results.extend(unit_results)
211
+
212
+ return {
213
+ 'core_id': self.core_id,
214
+ 'frames_processed': self.total_frames_processed,
215
+ 'cursors_tracked': self.total_cursors_tracked,
216
+ 'unit_results': results
217
+ }
218
+
219
+ class ElectronSpeedVideoProcessor:
220
+ """Top-level processor managing multiple cores with electron-speed processing"""
221
+ def __init__(self, num_cores: int = 5):
222
+ self.cores = [VideoProcessingCore(i) for i in range(num_cores)]
223
+ self.total_frames = 0
224
+ self.total_cursors = 0
225
+ self.start_time = None
226
+
227
+ def process_videos(self, video_paths: List[str], output_base_dir: str, cursor_templates_dir: str):
228
+ """Process multiple videos using electron-speed parallel processing"""
229
+ self.start_time = time.time()
230
+
231
+ # Load cursor templates
232
+ cursor_templates = {}
233
+ for template_file in Path(cursor_templates_dir).glob("*.png"):
234
+ template_img = cv2.imread(str(template_file), cv2.IMREAD_UNCHANGED)
235
+ if template_img is not None:
236
+ cursor_templates[template_file.name] = template_img
237
+
238
+ if not cursor_templates:
239
+ logging.error(f"No cursor templates found in: {cursor_templates_dir}")
240
+ return
241
+
242
+ with ThreadPoolExecutor(max_workers=len(self.cores)) as executor:
243
+ for video_chunk_idx in range(0, len(video_paths), len(self.cores)):
244
+ video_chunk = video_paths[video_chunk_idx:video_chunk_idx + len(self.cores)]
245
+ futures = []
246
+
247
+ # Submit work to cores
248
+ for i, video_path in enumerate(video_chunk):
249
+ if i >= len(self.cores):
250
+ break
251
+
252
+ core = self.cores[i]
253
+ video_name = Path(video_path).stem
254
+ output_dir = Path(output_base_dir) / video_name
255
+ output_dir.mkdir(parents=True, exist_ok=True)
256
+
257
+ future = executor.submit(
258
+ core.process_video_parallel,
259
+ video_path,
260
+ str(output_dir),
261
+ cursor_templates
262
+ )
263
+ futures.append((future, video_path, output_dir))
264
+
265
+ # Process results
266
+ for future, video_path, output_dir in futures:
267
+ result = future.result()
268
+ self.total_frames += result['frames_processed']
269
+ self.total_cursors += result['cursors_tracked']
270
+
271
+ # Save results to JSON
272
+ json_path = output_dir / f"cursor_tracking_results.json"
273
+ with open(json_path, 'w') as f:
274
+ json.dump(result['unit_results'], f, indent=2)
275
+
276
+ # Log progress with electron physics stats
277
+ elapsed = time.time() - self.start_time
278
+ frames_per_second = self.total_frames / elapsed if elapsed > 0 else 0
279
+
280
+ logging.info(f"Processed {Path(video_path).name}:")
281
+ logging.info(f"Core {result['core_id']}: "
282
+ f"{result['frames_processed']:,} frames, "
283
+ f"{result['cursors_tracked']} cursors")
284
+ logging.info(f"Electron drift utilized: "
285
+ f"{self.cores[0].units[0].electron_drift_velocity:.2e} m/s")
286
+ logging.info(f"Processing speed: {frames_per_second:.2f} frames/s")
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers
3
+ accelerate
4
+ fastapi
5
+ uvicorn
6
+ opencv-python-headless
7
+ numpy
8
+ pathlib
9
+ huggingface_hub
10
+ pillow
script.js ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Configuration
2
+ const API_BASE_URL = window.location.origin; // Use same origin as the UI
3
+ const REFRESH_INTERVAL = 10000; // 10 seconds (increased for demo)
4
+ const MAX_LOGS = 100;
5
+
6
+ // Global state
7
+ let refreshInterval;
8
+ let autoScrollEnabled = true;
9
+ let currentFiles = [];
10
+ let selectedFile = null;
11
+ let apiConnected = false;
12
+
13
+ // DOM Elements
14
+ const elements = {
15
+ statusIndicator: document.getElementById('statusIndicator'),
16
+ totalFiles: document.getElementById('totalFiles'),
17
+ processedFiles: document.getElementById('processedFiles'),
18
+ extractedCourses: document.getElementById('extractedCourses'),
19
+ extractedVideos: document.getElementById('extractedVideos'),
20
+ extractedFrames: document.getElementById('extractedFrames'),
21
+ trackedCursors: document.getElementById('trackedCursors'),
22
+ currentFile: document.getElementById('currentFile'),
23
+ progressFill: document.getElementById('progressFill'),
24
+ progressText: document.getElementById('progressText'),
25
+ startIndex: document.getElementById('startIndex'),
26
+ startProcessing: document.getElementById('startProcessing'),
27
+ stopProcessing: document.getElementById('stopProcessing'),
28
+ refreshBtn: document.getElementById('refreshBtn'),
29
+ themeToggle: document.getElementById('themeToggle'),
30
+ fileCount: document.getElementById('fileCount'),
31
+ filesGrid: document.getElementById('filesGrid'),
32
+ logsContainer: document.getElementById('logsContainer'),
33
+ clearLogs: document.getElementById('clearLogs'),
34
+ autoScroll: document.getElementById('autoScroll'),
35
+ fileModal: document.getElementById('fileModal'),
36
+ modalTitle: document.getElementById('modalTitle'),
37
+ modalBody: document.getElementById('modalBody'),
38
+ modalClose: document.getElementById('modalClose'),
39
+ downloadFile: document.getElementById('downloadFile'),
40
+ viewFrames: document.getElementById('viewFrames'),
41
+ loadingOverlay: document.getElementById('loadingOverlay'),
42
+ toastContainer: document.getElementById('toastContainer')
43
+ };
44
+
45
+ // Initialize the application
46
+ document.addEventListener('DOMContentLoaded', function() {
47
+ initializeTheme();
48
+ setupEventListeners();
49
+ startAutoRefresh();
50
+ fetchInitialData();
51
+ });
52
+
53
+ // Theme Management
54
+ function initializeTheme() {
55
+ const savedTheme = localStorage.getItem('theme') || 'light';
56
+ document.documentElement.setAttribute('data-theme', savedTheme);
57
+ updateThemeIcon(savedTheme);
58
+ }
59
+
60
+ function toggleTheme() {
61
+ const currentTheme = document.documentElement.getAttribute('data-theme');
62
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
63
+ document.documentElement.setAttribute('data-theme', newTheme);
64
+ localStorage.setItem('theme', newTheme);
65
+ updateThemeIcon(newTheme);
66
+ }
67
+
68
+ function updateThemeIcon(theme) {
69
+ const icon = elements.themeToggle.querySelector('i');
70
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
71
+ }
72
+
73
+ // Event Listeners
74
+ function setupEventListeners() {
75
+ elements.themeToggle.addEventListener('click', toggleTheme);
76
+ elements.refreshBtn.addEventListener('click', () => {
77
+ showToast('Refreshing data...', 'info');
78
+ fetchAllData();
79
+ });
80
+
81
+ elements.startProcessing.addEventListener('click', startProcessing);
82
+ elements.stopProcessing.addEventListener('click', stopProcessing);
83
+
84
+ elements.clearLogs.addEventListener('click', clearLogs);
85
+ elements.autoScroll.addEventListener('click', toggleAutoScroll);
86
+
87
+ elements.modalClose.addEventListener('click', closeModal);
88
+ elements.fileModal.addEventListener('click', (e) => {
89
+ if (e.target === elements.fileModal) closeModal();
90
+ });
91
+
92
+ elements.downloadFile.addEventListener('click', downloadSelectedFile);
93
+ elements.viewFrames.addEventListener('click', viewFrames);
94
+
95
+ // Keyboard shortcuts
96
+ document.addEventListener('keydown', (e) => {
97
+ if (e.key === 'Escape') closeModal();
98
+ if (e.key === 'F5') {
99
+ e.preventDefault();
100
+ fetchAllData();
101
+ }
102
+ });
103
+ }
104
+
105
+ // API Functions
106
+ async function apiRequest(endpoint, options = {}) {
107
+ try {
108
+ showLoading();
109
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ ...options.headers
113
+ },
114
+ ...options
115
+ });
116
+
117
+ if (!response.ok) {
118
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
119
+ }
120
+
121
+ apiConnected = true;
122
+ return await response.json();
123
+ } catch (error) {
124
+ console.error('API request failed:', error);
125
+ apiConnected = false;
126
+
127
+ // Show different messages based on error type
128
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
129
+ showToast('API server not running. Please start the cursor tracking API on port 8000.', 'warning');
130
+ } else {
131
+ showToast(`API Error: ${error.message}`, 'error');
132
+ }
133
+ throw error;
134
+ } finally {
135
+ hideLoading();
136
+ }
137
+ }
138
+
139
+ async function fetchStatus() {
140
+ try {
141
+ const data = await apiRequest('/status');
142
+ updateStatusDisplay(data.processing_status);
143
+ return data;
144
+ } catch (error) {
145
+ // Show demo data when API is not connected
146
+ const demoStatus = {
147
+ is_running: false,
148
+ total_files: 150,
149
+ processed_files: 45,
150
+ extracted_courses: 12,
151
+ extracted_videos: 89,
152
+ extracted_frames_count: 15420,
153
+ tracked_cursors_count: 8934,
154
+ current_file: null,
155
+ logs: [
156
+ '[Demo Mode] API server not connected',
157
+ '[Demo Mode] This is a demonstration of the UI',
158
+ '[Demo Mode] Start the API server on port 8000 to see real data'
159
+ ]
160
+ };
161
+ updateStatusDisplay(demoStatus);
162
+ }
163
+ }
164
+
165
+ async function fetchCursorData() {
166
+ try {
167
+ const data = await apiRequest('/cursor-data');
168
+ currentFiles = data.files || [];
169
+ updateFilesDisplay(currentFiles);
170
+ return data;
171
+ } catch (error) {
172
+ // Show demo files when API is not connected
173
+ const demoFiles = [
174
+ {
175
+ filename: 'course_1_video_1_mp4_cursor_data.json',
176
+ size_bytes: 45678,
177
+ modified_time: 'Sun Jul 13 19:30:15 2025'
178
+ },
179
+ {
180
+ filename: 'course_2_video_3_mp4_cursor_data.json',
181
+ size_bytes: 67890,
182
+ modified_time: 'Sun Jul 13 18:45:22 2025'
183
+ },
184
+ {
185
+ filename: 'course_3_video_2_mp4_cursor_data.json',
186
+ size_bytes: 34567,
187
+ modified_time: 'Sun Jul 13 17:20:10 2025'
188
+ }
189
+ ];
190
+ currentFiles = demoFiles;
191
+ updateFilesDisplay(demoFiles);
192
+ }
193
+ }
194
+
195
+ async function fetchFileDetails(filename) {
196
+ try {
197
+ const data = await apiRequest(`/cursor-data/${filename}/summary`);
198
+ return data;
199
+ } catch (error) {
200
+ showToast(`Failed to fetch details for ${filename}`, 'error');
201
+ return null;
202
+ }
203
+ }
204
+
205
+ async function startProcessing() {
206
+ try {
207
+ const startIndex = parseInt(elements.startIndex.value) || 0;
208
+ const data = await apiRequest('/start-processing', {
209
+ method: 'POST',
210
+ body: JSON.stringify({ start_index: startIndex })
211
+ });
212
+
213
+ showToast(data.message, data.status === 'started' ? 'success' : 'info');
214
+
215
+ if (data.status === 'started') {
216
+ elements.startProcessing.disabled = true;
217
+ elements.stopProcessing.disabled = false;
218
+ }
219
+ } catch (error) {
220
+ showToast('Failed to start processing', 'error');
221
+ }
222
+ }
223
+
224
+ async function stopProcessing() {
225
+ try {
226
+ const data = await apiRequest('/stop-processing', {
227
+ method: 'POST'
228
+ });
229
+
230
+ showToast(data.message, 'info');
231
+ elements.startProcessing.disabled = false;
232
+ elements.stopProcessing.disabled = true;
233
+ } catch (error) {
234
+ showToast('Failed to stop processing', 'error');
235
+ }
236
+ }
237
+
238
+ // Display Update Functions
239
+ function updateStatusDisplay(status) {
240
+ // Update status indicator
241
+ const statusDot = elements.statusIndicator.querySelector('.status-dot');
242
+ const statusText = elements.statusIndicator.querySelector('.status-text');
243
+
244
+ if (status.is_running) {
245
+ statusDot.className = 'status-dot running';
246
+ statusText.textContent = 'Processing';
247
+ elements.startProcessing.disabled = true;
248
+ elements.stopProcessing.disabled = false;
249
+ } else {
250
+ statusDot.className = 'status-dot stopped';
251
+ statusText.textContent = 'Idle';
252
+ elements.startProcessing.disabled = false;
253
+ elements.stopProcessing.disabled = true;
254
+ }
255
+
256
+ // Update statistics
257
+ elements.totalFiles.textContent = status.total_files || 0;
258
+ elements.processedFiles.textContent = status.processed_files || 0;
259
+ elements.extractedCourses.textContent = status.extracted_courses || 0;
260
+ elements.extractedVideos.textContent = status.extracted_videos || 0;
261
+ elements.extractedFrames.textContent = status.extracted_frames_count || 0;
262
+ elements.trackedCursors.textContent = status.tracked_cursors_count || 0;
263
+
264
+ // Update current file and progress
265
+ const currentFile = status.current_file || 'None';
266
+ elements.currentFile.textContent = currentFile;
267
+
268
+ const progress = status.total_files > 0 ?
269
+ Math.round((status.processed_files / status.total_files) * 100) : 0;
270
+ elements.progressFill.style.width = `${progress}%`;
271
+ elements.progressText.textContent = `${progress}%`;
272
+
273
+ // Update logs
274
+ if (status.logs && status.logs.length > 0) {
275
+ updateLogs(status.logs);
276
+ }
277
+ }
278
+
279
+ function updateFilesDisplay(files) {
280
+ elements.fileCount.textContent = `${files.length} files`;
281
+
282
+ if (files.length === 0) {
283
+ elements.filesGrid.innerHTML = `
284
+ <div class="no-files">
285
+ <i class="fas fa-folder-open" style="font-size: 3rem; color: var(--text-muted); margin-bottom: 1rem;"></i>
286
+ <p style="color: var(--text-muted);">No cursor tracking files found yet.</p>
287
+ <p style="color: var(--text-muted); font-size: 0.875rem;">Files will appear here after processing completes.</p>
288
+ </div>
289
+ `;
290
+ return;
291
+ }
292
+
293
+ elements.filesGrid.innerHTML = files.map(file => `
294
+ <div class="file-card" onclick="openFileModal('${file.filename}')">
295
+ <div class="file-header">
296
+ <div class="file-name">${file.filename}</div>
297
+ <div class="file-size">${formatFileSize(file.size_bytes)}</div>
298
+ </div>
299
+ <div class="file-stats">
300
+ <div class="file-stat">
301
+ <span class="file-stat-label">Modified:</span>
302
+ <span class="file-stat-value">${formatDate(file.modified_time)}</span>
303
+ </div>
304
+ </div>
305
+ <div class="file-actions">
306
+ <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')">
307
+ <i class="fas fa-download"></i>
308
+ Download
309
+ </button>
310
+ <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')">
311
+ <i class="fas fa-eye"></i>
312
+ Details
313
+ </button>
314
+ </div>
315
+ </div>
316
+ `).join('');
317
+ }
318
+
319
+ function updateLogs(logs) {
320
+ const container = elements.logsContainer;
321
+
322
+ // Clear existing logs if we have new ones
323
+ if (logs.length > 0) {
324
+ container.innerHTML = '';
325
+ }
326
+
327
+ logs.slice(-MAX_LOGS).forEach(log => {
328
+ const logEntry = document.createElement('div');
329
+ logEntry.className = 'log-entry';
330
+
331
+ // Determine log type based on content
332
+ let logType = '';
333
+ if (log.includes('❌') || log.includes('ERROR') || log.includes('Failed')) {
334
+ logType = 'error';
335
+ } else if (log.includes('✅') || log.includes('SUCCESS') || log.includes('Successfully')) {
336
+ logType = 'success';
337
+ } else if (log.includes('⚠️') || log.includes('WARN')) {
338
+ logType = 'warning';
339
+ }
340
+
341
+ if (logType) {
342
+ logEntry.classList.add(logType);
343
+ }
344
+
345
+ // Extract timestamp and message
346
+ const timestampMatch = log.match(/^\[([^\]]+)\]/);
347
+ const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString();
348
+ const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log;
349
+
350
+ logEntry.innerHTML = `
351
+ <span class="log-time">[${timestamp}]</span>
352
+ <span class="log-message">${escapeHtml(message)}</span>
353
+ `;
354
+
355
+ container.appendChild(logEntry);
356
+ });
357
+
358
+ // Auto-scroll to bottom if enabled
359
+ if (autoScrollEnabled) {
360
+ container.scrollTop = container.scrollHeight;
361
+ }
362
+ }
363
+
364
+ // Modal Functions
365
+ async function openFileModal(filename) {
366
+ selectedFile = filename;
367
+ elements.modalTitle.textContent = `File Details: ${filename}`;
368
+
369
+ showModal();
370
+
371
+ const details = await fetchFileDetails(filename);
372
+ if (details) {
373
+ elements.modalBody.innerHTML = `
374
+ <div class="file-details">
375
+ <div class="detail-section">
376
+ <h4>File Information</h4>
377
+ <div class="detail-grid">
378
+ <div class="detail-item">
379
+ <span class="detail-label">File Size:</span>
380
+ <span class="detail-value">${formatFileSize(details.file_size_bytes)}</span>
381
+ </div>
382
+ <div class="detail-item">
383
+ <span class="detail-label">Modified:</span>
384
+ <span class="detail-value">${details.modified_time}</span>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ <div class="detail-section">
390
+ <h4>Frame Statistics</h4>
391
+ <div class="detail-grid">
392
+ <div class="detail-item">
393
+ <span class="detail-label">Total Frames:</span>
394
+ <span class="detail-value">${details.total_frames}</span>
395
+ </div>
396
+ <div class="detail-item">
397
+ <span class="detail-label">Cursor Active:</span>
398
+ <span class="detail-value">${details.cursor_active_frames}</span>
399
+ </div>
400
+ <div class="detail-item">
401
+ <span class="detail-label">Cursor Inactive:</span>
402
+ <span class="detail-value">${details.cursor_inactive_frames}</span>
403
+ </div>
404
+ <div class="detail-item">
405
+ <span class="detail-label">Detection Rate:</span>
406
+ <span class="detail-value">${(details.cursor_detection_rate * 100).toFixed(1)}%</span>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <div class="detail-section">
412
+ <h4>Confidence Statistics</h4>
413
+ <div class="detail-grid">
414
+ <div class="detail-item">
415
+ <span class="detail-label">Average:</span>
416
+ <span class="detail-value">${details.confidence_stats.average.toFixed(3)}</span>
417
+ </div>
418
+ <div class="detail-item">
419
+ <span class="detail-label">Maximum:</span>
420
+ <span class="detail-value">${details.confidence_stats.maximum.toFixed(3)}</span>
421
+ </div>
422
+ <div class="detail-item">
423
+ <span class="detail-label">Minimum:</span>
424
+ <span class="detail-value">${details.confidence_stats.minimum.toFixed(3)}</span>
425
+ </div>
426
+ <div class="detail-item">
427
+ <span class="detail-label">Measurements:</span>
428
+ <span class="detail-value">${details.confidence_stats.total_measurements}</span>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ <div class="detail-section">
434
+ <h4>Templates Used</h4>
435
+ <div class="template-list">
436
+ ${details.templates_used.length > 0 ?
437
+ details.templates_used.map(template => `<span class="template-tag">${template}</span>`).join('') :
438
+ '<span class="no-templates">No templates detected</span>'
439
+ }
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <style>
445
+ .file-details { font-size: 0.875rem; }
446
+ .detail-section { margin-bottom: 1.5rem; }
447
+ .detail-section h4 {
448
+ margin-bottom: 0.75rem;
449
+ color: var(--accent-primary);
450
+ font-weight: 600;
451
+ }
452
+ .detail-grid {
453
+ display: grid;
454
+ grid-template-columns: 1fr 1fr;
455
+ gap: 0.5rem;
456
+ }
457
+ .detail-item {
458
+ display: flex;
459
+ justify-content: space-between;
460
+ padding: 0.5rem;
461
+ background: var(--bg-secondary);
462
+ border-radius: var(--radius);
463
+ }
464
+ .detail-label { color: var(--text-secondary); }
465
+ .detail-value { font-weight: 500; }
466
+ .template-list {
467
+ display: flex;
468
+ flex-wrap: wrap;
469
+ gap: 0.5rem;
470
+ }
471
+ .template-tag {
472
+ background: var(--accent-primary);
473
+ color: white;
474
+ padding: 0.25rem 0.5rem;
475
+ border-radius: var(--radius);
476
+ font-size: 0.75rem;
477
+ }
478
+ .no-templates {
479
+ color: var(--text-muted);
480
+ font-style: italic;
481
+ }
482
+ </style>
483
+ `;
484
+ } else {
485
+ elements.modalBody.innerHTML = '<p>Failed to load file details.</p>';
486
+ }
487
+ }
488
+
489
+ function showModal() {
490
+ elements.fileModal.classList.add('show');
491
+ document.body.style.overflow = 'hidden';
492
+ }
493
+
494
+ function closeModal() {
495
+ elements.fileModal.classList.remove('show');
496
+ document.body.style.overflow = '';
497
+ selectedFile = null;
498
+ }
499
+
500
+ // File Operations
501
+ async function downloadFile(filename) {
502
+ try {
503
+ const response = await fetch(`${API_BASE_URL}/cursor-data/${filename}`);
504
+ if (!response.ok) throw new Error('Download failed');
505
+
506
+ const blob = await response.blob();
507
+ const url = window.URL.createObjectURL(blob);
508
+ const a = document.createElement('a');
509
+ a.href = url;
510
+ a.download = filename;
511
+ document.body.appendChild(a);
512
+ a.click();
513
+ document.body.removeChild(a);
514
+ window.URL.revokeObjectURL(url);
515
+
516
+ showToast(`Downloaded ${filename}`, 'success');
517
+ } catch (error) {
518
+ showToast(`Failed to download ${filename}`, 'error');
519
+ }
520
+ }
521
+
522
+ function downloadSelectedFile() {
523
+ if (selectedFile) {
524
+ downloadFile(selectedFile);
525
+ }
526
+ }
527
+
528
+ function viewFrames() {
529
+ if (selectedFile) {
530
+ showToast('Frame viewer feature coming soon!', 'info');
531
+ }
532
+ }
533
+
534
+ // Utility Functions
535
+ function formatFileSize(bytes) {
536
+ if (bytes === 0) return '0 B';
537
+ const k = 1024;
538
+ const sizes = ['B', 'KB', 'MB', 'GB'];
539
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
540
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
541
+ }
542
+
543
+ function formatDate(dateString) {
544
+ try {
545
+ return new Date(dateString).toLocaleDateString();
546
+ } catch {
547
+ return dateString;
548
+ }
549
+ }
550
+
551
+ function escapeHtml(text) {
552
+ const div = document.createElement('div');
553
+ div.textContent = text;
554
+ return div.innerHTML;
555
+ }
556
+
557
+ // Log Management
558
+ function clearLogs() {
559
+ elements.logsContainer.innerHTML = '<div class="log-entry"><span class="log-time">[' +
560
+ new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>';
561
+ showToast('Logs cleared', 'info');
562
+ }
563
+
564
+ function toggleAutoScroll() {
565
+ autoScrollEnabled = !autoScrollEnabled;
566
+ elements.autoScroll.classList.toggle('active', autoScrollEnabled);
567
+
568
+ if (autoScrollEnabled) {
569
+ elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight;
570
+ }
571
+ }
572
+
573
+ // Loading and Toast Functions
574
+ function showLoading() {
575
+ elements.loadingOverlay.classList.add('show');
576
+ }
577
+
578
+ function hideLoading() {
579
+ elements.loadingOverlay.classList.remove('show');
580
+ }
581
+
582
+ function showToast(message, type = 'info', duration = 5000) {
583
+ const toast = document.createElement('div');
584
+ toast.className = `toast ${type}`;
585
+
586
+ const icons = {
587
+ success: 'fas fa-check-circle',
588
+ error: 'fas fa-exclamation-circle',
589
+ warning: 'fas fa-exclamation-triangle',
590
+ info: 'fas fa-info-circle'
591
+ };
592
+
593
+ toast.innerHTML = `
594
+ <i class="toast-icon ${icons[type]}"></i>
595
+ <div class="toast-content">
596
+ <div class="toast-message">${escapeHtml(message)}</div>
597
+ </div>
598
+ <button class="toast-close">
599
+ <i class="fas fa-times"></i>
600
+ </button>
601
+ `;
602
+
603
+ const closeBtn = toast.querySelector('.toast-close');
604
+ closeBtn.addEventListener('click', () => removeToast(toast));
605
+
606
+ elements.toastContainer.appendChild(toast);
607
+
608
+ // Auto-remove after duration
609
+ setTimeout(() => removeToast(toast), duration);
610
+ }
611
+
612
+ function removeToast(toast) {
613
+ if (toast && toast.parentNode) {
614
+ toast.style.animation = 'slideInRight 0.3s ease reverse';
615
+ setTimeout(() => {
616
+ if (toast.parentNode) {
617
+ toast.parentNode.removeChild(toast);
618
+ }
619
+ }, 300);
620
+ }
621
+ }
622
+
623
+ // Auto-refresh Management
624
+ function startAutoRefresh() {
625
+ fetchAllData(); // Initial fetch
626
+ refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL);
627
+ }
628
+
629
+ function stopAutoRefresh() {
630
+ if (refreshInterval) {
631
+ clearInterval(refreshInterval);
632
+ refreshInterval = null;
633
+ }
634
+ }
635
+
636
+ async function fetchInitialData() {
637
+ await fetchAllData();
638
+ }
639
+
640
+ async function fetchAllData() {
641
+ try {
642
+ await Promise.all([
643
+ fetchStatus(),
644
+ fetchCursorData()
645
+ ]);
646
+ } catch (error) {
647
+ console.error('Failed to fetch data:', error);
648
+ }
649
+ }
650
+
651
+ // Cleanup on page unload
652
+ window.addEventListener('beforeunload', () => {
653
+ stopAutoRefresh();
654
+ });
655
+
static/readme.md ADDED
File without changes
static/script.js ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Configuration
2
+ const API_BASE_URL = window.location.origin; // Use same origin as the UI
3
+ const REFRESH_INTERVAL = 10000; // 10 seconds (increased for demo)
4
+ const MAX_LOGS = 100;
5
+
6
+ // Global state
7
+ let refreshInterval;
8
+ let autoScrollEnabled = true;
9
+ let currentFiles = [];
10
+ let selectedFile = null;
11
+ let apiConnected = false;
12
+
13
+ // DOM Elements
14
+ const elements = {
15
+ statusIndicator: document.getElementById('statusIndicator'),
16
+ totalFiles: document.getElementById('totalFiles'),
17
+ processedFiles: document.getElementById('processedFiles'),
18
+ extractedCourses: document.getElementById('extractedCourses'),
19
+ extractedVideos: document.getElementById('extractedVideos'),
20
+ extractedFrames: document.getElementById('extractedFrames'),
21
+ trackedCursors: document.getElementById('trackedCursors'),
22
+ currentFile: document.getElementById('currentFile'),
23
+ progressFill: document.getElementById('progressFill'),
24
+ progressText: document.getElementById('progressText'),
25
+ startIndex: document.getElementById('startIndex'),
26
+ startProcessing: document.getElementById('startProcessing'),
27
+ stopProcessing: document.getElementById('stopProcessing'),
28
+ refreshBtn: document.getElementById('refreshBtn'),
29
+ themeToggle: document.getElementById('themeToggle'),
30
+ fileCount: document.getElementById('fileCount'),
31
+ filesGrid: document.getElementById('filesGrid'),
32
+ logsContainer: document.getElementById('logsContainer'),
33
+ clearLogs: document.getElementById('clearLogs'),
34
+ autoScroll: document.getElementById('autoScroll'),
35
+ fileModal: document.getElementById('fileModal'),
36
+ modalTitle: document.getElementById('modalTitle'),
37
+ modalBody: document.getElementById('modalBody'),
38
+ modalClose: document.getElementById('modalClose'),
39
+ downloadFile: document.getElementById('downloadFile'),
40
+ viewFrames: document.getElementById('viewFrames'),
41
+ loadingOverlay: document.getElementById('loadingOverlay'),
42
+ toastContainer: document.getElementById('toastContainer')
43
+ };
44
+
45
+ // Initialize the application
46
+ document.addEventListener('DOMContentLoaded', function() {
47
+ initializeTheme();
48
+ setupEventListeners();
49
+ startAutoRefresh();
50
+ fetchInitialData();
51
+ });
52
+
53
+ // Theme Management
54
+ function initializeTheme() {
55
+ const savedTheme = localStorage.getItem('theme') || 'light';
56
+ document.documentElement.setAttribute('data-theme', savedTheme);
57
+ updateThemeIcon(savedTheme);
58
+ }
59
+
60
+ function toggleTheme() {
61
+ const currentTheme = document.documentElement.getAttribute('data-theme');
62
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
63
+ document.documentElement.setAttribute('data-theme', newTheme);
64
+ localStorage.setItem('theme', newTheme);
65
+ updateThemeIcon(newTheme);
66
+ }
67
+
68
+ function updateThemeIcon(theme) {
69
+ const icon = elements.themeToggle.querySelector('i');
70
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
71
+ }
72
+
73
+ // Event Listeners
74
+ function setupEventListeners() {
75
+ elements.themeToggle.addEventListener('click', toggleTheme);
76
+ elements.refreshBtn.addEventListener('click', () => {
77
+ showToast('Refreshing data...', 'info');
78
+ fetchAllData();
79
+ });
80
+
81
+ elements.startProcessing.addEventListener('click', startProcessing);
82
+ elements.stopProcessing.addEventListener('click', stopProcessing);
83
+
84
+ elements.clearLogs.addEventListener('click', clearLogs);
85
+ elements.autoScroll.addEventListener('click', toggleAutoScroll);
86
+
87
+ elements.modalClose.addEventListener('click', closeModal);
88
+ elements.fileModal.addEventListener('click', (e) => {
89
+ if (e.target === elements.fileModal) closeModal();
90
+ });
91
+
92
+ elements.downloadFile.addEventListener('click', downloadSelectedFile);
93
+ elements.viewFrames.addEventListener('click', viewFrames);
94
+
95
+ // Keyboard shortcuts
96
+ document.addEventListener('keydown', (e) => {
97
+ if (e.key === 'Escape') closeModal();
98
+ if (e.key === 'F5') {
99
+ e.preventDefault();
100
+ fetchAllData();
101
+ }
102
+ });
103
+ }
104
+
105
+ // API Functions
106
+ async function apiRequest(endpoint, options = {}) {
107
+ try {
108
+ showLoading();
109
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ ...options.headers
113
+ },
114
+ ...options
115
+ });
116
+
117
+ if (!response.ok) {
118
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
119
+ }
120
+
121
+ apiConnected = true;
122
+ return await response.json();
123
+ } catch (error) {
124
+ console.error('API request failed:', error);
125
+ apiConnected = false;
126
+
127
+ // Show different messages based on error type
128
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
129
+ showToast('API server not running. Please start the cursor tracking API on port 8000.', 'warning');
130
+ } else {
131
+ showToast(`API Error: ${error.message}`, 'error');
132
+ }
133
+ throw error;
134
+ } finally {
135
+ hideLoading();
136
+ }
137
+ }
138
+
139
+ async function fetchStatus() {
140
+ try {
141
+ const data = await apiRequest('/status');
142
+ updateStatusDisplay(data.processing_status);
143
+ return data;
144
+ } catch (error) {
145
+ // Show demo data when API is not connected
146
+ const demoStatus = {
147
+ is_running: false,
148
+ total_files: 150,
149
+ processed_files: 45,
150
+ extracted_courses: 12,
151
+ extracted_videos: 89,
152
+ extracted_frames_count: 15420,
153
+ tracked_cursors_count: 8934,
154
+ current_file: null,
155
+ logs: [
156
+ '[Demo Mode] API server not connected',
157
+ '[Demo Mode] This is a demonstration of the UI',
158
+ '[Demo Mode] Start the API server on port 8000 to see real data'
159
+ ]
160
+ };
161
+ updateStatusDisplay(demoStatus);
162
+ }
163
+ }
164
+
165
+ async function fetchCursorData() {
166
+ try {
167
+ const data = await apiRequest('/cursor-data');
168
+ currentFiles = data.files || [];
169
+ updateFilesDisplay(currentFiles);
170
+ return data;
171
+ } catch (error) {
172
+ // Show demo files when API is not connected
173
+ const demoFiles = [
174
+ {
175
+ filename: 'course_1_video_1_mp4_cursor_data.json',
176
+ size_bytes: 45678,
177
+ modified_time: 'Sun Jul 13 19:30:15 2025'
178
+ },
179
+ {
180
+ filename: 'course_2_video_3_mp4_cursor_data.json',
181
+ size_bytes: 67890,
182
+ modified_time: 'Sun Jul 13 18:45:22 2025'
183
+ },
184
+ {
185
+ filename: 'course_3_video_2_mp4_cursor_data.json',
186
+ size_bytes: 34567,
187
+ modified_time: 'Sun Jul 13 17:20:10 2025'
188
+ }
189
+ ];
190
+ currentFiles = demoFiles;
191
+ updateFilesDisplay(demoFiles);
192
+ }
193
+ }
194
+
195
+ async function fetchFileDetails(filename) {
196
+ try {
197
+ const data = await apiRequest(`/cursor-data/${filename}/summary`);
198
+ return data;
199
+ } catch (error) {
200
+ showToast(`Failed to fetch details for ${filename}`, 'error');
201
+ return null;
202
+ }
203
+ }
204
+
205
+ async function startProcessing() {
206
+ try {
207
+ const startIndex = parseInt(elements.startIndex.value) || 0;
208
+ const data = await apiRequest('/start-processing', {
209
+ method: 'POST',
210
+ body: JSON.stringify({ start_index: startIndex })
211
+ });
212
+
213
+ showToast(data.message, data.status === 'started' ? 'success' : 'info');
214
+
215
+ if (data.status === 'started') {
216
+ elements.startProcessing.disabled = true;
217
+ elements.stopProcessing.disabled = false;
218
+ }
219
+ } catch (error) {
220
+ showToast('Failed to start processing', 'error');
221
+ }
222
+ }
223
+
224
+ async function stopProcessing() {
225
+ try {
226
+ const data = await apiRequest('/stop-processing', {
227
+ method: 'POST'
228
+ });
229
+
230
+ showToast(data.message, 'info');
231
+ elements.startProcessing.disabled = false;
232
+ elements.stopProcessing.disabled = true;
233
+ } catch (error) {
234
+ showToast('Failed to stop processing', 'error');
235
+ }
236
+ }
237
+
238
+ // Display Update Functions
239
+ function updateStatusDisplay(status) {
240
+ // Update status indicator
241
+ const statusDot = elements.statusIndicator.querySelector('.status-dot');
242
+ const statusText = elements.statusIndicator.querySelector('.status-text');
243
+
244
+ if (status.is_running) {
245
+ statusDot.className = 'status-dot running';
246
+ statusText.textContent = 'Processing';
247
+ elements.startProcessing.disabled = true;
248
+ elements.stopProcessing.disabled = false;
249
+ } else {
250
+ statusDot.className = 'status-dot stopped';
251
+ statusText.textContent = 'Idle';
252
+ elements.startProcessing.disabled = false;
253
+ elements.stopProcessing.disabled = true;
254
+ }
255
+
256
+ // Update statistics
257
+ elements.totalFiles.textContent = status.total_files || 0;
258
+ elements.processedFiles.textContent = status.processed_files || 0;
259
+ elements.extractedCourses.textContent = status.extracted_courses || 0;
260
+ elements.extractedVideos.textContent = status.extracted_videos || 0;
261
+ elements.extractedFrames.textContent = status.extracted_frames_count || 0;
262
+ elements.trackedCursors.textContent = status.tracked_cursors_count || 0;
263
+
264
+ // Update current file and progress
265
+ const currentFile = status.current_file || 'None';
266
+ elements.currentFile.textContent = currentFile;
267
+
268
+ const progress = status.total_files > 0 ?
269
+ Math.round((status.processed_files / status.total_files) * 100) : 0;
270
+ elements.progressFill.style.width = `${progress}%`;
271
+ elements.progressText.textContent = `${progress}%`;
272
+
273
+ // Update logs
274
+ if (status.logs && status.logs.length > 0) {
275
+ updateLogs(status.logs);
276
+ }
277
+ }
278
+
279
+ function updateFilesDisplay(files) {
280
+ elements.fileCount.textContent = `${files.length} files`;
281
+
282
+ if (files.length === 0) {
283
+ elements.filesGrid.innerHTML = `
284
+ <div class="no-files">
285
+ <i class="fas fa-folder-open" style="font-size: 3rem; color: var(--text-muted); margin-bottom: 1rem;"></i>
286
+ <p style="color: var(--text-muted);">No cursor tracking files found yet.</p>
287
+ <p style="color: var(--text-muted); font-size: 0.875rem;">Files will appear here after processing completes.</p>
288
+ </div>
289
+ `;
290
+ return;
291
+ }
292
+
293
+ elements.filesGrid.innerHTML = files.map(file => `
294
+ <div class="file-card" onclick="openFileModal('${file.filename}')">
295
+ <div class="file-header">
296
+ <div class="file-name">${file.filename}</div>
297
+ <div class="file-size">${formatFileSize(file.size_bytes)}</div>
298
+ </div>
299
+ <div class="file-stats">
300
+ <div class="file-stat">
301
+ <span class="file-stat-label">Modified:</span>
302
+ <span class="file-stat-value">${formatDate(file.modified_time)}</span>
303
+ </div>
304
+ </div>
305
+ <div class="file-actions">
306
+ <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')">
307
+ <i class="fas fa-download"></i>
308
+ Download
309
+ </button>
310
+ <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')">
311
+ <i class="fas fa-eye"></i>
312
+ Details
313
+ </button>
314
+ </div>
315
+ </div>
316
+ `).join('');
317
+ }
318
+
319
+ function updateLogs(logs) {
320
+ const container = elements.logsContainer;
321
+
322
+ // Clear existing logs if we have new ones
323
+ if (logs.length > 0) {
324
+ container.innerHTML = '';
325
+ }
326
+
327
+ logs.slice(-MAX_LOGS).forEach(log => {
328
+ const logEntry = document.createElement('div');
329
+ logEntry.className = 'log-entry';
330
+
331
+ // Determine log type based on content
332
+ let logType = '';
333
+ if (log.includes('❌') || log.includes('ERROR') || log.includes('Failed')) {
334
+ logType = 'error';
335
+ } else if (log.includes('✅') || log.includes('SUCCESS') || log.includes('Successfully')) {
336
+ logType = 'success';
337
+ } else if (log.includes('⚠️') || log.includes('WARN')) {
338
+ logType = 'warning';
339
+ }
340
+
341
+ if (logType) {
342
+ logEntry.classList.add(logType);
343
+ }
344
+
345
+ // Extract timestamp and message
346
+ const timestampMatch = log.match(/^\[([^\]]+)\]/);
347
+ const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString();
348
+ const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log;
349
+
350
+ logEntry.innerHTML = `
351
+ <span class="log-time">[${timestamp}]</span>
352
+ <span class="log-message">${escapeHtml(message)}</span>
353
+ `;
354
+
355
+ container.appendChild(logEntry);
356
+ });
357
+
358
+ // Auto-scroll to bottom if enabled
359
+ if (autoScrollEnabled) {
360
+ container.scrollTop = container.scrollHeight;
361
+ }
362
+ }
363
+
364
+ // Modal Functions
365
+ async function openFileModal(filename) {
366
+ selectedFile = filename;
367
+ elements.modalTitle.textContent = `File Details: ${filename}`;
368
+
369
+ showModal();
370
+
371
+ const details = await fetchFileDetails(filename);
372
+ if (details) {
373
+ elements.modalBody.innerHTML = `
374
+ <div class="file-details">
375
+ <div class="detail-section">
376
+ <h4>File Information</h4>
377
+ <div class="detail-grid">
378
+ <div class="detail-item">
379
+ <span class="detail-label">File Size:</span>
380
+ <span class="detail-value">${formatFileSize(details.file_size_bytes)}</span>
381
+ </div>
382
+ <div class="detail-item">
383
+ <span class="detail-label">Modified:</span>
384
+ <span class="detail-value">${details.modified_time}</span>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ <div class="detail-section">
390
+ <h4>Frame Statistics</h4>
391
+ <div class="detail-grid">
392
+ <div class="detail-item">
393
+ <span class="detail-label">Total Frames:</span>
394
+ <span class="detail-value">${details.total_frames}</span>
395
+ </div>
396
+ <div class="detail-item">
397
+ <span class="detail-label">Cursor Active:</span>
398
+ <span class="detail-value">${details.cursor_active_frames}</span>
399
+ </div>
400
+ <div class="detail-item">
401
+ <span class="detail-label">Cursor Inactive:</span>
402
+ <span class="detail-value">${details.cursor_inactive_frames}</span>
403
+ </div>
404
+ <div class="detail-item">
405
+ <span class="detail-label">Detection Rate:</span>
406
+ <span class="detail-value">${(details.cursor_detection_rate * 100).toFixed(1)}%</span>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <div class="detail-section">
412
+ <h4>Confidence Statistics</h4>
413
+ <div class="detail-grid">
414
+ <div class="detail-item">
415
+ <span class="detail-label">Average:</span>
416
+ <span class="detail-value">${details.confidence_stats.average.toFixed(3)}</span>
417
+ </div>
418
+ <div class="detail-item">
419
+ <span class="detail-label">Maximum:</span>
420
+ <span class="detail-value">${details.confidence_stats.maximum.toFixed(3)}</span>
421
+ </div>
422
+ <div class="detail-item">
423
+ <span class="detail-label">Minimum:</span>
424
+ <span class="detail-value">${details.confidence_stats.minimum.toFixed(3)}</span>
425
+ </div>
426
+ <div class="detail-item">
427
+ <span class="detail-label">Measurements:</span>
428
+ <span class="detail-value">${details.confidence_stats.total_measurements}</span>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ <div class="detail-section">
434
+ <h4>Templates Used</h4>
435
+ <div class="template-list">
436
+ ${details.templates_used.length > 0 ?
437
+ details.templates_used.map(template => `<span class="template-tag">${template}</span>`).join('') :
438
+ '<span class="no-templates">No templates detected</span>'
439
+ }
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <style>
445
+ .file-details { font-size: 0.875rem; }
446
+ .detail-section { margin-bottom: 1.5rem; }
447
+ .detail-section h4 {
448
+ margin-bottom: 0.75rem;
449
+ color: var(--accent-primary);
450
+ font-weight: 600;
451
+ }
452
+ .detail-grid {
453
+ display: grid;
454
+ grid-template-columns: 1fr 1fr;
455
+ gap: 0.5rem;
456
+ }
457
+ .detail-item {
458
+ display: flex;
459
+ justify-content: space-between;
460
+ padding: 0.5rem;
461
+ background: var(--bg-secondary);
462
+ border-radius: var(--radius);
463
+ }
464
+ .detail-label { color: var(--text-secondary); }
465
+ .detail-value { font-weight: 500; }
466
+ .template-list {
467
+ display: flex;
468
+ flex-wrap: wrap;
469
+ gap: 0.5rem;
470
+ }
471
+ .template-tag {
472
+ background: var(--accent-primary);
473
+ color: white;
474
+ padding: 0.25rem 0.5rem;
475
+ border-radius: var(--radius);
476
+ font-size: 0.75rem;
477
+ }
478
+ .no-templates {
479
+ color: var(--text-muted);
480
+ font-style: italic;
481
+ }
482
+ </style>
483
+ `;
484
+ } else {
485
+ elements.modalBody.innerHTML = '<p>Failed to load file details.</p>';
486
+ }
487
+ }
488
+
489
+ function showModal() {
490
+ elements.fileModal.classList.add('show');
491
+ document.body.style.overflow = 'hidden';
492
+ }
493
+
494
+ function closeModal() {
495
+ elements.fileModal.classList.remove('show');
496
+ document.body.style.overflow = '';
497
+ selectedFile = null;
498
+ }
499
+
500
+ // File Operations
501
+ async function downloadFile(filename) {
502
+ try {
503
+ const response = await fetch(`${API_BASE_URL}/cursor-data/${filename}`);
504
+ if (!response.ok) throw new Error('Download failed');
505
+
506
+ const blob = await response.blob();
507
+ const url = window.URL.createObjectURL(blob);
508
+ const a = document.createElement('a');
509
+ a.href = url;
510
+ a.download = filename;
511
+ document.body.appendChild(a);
512
+ a.click();
513
+ document.body.removeChild(a);
514
+ window.URL.revokeObjectURL(url);
515
+
516
+ showToast(`Downloaded ${filename}`, 'success');
517
+ } catch (error) {
518
+ showToast(`Failed to download ${filename}`, 'error');
519
+ }
520
+ }
521
+
522
+ function downloadSelectedFile() {
523
+ if (selectedFile) {
524
+ downloadFile(selectedFile);
525
+ }
526
+ }
527
+
528
+ function viewFrames() {
529
+ if (selectedFile) {
530
+ showToast('Frame viewer feature coming soon!', 'info');
531
+ }
532
+ }
533
+
534
+ // Utility Functions
535
+ function formatFileSize(bytes) {
536
+ if (bytes === 0) return '0 B';
537
+ const k = 1024;
538
+ const sizes = ['B', 'KB', 'MB', 'GB'];
539
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
540
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
541
+ }
542
+
543
+ function formatDate(dateString) {
544
+ try {
545
+ return new Date(dateString).toLocaleDateString();
546
+ } catch {
547
+ return dateString;
548
+ }
549
+ }
550
+
551
+ function escapeHtml(text) {
552
+ const div = document.createElement('div');
553
+ div.textContent = text;
554
+ return div.innerHTML;
555
+ }
556
+
557
+ // Log Management
558
+ function clearLogs() {
559
+ elements.logsContainer.innerHTML = '<div class="log-entry"><span class="log-time">[' +
560
+ new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>';
561
+ showToast('Logs cleared', 'info');
562
+ }
563
+
564
+ function toggleAutoScroll() {
565
+ autoScrollEnabled = !autoScrollEnabled;
566
+ elements.autoScroll.classList.toggle('active', autoScrollEnabled);
567
+
568
+ if (autoScrollEnabled) {
569
+ elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight;
570
+ }
571
+ }
572
+
573
+ // Loading and Toast Functions
574
+ function showLoading() {
575
+ elements.loadingOverlay.classList.add('show');
576
+ }
577
+
578
+ function hideLoading() {
579
+ elements.loadingOverlay.classList.remove('show');
580
+ }
581
+
582
+ function showToast(message, type = 'info', duration = 5000) {
583
+ const toast = document.createElement('div');
584
+ toast.className = `toast ${type}`;
585
+
586
+ const icons = {
587
+ success: 'fas fa-check-circle',
588
+ error: 'fas fa-exclamation-circle',
589
+ warning: 'fas fa-exclamation-triangle',
590
+ info: 'fas fa-info-circle'
591
+ };
592
+
593
+ toast.innerHTML = `
594
+ <i class="toast-icon ${icons[type]}"></i>
595
+ <div class="toast-content">
596
+ <div class="toast-message">${escapeHtml(message)}</div>
597
+ </div>
598
+ <button class="toast-close">
599
+ <i class="fas fa-times"></i>
600
+ </button>
601
+ `;
602
+
603
+ const closeBtn = toast.querySelector('.toast-close');
604
+ closeBtn.addEventListener('click', () => removeToast(toast));
605
+
606
+ elements.toastContainer.appendChild(toast);
607
+
608
+ // Auto-remove after duration
609
+ setTimeout(() => removeToast(toast), duration);
610
+ }
611
+
612
+ function removeToast(toast) {
613
+ if (toast && toast.parentNode) {
614
+ toast.style.animation = 'slideInRight 0.3s ease reverse';
615
+ setTimeout(() => {
616
+ if (toast.parentNode) {
617
+ toast.parentNode.removeChild(toast);
618
+ }
619
+ }, 300);
620
+ }
621
+ }
622
+
623
+ // Auto-refresh Management
624
+ function startAutoRefresh() {
625
+ fetchAllData(); // Initial fetch
626
+ refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL);
627
+ }
628
+
629
+ function stopAutoRefresh() {
630
+ if (refreshInterval) {
631
+ clearInterval(refreshInterval);
632
+ refreshInterval = null;
633
+ }
634
+ }
635
+
636
+ async function fetchInitialData() {
637
+ await fetchAllData();
638
+ }
639
+
640
+ async function fetchAllData() {
641
+ try {
642
+ await Promise.all([
643
+ fetchStatus(),
644
+ fetchCursorData()
645
+ ]);
646
+ } catch (error) {
647
+ console.error('Failed to fetch data:', error);
648
+ }
649
+ }
650
+
651
+ // Cleanup on page unload
652
+ window.addEventListener('beforeunload', () => {
653
+ stopAutoRefresh();
654
+ });
655
+
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
+
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
+