Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"env": {
"ANTHROPIC_API_KEY": "aero_live_XUjsoGcPnyw0JJ-DtO1ZfHGdoORLaO4LxR808wU2s7k",
"ANTHROPIC_API_KEY": "aero_live_qiFbk-Et6G9qRZaElOvjjHgOWIz_O_i6Fts6W9cioFU",
"ANTHROPIC_BASE_URL": "https://capi.aerolink.lat/",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
},
"permissions": {
"allow": [],
"deny": []
},
"apiKeyHelper": "echo 'aero_live_XUjsoGcPnyw0JJ-DtO1ZfHGdoORLaO4LxR808wU2s7k'"
}
"apiKeyHelper": "echo 'aero_live_qiFbk-Et6G9qRZaElOvjjHgOWIz_O_i6Fts6W9cioFU'"
Comment on lines +3 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Remove and rotate the hard-coded API credential.

This live-looking key is committed twice, including a helper that prints it directly. Revoke/rotate it immediately, remove both plaintext values from repository history, and load the credential from a secret manager or local environment variable instead.

🧰 Tools
🪛 Betterleaks (1.7.0)

[high] 3-3: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/settings.json around lines 3 - 11, Remove the hard-coded credential
from the settings environment block and the apiKeyHelper command, then rotate or
revoke the exposed key and purge both values from repository history. Update
credential loading to use the existing secret manager or a local environment
variable without storing plaintext secrets in .claude/settings.json.

Source: Linters/SAST tools

}
4 changes: 2 additions & 2 deletions backend/Services/RiskEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class RiskConfig
[nameof(AiEventType.Dwell)] = 20, // max contribution; scaled by dwell duration
[nameof(AiEventType.RepeatedHandling)] = 15,
[nameof(AiEventType.BagOpen)] = 20,
[nameof(AiEventType.Concealment)] = 20,
[nameof(AiEventType.Concealment)] = 40, // directly observed act of hiding an item — strongest single camera signal

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the concealment evidence description.

