● Zion Boggan
repos/Sightline/backend/live_feed.py
zionboggan.com ↗
86 lines · python
History for this file →
1
"""Live ingestion daemon. Continuously feeds fresh plate reads into Sightline through the same
2
normalize/dedup/watchlist pipeline as everything else, so the platform shows genuine real-time
3
activity. Reads carry the current timestamp, hit random cameras nationwide, reuse existing
4
plates (so vehicle histories grow) with occasional new plates and hotlist hits.
5
 
6
Run detached:  python live_feed.py
7
Stops on SIGTERM/SIGINT.
8
"""
9
from __future__ import annotations
10
 
11
import random
12
import signal
13
import time
14
from datetime import datetime, timezone
15
 
16
from sqlalchemy import select
17
 
18
from app.db.session import SessionLocal
19
from app.models import LicensePlate, Source
20
from app.services.ingest import ingest_live_reads
21
from generate_data import CAMERAS
22
 
23
INTERVAL_SECONDS = 4
24
BATCH_MIN, BATCH_MAX = 2, 7
25
 
26
_running = True
27
 
28
 
29
def _stop(*_):
30
    global _running
31
    _running = False
32
 
33
 
34
def main():
35
    signal.signal(signal.SIGTERM, _stop)
36
    signal.signal(signal.SIGINT, _stop)
37
    db = SessionLocal()
38
    source = db.scalar(select(Source).order_by(Source.created_at))
39
    src_id = source.id if source else None
40
 
41
    pool = [(p.plate_norm, p.state) for p in
42
            db.scalars(select(LicensePlate).limit(400)).all()]
43
    if not pool:
44
        pool = [("LIVE001", "TN")]
45
    print(f"live feed started: source={source.name if source else None}, pool={len(pool)} plates, "
46
          f"{len(CAMERAS)} cameras, every {INTERVAL_SECONDS}s", flush=True)
47
 
48
    total = 0
49
    while _running:
50
        now = datetime.now(timezone.utc)
51
        batch = []
52
        for _ in range(random.randint(BATCH_MIN, BATCH_MAX)):
53
            cam = random.choice(CAMERAS)
54
            if random.random() < 0.05:
55
                plate, state = "BNDT447", "TN"
56
                hotlist = "true"
57
            elif random.random() < 0.15:
58
                plate = "".join(random.choices("ABCDEFGHJKLMNPQRSTUVWXYZ", k=3)) + \
59
                        "".join(random.choices("0123456789", k=4))
60
                state = random.choice([s for s in {c[4].split(", ")[1] for c in CAMERAS}])
61
                hotlist = "false"
62
            else:
63
                plate, state = random.choice(pool)
64
                hotlist = "false"
65
            batch.append({
66
                "plate": plate, "state": state,
67
                "timestamp": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
68
                "camera": cam[0], "latitude": cam[2], "longitude": cam[3],
69
                "confidence": random.randint(82, 99), "hotlist": hotlist,
70
            })
71
        try:
72
            res = ingest_live_reads(db, src_id, batch, label=f"live-{int(now.timestamp())}")
73
            total += res.get("inserted", 0)
74
        except Exception as e:
75
            print(f"live feed error: {e}", flush=True)
76
            db.rollback()
77
        for _ in range(INTERVAL_SECONDS):
78
            if not _running:
79
                break
80
            time.sleep(1)
81
    db.close()
82
    print(f"live feed stopped after {total} reads", flush=True)
83
 
84
 
85
if __name__ == "__main__":
86
    main()