Spaces:
Sleeping
Sleeping
| """ | |
| Fight / Violence Detector - MVP | |
| Zero-shot video action detection using X-CLIP. | |
| - Upload-video tab | |
| - Live-webcam tab | |
| - Telegram alerts + browser alarm sound on trigger | |
| """ | |
| import os | |
| import time | |
| import sqlite3 | |
| import datetime | |
| import threading | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import cv2 | |
| import gradio as gr | |
| import requests | |
| from dotenv import load_dotenv | |
| from transformers import VideoMAEImageProcessor, VideoMAEForVideoClassification | |
| load_dotenv() | |
| MODEL_ID = os.environ.get( | |
| "MODEL_ID", | |
| "archit11/videomae-base-finetuned-fight-nofight-subset2", | |
| ) | |
| ALERT_THRESHOLD = float(os.environ.get("ALERT_THRESHOLD", "0.60")) | |
| NUM_FRAMES = 16 | |
| DB_PATH = Path("events.db") | |
| ALERT_COOLDOWN_SEC = 10 | |
| TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN") | |
| TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID") | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| processor = VideoMAEImageProcessor.from_pretrained(MODEL_ID) | |
| model = VideoMAEForVideoClassification.from_pretrained(MODEL_ID).to(device).eval() | |
| ID2LABEL = {int(k): v for k, v in model.config.id2label.items()} | |
| def _find_alert_index(): | |
| for idx, label in ID2LABEL.items(): | |
| norm = label.lower().replace(" ", "").replace("_", "").replace("-", "") | |
| if norm in ("fight", "violence"): | |
| return idx | |
| return 0 | |
| ALERT_IDX = _find_alert_index() | |
| ALERT_LABEL = ID2LABEL[ALERT_IDX] | |
| print(f"[config] model={MODEL_ID} labels={ID2LABEL} alert_on={ALERT_LABEL}") | |
| _last_alert_ts = 0.0 | |
| _alert_lock = threading.Lock() | |
| def init_db(): | |
| con = sqlite3.connect(DB_PATH) | |
| con.execute( | |
| "CREATE TABLE IF NOT EXISTS events " | |
| "(ts TEXT, label TEXT, confidence REAL, source TEXT)" | |
| ) | |
| con.commit() | |
| con.close() | |
| def log_event(label, confidence, source): | |
| con = sqlite3.connect(DB_PATH) | |
| con.execute( | |
| "INSERT INTO events VALUES (?,?,?,?)", | |
| (datetime.datetime.now().isoformat(timespec="seconds"), label, confidence, source), | |
| ) | |
| con.commit() | |
| con.close() | |
| def recent_events(limit=20): | |
| con = sqlite3.connect(DB_PATH) | |
| rows = con.execute( | |
| "SELECT ts, label, confidence, source FROM events ORDER BY ts DESC LIMIT ?", | |
| (limit,), | |
| ).fetchall() | |
| con.close() | |
| return rows | |
| def send_telegram(message): | |
| if not TELEGRAM_TOKEN or not TELEGRAM_CHAT_ID: | |
| return | |
| try: | |
| requests.post( | |
| f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage", | |
| data={"chat_id": TELEGRAM_CHAT_ID, "text": message}, | |
| timeout=5, | |
| ) | |
| except requests.RequestException: | |
| pass | |
| def generate_alarm(): | |
| sr = 22050 | |
| duration = 1.5 | |
| t = np.linspace(0, duration, int(sr * duration), endpoint=False) | |
| wave = np.where( | |
| (t * 4) % 1 < 0.5, | |
| np.sin(2 * np.pi * 880 * t), | |
| np.sin(2 * np.pi * 660 * t), | |
| ) | |
| return sr, (wave * 0.4 * 32767).astype(np.int16) | |
| def should_alert(confidence): | |
| global _last_alert_ts | |
| if confidence < ALERT_THRESHOLD: | |
| return False | |
| with _alert_lock: | |
| now = time.time() | |
| if now - _last_alert_ts < ALERT_COOLDOWN_SEC: | |
| return False | |
| _last_alert_ts = now | |
| return True | |
| def run_inference(frames): | |
| inputs = processor(list(frames), return_tensors="pt").to(device) | |
| with torch.no_grad(): | |
| outputs = model(**inputs) | |
| probs = outputs.logits.softmax(dim=-1)[0].cpu().numpy() | |
| return {ID2LABEL[i]: float(p) for i, p in enumerate(probs)} | |
| def trigger_alert(confidence, source): | |
| log_event(ALERT_LABEL, confidence, source) | |
| send_telegram(f"ALERT: fight detected ({confidence:.0%}) - source: {source}") | |
| return generate_alarm() | |
| def sample_frames_from_video(video_path, num_frames=NUM_FRAMES): | |
| cap = cv2.VideoCapture(video_path) | |
| total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1 | |
| indices = set(np.linspace(0, max(total - 1, 0), num_frames).astype(int).tolist()) | |
| frames, i = [], 0 | |
| while True: | |
| ok, frame = cap.read() | |
| if not ok: | |
| break | |
| if i in indices: | |
| frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) | |
| i += 1 | |
| cap.release() | |
| while len(frames) < num_frames and frames: | |
| frames.append(frames[-1]) | |
| return np.stack(frames) | |
| def detect_uploaded(video_path): | |
| if video_path is None: | |
| return {}, "Upload a video", None, recent_events() | |
| frames = sample_frames_from_video(video_path) | |
| scores = run_inference(frames) | |
| alert_conf = scores[ALERT_LABEL] | |
| alarm = None | |
| if should_alert(alert_conf): | |
| alarm = trigger_alert(alert_conf, source=os.path.basename(video_path)) | |
| verdict = f"ALERT: fight detected ({alert_conf:.0%})" | |
| else: | |
| verdict = f"Normal scene ({alert_conf:.0%} fight confidence)" | |
| return scores, verdict, alarm, recent_events() | |
| def stream_frame(frame, buffer): | |
| if frame is None: | |
| return buffer or [], gr.update(), "waiting for webcam...", None, gr.update() | |
| buffer = (buffer or []) + [frame] | |
| if len(buffer) < NUM_FRAMES: | |
| return ( | |
| buffer, | |
| gr.update(), | |
| f"collecting frames {len(buffer)}/{NUM_FRAMES}", | |
| None, | |
| gr.update(), | |
| ) | |
| scores = run_inference(np.stack(buffer)) | |
| alert_conf = scores[ALERT_LABEL] | |
| alarm = None | |
| verdict = f"Normal ({alert_conf:.0%} fight confidence)" | |
| if should_alert(alert_conf): | |
| alarm = trigger_alert(alert_conf, source="webcam") | |
| verdict = f"ALERT: fight detected ({alert_conf:.0%})" | |
| return [], scores, verdict, alarm, recent_events() | |
| def monitor_stream(url): | |
| """Generator that opens an RTSP/HTTP stream and yields inference results in a loop.""" | |
| if not url or not url.strip(): | |
| yield None, {}, "Enter a stream URL and click Start", None, recent_events() | |
| return | |
| cap = cv2.VideoCapture(url.strip()) | |
| if not cap.isOpened(): | |
| yield None, {}, f"Cannot open stream: {url}", None, recent_events() | |
| return | |
| buffer = [] | |
| try: | |
| while True: | |
| ok, frame = cap.read() | |
| if not ok: | |
| yield None, gr.update(), "Stream ended or connection lost", None, gr.update() | |
| return | |
| rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) | |
| buffer.append(rgb) | |
| if len(buffer) < NUM_FRAMES: | |
| yield rgb, gr.update(), f"collecting frames {len(buffer)}/{NUM_FRAMES}", None, gr.update() | |
| continue | |
| scores = run_inference(np.stack(buffer)) | |
| buffer = [] | |
| alert_conf = scores[ALERT_LABEL] | |
| alarm = None | |
| verdict = f"Normal ({alert_conf:.0%} fight confidence)" | |
| if should_alert(alert_conf): | |
| alarm = trigger_alert(alert_conf, source=url[:80]) | |
| verdict = f"ALERT: fight detected ({alert_conf:.0%})" | |
| yield rgb, scores, verdict, alarm, recent_events() | |
| finally: | |
| cap.release() | |
| init_db() | |
| tg_status = ( | |
| "configured" | |
| if (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID) | |
| else "NOT configured — set TELEGRAM_TOKEN and TELEGRAM_CHAT_ID in .env" | |
| ) | |
| HIDE_FOOTER_CSS = """ | |
| footer { visibility: hidden !important; } | |
| """ | |
| with gr.Blocks(title="Fight Detector MVP", css=HIDE_FOOTER_CSS) as demo: | |
| gr.Markdown("# Fight / Violence Detector") | |
| gr.Markdown(f"**Telegram alerts:** {tg_status} | **Cooldown:** {ALERT_COOLDOWN_SEC}s between alerts") | |
| with gr.Tabs(): | |
| with gr.Tab("Upload Video"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| video_in = gr.Video(label="Input video", sources=["upload"]) | |
| run_btn = gr.Button("Analyze", variant="primary") | |
| with gr.Column(): | |
| verdict1 = gr.Textbox(label="Result", interactive=False) | |
| scores1 = gr.Label(label="Scores", num_top_classes=3) | |
| alarm1 = gr.Audio(label="Alarm", autoplay=True) | |
| events_table1 = gr.Dataframe( | |
| headers=["Time", "Label", "Confidence", "Source"], | |
| label="Recent alerts", | |
| value=recent_events(), | |
| interactive=False, | |
| ) | |
| run_btn.click( | |
| detect_uploaded, | |
| inputs=video_in, | |
| outputs=[scores1, verdict1, alarm1, events_table1], | |
| ) | |
| with gr.Tab("Live Webcam"): | |
| gr.Markdown("Grant webcam permission, then just watch. Inference runs every 8 frames.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| cam = gr.Image( | |
| sources=["webcam"], | |
| streaming=True, | |
| label="Live stream", | |
| type="numpy", | |
| ) | |
| with gr.Column(): | |
| verdict2 = gr.Textbox(label="Result", interactive=False) | |
| scores2 = gr.Label(label="Scores", num_top_classes=3) | |
| alarm2 = gr.Audio(label="Alarm", autoplay=True) | |
| buffer_state = gr.State([]) | |
| events_table2 = gr.Dataframe( | |
| headers=["Time", "Label", "Confidence", "Source"], | |
| label="Recent alerts", | |
| value=recent_events(), | |
| interactive=False, | |
| ) | |
| cam.stream( | |
| stream_frame, | |
| inputs=[cam, buffer_state], | |
| outputs=[buffer_state, scores2, verdict2, alarm2, events_table2], | |
| ) | |
| with gr.Tab("IP Camera / Mobile"): | |
| gr.Markdown( | |
| "Paste a stream URL from a security camera (RTSP) or phone.\n\n" | |
| "**Examples:**\n" | |
| "- Android `IP Webcam` app: `http://192.168.1.10:8080/video`\n" | |
| "- Security camera RTSP: `rtsp://user:pass@192.168.1.20:554/stream`\n" | |
| "- Public HLS/RTSP test stream\n" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| url_in = gr.Textbox( | |
| label="Stream URL", | |
| placeholder="rtsp://user:pass@192.168.1.20:554/stream", | |
| ) | |
| with gr.Row(): | |
| start_btn = gr.Button("Start monitoring", variant="primary") | |
| stop_btn = gr.Button("Stop") | |
| preview = gr.Image(label="Live preview", type="numpy", streaming=False) | |
| with gr.Column(): | |
| verdict3 = gr.Textbox(label="Result", interactive=False) | |
| scores3 = gr.Label(label="Scores", num_top_classes=3) | |
| alarm3 = gr.Audio(label="Alarm", autoplay=True) | |
| events_table3 = gr.Dataframe( | |
| headers=["Time", "Label", "Confidence", "Source"], | |
| label="Recent alerts", | |
| value=recent_events(), | |
| interactive=False, | |
| ) | |
| stream_event = start_btn.click( | |
| monitor_stream, | |
| inputs=[url_in], | |
| outputs=[preview, scores3, verdict3, alarm3, events_table3], | |
| ) | |
| stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[stream_event]) | |
| if __name__ == "__main__": | |
| host = "0.0.0.0" if os.environ.get("SPACE_ID") else "127.0.0.1" | |
| demo.queue().launch(server_name=host, server_port=7860, show_api=False) | |