A Concealment event may now originate from sustained direct clothing/jacket concealment, but the operator-facing evidence still claims a bag/open-bag followed shelf handling. Describe both supported paths so alerts remain auditable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/Services/RiskEngine.cs` at line 17, Update the evidence description
for the Concealment entry in the RiskEngine mapping to describe both supported
paths: direct clothing/jacket concealment and bag or open-bag concealment
following shelf handling. Keep the description operator-facing and auditable
while preserving the existing score and mapping.

[nameof(AiEventType.ExitWithoutCheckout)] = 20,
[nameof(AiEventType.ShelfPickupNoCheckout)] = 25,
[nameof(AiEventType.BlindSpotMovement)] = 15,
Expand All @@ -36,7 +36,7 @@ public class RiskConfig
public int MediumBand { get; set; } = 70; // >=70 medium alert
public int HighBand { get; set; } = 90; // >=90 high alert

public const string RuleVersion = "v4-starter-1.1";
public const string RuleVersion = "v4-starter-1.2";

public bool IsLowStaffHour(int hour)
{
Expand Down
10 changes: 9 additions & 1 deletion cloud-ai/app/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Callable, Protocol

# Canonical retail cues used across the whole pipeline.
CANONICAL_CUES = {"person", "bag", "open_bag", "product_in_hand"}
CANONICAL_CUES = {"person", "bag", "open_bag", "product_in_hand", "concealment"}

# Closed-set COCO class id -> canonical cue (YOLO / RF-DETR).
COCO_TO_CUE = {
Expand All @@ -33,8 +33,16 @@
"handbag": "bag",
"open bag": "open_bag",
"open backpack": "open_bag",
# Direct concealment cues: the phrase itself encodes item-being-hidden, so these
# map to a dedicated cue. A plain open bag is NOT concealment — only the act of
# putting/hiding an item inside clothing or a bag is.
"person hiding item inside jacket": "concealment",
"person putting object under clothing": "concealment",
"hand inside jacket": "concealment",
"product in hand": "product_in_hand",
"item in hand": "product_in_hand",
"person holding a bottle": "product_in_hand",
"person holding clothes": "product_in_hand",
}


Expand Down
52 changes: 49 additions & 3 deletions cloud-ai/app/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
BLIND_SPOT = "BlindSpot"
SHELF_LIKE = {SHELF, HIGH_VALUE}

# Bag/concealment cues must be seen on at least this many detections before they can
# fire BagOpen / Concealment / count toward HighValueActivity. Rejects one-off false
# positives (e.g. a normal shopper adjusting a jacket for a few frames).
SUSTAINED_CUE_DETECTIONS = 10


def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone]) -> list[dict]:
now = datetime.now(timezone.utc).isoformat()
Expand All @@ -38,9 +43,11 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
open_bag_zone: str | None = None
open_bag_conf = 0.0
open_bag_last_idx = -1
open_bag_dets = 0
bag_fallback_zone: str | None = None
bag_fallback_conf = 0.0
bag_last_idx = -1
bag_dets = 0

# Product-in-hand handling episodes + first occurrence (for concealment ordering).
handling_episodes = 0
Expand All @@ -49,6 +56,12 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
product_first_idx = -1
product_in_shelf = False

# Direct concealment cue (item being hidden inside jacket/clothing).
conceal_dets = 0
conceal_zone: str | None = None
conceal_last_idx = -1
conceal_in_hv = False

# High-value zone activity categories.
hv_has_person = hv_has_product = hv_has_bag = False
hv_zone_id: str | None = None
Expand Down Expand Up @@ -81,13 +94,15 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
hv_zone_id = hv_zone_id or hv_zone.id

elif d.cue == "open_bag":
open_bag_dets += 1
for z in shelf_zones:
if d.conf >= open_bag_conf:
open_bag_zone, open_bag_conf, open_bag_last_idx = z.id, d.conf, idx
if hv_zone is not None:
hv_has_bag = True

elif d.cue == "bag":
bag_dets += 1
for z in shelf_zones:
if d.conf >= bag_fallback_conf:
bag_fallback_zone, bag_fallback_conf, bag_last_idx = z.id, d.conf, idx
Comment on lines +97 to 108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use sustained cues and actual last-seen indices for ordering.

open_bag_last_idx only updates when confidence increases, so it is not necessarily the latest bag observation. More critically, it remains valid when the cue is below the sustained threshold, allowing one post-handling bag detection to emit Concealment through handled_then_bag.

Track the last eligible shelf detection independently from the highest-confidence zone, and reset/ignore each index unless its cue is sustained.

Also applies to: 202-235

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-ai/app/events.py` around lines 97 - 108, Update the detection state
around the open-bag and bag handling branches, including the related
handled_then_bag logic, to track each cue’s latest eligible shelf-detection
index separately from its highest-confidence zone. Only record or use
open_bag_last_idx and bag_last_idx when the corresponding cue meets its
sustained threshold; otherwise reset or ignore the index so an unsustained
post-handling detection cannot emit Concealment.

Expand All @@ -104,6 +119,14 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
if hv_zone is not None:
hv_has_product = True

elif d.cue == "concealment":
conceal_dets += 1
conceal_last_idx = idx
if shelf_zones:
conceal_zone = conceal_zone or shelf_zones[0].id
if hv_zone is not None:
conceal_in_hv = True

if handling_now and not prev_handling:
handling_episodes += 1
prev_handling = handling_now
Expand Down Expand Up @@ -176,6 +199,17 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
blind_spot_zone = blind_spot_zone or blindspot_zone_id

# --- Emit events (all evidence is observable-signal language, never conclusions) ---
# Gate bag/concealment cues on sustained detection counts so a handful of
# one-off detections (jacket adjust, misfire) cannot drive BagOpen/Concealment.
open_bag_sustained = open_bag_dets >= SUSTAINED_CUE_DETECTIONS
bag_sustained = bag_dets >= SUSTAINED_CUE_DETECTIONS
if not open_bag_sustained:
open_bag_zone = None
if not bag_sustained:
bag_fallback_zone = None
if not (open_bag_sustained or bag_sustained):
hv_has_bag = False

