infinicode 2.8.22 → 2.8.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,6 +50,15 @@ webhook_url = None
50
50
  last_webhook_send_time = 0
51
51
  webhook_send_interval = 0.5
52
52
 
53
+ # Vision-confirm (Ollama Cloud) config — stage 2 of the motion pipeline.
54
+ # Motion detection (frame-diff) is a cheap pre-filter; before we fire the
55
+ # session webhook we ask a vision model to confirm a person is actually in
56
+ # frame, to cut down on false triggers from pets/shadows/wind.
57
+ OLLAMA_CLOUD_API_KEY = os.getenv("OLLAMA_CLOUD_API_KEY", "")
58
+ OLLAMA_CLOUD_VISION_MODEL = os.getenv("OLLAMA_CLOUD_VISION_MODEL", "gemma3:27b")
59
+ OLLAMA_CLOUD_VISION_URL = "https://ollama.com/v1/chat/completions"
60
+ VISION_CONFIRM_TIMEOUT = 8
61
+
53
62
  # Simple object detection using OpenCV DNN (MobileNet SSD)
54
63
  try:
55
64
  # Load pre-trained MobileNet SSD model
@@ -111,6 +120,66 @@ def detect_objects(frame, conf_threshold=0.5):
111
120
 
112
121
  return frame, detected_objects
113
122
 
123
+ def confirm_person_present(frame):
124
+ """Ask an Ollama Cloud vision model whether a person is visible in `frame`.
125
+
126
+ This is stage 2 of the motion pipeline: motion detection (frame-diff) is a
127
+ cheap pre-filter, and this confirms a person is actually present before we
128
+ fire the session webhook — cuts down on false triggers from pets, shadows,
129
+ wind, etc.
130
+
131
+ Fails OPEN (returns True) on any error — missing key, network failure,
132
+ timeout, bad response — since the pre-existing motion-only trigger is the
133
+ fallback behavior and a vision-API outage shouldn't silently disable the
134
+ whole trigger system.
135
+ """
136
+ if not OLLAMA_CLOUD_API_KEY:
137
+ # No key configured: skip the check entirely, preserve motion-only
138
+ # behavior as the zero-config default. Caller logs this case.
139
+ return True
140
+
141
+ try:
142
+ _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
143
+ img_base64 = base64.b64encode(buffer).decode('utf-8')
144
+
145
+ payload = {
146
+ "model": OLLAMA_CLOUD_VISION_MODEL,
147
+ "messages": [
148
+ {
149
+ "role": "user",
150
+ "content": [
151
+ {
152
+ "type": "text",
153
+ "text": "Is there a person clearly visible in this image? Answer with only YES or NO."
154
+ },
155
+ {
156
+ "type": "image_url",
157
+ "image_url": {"url": f"data:image/jpeg;base64,{img_base64}"}
158
+ }
159
+ ]
160
+ }
161
+ ],
162
+ "stream": False
163
+ }
164
+
165
+ response = requests.post(
166
+ OLLAMA_CLOUD_VISION_URL,
167
+ json=payload,
168
+ headers={
169
+ "Authorization": f"Bearer {OLLAMA_CLOUD_API_KEY}",
170
+ "Content-Type": "application/json"
171
+ },
172
+ timeout=VISION_CONFIRM_TIMEOUT
173
+ )
174
+ response.raise_for_status()
175
+
176
+ answer = response.json()["choices"][0]["message"]["content"].strip()
177
+ return answer.upper().startswith("YES")
178
+ except Exception as e:
179
+ print(f"WARNING: vision-confirm error, failing open (treating as person present): {e}")
180
+ return True
181
+
182
+
114
183
  def send_webhook(frame_data):
115
184
  """Send frame to webhook URL"""
116
185
  global webhook_url, last_webhook_send_time
@@ -194,7 +263,18 @@ def generate_frames():
194
263
  last_motion_time = time.time()
195
264
  motion_frame_buffer = processed_frame.copy()
196
265
  print(f"Motion detected!")
197
- send_webhook(processed_frame)
266
+
267
+ # Only run the (network-bound) vision-confirm + webhook
268
+ # once per motion "event" — reuse the same debounce timer
269
+ # send_webhook() itself uses, rather than calling the
270
+ # vision API on every single frame while motion continues.
271
+ if webhook_url and (time.time() - last_webhook_send_time >= webhook_send_interval):
272
+ if not OLLAMA_CLOUD_API_KEY:
273
+ send_webhook(processed_frame)
274
+ elif confirm_person_present(processed_frame):
275
+ send_webhook(processed_frame)
276
+ else:
277
+ print("Motion event suppressed: vision-confirm found no person present")
198
278
  else:
199
279
  motion_detected_state = False
200
280