fight-detector / app.py
fayedd's picture
switch to VideoMAE archit11 model
e24bfb5 verified
Raw
History Blame Contribute Delete
11.4 kB
"""
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)