if high_value_seen_zone is not None:
events.append(_ev("HighValueZoneEntry", high_value_seen_zone, 1.0, 0.9, now))

Expand All @@ -187,10 +221,18 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
elif max_reentries > 0:
events.append(_ev("RepeatedHandling", reentry_zone, float(max_reentries), 0.7, now))

# Concealment: item handled at a shelf, then a bag/open-bag cue appears afterwards.
# Concealment requires evidence of an ITEM being hidden — a product in hand alone,
# or an open bag alone, is never concealment. Two valid paths:
# A) product handled at a shelf, THEN a bag/open-bag cue appears afterwards
# (item picked up -> moved into a bag), or
# B) a sustained direct concealment cue ("hiding item inside jacket" style) —
# the detection itself encodes item-being-hidden.
latest_bag_idx = max(open_bag_last_idx, bag_last_idx)
concealment_zone = open_bag_zone or bag_fallback_zone or handling_zone
if product_in_shelf and product_first_idx >= 0 and latest_bag_idx > product_first_idx:
conceal_sustained = conceal_dets >= SUSTAINED_CUE_DETECTIONS
handled_then_bag = product_in_shelf and product_first_idx >= 0 and latest_bag_idx > product_first_idx
if handled_then_bag or conceal_sustained:
concealment_zone = (conceal_zone if conceal_sustained else None) \
or open_bag_zone or bag_fallback_zone or handling_zone
events.append(_ev("Concealment", concealment_zone, 1.0, 0.7, now))
Comment on lines 230 to 236

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Concealment bypasses sustained gate 🐞 Bug ≡ Correctness

In extract_events(), BagOpen/HighValueActivity are gated by sustained bag/open_bag counts, but the
“handled_then_bag” concealment path still uses ungated open_bag_last_idx/bag_last_idx. This can emit
a Concealment event from a single transient bag/open_bag detection after product_first_idx,
contradicting the new false-positive suppression intent.
Agent Prompt
### Issue description
`extract_events()` introduced sustained gating for bag/open-bag cues, but `handled_then_bag` still keys off `open_bag_last_idx` / `bag_last_idx` even when the cue was not sustained. This allows one-off bag detections to trigger `Concealment`.

### Issue Context
The code explicitly claims sustained detections should be required to drive BagOpen/Concealment. The current implementation only nulls `open_bag_zone` / `bag_fallback_zone`, not the indexes used by concealment ordering.

### Fix Focus Areas
- cloud-ai/app/events.py[201-236]

### Suggested fix
- Compute a **gated** latest bag index, e.g.:
  - `latest_bag_idx = max(open_bag_last_idx if open_bag_sustained else -1, bag_last_idx if bag_sustained else -1)`
  - and use that for `handled_then_bag`.
- Alternatively, if `not open_bag_sustained`, also reset `open_bag_last_idx = -1` (same for `bag_last_idx`).
- Keep the zone selection consistent with the gated path (i.e., only allow bag-based concealment if the corresponding sustained flag is true).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


# Exit without checkout, and its stronger "carried a product out" variant.
Expand All @@ -216,6 +258,10 @@ def extract_events(fps: float, frames: list[list[Detection]], zones: list[Zone])
if max_group >= 2:
events.append(_ev("GroupDistraction", group_zone, float(max_group), 0.7, now))

# A sustained concealment cue in a high-value zone implies an item is involved.
if conceal_sustained and conceal_in_hv:
hv_has_product = True

Comment on lines +261 to +264

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require sustained concealment within the high-value zone.

conceal_sustained counts detections across the whole clip while conceal_in_hv only requires one high-value-zone detection. Ten detections elsewhere plus one incidental high-value detection will incorrectly credit high-value product activity. Count sustained concealment per high-value zone/frame sequence instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-ai/app/events.py` around lines 261 - 264, Update the high-value-zone
product inference in the event-processing logic around conceal_sustained and
conceal_in_hv so both conditions are calculated from the same high-value-zone
frame sequence. Track or derive sustained concealment within the high-value zone
rather than combining clip-wide concealment counts with a single zone detection,
then set hv_has_product only when that zone-local threshold is met.

hv_activity = int(hv_has_person) + int(hv_has_product) + int(hv_has_bag)
if hv_activity >= 2:
events.append(_ev("HighValueActivity", hv_zone_id, float(hv_activity), 0.8, now))
Expand Down
27 changes: 16 additions & 11 deletions cloud-ai/eval/ground_truth.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"description": "Ground-truth labels for ONEVO Phase 1A camera evaluation. Source: Shoplifting Dataset (2022) - CV Laboratory MNNIT Allahabad. label: 'shoplifting' (positive) or 'normal' (negative).",
"description": "Ground-truth labels for ONEVO Phase 1A camera evaluation. Custom local test videos. label: 'shoplifting' (positive) or 'normal' (negative).",
"alert_threshold": 70,
"default_zone": {
"name": "Whole Frame (High Value)",
Expand All @@ -26,24 +26,29 @@
},
"clips": [
{
"id": "normal_001",
"file": "clips/normal_001.mp4",
"label": "normal"
"id": "shoplifting_1",
"file": "clips/1.mp4",
"label": "shoplifting"
},
{
"id": "shoplifting_2",
"file": "clips/2.mp4",
"label": "shoplifting"
},
{
"id": "normal_002",
"file": "clips/normal_002.mp4",
"id": "normal_3",
"file": "clips/3.mp4",
"label": "normal"
},
{
"id": "shoplifting_001",
"file": "clips/shoplifting_001.mp4",
"id": "shoplifting_4",
"file": "clips/4.mp4",
"label": "shoplifting"
},
{
"id": "shoplifting_002",
"file": "clips/shoplifting_002.mp4",
"id": "shoplifting_testvideo",
"file": "clips/testvideo.mp4",
"label": "shoplifting"
Comment on lines +29 to 51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import json
from pathlib import Path

manifest = Path("cloud-ai/eval/ground_truth.json")
for clip in json.loads(manifest.read_text(encoding="utf-8"))["clips"]:
    path = manifest.parent / clip["file"]
    print(f"{clip['id']}: {path}")
    assert path.is_file(), f"Missing evaluation clip: {path}"
PY

Repository: baasith6/camera-phase-1

Length of output: 342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate eval assets =="
if [ -d cloud-ai/eval ]; then
  git ls-files cloud-ai/eval | sed -n '1,200p'
else
  echo "cloud-ai/eval not tracked"
fi

echo
echo "== manifest contents =="
cat -n cloud-ai/eval/ground_truth.json | sed -n '1,120p'

echo
echo "== files named like expected clips in repo =="
git ls-files | rg '(^|/)clips/|1\.mp4$|2\.mp4$|3\.mp4$|4\.mp4$|testvideo\.mp4$' || true

Repository: baasith6/camera-phase-1

Length of output: 1896


🌐 Web query:

GitHub baasith6 camera-phase-1 cloud-ai/eval clips ground_truth.json

💡 Result:

The specific path "camera-phase-1/cloud-ai/eval/clips/ground_truth.json" does not appear to be a standard or publicly documented file path associated with the user "baasith6" on GitHub [1]. The GitHub user "baasith6" (Abdul Baasith) is a developer whose public repositories focus on full-stack development, including projects like "RepoGraph-Protocol," agricultural AI vision backends ("agri-ai-vision-back"), and payment system integrations [1][2][3]. A search of his public repositories does not reveal a project named "camera-phase-1" or the specific file structure requested. It is possible that this path refers to: 1. A private repository belonging to the user. 2. A locally organized project structure that has not been pushed to a public GitHub repository. 3. A misremembered file path or project name. While there are many repositories on GitHub related to "camera," "ground truth," and "evaluation" (often involving computer vision, SLAM, or AI training pipelines), they are not linked to the user "baasith6" [4][5][6][7][8]. If this is a file you expect to exist in a public repository, you may want to verify the repository name or check if the repository is private.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== eval scripts mentioning clips =="
rg -n "ground_truth|clips/|VideoCapture|cv2|ground_truth\\.json|results\\.json" cloud-ai/eval -

echo
echo "== gitignored files under cloud-ai/eval or matching clip names =="
if [ -f .gitignore ]; then
  cat -n .gitignore | rg 'cloud-ai|eval|\\.mp4$|\\.MP4$|clips' || true
fi

echo
echo "== tracked all files under cloud-ai =="
git ls-files cloud-ai | sed -n '1,200p'

Repository: baasith6/camera-phase-1

Length of output: 1157


Add the evaluation clip assets referenced by ground_truth.json.

cloud-ai/eval/ground_truth.json references cloud-ai/eval/clips/{1,2,3,4}.mp4 and cloud-ai/eval/clips/testvideo.mp4, but none of these files are present. The evaluation scripts depend on these paths, so add the clips to the repo or skip/bundle them through the evaluation setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-ai/eval/ground_truth.json` around lines 29 - 51, Add the five video
assets referenced by the ground-truth entries shoplifting_1, shoplifting_2,
normal_3, shoplifting_4, and shoplifting_testvideo under the expected clips
directory, or update the evaluation setup to reliably provide or skip them while
preserving those referenced paths.

}
]
}
}
76 changes: 51 additions & 25 deletions cloud-ai/eval/results.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"backend": "yoloe",
"model": "yoloe-11s-seg.pt",
"alert_threshold": 70,
"num_scored": 4,
"num_scored": 5,
"num_skipped": 0,
"metrics": {
"tp": 2,
"tp": 4,
"fp": 0,
"tn": 2,
"tn": 1,
"fn": 0,
"precision": 1.0,
"recall": 1.0,
Expand All @@ -16,60 +16,86 @@
},
"per_clip": [
{
"id": "normal_001",
"file": "clips/normal_001.mp4",
"gt_label": "normal",
"score": 0,
"predicted": "no_alert",
"events": [],
"elapsed_s": 64.94
},
{
"id": "normal_002",
"file": "clips/normal_002.mp4",
"gt_label": "normal",
"score": 60,
"predicted": "no_alert",
"id": "shoplifting_1",
"file": "clips/1.mp4",
"gt_label": "shoplifting",
"score": 125,
"predicted": "alert",
"events": [
"HighValueZoneEntry",
"Dwell",
"RepeatedHandling",
"Concealment",
"GroupDistraction",
"HighValueActivity",
"LowStaffRemoval",
"BagOpen"
],
"elapsed_s": 65.59
"elapsed_s": 111.61
},
{
"id": "shoplifting_001",
"file": "clips/shoplifting_001.mp4",
"id": "shoplifting_2",
"file": "clips/2.mp4",
"gt_label": "shoplifting",
"score": 70,
"score": 102,
"predicted": "alert",
"events": [
"HighValueZoneEntry",
"Dwell",
"RepeatedHandling",
"Concealment",
"GroupDistraction",
"HighValueActivity",
"BagOpen"
],
"elapsed_s": 65.28
"elapsed_s": 202.02
},
{
"id": "normal_3",
"file": "clips/3.mp4",
"gt_label": "normal",
"score": 35,
"predicted": "no_alert",
"events": [
"HighValueZoneEntry",
"Dwell",
"RepeatedHandling",
"GroupDistraction"
],
"elapsed_s": 184.12
},
{
"id": "shoplifting_002",
"file": "clips/shoplifting_002.mp4",
"id": "shoplifting_4",
"file": "clips/4.mp4",
"gt_label": "shoplifting",
"score": 70,
"predicted": "alert",
"events": [
"HighValueZoneEntry",
"Dwell",
"RepeatedHandling",
"Concealment",
"HighValueActivity"
],
"elapsed_s": 105.18
},
{
"id": "shoplifting_testvideo",
"file": "clips/testvideo.mp4",
"gt_label": "shoplifting",
"score": 120,
"predicted": "alert",
"events": [
"HighValueZoneEntry",
"Dwell",
"RepeatedHandling",
"Concealment",
"GroupDistraction",
"HighValueActivity",
"LowStaffRemoval",
"BagOpen"
],
"elapsed_s": 71.08
"elapsed_s": 164.94
}
]
}
Loading
Loading