<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sherin Joseph Roy</title>
    <description>The latest articles on DEV Community by Sherin Joseph Roy (@sherinjosephroy).</description>
    <link>https://dev.to/sherinjosephroy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3339374%2F0000eca7-9c52-452e-8d4d-14521643b941.jpeg</url>
      <title>DEV Community: Sherin Joseph Roy</title>
      <link>https://dev.to/sherinjosephroy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sherinjosephroy"/>
    <language>en</language>
    <item>
      <title>I Turned My Old Android Phone Into an L2 Autonomous Driving System (Flutter + C++)</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Mon, 13 Apr 2026 10:19:32 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/i-turned-my-old-android-phone-into-an-l2-autonomous-driving-system-flutter-c-12fd</link>
      <guid>https://dev.to/sherinjosephroy/i-turned-my-old-android-phone-into-an-l2-autonomous-driving-system-flutter-c-12fd</guid>
      <description>&lt;p&gt;Modern cars ship with expensive L2 driver assistance systems. Most of the heavy lifting for these systems is just computer vision running on a small chip behind the dashboard. Guess what else has a camera, a GPU, and a decent SoC? Your phone.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/pcfjbeNWuYw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I decided to build a shadow mode ADAS that runs entirely in my pocket. I call it Zyra ADAS. It requires absolutely no cloud connectivity. There is no network latency. It just watches the road and predicts what a real autonomous system would do in real time. &lt;/p&gt;

&lt;p&gt;Here is how I built a highly optimized, lock-free perception engine using Flutter, C++, and Vulkan.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with Cloud AI for Safety
&lt;/h3&gt;

&lt;p&gt;Sending video frames to a cloud server for processing is fine for basic image recognition. It is completely useless when you are moving at 80 km/h and need to know if a car is braking ahead of you. You need on-device processing. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture: Bypassing the Framework
&lt;/h3&gt;

&lt;p&gt;The app is built with Flutter, but you cannot afford Flutter's MethodChannel serialization costs when processing video at high frame rates. Bouncing JSON strings across threads adds way too much overhead.&lt;/p&gt;

&lt;p&gt;I bypassed it completely using &lt;code&gt;dart:ffi&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The Flutter UI only handles the camera stream and drawing the overlays. The actual hot path lives in a single C++ shared object (&lt;code&gt;libzyra_perception.so&lt;/code&gt;). We pass the raw YUV camera frames directly from the hardware into the native engine with zero copy memory pointers. &lt;/p&gt;

&lt;p&gt;C++ owns the entire heavy lifting process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting YUV to RGB and letterboxing&lt;/li&gt;
&lt;li&gt;Running YOLOv8n inference for object detection&lt;/li&gt;
&lt;li&gt;Per class Non-Maximum Suppression (NMS)&lt;/li&gt;
&lt;li&gt;Canny and HoughLinesP for classical lane tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dart just reads the final struct of bounding boxes and lane coordinates. &lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Performance on Mobile Silicon
&lt;/h3&gt;

&lt;p&gt;To make YOLOv8n run smoothly on a phone, I used NCNN with Vulkan compute. NCNN is incredible for mobile deployment. It uses FP16 packed storage and Winograd convolutions to squeeze every drop of performance out of the mobile GPU.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4i6b1fr2rcf8b02yy9v.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4i6b1fr2rcf8b02yy9v.gif" alt=" " width="720" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results speak for themselves. On my daily driver Realme smartphone with a Snapdragon 662 (a mid range chip from 2020), I am hitting about &lt;strong&gt;105ms end to end inference time&lt;/strong&gt;. That is roughly 10 FPS on older hardware. On modern flagship chips like the Snapdragon 8 Gen 2, it easily hits a sustained 30 FPS. &lt;/p&gt;

&lt;p&gt;I designed the engine with a bounded queue. If the inference falls behind, the engine explicitly drops the older frame. There is no silent buffering and no latency creep. It always shows you what is happening exactly right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Next?
&lt;/h3&gt;

&lt;p&gt;I am currently prepping this software to mount inside a Tata Tigor EV for a massive data collection rig. The next step is fusing the phone IMU and GPS data into the perception pipeline to build out proper vehicle dynamics and forward collision warnings.&lt;/p&gt;

&lt;p&gt;You can dig into the C++ engine and the FFI bridge here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Sherin-SEF-AI" rel="noopener noreferrer"&gt;
        Sherin-SEF-AI
      &lt;/a&gt; / &lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS" rel="noopener noreferrer"&gt;
        Zyra-ADAS
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Android L2 ADAS shadow-mode system. On-device YOLOv8n + classical lane tracking with Vulkan-accelerated NCNN inference. Flutter UI + C++ NDK engine.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Zyra ADAS&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Your phone is now an L2 ADAS shadow system.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Real-time object detection + lane tracking on Android, powered by on-device NCNN inference with Vulkan acceleration. No cloud, no latency, no compromise.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.android.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f650957970fea395a2fcda7c2ac7389a9fa72811779dde877ed0cb1499e15982/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f416e64726f69642d33302532422d3344444338343f7374796c653d666f722d7468652d6261646765266c6f676f3d616e64726f6964266c6f676f436f6c6f723d7768697465" alt="Android"&gt;&lt;/a&gt;
&lt;a href="https://flutter.dev" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5da023d4da567db7bf16d94f037e9a0069875473e56e50f35ca00618d0b8812a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466c75747465722d332e34312d3032353639423f7374796c653d666f722d7468652d6261646765266c6f676f3d666c7574746572266c6f676f436f6c6f723d7768697465" alt="Flutter"&gt;&lt;/a&gt;
&lt;a href="https://isocpp.org" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/014fd07253568ce9d4cf0b28a396937d46d8612fe44bbe04a28cc6731390156e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f432532422532422d31372d3030353939433f7374796c653d666f722d7468652d6261646765266c6f676f3d63706c7573706c7573266c6f676f436f6c6f723d7768697465" alt="C++"&gt;&lt;/a&gt;
&lt;a href="https://github.com/Tencent/ncnn" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1c2350cc2bbe38149956956fa45c291daf6edf15fdfab5eb103484412ddb3214/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4e434e4e2d56756c6b616e2d4646364233353f7374796c653d666f722d7468652d6261646765" alt="NCNN"&gt;&lt;/a&gt;
&lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/91d800b82b62c338a63e111e726aed1b94807eb76612270e92bd27f2c5e29ef3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d3445434443343f7374796c653d666f722d7468652d6261646765" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS#what-it-does" rel="noopener noreferrer"&gt; What it does &lt;/a&gt;&lt;/strong&gt;  •  &lt;strong&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS#architecture" rel="noopener noreferrer"&gt; Architecture &lt;/a&gt;&lt;/strong&gt;  •  &lt;strong&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS#performance" rel="noopener noreferrer"&gt; Performance &lt;/a&gt;&lt;/strong&gt;  •  &lt;strong&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS#quick-start" rel="noopener noreferrer"&gt; Quick start &lt;/a&gt;&lt;/strong&gt;  •  &lt;strong&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Zyra-ADAS#roadmap" rel="noopener noreferrer"&gt; Roadmap &lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why Zyra ADAS&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Modern cars ship L2 driver assistance that costs thousands of dollars. Most of the hard work is computer vision running on a small SoC behind the dashboard. Your phone has that same SoC. It has a camera, a GPU, GPS, accelerometers, and a screen bright enough to see in sunlight.&lt;/p&gt;
&lt;p&gt;Zyra turns it into a &lt;strong&gt;shadow-mode ADAS&lt;/strong&gt;: it watches the road and predicts what a real L2 system would do, side by side with what you actually do. No vehicle control, no liability, just perception that runs in your pocket.&lt;/p&gt;
&lt;p&gt;Built for riders, fleet operators, researchers, and anyone who wants to…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Sherin-SEF-AI/Zyra-ADAS" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Have you ever tried bridging heavy C++ computer vision pipelines directly to Flutter? Let me know your architecture choices in the comments.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>flutter</category>
      <category>computervision</category>
      <category>android</category>
    </item>
    <item>
      <title>200GB of Raw Rover Data and No Pipeline to Process It. So I Wrote One.</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Mon, 30 Mar 2026 17:00:42 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/200gb-of-raw-rover-data-and-no-pipeline-to-process-it-so-i-wrote-one-5abb</link>
      <guid>https://dev.to/sherinjosephroy/200gb-of-raw-rover-data-and-no-pipeline-to-process-it-so-i-wrote-one-5abb</guid>
      <description>&lt;p&gt;&lt;strong&gt;Building a 33-module Python pipeline that takes raw GoPro/Insta360 recordings and turns them into SLAM-ready datasets with auto-labeling, depth estimation, and edge deployment. The unglamorous engineering that nobody writes about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujqcl6nyajj3w89q8pg0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujqcl6nyajj3w89q8pg0.gif" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three field sessions in Kerala with a GoPro Hero 11 on a rover chassis, an Insta360 X4 for 360-degree coverage, and an Android phone running Sensor Logger.&lt;/p&gt;

&lt;p&gt;Result: 200GB of raw video, a growing spreadsheet tracking which files had GPS lock, which files had HyperSmooth accidentally enabled (destroying IMU correlation), and one SD card that corrupted mid-write.&lt;/p&gt;

&lt;p&gt;I tried stitching together scripts. FFmpeg for frames, a GPMF parser for telemetry, a separate tool for calibration, manual CSV wrangling for synchronization. After the third time I forgot which script ran in which order, I stopped and asked myself what I actually needed.&lt;/p&gt;

&lt;p&gt;The answer was not a better script. It was a system.&lt;/p&gt;

&lt;p&gt;That system became &lt;strong&gt;Orvex&lt;/strong&gt;, and over six months it grew into 33 core modules, a 28-panel PyQt6 desktop app, a FastAPI backend with 96+ endpoints, and a React frontend. All sharing identical business logic. Zero code duplication.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Gap That Started Everything
&lt;/h2&gt;

&lt;p&gt;The autonomous driving community has world-class open source perception tools. ORBSLAM3 for visual-inertial SLAM. VINS-Mono for state estimation. DepthAnything for monocular depth. SegFormer for semantic segmentation. YOLOv8 for detection.&lt;/p&gt;

&lt;p&gt;But all of these tools expect their input in a specific format with specific conventions. Getting your own raw field recordings into that format is where weeks disappear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujd0xaaw7h6x0nx2ugsq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujd0xaaw7h6x0nx2ugsq.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I needed, concretely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit 50+ GoPro files and flag stabilization artifacts automatically&lt;/li&gt;
&lt;li&gt;Extract IMU data at 200Hz and GPS from binary GPMF streams inside MP4 containers&lt;/li&gt;
&lt;li&gt;Synchronize three devices that have independent clocks&lt;/li&gt;
&lt;li&gt;Run a guided camera calibration workflow with validation gates&lt;/li&gt;
&lt;li&gt;Auto-label 10,000 frames with classes specific to Indian roads (autorickshaws, potholes, unmarked speed bumps)&lt;/li&gt;
&lt;li&gt;Train a detector, export to ONNX, convert to TensorRT for Jetson deployment&lt;/li&gt;
&lt;li&gt;Version datasets as they evolve over collection campaigns&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No single tool covered this. Every tool covered one step. The pipeline between steps was my problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8eu5eq47rj8c18hxlspe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8eu5eq47rj8c18hxlspe.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The One Rule That Saved the Project
&lt;/h2&gt;

&lt;p&gt;Early on I made one architectural decision that paid off more than any other:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every line of business logic lives in &lt;code&gt;core/&lt;/code&gt;. No UI imports allowed.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;core/              33 Python modules. No PyQt6. No FastAPI.
  |
  +-- desktop/     PyQt6 app (28 stacked widget panels)
  +-- web/
       +-- backend/   FastAPI (96+ endpoints)
       +-- frontend/  React + Vite (25 pages)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The desktop app and web app are both thin wrappers that call the same &lt;code&gt;core/&lt;/code&gt; functions. I fixed a GPS timestamp parsing bug once in &lt;code&gt;core/extractor_gopro.py&lt;/code&gt; and both interfaces got the fix immediately.&lt;/p&gt;

&lt;p&gt;When a colleague wanted to run extraction on a headless server, they imported &lt;code&gt;core.extractor_gopro&lt;/code&gt; directly. No Qt dependency chain. No display server required.&lt;/p&gt;

&lt;p&gt;The enforcement was simple: if the string &lt;code&gt;import PyQt6&lt;/code&gt; or &lt;code&gt;import fastapi&lt;/code&gt; appears in any file under &lt;code&gt;core/&lt;/code&gt;, the code does not merge. No exceptions.&lt;/p&gt;

&lt;p&gt;This also meant every &lt;code&gt;core/&lt;/code&gt; module was independently testable with pytest. No GUI mocking required.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Hardest Lesson: Never Store Timestamps as Floats
&lt;/h2&gt;

&lt;p&gt;If you remember one thing from this post, let it be this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store all timestamps as 64-bit integer nanoseconds. Not floats. Not milliseconds. Not ISO strings.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I lost three days to a synchronization bug before learning this. Here is the problem:&lt;/p&gt;

&lt;p&gt;GoPro GPMF gives timestamps as microseconds. Android Sensor Logger gives &lt;code&gt;seconds_elapsed&lt;/code&gt; as a float counting from app start. Insta360 encodes timestamps in a proprietary binary format that exiftool can extract.&lt;/p&gt;

&lt;p&gt;When you cross-correlate 200Hz accelerometer signals between two devices to compute their clock offset, a rounding error of 0.001 seconds in a float shifts your alignment by an entire IMU sample. At 200Hz, one sample is 5 milliseconds. That error propagates through your entire SLAM pipeline.&lt;/p&gt;

&lt;p&gt;The fix is absolute:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Correct. Every timestamp in the pipeline.
&lt;/span&gt;&lt;span class="n"&gt;timestamp_ns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;  &lt;span class="c1"&gt;# nanoseconds since Unix epoch
&lt;/span&gt;
&lt;span class="c1"&gt;# Wrong. Precision loss at high sample rates.
&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This constraint is enforced at the Pydantic model level. The &lt;code&gt;IMUSample&lt;/code&gt; model accepts &lt;code&gt;timestamp_ns: int&lt;/code&gt; and nothing else. If you pass a float, validation fails immediately rather than producing subtle drift errors 3 modules downstream.&lt;/p&gt;


&lt;h2&gt;
  
  
  Synchronizing Devices That Don't Share a Clock
&lt;/h2&gt;

&lt;p&gt;This was the most interesting engineering challenge in the project.&lt;/p&gt;

&lt;p&gt;Three devices. Three independent clocks. Three different timestamp formats. The goal: compute a nanosecond offset for each device so all data aligns to a common timeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  Method 1: GPS Time Anchor
&lt;/h3&gt;

&lt;p&gt;GoPro Hero 11 records GPS timestamps in UTC. If the Android phone also has GPS lock, you can compute the offset directly by comparing GPS time readings from the same moment. This is the most accurate method, typically within a few milliseconds.&lt;/p&gt;

&lt;p&gt;The catch: both devices need solid GPS fix. Indoor recordings, dense urban canyons, and cloudy conditions can leave you without GPS on one or both devices.&lt;/p&gt;
&lt;h3&gt;
  
  
  Method 2: IMU Cross-Correlation
&lt;/h3&gt;

&lt;p&gt;When GPS is unavailable, you can exploit the fact that all devices attached to the same rover experienced the same physical motion. The acceleration magnitude signal should be nearly identical across devices, just shifted in time.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Resample both signals to 200Hz, compute acceleration magnitude
&lt;/span&gt;&lt;span class="n"&gt;mag_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ax_a&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ay_a&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;az_a&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mag_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ax_b&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ay_b&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;az_b&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Normalized cross-correlation to find the lag
&lt;/span&gt;&lt;span class="n"&gt;corr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;correlate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mag_a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mag_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;mag_b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mag_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;lag_samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mag_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;offset_ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lag_samples&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;200.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If the computed lag exceeds 2 seconds, Orvex raises a warning. A lag that large usually means one device started recording before the other, and you need to trim the non-overlapping segment.&lt;/p&gt;
&lt;h3&gt;
  
  
  Method 3: Manual
&lt;/h3&gt;

&lt;p&gt;Sometimes you just know. You clapped in front of all cameras at the start of the recording, and you can identify the spike in the accelerometer data manually. Orvex accepts manual offsets in milliseconds.&lt;/p&gt;


&lt;h2&gt;
  
  
  The 4GB Trap
&lt;/h2&gt;

&lt;p&gt;GoPro cameras split continuous recordings into chapter files at approximately 4GB boundaries. A 20-minute drive produces &lt;code&gt;GH010001.MP4&lt;/code&gt;, &lt;code&gt;GH020001.MP4&lt;/code&gt;, &lt;code&gt;GH030001.MP4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The GPMF telemetry stream is split across these chapters. If you process each chapter independently, you get timestamp discontinuities at every boundary. Your SLAM system sees a time jump of several hundred milliseconds and either loses tracking or inserts a phantom loop closure.&lt;/p&gt;

&lt;p&gt;Orvex detects chapter sequences automatically:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_GOPRO_CHAPTER_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;G[HX](\d{2})(\d{4})\.MP4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Files sharing the same last 4 digits (recording ID) are grouped and their GPMF streams concatenated with corrected timestamps. The user sees a single continuous recording. The chapter boundaries become invisible.&lt;/p&gt;

&lt;p&gt;This is not an edge case. Any GoPro recording longer than about 12 minutes at 4K hits the 4GB split. If your pipeline does not handle this, it does not work on real data.&lt;/p&gt;


&lt;h2&gt;
  
  
  Auto-Labeling for Roads That COCO Never Saw
&lt;/h2&gt;

&lt;p&gt;Stock YOLOv8 trained on MS COCO gives you 80 classes. Indian roads have objects that COCO never included:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;In COCO?&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;autorickshaw&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Most common vehicle in Kerala&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pothole_region&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Critical for path planning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;speed_bump&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Often unmarked, no paint, no signs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cow&lt;/td&gt;
&lt;td&gt;Yes (class 19)&lt;/td&gt;
&lt;td&gt;Regularly standing in traffic lanes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;person&lt;/td&gt;
&lt;td&gt;Yes (class 0)&lt;/td&gt;
&lt;td&gt;Far higher density than Western datasets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Orvex defines 13 rover-relevant classes and maps them to COCO class IDs where a mapping exists. For the three India-specific classes (autorickshaw, pothole, speed bump), a stock COCO model simply produces no detections. Those require a fine-tuned model.&lt;/p&gt;

&lt;p&gt;The practical workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;yolov8n.pt&lt;/code&gt; at 0.25 confidence on all frames (fast, catches roughly 70% of objects)&lt;/li&gt;
&lt;li&gt;Export to CVAT XML for human review&lt;/li&gt;
&lt;li&gt;Reviewers correct false positives and add the missing India-specific classes&lt;/li&gt;
&lt;li&gt;Use corrected annotations to fine-tune&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ym7jbhkgw7aqg8vcpq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ym7jbhkgw7aqg8vcpq4.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One reviewer can process 500 auto-labeled frames in the time it takes to manually label 50 from scratch. The model does not need to be perfect. It needs to be faster than starting from zero.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9lgjx1cwthl1niz4acs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9lgjx1cwthl1niz4acs.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Camera Calibration: Four Steps, Three Weeks of Debugging
&lt;/h2&gt;

&lt;p&gt;Camera-IMU calibration is a guided 4-step workflow in Orvex. Each step has requirements that are not obvious until you get them wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: IMU Static Noise Characterization.&lt;/strong&gt; Place the GoPro flat on a stable table and record for at least 4 hours. Allan deviation analysis extracts accelerometer noise density, accelerometer random walk, gyroscope noise density, and gyroscope random walk. A 30-minute recording gives you noise density but not random walk. VINS-Mono needs both parameters. There is no shortcut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Camera Intrinsics.&lt;/strong&gt; Standard chessboard calibration using OpenCV. The requirement that trips people up: you need a minimum of 15 detected poses, and they must cover the corners and edges of the frame, not just the center. Reprojection error must be below 0.5 pixels. Orvex rejects calibrations that fail this threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Camera-IMU Extrinsics.&lt;/strong&gt; This invokes OpenImuCameraCalibrator as a subprocess. It runs for 20 to 40 minutes. Orvex streams the subprocess stdout to the log panel in real time so you can watch it converge. The output is a 4x4 transformation matrix relating the camera frame to the IMU frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Validation.&lt;/strong&gt; Automated checks verify that reprojection error is below 0.5 pixels, translation magnitude is below 10 centimeters, and rotation magnitude is below 0.5 degrees.&lt;/p&gt;

&lt;p&gt;Each step saves results to a JSON file. If Step 2 results already exist when you start, Orvex skips directly to Step 3. You calibrate once per physical camera mount, not once per session.&lt;/p&gt;


&lt;h2&gt;
  
  
  ByteTrack in 839 Lines
&lt;/h2&gt;

&lt;p&gt;I implemented multi-object tracking from scratch rather than pulling in a tracking library. The reasons were practical:&lt;/p&gt;

&lt;p&gt;Every tracking library I evaluated either required detections in a specific format that did not match my pipeline, pulled in dozens of transitive dependencies, or could not export results in MOT Challenge CSV format for evaluation.&lt;/p&gt;

&lt;p&gt;The from-scratch implementation uses three components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;Kalman filter&lt;/strong&gt; with 8 states (position x/y, width/height, and their velocities) that predicts where each tracked object will appear in the next frame&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-stage IoU matching&lt;/strong&gt;: high-confidence new detections are matched to active tracks first, then low-confidence detections are matched to recently lost tracks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hungarian assignment&lt;/strong&gt; via &lt;code&gt;scipy.linear_sum_assignment&lt;/code&gt; for globally optimal matching&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is 839 lines of Python with no dependencies beyond NumPy and SciPy. It outputs track statistics, MOT Challenge formatted CSVs, and heatmap visualizations of track density.&lt;/p&gt;


&lt;h2&gt;
  
  
  28 Panels, Zero Frozen Frames
&lt;/h2&gt;

&lt;p&gt;Every long-running operation in the desktop app runs in a &lt;code&gt;QThread&lt;/code&gt; worker. The base pattern:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QThread&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pyqtSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# 0 to 100
&lt;/span&gt;    &lt;span class="n"&gt;status&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pyqtSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# "Processing frame 45/200..."
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pyqtSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# Final payload on success
&lt;/span&gt;    &lt;span class="n"&gt;error&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pyqtSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# Actionable error on failure
&lt;/span&gt;    &lt;span class="n"&gt;timing&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pyqtSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (elapsed_seconds, eta_seconds)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every worker inherits from &lt;code&gt;BaseWorker&lt;/code&gt;. The UI connects signals to update progress bars, status labels, and the collapsible log panel. The main thread never blocks. Model downloads, inference passes, subprocess calls to COLMAP or ORBSLAM3... all in workers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt55n3w6dx0wy6djaqio.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt55n3w6dx0wy6djaqio.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdcf19mv2qkjz9bkvtxel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdcf19mv2qkjz9bkvtxel.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sidebar is organized as a sequential workflow. Not by technical category, but by what a beginner does first, then second, then third:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00cpr96ule0pkgqmheqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00cpr96ule0pkgqmheqj.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SETUP &amp;amp; IMPORT       (1-4)    Create session, audit files, import data
PROCESS &amp;amp; EXTRACT    (5-9)    Extract frames, calibrate, view telemetry
ANALYZE &amp;amp; ANNOTATE   (10-19)  Auto-label, segment, depth, track, review
TRAIN &amp;amp; DEPLOY       (20-28)  Augment, train, export, version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A beginner follows top to bottom. An experienced user jumps directly to whatever they need.&lt;/p&gt;


&lt;h2&gt;
  
  
  Same Logic, Two Interfaces
&lt;/h2&gt;

&lt;p&gt;The FastAPI backend wraps the same &lt;code&gt;core/&lt;/code&gt; functions in 23 route modules. Long operations return a task ID immediately:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/api/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/autolabel/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"task_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The client opens a WebSocket to &lt;code&gt;/ws/tasks/abc-123&lt;/code&gt; and receives progress updates:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"running"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"progress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Annotating frames 41-48 / 200"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The React frontend has 25 pages that mirror every desktop panel. The web version enables collaborative annotation workflows where multiple people can work on the same project.&lt;/p&gt;

&lt;p&gt;No business logic was duplicated. The web routes call the same functions the desktop workers call.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with the data model.&lt;/strong&gt; I wrote &lt;code&gt;core/models.py&lt;/code&gt; before anything else. Every module knows exactly what shape of data it receives and what it returns. Pydantic v2 catches type mismatches at the function boundary, not three calls deep in a traceback. This was the single best decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resist premature abstraction.&lt;/strong&gt; The first version of the synchronizer tried to handle N arbitrary devices with a plugin system. The second version handles exactly three concrete methods with explicit code paths. Half the lines. Twice as debuggable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with real recordings, not synthetic data.&lt;/strong&gt; A CSV with perfectly uniform timestamps at exactly 200.000Hz will never expose the bug where real GoPro GPMF timestamps jitter by plus or minus 50 microseconds between samples. Every core module was tested against actual GoPro Hero 11 and Insta360 X4 files before being marked complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make errors actionable.&lt;/strong&gt; "Error occurred" is not an error message. "HyperSmooth is enabled in GH010001.MP4. Re-record with HyperSmooth OFF. Path: Settings &amp;gt; Stabilization &amp;gt; Off" is an error message. Every exception in Orvex tells the user what went wrong and what to do about it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core business logic modules&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop UI panels&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web API endpoints&lt;/td&gt;
&lt;td&gt;96+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React frontend pages&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of Python and JavaScript&lt;/td&gt;
&lt;td&gt;~60,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI models integrated&lt;/td&gt;
&lt;td&gt;7 (YOLOv8, SegFormer, DepthAnything, ByteTrack, COLMAP, ORBSLAM3, UFLD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export formats&lt;/td&gt;
&lt;td&gt;EuRoC, ROS bag, HDF5, CVAT XML, YOLO txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test files&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Sherin-SEF-AI/Orvex.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Orvex
python3.11 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; python &lt;span class="nt"&gt;-m&lt;/span&gt; desktop.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Requirements: Python 3.11+, FFmpeg, exiftool. GPU recommended for AI features but not required.&lt;/p&gt;

&lt;p&gt;Orvex is MIT licensed. If you are collecting multi-sensor data for robotics, autonomous navigation, road condition monitoring, or dataset research, particularly in environments where Western datasets and assumptions do not apply, take a look.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Sherin-SEF-AI" rel="noopener noreferrer"&gt;
        Sherin-SEF-AI
      &lt;/a&gt; / &lt;a href="https://github.com/Sherin-SEF-AI/Orvex" rel="noopener noreferrer"&gt;
        Orvex
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Data collection and processing  platform for autonomous vehicle development
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Orvex&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Production-grade data pipeline for autonomous rover dataset collection, processing, and perception.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Orvex handles the full lifecycle of multi-sensor data — from raw GoPro/Insta360/Android recordings through telemetry extraction, calibration, synchronization, dataset assembly, auto-labeling, training, 3D reconstruction, SLAM validation, and edge deployment. Built for Indian road conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Sherin Joseph Roy
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/Sherin-SEF-AI/Orvex.git" rel="noopener noreferrer"&gt;https://github.com/Sherin-SEF-AI/Orvex.git&lt;/a&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT
&lt;strong&gt;Python:&lt;/strong&gt; 3.11+&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demos&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;IMU Telemetry Graph&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Sherin-SEF-AI/Orvex/media/imu-graph.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FSherin-SEF-AI%2FOrvex%2Fmedia%2Fimu-graph.gif" alt="IMU Telemetry Graph"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Depth Estimation&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Sherin-SEF-AI/Orvex/media/depthestimation.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FSherin-SEF-AI%2FOrvex%2Fmedia%2Fdepthestimation.gif" alt="Depth Estimation"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Frame Extraction&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Sherin-SEF-AI/Orvex/media/frame-extraction.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2FSherin-SEF-AI%2FOrvex%2Fmedia%2Fframe-extraction.gif" alt="Frame Extraction"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of Contents&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#demos" rel="noopener noreferrer"&gt;Demos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#architecture" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#supported-devices" rel="noopener noreferrer"&gt;Supported Devices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#quick-start" rel="noopener noreferrer"&gt;Quick Start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#desktop-application" rel="noopener noreferrer"&gt;Desktop Application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#web-application" rel="noopener noreferrer"&gt;Web Application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#complete-feature-reference" rel="noopener noreferrer"&gt;Complete Feature Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#a-to-z-usage-guide" rel="noopener noreferrer"&gt;A-to-Z Usage Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#calibration-workflow" rel="noopener noreferrer"&gt;Calibration Workflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#ai-models" rel="noopener noreferrer"&gt;AI Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#export-formats" rel="noopener noreferrer"&gt;Export Formats&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#api-reference" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#testing" rel="noopener noreferrer"&gt;Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#project-structure" rel="noopener noreferrer"&gt;Project Structure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/Orvex#system-requirements" rel="noopener noreferrer"&gt;System Requirements&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Architecture&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Orvex is a dual-interface application: a PyQt6 desktop app and a FastAPI + React web app. Both share identical business logic through the &lt;code&gt;core/&lt;/code&gt; module — zero code duplication.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;                  +------------------+
                  |     core/        |   33 pure-Python modules
                  |  (business logic)|   No UI imports
                  +--------+---------+
                           |
              +------------+------------+
              |                         |
    +---------+----------+   +----------+---------+
    |   desktop/         |&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Sherin-SEF-AI/Orvex" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;&lt;em&gt;I am Sherin Joseph Roy. I build tools for autonomous systems operating on Indian roads: unstructured surfaces, mixed traffic, limited GPS coverage, and no lane markings. If you are working on similar problems, I would like to hear from you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>robotics</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Thought I Understood the Autonomous Vehicle Problem. Indian Roads Corrected Me.</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Sat, 21 Mar 2026 10:55:16 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/i-built-a-real-time-perception-system-and-tested-it-on-indian-roads-the-results-were-humbling-1an5</link>
      <guid>https://dev.to/sherinjosephroy/i-built-a-real-time-perception-system-and-tested-it-on-indian-roads-the-results-were-humbling-1an5</guid>
      <description>&lt;p&gt;Let me describe a specific moment from one of my test drives.&lt;/p&gt;

&lt;p&gt;The system had 33 simultaneous object IDs active in frame. Trucks, motorcycles, pedestrians, autos, cars. All moving, all tracked, all getting individual TTC calculations in real time. The collision warning was firing. Processing latency was sitting at 16ms. The road looked like controlled chaos from the outside.&lt;/p&gt;

&lt;p&gt;From inside the car, it just looked like a normal Bangalore afternoon.&lt;/p&gt;

&lt;p&gt;That was the moment I realized how broken most ADAS benchmarks are for this part of the world.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Orvex Actually Is
&lt;/h2&gt;

&lt;p&gt;Orvex is a multi-camera real-time perception system I built specifically for Indian urban driving conditions. Not adapted from something Western. Built from scratch with Indian roads as the primary design constraint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1ocgzad7d4f0peca4ap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1ocgzad7d4f0peca4ap.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system runs 4 simultaneous camera feeds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary forward-facing perception channel&lt;/li&gt;
&lt;li&gt;Dedicated pedestrian and license plate detection channel&lt;/li&gt;
&lt;li&gt;Optical flow motion analysis channel&lt;/li&gt;
&lt;li&gt;Wide-angle coverage feed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The perception dashboard tracks in real time: multi-class object detection across cars, trucks, buses, motorcycles, and pedestrians. Persistent ID-based tracking that survives occlusion. Per-object distance in meters and Time to Collision in seconds. Lane status and lateral offset. Risk classification from INFO to CRITICAL. Scene metadata including road type, traffic density, and visibility score.&lt;/p&gt;

&lt;p&gt;Processing latency runs between 15 and 17ms. Everything runs locally. No cloud dependency, no edge server offload. Just a laptop mounted in the car.&lt;/p&gt;

&lt;p&gt;Here is actual footage from real road tests:&lt;/p&gt;

&lt;h2&gt;
  
  
    &lt;iframe src="https://www.youtube.com/embed/xc3Cs2sERjI"&gt;
  &lt;/iframe&gt;

&lt;/h2&gt;

&lt;h2&gt;
  
  
  Three Things That Failed Immediately
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mokc4srdwvi7l72rmah.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8mokc4srdwvi7l72rmah.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I expected the system to struggle. I did not expect it to fail in the specific ways it did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The behavior scorer scored everyone zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built a 0 to 100 driver safety scoring module. Every single test run on Indian roads returned a score of 0 out of 100 with the label AGGRESSIVE. Not because the driving was reckless. Because the scorer was calibrated on assumptions that do not apply here.&lt;/p&gt;

&lt;p&gt;Hard braking, tight gap acceptance, rapid directional changes: these are aggression markers in Western driving norms. In Bangalore or Kochi, they are baseline competence. You cannot navigate a city intersection without doing all three simultaneously. The model was correct by its own logic. It was just the wrong logic entirely.&lt;/p&gt;

&lt;p&gt;I had to disable the scorer and rethink it from the ground up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTC becomes useless when trajectories are nonlinear.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Time to Collision assumes some continuity of movement. Object is at distance X, moving at velocity V, TTC is X divided by V. Clean math.&lt;/p&gt;

&lt;p&gt;Motorcycles in Indian traffic do not follow continuous trajectories. They operate on opportunistic pathing: constantly scanning for gaps, switching lanes without signaling, responding to micro-gaps in traffic that open and close in under a second. A 0.2s TTC reading is not an early warning. It is a post-hoc notification.&lt;/p&gt;

&lt;p&gt;This is a fundamental behavioral prediction problem, not a detection problem. Orvex catches the object. It cannot yet predict what the object is about to do. That gap is the real unsolved problem in urban AV for high-density mixed traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracker ID counts exposed the true scale of the problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By mid-session in the second road test, tracker IDs were in the 1100s. That means the system had individually identified and tracked over 1100 distinct objects across the session. In roughly 50 minutes of driving.&lt;/p&gt;

&lt;p&gt;Western AV test datasets do not have this density. nuScenes scenes average around 30 to 40 annotated objects. We were hitting 33 simultaneously active tracks in a single frame in a parking lot. The computational budget assumptions that underpin most published perception architectures are simply not calibrated for this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thing Nobody Talks About: Benchmark Hallucination
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf0g9gw0ozbcpjhkevg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf0g9gw0ozbcpjhkevg7.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is an uncomfortable truth about AV development.&lt;/p&gt;

&lt;p&gt;A model that hits state of the art on nuScenes, Waymo Open, or KITTI is not a model that works. It is a model that works on those datasets. That is not the same thing.&lt;/p&gt;

&lt;p&gt;The entire industry optimizes for benchmark performance because that is how research gets published, how companies get funded, and how progress gets measured. Benchmarks are a useful proxy. In markets where the real-world distribution diverges heavily from benchmark data, that proxy fails completely.&lt;/p&gt;

&lt;p&gt;Orvex performs worse than several open-source ADAS baselines on standard benchmarks. The FPS fluctuates under density load. The tracker gets stressed at peak object count. The behavior scorer had to be scrapped.&lt;/p&gt;

&lt;p&gt;But it runs on real Indian roads. It catches real collision threats on streets that do not exist in any benchmark dataset. It handles traffic compositions that academic datasets have never seen.&lt;/p&gt;

&lt;p&gt;That gap between benchmark performance and deployment performance is the central problem of applied AV work. The teams that understand it build systems that actually work. The teams that do not build great benchmark numbers and then wonder why their system freezes at a Bangalore intersection.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Optical Flow Channel Was an Accident That Became Essential
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F79xdknep7t3g5tnt26b2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F79xdknep7t3g5tnt26b2.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
The motion analysis feed running optical flow was added almost as an afterthought. It turned out to be one of the most operationally useful parts of the entire system.&lt;/p&gt;

&lt;p&gt;In dense traffic, optical flow captures motion vectors for everything in the scene, not just objects that have cleared the detection confidence threshold. Partially occluded vehicles. Objects near frame boundaries. Fast-moving targets that blur enough to drop below the detector's confidence cutoff.&lt;/p&gt;

&lt;p&gt;In practice it functions as a soft pre-detection layer. The primary pipeline gives you identity, class, and distance. The optical flow gives you motion context for objects that are not yet fully resolved. In the chaos frames, 30-plus active objects, overlapping bounding boxes, collision warnings firing, the optical flow channel is the thing that keeps the system from being completely blind to unclassified threats.&lt;/p&gt;

&lt;p&gt;I did not design it that way. The road taught me that was necessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next and Why It Changes Everything
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffaj4t4qngeleuxihrkqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffaj4t4qngeleuxihrkqj.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
Here is something I have not talked about publicly until now.&lt;/p&gt;

&lt;p&gt;I have been in conversations with a friend based in Bangalore. Through his connections, we have access to a 200-vehicle electric fleet in active service across the city. Real routes, real operational data, real urban driving at scale, every single day.&lt;/p&gt;

&lt;p&gt;We are planning to build a full autonomous vehicle system from scratch together.&lt;/p&gt;

&lt;p&gt;Not retrofit. Not integrate a third-party stack. From scratch. Data collection infrastructure, annotation pipelines, perception model training, sensor integration, edge deployment. All of it.&lt;/p&gt;

&lt;p&gt;The fleet is the asset that changes the equation. Most AV startups spend years and tens of millions building access to what we already have: a real operational environment with 200 vehicles worth of driving data across one of the densest urban road networks in the world. Every route, every intersection, every edge case: ours to instrument and learn from.&lt;/p&gt;

&lt;p&gt;The EV platform matters for a less obvious reason. Clean electrical architecture, no combustion powertrain complexity, standardized actuation interfaces. Integrating drive-by-wire controls with a custom AV stack is a significantly cleaner problem on an EV than retrofitting a conventional vehicle. The integration surface is known and controllable.&lt;/p&gt;

&lt;p&gt;The plan in three phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1.&lt;/strong&gt; Data infrastructure. Instrument the fleet, build the collection and annotation pipeline, start generating a proprietary Indian urban driving dataset that does not exist anywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2.&lt;/strong&gt; Rebuild the Orvex perception stack properly. Multi-camera calibration done right. BEV fusion. Behavioral prediction models trained on local data, not transferred from Waymo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3.&lt;/strong&gt; Vehicle integration. Closed-course autonomy first, then expanding the operational domain incrementally with real data informing every decision.&lt;/p&gt;

&lt;p&gt;This is not a short timeline. But the foundation is real. The fleet is real. The perception work from Orvex is real. The distance between where we are and a working prototype is smaller than it looks from outside.&lt;/p&gt;




&lt;h2&gt;
  
  
  The One Thing I Would Tell Anyone Starting in AV
&lt;/h2&gt;

&lt;p&gt;Test on your actual deployment environment from day one. Not when the system is "ready." Day one.&lt;/p&gt;

&lt;p&gt;The failures you discover in your real environment are not setbacks. They are the curriculum. Every broken assumption, the behavior scorer, the TTC model, the density ceiling, became a design requirement that made the system more honest about what it actually needs to do.&lt;/p&gt;

&lt;p&gt;Simulation matters. I have built months of Indian road scenarios in CARLA and the synthetic data work has real value. But simulation is a tool for exploring the design space. It is not a substitute for the road.&lt;/p&gt;

&lt;p&gt;The road has opinions. You should hear them early.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Orvex is part of the broader perception and safety work at PerceptionAV. If you are building in AV, edge perception, or safety intelligence for high-density urban environments, especially outside Western road contexts, I would like to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>autonomouscar</category>
      <category>ai</category>
      <category>computervision</category>
      <category>deeplearning</category>
    </item>
    <item>
      <title>Building a Voice-Controlled Browser Agent with Three Gemini Models</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Mon, 16 Mar 2026 04:38:08 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/building-a-voice-controlled-browser-agent-with-three-gemini-models-3j3h</link>
      <guid>https://dev.to/sherinjosephroy/building-a-voice-controlled-browser-agent-with-three-gemini-models-3j3h</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was written as part of my submission to the Gemini Live Agent Challenge hackathon on Devpost. #GeminiLiveAgentChallenge&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem That Started This
&lt;/h2&gt;

&lt;p&gt;My grandmother owns a smartphone. She has a broadband connection. She cannot book a train ticket online.&lt;/p&gt;

&lt;p&gt;This is not a technology access problem. She has the hardware and the connectivity. What she cannot do is navigate a website. She does not understand dropdown menus. She cannot read small text on form labels. She does not know what "Enter OTP" means. When something goes wrong, she sees an error message she cannot parse and hands the phone to someone younger.&lt;/p&gt;

&lt;p&gt;She is not alone. According to government data, 85% of India's elderly population cannot independently use digital services. Over 900 million people globally are in the same situation. India moved pensions online, digitized Aadhaar, made train booking web-only, and shifted bill payments to portals. The interfaces got built. The people who need these services the most got left behind.&lt;/p&gt;

&lt;p&gt;I wanted to build something that removes the interface from the equation entirely. Not a simpler interface. Not a tutorial. A system where the user speaks what they need and the computer handles the rest.&lt;/p&gt;

&lt;p&gt;That project became SAHAY.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SAHAY Does
&lt;/h2&gt;

&lt;p&gt;SAHAY listens to the user in their language. Hindi, Malayalam, Tamil, Telugu, English, or any of 24 supported languages. It opens a real Chromium browser, finds the correct website, navigates through the pages, fills forms, clicks buttons, and speaks the results back.&lt;/p&gt;

&lt;p&gt;The user says "Amazon par earbuds dikhao 1000 rupaye se kam" and SAHAY opens Amazon with a price-filtered search, reads the results, and reports the top options with prices. In Hindi. Because that is the language the user spoke.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcs89skrgvx8jggicxi44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcs89skrgvx8jggicxi44.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7kzjezjaadccvaiugu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx7kzjezjaadccvaiugu6.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The user says "Download my Aadhaar card" and SAHAY navigates to the UIDAI portal, asks for the Aadhaar number, repeats it back digit by digit for confirmation, enters it, and proceeds through the OTP flow.&lt;/p&gt;

&lt;p&gt;Before any login, payment, or form submission, SAHAY stops and asks for permission. The user confirms by voice or by clicking a button. For passwords and CAPTCHAs, the user can take direct control by clicking on the browser screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Agent Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4tcqx466xn5kx5wazt7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4tcqx466xn5kx5wazt7w.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SAHAY runs three separate Gemini agents that coordinate to complete each task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent 1: The Planner
&lt;/h3&gt;

&lt;p&gt;The Planner runs on Gemini 2.5 Flash with Google Search grounding through the GenAI SDK. When the user describes what they want, the Planner searches the internet in real time to find the correct website. It does not use hardcoded URLs. It does not rely on a static list of known portals. It searches, reads the results, and identifies the right destination.&lt;/p&gt;

&lt;p&gt;This matters because websites change. The UIDAI download page moved URLs twice in the past year. Government portals restructure their navigation without warning. A hardcoded URL from last month might 404 today. The Planner always researches the current state before creating a plan.&lt;/p&gt;

&lt;p&gt;After finding the target, the Planner creates a structured execution plan with step-by-step instructions, visual descriptions of what to look for on each page, and flags for which steps involve sensitive data.&lt;/p&gt;

&lt;p&gt;The Planner is a separate ADK agent because Google Search grounding cannot be combined with other tools in the same agent. This constraint shaped the architecture. It turned out to be the right design anyway because it cleanly separates research from execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.genai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vertexai&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GenerateContentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google_search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GoogleSearch&lt;/span&gt;&lt;span class="p"&gt;())],&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Agent 2: The Browser
&lt;/h3&gt;

&lt;p&gt;The Browser Agent runs on Gemini's Computer Use model (&lt;code&gt;gemini-2.5-computer-use-preview-10-2025&lt;/code&gt;). It receives the plan from the Planner and executes it step by step.&lt;/p&gt;

&lt;p&gt;The execution loop works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a screenshot of the current browser state via Playwright&lt;/li&gt;
&lt;li&gt;Send the screenshot to the Computer Use model along with the current plan step&lt;/li&gt;
&lt;li&gt;The model analyzes the screenshot visually and returns coordinates for where to click or what to type&lt;/li&gt;
&lt;li&gt;Playwright executes the action against the real browser&lt;/li&gt;
&lt;li&gt;Take a new screenshot&lt;/li&gt;
&lt;li&gt;Repeat until the task is complete&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Browser Agent does not read HTML. It does not use CSS selectors for understanding page layout. It does not call any website APIs. It looks at the screenshot the same way a person would look at a screen and decides what to do next. This means it works on any website without site-specific configuration.&lt;/p&gt;

&lt;p&gt;The Computer Use model outputs normalized coordinates (0 to 999). SAHAY converts these to actual pixel positions on the 1440x900 viewport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;actual_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized_x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;actual_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized_y&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual_y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Browser Agent is wrapped in ADK's ComputerUseToolset, which manages the screenshot-action loop and handles the coordinate conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent 3: The Voice
&lt;/h3&gt;

&lt;p&gt;The Voice Agent runs on Gemini 2.5 Flash Native Audio through the Live API. It handles all communication with the user through bidirectional audio streaming.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/lu02QIqr-aM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;The user's microphone audio is captured by the browser using AudioWorklet, converted to PCM 16-bit 16kHz mono, and streamed over a WebSocket to the FastAPI backend. The backend pipes this audio into the Live API session. When Gemini responds with audio, it streams back through the same WebSocket to the browser for playback.&lt;/p&gt;

&lt;p&gt;The Voice Agent automatically detects the user's language and responds in the same language. If the user starts in English and switches to Hindi mid-sentence, the response comes back in Hindi. No language selection menu. No configuration.&lt;/p&gt;

&lt;p&gt;For sensitive inputs like Aadhaar numbers and phone numbers, the Voice Agent repeats back what it heard and waits for explicit confirmation before passing the data to the Browser Agent. This prevents the misheard-digit problem that would otherwise cause the entire flow to fail silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;voice_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sahay_voice_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-live-2.5-flash-native-audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VOICE_INSTRUCTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;plan_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;browser_action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Google Cloud Services
&lt;/h2&gt;

&lt;p&gt;SAHAY uses three Google Cloud services in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertex AI&lt;/strong&gt; hosts all three Gemini model endpoints. The Voice Agent connects to the Live API through Vertex AI. The Planner Agent calls Gemini Flash with Google Search grounding through Vertex AI. The Computer Use model calls go through the Gemini API directly using an API key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Firestore&lt;/strong&gt; stores task logs, session state, and workflow recordings. Every task gets a document with the task description, each step taken, screenshots at key moments, the final outcome, and timestamps. This serves as an audit trail and also powers the workflow replay feature where repeated tasks execute faster by following a previously recorded path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Run&lt;/strong&gt; hosts the containerized application. The Dockerfile installs Playwright and Chromium inside the container, so the browser automation works in the cloud environment. The deploy script and Terraform configuration automate the entire deployment process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Made This Hard
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google CAPTCHA.&lt;/strong&gt; The first version of SAHAY used headless Chromium to search Google directly. After a few searches, Google would show a CAPTCHA and the agent would get stuck on the verification page, clicking randomly and wasting steps. Moving search to the Planner Agent via Google Search grounding API eliminated this problem completely. The browser never touches Google Search anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bot detection.&lt;/strong&gt; IRCTC, MakeMyTrip, and several banking portals detect Playwright and refuse to load the page. Stealth flags and spoofed user agents helped with some sites but not all. The solution was building a smart browser selection system. SAHAY analyzes the target URL and task description and decides whether to use headless Chromium (fast, works for most sites) or a headed browser window (slower, but bypasses bot detection on protected sites). This decision happens automatically per task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Computer Use model finishing early.&lt;/strong&gt; The ADK runner's &lt;code&gt;run_async()&lt;/code&gt; generator exits when the model returns a text response without a function call. The model would sometimes describe what it sees on screen instead of clicking on it, which would end the task prematurely after two or three steps. The fix was a continuation loop that detects when the model exits without reporting completion, re-prompts it with "You have not finished the task. Take an action.", and resumes execution. This loop runs up to three times before giving up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice number accuracy.&lt;/strong&gt; The Live API voice model occasionally mishears digits. "9895" becomes "9985". For an Aadhaar number, a single wrong digit means the download fails and the user does not understand why. The repeat-back-and-confirm pattern solved this. It adds a few seconds to each interaction but prevents silent failures that would destroy user trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent framework&lt;/td&gt;
&lt;td&gt;Google ADK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice model&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash Native Audio (Live API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser model&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Computer Use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planner model&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash + Google Search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser automation&lt;/td&gt;
&lt;td&gt;Playwright (Chromium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI + WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Vanilla JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Google Cloud Firestore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Google Cloud Run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;The visual-only approach is the right architectural choice for universality but the wrong choice for speed. Every action requires a full screenshot capture, a round trip to the Gemini API, and coordinate parsing. A hybrid approach that uses visual understanding for navigation decisions but DOM selectors for precise form filling would be significantly faster.&lt;/p&gt;

&lt;p&gt;The three-agent architecture introduces latency at the boundaries. The Planner takes 5 to 15 seconds to research and produce a plan. During this time, the browser sits idle and the user waits in silence. Pre-fetching the target URL while the Planner is still working would cut perceived latency in half.&lt;/p&gt;

&lt;p&gt;The continuation loop is a workaround for a fundamental issue with how the Computer Use model signals task completion. A better approach would be fine-tuning the model prompt so it always ends with either a function call or an explicit completion message, never a bare text description.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;The full source code is available at:&lt;br&gt;
&lt;a href="https://github.com/Sherin-SEF-AI/Sahay-Voice-First-Digital-Navigator" rel="noopener noreferrer"&gt;github.com/Sherin-SEF-AI/Sahay-Voice-First-Digital-Navigator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built by &lt;a href="https://github.com/Sherin-SEF-AI" rel="noopener noreferrer"&gt;Sherin Joseph Roy&lt;/a&gt;, Head of Products at DeepMost AI.&lt;/p&gt;

&lt;p&gt;This project was built for the &lt;a href="https://devpost.com/" rel="noopener noreferrer"&gt;Gemini Live Agent Challenge&lt;/a&gt; hackathon, UI Navigator track.&lt;/p&gt;

&lt;h1&gt;
  
  
  GeminiLiveAgentChallenge
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>gemini</category>
      <category>google</category>
      <category>geminiliveagentchallenge</category>
    </item>
    <item>
      <title>Why my AI crash reconstruction MVP isn't ready for production (and why I'm rebuilding it)</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Sun, 08 Mar 2026 15:24:59 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/why-my-ai-crash-reconstruction-mvp-isnt-ready-for-production-and-why-im-rebuilding-it-5e0k</link>
      <guid>https://dev.to/sherinjosephroy/why-my-ai-crash-reconstruction-mvp-isnt-ready-for-production-and-why-im-rebuilding-it-5e0k</guid>
      <description>&lt;p&gt;We all love the demo phase. You hook up an API, the UI updates, and for a second, the software feels like absolute magic. &lt;/p&gt;

&lt;p&gt;I recently hit that phase with a project called &lt;strong&gt;Incident Lens AI&lt;/strong&gt;. It is a forensic video analysis suite I have been building to automate crash reconstruction for insurance and legal teams. The goal is to take raw dashcam or CCTV footage and turn it into a defensible liability report.&lt;/p&gt;

&lt;p&gt;To validate the idea quickly, I built a frontend-first proof of concept using React, Vite, and the Gemini 3 Pro SDK. I piped the video frames and audio directly from the browser to the LLM and asked it to act as a forensic expert.&lt;/p&gt;

&lt;p&gt;And honestly, it makes for an incredible demo. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/QUVeahUrCTg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You drop a video in, and the system instantly starts reasoning about the crash. It generates liability timelines, cites traffic laws, and outputs structured JSON that drives interactive charts on the dashboard. Building it this way let me iterate on the UI and prove the multimodal concept without writing a single line of backend infrastructure.&lt;/p&gt;

&lt;p&gt;But as I transition from pitching a vision to building the actual product, I have to face a hard engineering truth. A cool demo is not a defensible legal tool. &lt;/p&gt;

&lt;p&gt;The architecture I used to validate the idea is the exact architecture I now have to dismantle. Here is why.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Security Issue
&lt;/h3&gt;

&lt;p&gt;First, there is the obvious security issue. Hitting a public LLM API directly from a client application is a complete non-starter when you are dealing with sensitive enterprise data and personally identifiable information. No insurance pilot program will ever approve that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hallucination Trap
&lt;/h3&gt;

&lt;p&gt;But the much bigger issue is the hallucination trap. &lt;/p&gt;

&lt;p&gt;My current documentation states that the AI calculates vehicle speed using photogrammetry and motion mechanics. The reality is that LLMs are not physics engines. If you ask an LLM to estimate the speed of a car from a 2D video without precise camera calibration, it is just guessing. It might sound incredibly confident, but in a courtroom setting, an "AI-estimated" speed calculation would be destroyed by opposing counsel in seconds. &lt;/p&gt;

&lt;p&gt;You cannot build a forensic tool on prompt engineering alone. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Hybrid Architecture Pivot
&lt;/h3&gt;

&lt;p&gt;So, I am moving away from the pure LLM wrapper approach and building a hybrid architecture. &lt;/p&gt;

&lt;p&gt;I am shifting the heavy lifting to a secure Python backend. The new pipeline will rely on deterministic computer vision models like OpenCV to extract hard, mathematical data from the footage, such as pixel velocities and exact collision coordinates. Once I have those concrete numbers, I will feed them into established physics formulas to get the actual speed and force. &lt;/p&gt;

&lt;p&gt;Only then does Gemini re-enter the picture. I will pass those verified, deterministic numbers to the LLM so it can do what it actually excels at: cross-referencing case law, synthesizing the timeline, and writing the final human-readable dossier. &lt;/p&gt;

&lt;p&gt;Building in the public safety and forensics space requires an incredibly high bar for trust and accuracy. It is easy to get caught up in the magic of what generative AI can do out of the box. &lt;/p&gt;

&lt;p&gt;I am leaving the current repository up as a proof of concept because it perfectly illustrates the vision of where multimodal AI is heading. But the real engineering work of making it secure, deterministic, and legally defensible starts now. &lt;/p&gt;

&lt;p&gt;If anyone else is navigating the jump from AI prototype to production in a zero-trust industry, I would love to hear how you are handling it. &lt;/p&gt;

&lt;p&gt;You can check out the frontend prototype here: &lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Sherin-SEF-AI" rel="noopener noreferrer"&gt;
        Sherin-SEF-AI
      &lt;/a&gt; / &lt;a href="https://github.com/Sherin-SEF-AI/Incident-Lens-AI" rel="noopener noreferrer"&gt;
        Incident-Lens-AI
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Incident Lens AI is a professional-grade forensic video analysis suite. It transforms raw crash footage into a defensible legal case file in seconds.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Incident Lens AI 🔍⚖️&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Professional Forensic Video Analysis &amp;amp; Accident Reconstruction Platform&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/QUVeahUrCTg?si=0KiQewMFjjllYqv4" rel="nofollow noopener noreferrer"&gt;https://youtu.be/QUVeahUrCTg?si=0KiQewMFjjllYqv4&lt;/a&gt;&lt;/p&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483923-13a1203b-a241-407d-b3a1-ff6dd1a076e6.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTIzLTEzYTEyMDNiLWEyNDEtNDA3ZC1iM2ExLWZmNmRkMWEwNzZlNi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01Mjg5MDEzZDg4ZGNkNWRhZWJiZDdkNGI0YzM1YzBlNmI5YjVkNzBkY2FhZmRiZjNjYTliYzA3YWM2MGEzOTlkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.p7kkj1RS5cJDEUxXsldIdmR9Zf3VBczlkgK8uzb44CM"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-23" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483923-13a1203b-a241-407d-b3a1-ff6dd1a076e6.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTIzLTEzYTEyMDNiLWEyNDEtNDA3ZC1iM2ExLWZmNmRkMWEwNzZlNi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01Mjg5MDEzZDg4ZGNkNWRhZWJiZDdkNGI0YzM1YzBlNmI5YjVkNzBkY2FhZmRiZjNjYTliYzA3YWM2MGEzOTlkJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.p7kkj1RS5cJDEUxXsldIdmR9Zf3VBczlkgK8uzb44CM"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483922-ab0c59ac-782b-4b9b-9178-064198093b14.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTIyLWFiMGM1OWFjLTc4MmItNGI5Yi05MTc4LTA2NDE5ODA5M2IxNC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kZWJkMjA3MGY0MDBlOTgxNjJiMDE5ODgwZTQyNjU5MDA4MGQ2MjAwMzQ5MTVjMWFhNGQ0MTA4ODg5NjlkOTZlJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.49cQCoWQvXU9B2MATsNMD13Vx9HM-wlvlurKfJpst5Q"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-25" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483922-ab0c59ac-782b-4b9b-9178-064198093b14.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTIyLWFiMGM1OWFjLTc4MmItNGI5Yi05MTc4LTA2NDE5ODA5M2IxNC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kZWJkMjA3MGY0MDBlOTgxNjJiMDE5ODgwZTQyNjU5MDA4MGQ2MjAwMzQ5MTVjMWFhNGQ0MTA4ODg5NjlkOTZlJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.49cQCoWQvXU9B2MATsNMD13Vx9HM-wlvlurKfJpst5Q"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483919-bb72ab02-448a-4d92-8acb-1515e2d18f02.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE5LWJiNzJhYjAyLTQ0OGEtNGQ5Mi04YWNiLTE1MTVlMmQxOGYwMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03MTU0OTRkOGFkOTkxNGUwYTJhMDQ1ZThkZGIyZTQ2YjEyODg4YWMyMTAyMDgxZTM2NDlkZWI1NzcxOGNkMGU5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.ixtSGPSEvW1-QVW5KgjmE4eP4phtvGrVVeeG64gOCz8"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-28" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483919-bb72ab02-448a-4d92-8acb-1515e2d18f02.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE5LWJiNzJhYjAyLTQ0OGEtNGQ5Mi04YWNiLTE1MTVlMmQxOGYwMi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03MTU0OTRkOGFkOTkxNGUwYTJhMDQ1ZThkZGIyZTQ2YjEyODg4YWMyMTAyMDgxZTM2NDlkZWI1NzcxOGNkMGU5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.ixtSGPSEvW1-QVW5KgjmE4eP4phtvGrVVeeG64gOCz8"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483918-aac5cc23-c691-4050-93d9-6645ea4555fc.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE4LWFhYzVjYzIzLWM2OTEtNDA1MC05M2Q5LTY2NDVlYTQ1NTVmYy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hNTk2ZjRiNTU5Nzc3OWYxMmI0OTNlNmZjZjI3YzcxYTQxZjllN2MzODAwMTc3OGYzNzcxMjg2OWM4MDM5YzQ5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.DW6L3NKiyWRHyMOl47WNVjP81Wkgz1Rgf722v9QSCfY"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-30" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483918-aac5cc23-c691-4050-93d9-6645ea4555fc.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE4LWFhYzVjYzIzLWM2OTEtNDA1MC05M2Q5LTY2NDVlYTQ1NTVmYy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hNTk2ZjRiNTU5Nzc3OWYxMmI0OTNlNmZjZjI3YzcxYTQxZjllN2MzODAwMTc3OGYzNzcxMjg2OWM4MDM5YzQ5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.DW6L3NKiyWRHyMOl47WNVjP81Wkgz1Rgf722v9QSCfY"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483917-f1e5607c-848d-4cbb-b7ce-4fcfc2892ccb.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE3LWYxZTU2MDdjLTg0OGQtNGNiYi1iN2NlLTRmY2ZjMjg5MmNjYi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0wMzkwYmM3YWE1Y2VhOWJjYjE4YmM0MDc0NzM0MzM0YzU1MzhlY2FjMzRkZmU3ZTc3ZjAxODFhM2Y1YjY4ZGQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.zIfr5lYtatw2bbL-AW0BAhaLqrt_KWzL-DmiwH_MCI4"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-34" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483917-f1e5607c-848d-4cbb-b7ce-4fcfc2892ccb.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE3LWYxZTU2MDdjLTg0OGQtNGNiYi1iN2NlLTRmY2ZjMjg5MmNjYi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0wMzkwYmM3YWE1Y2VhOWJjYjE4YmM0MDc0NzM0MzM0YzU1MzhlY2FjMzRkZmU3ZTc3ZjAxODFhM2Y1YjY4ZGQyJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.zIfr5lYtatw2bbL-AW0BAhaLqrt_KWzL-DmiwH_MCI4"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483914-5131b9a9-cd51-44e0-b082-6789396da3fa.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE0LTUxMzFiOWE5LWNkNTEtNDRlMC1iMDgyLTY3ODkzOTZkYTNmYS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03YzQ4NjAxODY4NmQxZDE2ZGVkN2QwZDJlMjU0NGJkZDkyM2YzNTNlODE3NDIwNjVmZmFiZjliODBiNGM2NDFhJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.q80AwN3kjLOt8E0IJDTUKBNoZuNXlMbaZ7SJdIKRtRY"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-36" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483914-5131b9a9-cd51-44e0-b082-6789396da3fa.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTE0LTUxMzFiOWE5LWNkNTEtNDRlMC1iMDgyLTY3ODkzOTZkYTNmYS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03YzQ4NjAxODY4NmQxZDE2ZGVkN2QwZDJlMjU0NGJkZDkyM2YzNTNlODE3NDIwNjVmZmFiZjliODBiNGM2NDFhJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.q80AwN3kjLOt8E0IJDTUKBNoZuNXlMbaZ7SJdIKRtRY"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483912-1a825449-37da-44ad-835c-eaefa3a5312d.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTEyLTFhODI1NDQ5LTM3ZGEtNDRhZC04MzVjLWVhZWZhM2E1MzEyZC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lNTA4NDEzYWRkNGYyZDU4Mjg2N2IzYmRkOTBmNTgwZmIyMzhkNzE5YTI3MjAxMmU0ZGU3MjZjNDE1OWE0ODg1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Il8J39QTNTaT-XINQLafEUeb8YxAfLhHL7xBZLcWLns"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-41" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483912-1a825449-37da-44ad-835c-eaefa3a5312d.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTEyLTFhODI1NDQ5LTM3ZGEtNDRhZC04MzVjLWVhZWZhM2E1MzEyZC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1lNTA4NDEzYWRkNGYyZDU4Mjg2N2IzYmRkOTBmNTgwZmIyMzhkNzE5YTI3MjAxMmU0ZGU3MjZjNDE1OWE0ODg1JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Il8J39QTNTaT-XINQLafEUeb8YxAfLhHL7xBZLcWLns"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483911-52e1e3ed-77da-4d56-bc6f-1b1684040339.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTExLTUyZTFlM2VkLTc3ZGEtNGQ1Ni1iYzZmLTFiMTY4NDA0MDMzOS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03YTJiMmNiNzI4MTNjM2RhYzhjMDVhMTA3MDAyNTEwZjAzNjk0MGVlMTgwZmI5OWE3NTVjMzBkNDUyZTI3MmRjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.kfHX7qRMM5kr-hgz-QT7XEvA5VteJa39xFqciMFbVP0"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-45" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483911-52e1e3ed-77da-4d56-bc6f-1b1684040339.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTExLTUyZTFlM2VkLTc3ZGEtNGQ1Ni1iYzZmLTFiMTY4NDA0MDMzOS5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03YTJiMmNiNzI4MTNjM2RhYzhjMDVhMTA3MDAyNTEwZjAzNjk0MGVlMTgwZmI5OWE3NTVjMzBkNDUyZTI3MmRjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.kfHX7qRMM5kr-hgz-QT7XEvA5VteJa39xFqciMFbVP0"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483907-d227a2a0-f47e-409b-8588-042fce932518.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTA3LWQyMjdhMmEwLWY0N2UtNDA5Yi04NTg4LTA0MmZjZTkzMjUxOC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04MWI0OWU0NGYxMTFjMzU5OTUwNmE4MDU2ZGI4MjYzOGU2MDlkZWZlMGU5ZDFmMTA2N2M3YTdlMTA1NDIzYTUxJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.09q5k4yOSqOkE_kaY_Ju7Yn1XA2vqZyz04fvIE6tS_8"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-48" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483907-d227a2a0-f47e-409b-8588-042fce932518.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTA3LWQyMjdhMmEwLWY0N2UtNDA5Yi04NTg4LTA0MmZjZTkzMjUxOC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT04MWI0OWU0NGYxMTFjMzU5OTUwNmE4MDU2ZGI4MjYzOGU2MDlkZWZlMGU5ZDFmMTA2N2M3YTdlMTA1NDIzYTUxJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.09q5k4yOSqOkE_kaY_Ju7Yn1XA2vqZyz04fvIE6tS_8"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483905-cd4722d6-31d0-4648-837a-88762b317743.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTA1LWNkNDcyMmQ2LTMxZDAtNDY0OC04MzdhLTg4NzYyYjMxNzc0My5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kOWJmZDBhMjdkZjkxOTI1MWVmYWY2Mzc2ZGZkYTFiOWViOWY4ZWNhMTFmNzQ4NGIwOGYwYjc5NTAzYjA1NzUwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.MNarYbobKeB6rMxyV0mJaRiOURUmQRKRMfI7qt6Ut7Y"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-38-54" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483905-cd4722d6-31d0-4648-837a-88762b317743.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTA1LWNkNDcyMmQ2LTMxZDAtNDY0OC04MzdhLTg4NzYyYjMxNzc0My5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kOWJmZDBhMjdkZjkxOTI1MWVmYWY2Mzc2ZGZkYTFiOWViOWY4ZWNhMTFmNzQ4NGIwOGYwYjc5NTAzYjA1NzUwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.MNarYbobKeB6rMxyV0mJaRiOURUmQRKRMfI7qt6Ut7Y"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483902-beeaf5b2-520e-4612-beae-1321630c67f6.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTAyLWJlZWFmNWIyLTUyMGUtNDYxMi1iZWFlLTEzMjE2MzBjNjdmNi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01MDlmNDI1YTZmZTI0NTU3NzA1MWRiOWU2NTdhN2UyZTkzYmVmNDZjZmEyMWIwN2I4NzIzYmE2MWZlZDVhZDMzJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.7u_SppHFXO5JF1Ume1Q82XcuMzKk91R36muF-_tldsM"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-39-00" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483902-beeaf5b2-520e-4612-beae-1321630c67f6.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTAyLWJlZWFmNWIyLTUyMGUtNDYxMi1iZWFlLTEzMjE2MzBjNjdmNi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01MDlmNDI1YTZmZTI0NTU3NzA1MWRiOWU2NTdhN2UyZTkzYmVmNDZjZmEyMWIwN2I4NzIzYmE2MWZlZDVhZDMzJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.7u_SppHFXO5JF1Ume1Q82XcuMzKk91R36muF-_tldsM"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/530483901-3b836eae-5e6e-4384-be49-b8a8d786f36f.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTAxLTNiODM2ZWFlLTVlNmUtNDM4NC1iZTQ5LWI4YThkNzg2ZjM2Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hYTRiZDczM2EzZDc4M2U3N2I5ZjdmMWU0ODlmNTFkZDFhNjk5MjYwNWRlMzY3YWZlY2NkODVkM2Y5OWZhMTZjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.MzoWk5AsZhHcp7so66Fmc7Czb5VcP09LoKlY0ef-4MY"&gt;&lt;img width="1920" height="1080" alt="Screenshot from 2025-12-27 19-39-06" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F530483901-3b836eae-5e6e-4384-be49-b8a8d786f36f.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQzNTk1NzMsIm5iZiI6MTc3NDM1OTI3MywicGF0aCI6Ii8xNjk3MDAxMTkvNTMwNDgzOTAxLTNiODM2ZWFlLTVlNmUtNDM4NC1iZTQ5LWI4YThkNzg2ZjM2Zi5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyNFQxMzM0MzNaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hYTRiZDczM2EzZDc4M2U3N2I5ZjdmMWU0ODlmNTFkZDFhNjk5MjYwNWRlMzY3YWZlY2NkODVkM2Y5OWZhMTZjJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.MzoWk5AsZhHcp7so66Fmc7Czb5VcP09LoKlY0ef-4MY"&gt;&lt;/a&gt;
&lt;p&gt;Incident Lens AI is a production-grade application designed for insurance carriers, legal defense teams, and fleet safety managers. It leverages the multimodal capabilities of &lt;strong&gt;Google Gemini 3 Pro&lt;/strong&gt; to transform unstructured video evidence (dashcam, CCTV, bodycam) into legally admissible forensic reconstructions.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Unlike standard video players, Incident Lens AI "reasons" about the footage in real-time, calculating vehicle speeds, inferring traffic signal states from indirect visual cues, and citing specific legal statutes for fault determination.&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Key Features&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;🧠 Autonomous Reconstruction&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Physics Engine&lt;/strong&gt;: Automatically calculates vehicle speed ($v=d/t$) using photogrammetry and motion blur mechanics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal Inference&lt;/strong&gt;: Deduce the state of occluded traffic lights by analyzing cross-traffic flow and pedestrian behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debris Field Analysis&lt;/strong&gt;: Reverse-engineer impact vectors based on glass shard trajectories and fluid spray patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;⚖️ Legal Admissibility&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search Grounding&lt;/strong&gt;: Uses the Gemini…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Sherin-SEF-AI/Incident-Lens-AI" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>showdev</category>
      <category>softwareengineering</category>
      <category>startup</category>
    </item>
    <item>
      <title>I Got Tired of Mocking APIs in pytest. So I Built a Cleaner Way.</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Sun, 08 Mar 2026 15:01:33 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/i-got-tired-of-mocking-apis-in-pytest-so-i-built-a-cleaner-way-3nen</link>
      <guid>https://dev.to/sherinjosephroy/i-got-tired-of-mocking-apis-in-pytest-so-i-built-a-cleaner-way-3nen</guid>
      <description>&lt;p&gt;If you’ve written integration tests in Python long enough, you’ve hit this wall.&lt;/p&gt;

&lt;p&gt;Your test calls three external services.&lt;br&gt;&lt;br&gt;
You mock one endpoint.&lt;br&gt;&lt;br&gt;
Then another.&lt;br&gt;&lt;br&gt;
Then another.&lt;/p&gt;

&lt;p&gt;Suddenly your test is 60 lines long and half of it is patching.&lt;/p&gt;

&lt;p&gt;At that point you’re not testing behavior.&lt;br&gt;&lt;br&gt;
You’re maintaining scaffolding.&lt;/p&gt;

&lt;p&gt;I ran into this repeatedly while working on service-to-service flows, especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single operation triggered multiple HTTP calls
&lt;/li&gt;
&lt;li&gt;Different tests required different combinations of responses
&lt;/li&gt;
&lt;li&gt;Fixtures started turning into mini frameworks
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tooling ecosystem is strong. &lt;code&gt;responses&lt;/code&gt;, &lt;code&gt;unittest.mock&lt;/code&gt;, httpx mocking utilities. But once endpoint count increases, ergonomics start degrading.&lt;/p&gt;

&lt;p&gt;The issue is not capability.&lt;br&gt;&lt;br&gt;
It is readability.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Breaking Point
&lt;/h2&gt;

&lt;p&gt;Here is what multi-endpoint mocking often turns into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;

&lt;span class="nd"&gt;@responses.activate&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_checkout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/users/1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkout_flow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This works.&lt;/p&gt;

&lt;p&gt;But scale it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different combinations per test&lt;/li&gt;
&lt;li&gt;Conditional responses&lt;/li&gt;
&lt;li&gt;Dynamic payloads&lt;/li&gt;
&lt;li&gt;Partial URL matching&lt;/li&gt;
&lt;li&gt;Multiple external services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now your fixtures grow. Helpers grow. Patching spreads.&lt;/p&gt;

&lt;p&gt;Tests become infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Wanted Instead
&lt;/h2&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lives naturally inside pytest&lt;/li&gt;
&lt;li&gt;Keeps mocks close to test logic&lt;/li&gt;
&lt;li&gt;Makes multi-endpoint flows readable&lt;/li&gt;
&lt;li&gt;Avoids spinning up test servers&lt;/li&gt;
&lt;li&gt;Avoids deep patch trees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a small utility called &lt;strong&gt;api-mocker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The philosophy was simple: minimal surface area. No heavy DSL. No framework abstraction.&lt;/p&gt;

&lt;p&gt;Just explicit endpoint declarations.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms3cnjfywh0pm5ke6fpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms3cnjfywh0pm5ke6fpp.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`python&lt;br&gt;
def test_checkout_flow(api_mocker):&lt;br&gt;
    api_mocker.get("/users/1").respond_with(&lt;br&gt;
        status=200,&lt;br&gt;
        json={"id": 1, "name": "Alice"}&lt;br&gt;
    )&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api_mocker.post("/orders").respond_with(
    status=201,
    json={"order_id": 42}
)

result = checkout_flow()
assert result.success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No decorators.&lt;br&gt;
No activation context.&lt;br&gt;
No scattered patch logic.&lt;/p&gt;

&lt;p&gt;The fixture handles lifecycle and cleanup per test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Principles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Isolation Per Test
&lt;/h3&gt;

&lt;p&gt;Mocks reset automatically after each test. No shared state leakage.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Explicit Failure
&lt;/h3&gt;

&lt;p&gt;If an expected endpoint is not called, the test fails.&lt;br&gt;
If an unexpected endpoint is called, the test fails.&lt;/p&gt;

&lt;p&gt;Silent success hides integration problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lightweight Interception
&lt;/h3&gt;

&lt;p&gt;No embedded server. No process overhead.&lt;br&gt;
Interception happens at the request layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyd35ise0a8a4utju5k28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyd35ise0a8a4utju5k28.png" alt=" " width="800" height="85"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Approach Works Best
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Microservice architectures&lt;/li&gt;
&lt;li&gt;Services calling multiple third-party APIs&lt;/li&gt;
&lt;li&gt;Payment or auth flows&lt;/li&gt;
&lt;li&gt;Orchestrator style backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anywhere a single flow touches two or more HTTP integrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Questions I’m Exploring
&lt;/h2&gt;

&lt;p&gt;Mocking libraries always face tension between simplicity and flexibility.&lt;/p&gt;

&lt;p&gt;Some areas I’m actively thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async client handling&lt;/li&gt;
&lt;li&gt;Streaming responses&lt;/li&gt;
&lt;li&gt;When mocking should give way to contract testing&lt;/li&gt;
&lt;li&gt;Detecting over-mocking in large test suites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are tradeoffs that affect long-term test quality.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fue9wc1kl2b2ok44ifseg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fue9wc1kl2b2ok44ifseg.png" alt=" " width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I’m Sharing This
&lt;/h2&gt;

&lt;p&gt;Mocking strategy has a direct impact on codebase health.&lt;/p&gt;

&lt;p&gt;Readable tests scale.&lt;br&gt;
Fixture jungles do not.&lt;/p&gt;

&lt;p&gt;If you’ve dealt with messy multi-endpoint integration tests in Python, I’d genuinely like to hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What worked well&lt;/li&gt;
&lt;li&gt;Where it broke down&lt;/li&gt;
&lt;li&gt;When you moved to contract testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Project links if you want to explore the implementation:&lt;/p&gt;

&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/api-mocker/" rel="noopener noreferrer"&gt;https://pypi.org/project/api-mocker/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/Sherin-SEF-AI/api-mocker" rel="noopener noreferrer"&gt;https://github.com/Sherin-SEF-AI/api-mocker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious to hear how others approach this problem.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>showdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>OpsLens - I Built an Autonomous Incident Response System That Turns Notion Into a War Room</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Sat, 07 Mar 2026 13:31:04 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/opslens-i-built-an-autonomous-incident-response-system-that-turns-notion-into-a-war-room-88n</link>
      <guid>https://dev.to/sherinjosephroy/opslens-i-built-an-autonomous-incident-response-system-that-turns-notion-into-a-war-room-88n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/notion-2026-03-04"&gt;Notion MCP Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;OpsLens&lt;/strong&gt;, an autonomous incident response orchestrator that uses Notion MCP as its core data layer.&lt;/p&gt;

&lt;p&gt;Here is the problem I was solving: when a production incident fires at 3 AM, the on-call engineer has to do six things at once. Triage the alert. Search for past incidents. Find the runbook. Check recent deployments. Notify the team. Document everything for the postmortem. Every step is manual, scattered across different tools, and easy to mess up when you are running on two hours of sleep.&lt;/p&gt;

&lt;p&gt;OpsLens takes the alert, runs five AI agents against it, and writes everything back to Notion. The engineer opens their incident page and finds: severity assessment, related past incidents, applicable runbook steps, a draft postmortem, and a list of who to notify. All in one place, all searchable, all happened in seconds.&lt;/p&gt;

&lt;p&gt;But the part I am most proud of is that it is not a one-way pipe. OpsLens &lt;em&gt;watches&lt;/em&gt; for human edits in Notion. If you disagree with the AI triage and change the severity from P2 to P0, the system detects that within 30 seconds and re-runs the relevant agents with the updated context. The AI proposes. The human decides. The system adapts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually does
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Alert Ingestion&lt;/strong&gt;: Accepts real webhook payloads from Prometheus AlertManager, Grafana, PagerDuty, or any custom JSON source. Normalizes them into a canonical format, deduplicates, and groups related alerts into a single incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five AI Agents&lt;/strong&gt; run in sequence on every new incident:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Triage Agent&lt;/strong&gt; - Validates severity, identifies the affected service, assesses blast radius&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation Agent&lt;/strong&gt; - Searches past incidents, Slack conversations, Google Drive docs, Jira tickets via Notion MCP's connected tool search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remediation Agent&lt;/strong&gt; - Finds applicable runbooks, proposes specific commands and rollback steps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comms Agent&lt;/strong&gt; - Orchestrates notifications and escalation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortem Agent&lt;/strong&gt; - Generates a blameless postmortem when the incident resolves&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every agent writes its analysis as a structured comment on the Notion incident page. This is not dumped into a database somewhere. It lives in Notion, searchable, shareable, and visible to everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incident Commander&lt;/strong&gt;: A contextual AI co-pilot embedded in the dashboard. During an active incident, you can ask it questions like "What changed recently in this service?" or "Find the runbook for this." It searches Notion, fetches pages, checks past incidents, and comes back with specific answers and clickable action buttons (search, escalate, transition status, notify someone, run a remediation step).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bi-directional Notion Sync&lt;/strong&gt;: The Notion Watcher polls active incident pages every 30 seconds. It detects when a human changes severity, updates status, adds a root cause, or writes an escalation comment directly in Notion. When it spots a change, it fires the appropriate callback, re-runs agents, and updates the dashboard via WebSocket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-Time Dashboard&lt;/strong&gt;: React frontend with live updates. Incident list with filters, full timeline view, agent activity feed, audit trail, semantic search across Notion, a webhook playground for testing, and settings page for configuring integrations, all connected via WebSocket for instant updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Integrations&lt;/strong&gt;: Slack war rooms, GitHub deployment correlation, Jira ticket creation, Linear issue tracking, and outbound webhooks with retry logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The architecture in one picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prometheus/Grafana/PagerDuty
        |
        v (webhooks)
+------------------+       JSON-RPC 2.0       +------------------+
|  OpsLens Backend |  &amp;lt;-------------------&amp;gt;   |  Notion MCP      |
|  (FastAPI)       |   Streamable HTTP        |  Server (:3100)  |
|                  |                          |                  |
|  - Incident Mgr  |                          +--------+---------+
|  - 5 AI Agents   |                                   |
|  - Notion Watcher |                                   v
|  - WebSocket Hub  |                          +------------------+
+--------+---------+                          |  Notion          |
         |                                    |  - Incidents DB  |
         v                                    |  - Runbooks DB   |
+------------------+                          |  - Services DB   |
|  React Dashboard |                          |  - Postmortems   |
|  - Incident List |                          |  - On-Call DB    |
|  - Commander     |                          +------------------+
|  - Agent Feed    |
|  - Audit Trail   |
+------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F620j730q4c793lwrpa8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F620j730q4c793lwrpa8h.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/LwMiMLhYXVI"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;
&lt;h2&gt;
  
  
  Show us the code
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Sherin-SEF-AI" rel="noopener noreferrer"&gt;
        Sherin-SEF-AI
      &lt;/a&gt; / &lt;a href="https://github.com/Sherin-SEF-AI/OpsLens" rel="noopener noreferrer"&gt;
        OpsLens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      OpsLens: The World’s First Autonomous Incident Command Center powered by Notion MCP.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;OpsLens&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Autonomous Incident Response Orchestrator powered by Notion MCP&lt;/strong&gt;
&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/559724755-172bdf6f-fa02-4e3e-8361-49a32248051c.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQ2NDM0NDcsIm5iZiI6MTc3NDY0MzE0NywicGF0aCI6Ii8xNjk3MDAxMTkvNTU5NzI0NzU1LTE3MmJkZjZmLWZhMDItNGUzZS04MzYxLTQ5YTMyMjQ4MDUxYy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI3JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyN1QyMDI1NDdaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00NjlhNWE3MDhhYzFkMGEwNTI2YzhhMWY2YmU0NWVlMTExMDE2MTcwMGZiYWU1MmJmYTIzZDA5YTRlMWRiZGUwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Sz0zSqqmqD_qiHCUh1X_STV51ebkTVX4P4J8RXhjIcI"&gt;&lt;img width="1536" height="1024" alt="OpsLens-CoverImage" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F559724755-172bdf6f-fa02-4e3e-8361-49a32248051c.png%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQ2NDM0NDcsIm5iZiI6MTc3NDY0MzE0NywicGF0aCI6Ii8xNjk3MDAxMTkvNTU5NzI0NzU1LTE3MmJkZjZmLWZhMDItNGUzZS04MzYxLTQ5YTMyMjQ4MDUxYy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI3JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyN1QyMDI1NDdaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT00NjlhNWE3MDhhYzFkMGEwNTI2YzhhMWY2YmU0NWVlMTExMDE2MTcwMGZiYWU1MmJmYTIzZDA5YTRlMWRiZGUwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Sz0zSqqmqD_qiHCUh1X_STV51ebkTVX4P4J8RXhjIcI"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;OpsLens transforms Notion into an AI-powered incident command center. It ingests alerts from monitoring tools, runs a pipeline of specialized AI agents for triage, correlation, remediation, and postmortem generation, and writes every finding back to Notion as structured, searchable knowledge. Engineers interact through a real-time dashboard or directly in Notion. The system watches for human edits and reacts, creating a true human-in-the-loop incident response workflow.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/notion" rel="nofollow"&gt;Notion MCP Challenge&lt;/a&gt; on DEV.to.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/169700119/559726031-0582949b-ae8d-4609-870a-39050c6970b2.gif?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQ2NDM0NDcsIm5iZiI6MTc3NDY0MzE0NywicGF0aCI6Ii8xNjk3MDAxMTkvNTU5NzI2MDMxLTA1ODI5NDliLWFlOGQtNDYwOS04NzBhLTM5MDUwYzY5NzBiMi5naWY_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI3JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyN1QyMDI1NDdaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0wZWYwNDFkYTczNzFhMGFiNzA3Njg4MGFjNjIxMTZkYzYxNjBkZjA1ODE4ZTk0YzQxZTljNGJiYTRjODdjYzYwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.aFqq1XGnaK-jnbUR-BXWsBbW7IKA3iN1mbLDZcUQSq8"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F169700119%2F559726031-0582949b-ae8d-4609-870a-39050c6970b2.gif%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQ2NDM0NDcsIm5iZiI6MTc3NDY0MzE0NywicGF0aCI6Ii8xNjk3MDAxMTkvNTU5NzI2MDMxLTA1ODI5NDliLWFlOGQtNDYwOS04NzBhLTM5MDUwYzY5NzBiMi5naWY_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwMzI3JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDMyN1QyMDI1NDdaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0wZWYwNDFkYTczNzFhMGFiNzA3Njg4MGFjNjIxMTZkYzYxNjBkZjA1ODE4ZTk0YzQxZTljNGJiYTRjODdjYzYwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.aFqq1XGnaK-jnbUR-BXWsBbW7IKA3iN1mbLDZcUQSq8" alt="OpsLens-Low"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of Contents&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#problem-statement" rel="noopener noreferrer"&gt;Problem Statement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#how-it-works" rel="noopener noreferrer"&gt;How It Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#architecture" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#use-cases" rel="noopener noreferrer"&gt;Use Cases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#features" rel="noopener noreferrer"&gt;Features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#tech-stack" rel="noopener noreferrer"&gt;Tech Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#project-structure" rel="noopener noreferrer"&gt;Project Structure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#getting-started" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#configuration" rel="noopener noreferrer"&gt;Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#api-reference" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#webhook-integration" rel="noopener noreferrer"&gt;Webhook Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#slash-commands" rel="noopener noreferrer"&gt;Slash Commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#docker-deployment" rel="noopener noreferrer"&gt;Docker Deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#development" rel="noopener noreferrer"&gt;Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#author" rel="noopener noreferrer"&gt;Author&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/OpsLens#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Problem Statement&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;When a production incident fires at 3 AM, the on-call engineer faces a wall of context switching: triage the alert, search for past incidents, find the runbook, notify stakeholders, check recent deployments, and document everything for…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Sherin-SEF-AI/OpsLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;The full source is on GitHub. Key files if you want to dive in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/notion_mcp/client.py&lt;/code&gt; - The async JSON-RPC 2.0 client that talks to Notion MCP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/notion_mcp/tools.py&lt;/code&gt; - Typed wrappers around every MCP tool (search, fetch, create, comment)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/agents/orchestrator.py&lt;/code&gt; - The pipeline that coordinates all five agents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/agents/commander.py&lt;/code&gt; - The Incident Commander with agentic tool-use loop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/sync/notion_watcher.py&lt;/code&gt; - Bi-directional sync that detects human edits in Notion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/incidents/manager.py&lt;/code&gt; - Incident lifecycle, dedup, grouping, and Notion rehydration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/webhooks/normalizer.py&lt;/code&gt; - Converts Prometheus/Grafana/PagerDuty payloads to a canonical format&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Notion MCP
&lt;/h2&gt;

&lt;p&gt;Notion MCP is not a side feature in OpsLens. It is the foundation. Every piece of data flows through it. Here is how.&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP Client
&lt;/h3&gt;

&lt;p&gt;OpsLens communicates with the Notion MCP server over Streamable HTTP using JSON-RPC 2.0. The client at &lt;code&gt;src/notion_mcp/client.py&lt;/code&gt; handles session initialization, request/response parsing, and rate limiting (180 requests/min, 30 searches/min). Every call is async. Every error is retried with exponential backoff via tenacity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified view of how OpsLens talks to Notion MCP
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_parse_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Six MCP Tools in Active Use
&lt;/h3&gt;

&lt;p&gt;I use six of the Notion MCP tools throughout the system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;notion-search&lt;/code&gt;&lt;/strong&gt; - This is the workhorse. Every agent starts by searching the workspace for relevant context. The Triage Agent searches for service documentation. The Correlation Agent searches for past incidents with similar patterns. The Remediation Agent searches for runbooks. The Incident Commander uses it interactively when the engineer asks a question.&lt;/p&gt;

&lt;p&gt;The best part: &lt;code&gt;notion-search&lt;/code&gt; searches across connected tools too. When Slack, Google Drive, Jira, or Confluence are connected to the Notion workspace, the search results include matches from all of them. The Correlation Agent does not need separate API integrations for each tool. One MCP search call, and it gets Slack threads about the last time this service broke, the Jira ticket from the previous incident, and the Confluence page with the architecture diagram. That is a massive unlock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create-pages&lt;/code&gt;&lt;/strong&gt; - Every new incident gets a structured Notion page. Properties include incident ID, status, severity, alert source, service name, triggered timestamp, and impact description. The content includes a formatted summary with alert details, labels, and linked URLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create-comment&lt;/code&gt;&lt;/strong&gt; - Every agent writes its analysis as a comment on the incident page. This was a deliberate design choice. Comments are timestamped, attributed, and visible in the page history. When you open an incident page in Notion, you see the full conversation: the Triage Agent's severity assessment, the Correlation Agent's findings about similar past incidents, the Remediation Agent's suggested fix. It reads like a collaborative investigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;list-comments&lt;/code&gt;&lt;/strong&gt; - On startup, OpsLens rehydrates its in-memory state from Notion. It queries the Incidents database, then loads all comments for each incident and parses them back into timeline events. This means you can restart the server and lose nothing. The state lives in Notion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;update-page&lt;/code&gt;&lt;/strong&gt; - When agents update severity or status, the incident page properties are updated. The Command Center also uses this to maintain a living dashboard page in Notion with real-time metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;query-data-source&lt;/code&gt;&lt;/strong&gt; - Used during startup rehydration to query the Incidents database and rebuild in-memory state. Also used by the Incident Commander to find past incidents by specific criteria.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notion as the Single Source of Truth
&lt;/h3&gt;

&lt;p&gt;The key insight that shaped the architecture: if every agent writes to Notion, then Notion becomes the knowledge base automatically. When the Correlation Agent searches for "payment service memory leak," it finds not just manually written docs, but also the AI-generated analyses from previous incidents. The system builds institutional memory over time, and it all lives in a place where everyone on the team can see it, search it, and edit it.&lt;/p&gt;

&lt;p&gt;This also enables the bi-directional sync. Because the data is in Notion, humans can interact with it using the Notion UI they already know. Change a property, add a comment, update a status. The Notion Watcher picks it up and the system responds. No special tools needed. Just Notion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workspace Setup
&lt;/h3&gt;

&lt;p&gt;OpsLens creates six databases automatically on first run via &lt;code&gt;workspace_setup.py&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incidents&lt;/strong&gt; - Structured incident tracking with status, severity, service, and timeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runbooks&lt;/strong&gt; - Step-by-step remediation procedures, searchable by service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services&lt;/strong&gt; - Service registry with criticality tiers and ownership&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortems&lt;/strong&gt; - Blameless post-incident reviews linked to their incidents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-Call&lt;/strong&gt; - Rotation schedules for escalation routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence Tracker&lt;/strong&gt; - Agent confidence scores over time for quality monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setup is idempotent. Run it twice and it finds the existing databases instead of creating duplicates.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Notion MCP Unlocked
&lt;/h3&gt;

&lt;p&gt;Without Notion MCP, building this would have required maintaining a separate database, building a custom search index, and creating a UI for humans to interact with the data. Notion MCP eliminated all of that.&lt;/p&gt;

&lt;p&gt;The agents write to Notion. Humans read and edit in Notion. The search covers the entire workspace and connected tools. The data is structured, queryable, and shareable. The MCP server handles the API complexity. OpsLens just calls tools and gets results.&lt;/p&gt;

&lt;p&gt;That is what made this project possible in the scope of a challenge. The MCP layer turned what would have been months of infrastructure work into a few hundred lines of client code.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tech Stack&lt;/strong&gt;: Python 3.11, FastAPI, React 18, Vite, Tailwind CSS, Google Gemini API (primary LLM), Anthropic Claude API (fallback), Notion MCP Server (Streamable HTTP), Docker Compose&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Sherin-SEF-AI/OpsLens" rel="noopener noreferrer"&gt;github.com/Sherin-SEF-AI/OpsLens&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Got Tired of Mocking APIs in pytest. So I Built a Cleaner Way.</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Tue, 03 Mar 2026 08:15:17 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/i-got-tired-of-mocking-apis-in-pytest-so-i-built-a-cleaner-way-43jc</link>
      <guid>https://dev.to/sherinjosephroy/i-got-tired-of-mocking-apis-in-pytest-so-i-built-a-cleaner-way-43jc</guid>
      <description>&lt;p&gt;If you’ve written integration tests in Python long enough, you’ve hit this wall.&lt;/p&gt;

&lt;p&gt;Your test calls three external services.&lt;br&gt;&lt;br&gt;
You mock one endpoint.&lt;br&gt;&lt;br&gt;
Then another.&lt;br&gt;&lt;br&gt;
Then another.&lt;/p&gt;

&lt;p&gt;Suddenly your test is 60 lines long and half of it is patching.&lt;/p&gt;

&lt;p&gt;At that point you’re not testing behavior.&lt;br&gt;&lt;br&gt;
You’re maintaining scaffolding.&lt;/p&gt;

&lt;p&gt;I ran into this repeatedly while working on service-to-service flows, especially when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single operation triggered multiple HTTP calls
&lt;/li&gt;
&lt;li&gt;Different tests required different combinations of responses
&lt;/li&gt;
&lt;li&gt;Fixtures started turning into mini frameworks
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tooling ecosystem is strong. &lt;code&gt;responses&lt;/code&gt;, &lt;code&gt;unittest.mock&lt;/code&gt;, httpx mocking utilities. But once endpoint count increases, ergonomics start degrading.&lt;/p&gt;

&lt;p&gt;The issue is not capability.&lt;br&gt;&lt;br&gt;
It is readability.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Breaking Point
&lt;/h2&gt;

&lt;p&gt;Here is what multi-endpoint mocking often turns into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;

&lt;span class="nd"&gt;@responses.activate&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_checkout&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/users/1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkout_flow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This works.&lt;/p&gt;

&lt;p&gt;But scale it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different combinations per test&lt;/li&gt;
&lt;li&gt;Conditional responses&lt;/li&gt;
&lt;li&gt;Dynamic payloads&lt;/li&gt;
&lt;li&gt;Partial URL matching&lt;/li&gt;
&lt;li&gt;Multiple external services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now your fixtures grow. Helpers grow. Patching spreads.&lt;/p&gt;

&lt;p&gt;Tests become infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Wanted Instead
&lt;/h2&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lives naturally inside pytest&lt;/li&gt;
&lt;li&gt;Keeps mocks close to test logic&lt;/li&gt;
&lt;li&gt;Makes multi-endpoint flows readable&lt;/li&gt;
&lt;li&gt;Avoids spinning up test servers&lt;/li&gt;
&lt;li&gt;Avoids deep patch trees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a small utility called &lt;strong&gt;api-mocker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The philosophy was simple: minimal surface area. No heavy DSL. No framework abstraction.&lt;/p&gt;

&lt;p&gt;Just explicit endpoint declarations.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms3cnjfywh0pm5ke6fpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms3cnjfywh0pm5ke6fpp.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`python&lt;br&gt;
def test_checkout_flow(api_mocker):&lt;br&gt;
    api_mocker.get("/users/1").respond_with(&lt;br&gt;
        status=200,&lt;br&gt;
        json={"id": 1, "name": "Alice"}&lt;br&gt;
    )&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api_mocker.post("/orders").respond_with(
    status=201,
    json={"order_id": 42}
)

result = checkout_flow()
assert result.success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No decorators.&lt;br&gt;
No activation context.&lt;br&gt;
No scattered patch logic.&lt;/p&gt;

&lt;p&gt;The fixture handles lifecycle and cleanup per test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Principles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Isolation Per Test
&lt;/h3&gt;

&lt;p&gt;Mocks reset automatically after each test. No shared state leakage.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Explicit Failure
&lt;/h3&gt;

&lt;p&gt;If an expected endpoint is not called, the test fails.&lt;br&gt;
If an unexpected endpoint is called, the test fails.&lt;/p&gt;

&lt;p&gt;Silent success hides integration problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lightweight Interception
&lt;/h3&gt;

&lt;p&gt;No embedded server. No process overhead.&lt;br&gt;
Interception happens at the request layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyd35ise0a8a4utju5k28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyd35ise0a8a4utju5k28.png" alt=" " width="800" height="85"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Approach Works Best
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Microservice architectures&lt;/li&gt;
&lt;li&gt;Services calling multiple third-party APIs&lt;/li&gt;
&lt;li&gt;Payment or auth flows&lt;/li&gt;
&lt;li&gt;Orchestrator style backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anywhere a single flow touches two or more HTTP integrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Questions I’m Exploring
&lt;/h2&gt;

&lt;p&gt;Mocking libraries always face tension between simplicity and flexibility.&lt;/p&gt;

&lt;p&gt;Some areas I’m actively thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async client handling&lt;/li&gt;
&lt;li&gt;Streaming responses&lt;/li&gt;
&lt;li&gt;When mocking should give way to contract testing&lt;/li&gt;
&lt;li&gt;Detecting over-mocking in large test suites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are tradeoffs that affect long-term test quality.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fue9wc1kl2b2ok44ifseg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fue9wc1kl2b2ok44ifseg.png" alt=" " width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I’m Sharing This
&lt;/h2&gt;

&lt;p&gt;Mocking strategy has a direct impact on codebase health.&lt;/p&gt;

&lt;p&gt;Readable tests scale.&lt;br&gt;
Fixture jungles do not.&lt;/p&gt;

&lt;p&gt;If you’ve dealt with messy multi-endpoint integration tests in Python, I’d genuinely like to hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What worked well&lt;/li&gt;
&lt;li&gt;Where it broke down&lt;/li&gt;
&lt;li&gt;When you moved to contract testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Project links if you want to explore the implementation:&lt;/p&gt;

&lt;p&gt;PyPI: &lt;a href="https://pypi.org/project/api-mocker/" rel="noopener noreferrer"&gt;https://pypi.org/project/api-mocker/&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/Sherin-SEF-AI/api-mocker" rel="noopener noreferrer"&gt;https://github.com/Sherin-SEF-AI/api-mocker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious to hear how others approach this problem.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>showdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Security Cameras Don’t Understand. This One Does - Building an Agentic Physical Intelligence Platform with Gemini</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Mon, 02 Mar 2026 17:48:35 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/security-cameras-dont-understand-this-one-does-building-an-agentic-physical-intelligence-d1k</link>
      <guid>https://dev.to/sherinjosephroy/security-cameras-dont-understand-this-one-does-building-an-agentic-physical-intelligence-d1k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mlh/built-with-google-gemini-02-25-26"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;Most security systems are reactive. They record. They store. When something goes wrong, someone reviews footage after the fact. I've spent years building computer vision systems for autonomous vehicles - where a 200ms latency failure isn't an incident report, it's a crash. That mindset is what drove &lt;strong&gt;Aegis Sentinel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aegis Sentinel&lt;/strong&gt; is a real-time physical security intelligence platform powered by Gemini's multimodal and agentic capabilities. It's not a smarter camera. It's a surveillance system that &lt;em&gt;understands&lt;/em&gt; what it's watching, reasons about it, and acts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Perimeter Surveillance &amp;amp; Threat Detection
&lt;/h3&gt;

&lt;p&gt;Aegis Sentinel ingests live video feeds across monitored zones continuously. Using Gemini's vision capabilities, it doesn't just detect motion - it classifies &lt;em&gt;intent&lt;/em&gt;. An authorized maintenance worker moving through a restricted corridor is treated differently from an unrecognized individual at an entry point at 2 AM. The model differentiates between those scenarios through contextual reasoning, not bounding box matching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Anomaly Detection
&lt;/h3&gt;

&lt;p&gt;Beyond intruders, physical ops security is about operational integrity - equipment left in the wrong zone, a door held open past threshold, personnel deviating from standard procedures. Aegis Sentinel monitors these patterns by treating the spatial environment as a &lt;strong&gt;semantic map&lt;/strong&gt;, not a pixel grid.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Alerting with Natural Language Narration
&lt;/h3&gt;

&lt;p&gt;When a threat is detected, the system doesn't fire a binary alert. It generates a structured incident narrative:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Unidentified individual detected in Zone 4 server corridor at 02:14 AM. No access badge visible. Individual stationary for 3 minutes near rack B7."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That narration lands on the ops dashboard and triggers automated escalation workflows directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agentic Decision Loop
&lt;/h3&gt;

&lt;p&gt;This is where Gemini earned its architectural position. The system doesn't just detect - it &lt;strong&gt;decides&lt;/strong&gt;. Gemini acts as the reasoning agent, consuming multimodal context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📹 Live video frame&lt;/li&gt;
&lt;li&gt;🗺️ Zone metadata &amp;amp; spatial layout&lt;/li&gt;
&lt;li&gt;🔐 Access control logs&lt;/li&gt;
&lt;li&gt;🕐 Time-of-day context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...and resolving what escalation tier is appropriate: log only, notify on-call, lock down zone, or initiate full response. I built the tool-use layer so Gemini calls directly into the incident management API and access control systems as first-class operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/zC4TjNx4xrY"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Gemini's multimodal context window changes the architecture entirely
&lt;/h3&gt;

&lt;p&gt;Coming from traditional CV pipelines - where you chain a detector, tracker, classifier, and rules engine - the first instinct is to slot Gemini into one stage of that chain. &lt;strong&gt;That's the wrong mental model entirely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gemini can hold spatial context across multiple frames &lt;em&gt;and&lt;/em&gt; structured metadata simultaneously. That meant I could collapse what would've been a 4-stage pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detect → track → classify → reason
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...into a single inference call with the right frame context and prompt structure. Fewer moving parts, fewer failure surfaces, dramatically less engineering overhead. One model reasoning end-to-end beat a hand-crafted ensemble in both accuracy and operational simplicity - and in a startup, that simplicity compounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Agentic tool use requires you to think in failure modes first
&lt;/h3&gt;

&lt;p&gt;Giving Gemini the ability to call into access control APIs and incident management systems is powerful on paper. In practice, every tool call is a &lt;strong&gt;trust boundary&lt;/strong&gt;. I spent more engineering time on the &lt;em&gt;guard layer&lt;/em&gt; - validating Gemini's action intent before executing high-stakes calls like zone lockdowns - than on the tool definitions themselves.&lt;/p&gt;

&lt;p&gt;The lesson: agentic AI in physical security isn't a chatbot with side effects. It's a decision system operating in the real world. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit confidence thresholds before any action executes&lt;/li&gt;
&lt;li&gt;Human-in-the-loop escalation for irreversible operations&lt;/li&gt;
&lt;li&gt;Comprehensive audit logging as a foundational constraint, not an afterthought&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Streaming latency is your hardest constraint - not model accuracy
&lt;/h3&gt;

&lt;p&gt;Gemini's threat classification accuracy was strong. But in physical security ops, a 4-second response window on a live intrusion event is operationally unacceptable.&lt;/p&gt;

&lt;p&gt;This forced a &lt;strong&gt;two-tier architecture&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Live feed
   │
   ▼
[Tier 1] OpenCV pre-filter
 motion detection + ROI extraction
   │  (only candidate events pass through)
   ▼
[Tier 2] Gemini
 contextual reasoning + agentic decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemini API calls became targeted and high-value instead of continuous. Latency dropped, cost per inference dropped, and the system became predictable under load. In ADAS, we call this "compute budget allocation." The same principle transfers directly to physical security.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Natural language incident narration is not a polish feature
&lt;/h3&gt;

&lt;p&gt;I expected narrative alert generation to be a nice demo extra. It became the &lt;strong&gt;most operationally significant feature&lt;/strong&gt; across every stakeholder demo.&lt;/p&gt;

&lt;p&gt;Security ops staff are not ML engineers. Replacing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERT: CLASS=INTRUDER CONF=0.87 ZONE=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...with a plain-English incident description any operator can act on immediately eliminates the cognitive translation layer between detection and response. That directly reduces mean-time-to-response - which is the metric physical security actually cares about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Google Gemini Feedback
&lt;/h2&gt;

&lt;p&gt;I'll be direct, because that's more useful than praise.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ What worked exceptionally well
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Multimodal reasoning&lt;/strong&gt; exceeded my expectations. Passing a video frame, a text description of the spatial zone context, and the last 60 seconds of structured event metadata - and getting back coherent, actionable threat reasoning - is not something I can replicate with any other single API. The cross-modal context integration is the real differentiator, not any individual capability in isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Function calling / tool use&lt;/strong&gt; is reliable and structurally predictable. The explicit tool call output is clean enough to build production guard layers around. In a security domain, that predictability matters more than raw capability - false positives trigger real-world responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ Where I hit real friction
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Latency variability under concurrent load&lt;/strong&gt; was the most significant operational pain point. Single-request latency was workable. Under burst conditions - multiple concurrent feed analysis requests - latency became inconsistent in ways that are hard to tolerate in real-time security contexts. I ended up building adaptive backpressure queuing, which added complexity I hadn't originally scoped. A more predictable latency SLA under concurrent load would make production deployment planning substantially cleaner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native video stream ingestion&lt;/strong&gt; still doesn't exist at the API level. I'm doing frame-level decomposition and passing sampled frames with temporal metadata. A continuous video input endpoint with configurable temporal sampling would eliminate meaningful preprocessing overhead and unlock more natural architectures for continuous monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-tail visual edge cases&lt;/strong&gt; - partially obscured individuals, extreme low-light conditions, unusual camera angles - occasionally produced inconsistent threat classifications. This is the tail-distribution problem I know well from autonomous driving development. It's manageable with a human review queue, but it sets a ceiling on full automation today. Worth being honest about.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest take
&lt;/h3&gt;

&lt;p&gt;Gemini is the right architectural foundation for physical intelligence applications. Multimodal input, large context windows, and native tool use map almost exactly onto what a physical security reasoning agent needs. The gaps - real-time streaming infrastructure and edge-case visual consistency - are tractable. I expect them to close.&lt;/p&gt;

&lt;p&gt;For anyone building in physical security, critical infrastructure, or industrial operations where you need a system that &lt;em&gt;understands&lt;/em&gt; its environment rather than pattern-matches it - &lt;strong&gt;Gemini is worth the architecture investment.&lt;/strong&gt;&lt;/p&gt;




</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Building a 13-Agent AI System for Real-Time Road Safety Monitoring</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Thu, 26 Feb 2026 15:03:12 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/building-a-13-agent-ai-system-for-real-time-road-safety-monitoring-330o</link>
      <guid>https://dev.to/sherinjosephroy/building-a-13-agent-ai-system-for-real-time-road-safety-monitoring-330o</guid>
      <description>&lt;p&gt;Kerala, India has one of the highest road accident rates in the country — over 40,000 accidents annually across its narrow, winding highways. I built &lt;strong&gt;SurakshaNet&lt;/strong&gt;, a multi-agent intelligence platform that monitors 6 high-risk road segments in real time using 13 AI agents, Byzantine fault-tolerant voting, and Bayesian belief fusion.&lt;/p&gt;

&lt;p&gt;This post covers the architecture, the problems I solved, and what I learned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0w5mjdcu4dh00fhg2v5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0w5mjdcu4dh00fhg2v5y.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Traditional road safety systems rely on single data sources — a camera feed or a weather alert. But accidents are multi-causal. A wet road alone is not dangerous. A wet road at night, near a school zone, with heavy traffic and poor visibility — that is dangerous.&lt;/p&gt;

&lt;p&gt;I needed a system that could fuse multiple independent signals into a single calibrated risk score, attribute causality, and trigger the right response automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs75l84kdgw0kjsbb3be5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs75l84kdgw0kjsbb3be5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The system runs 3 agent clusters in parallel per road segment, every 5 minutes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SWARM-GUARD (Road Safety):&lt;/strong&gt; Weather friction analysis using IRC SP:73-2015 standards, historical accident pattern matching from NCRB data, traffic anomaly detection via HERE API, and YOLOv8 visual risk assessment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DRISHTI (Disaster Response):&lt;/strong&gt; IMD weather situation awareness, KSDMA resource inventory tracking, census-based population vulnerability scoring, OR-Tools logistics optimization, and Claude-powered counterfactual projection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SENTINEL (Surveillance):&lt;/strong&gt; RT-DETR edge perception, OSNet cross-camera re-identification, Bayesian Beta-Binomial threat escalation, and Section 65B-compliant evidence packaging.&lt;/p&gt;

&lt;p&gt;Each agent returns a standardized output: risk score (0-1), confidence (0-1), and a vote (ESCALATE / HOLD / DISMISS).&lt;/p&gt;

&lt;h2&gt;
  
  
  Consensus: Byzantine Voting + Bayesian Fusion
&lt;/h2&gt;

&lt;p&gt;With 13 agents, some will fail or return unreliable data. The system handles this in two stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1 — Byzantine Voting.&lt;/strong&gt; Agents vote weighted by confidence. ESCALATE contributes +1 x confidence, DISMISS contributes -1 x confidence, HOLD contributes 0. Agents with zero confidence are excluded. If more than 50% of agents fail, the system enters degraded mode. The weighted sum determines the consensus: above +0.50 triggers ESCALATE, below -0.50 triggers DISMISS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2 — Bayesian Belief Fusion.&lt;/strong&gt; Raw scores are fused accounting for inter-agent correlation. Visual risk and traffic anomaly share a correlation coefficient of 0.6 (both degrade in bad weather). Independent agents are fused via weighted average. Correlated agents are down-weighted. The final belief is computed through precision weighting:&lt;/p&gt;

&lt;p&gt;fused_belief = (mean_indep * prec_indep + mean_corr * prec_corr) / (prec_indep + prec_corr)&lt;/p&gt;

&lt;h2&gt;
  
  
  Causal Attribution
&lt;/h2&gt;

&lt;p&gt;If the system escalates, it does not just say "high risk detected." That would be useless. Instead, it calls Claude (claude-sonnet-4-20250514) with the raw agent scores and asks for a one-paragraph causal attribution explaining &lt;em&gt;why&lt;/em&gt; this segment is dangerous right now.&lt;/p&gt;

&lt;p&gt;If the Claude API fails, the attribution is set to null — never fabricated. A Slack alert is sent with raw scores marked as needing manual review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Python 3.12, FastAPI, async SQLAlchemy, PostgreSQL + PostGIS, Redis, LangGraph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI/ML:&lt;/strong&gt; YOLOv8, RT-DETR, OSNet, Anthropic Claude API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14, Leaflet.js, Recharts, Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; LangGraph StateGraph with 3-stage pipeline (vote, fuse, attribute)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; Docker Compose, Alembic migrations, structlog JSON logging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Frontend Shows
&lt;/h2&gt;

&lt;p&gt;The platform has 27 pages across multiple dashboards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command Center&lt;/strong&gt; — Full-screen operations view with live risk map, auto-cycling segment display, and real-time metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; — Risk heatmap (7x24 day-hour grid), agent reliability table, agent correlation matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KSDMA Dashboard&lt;/strong&gt; — Resource gap visualization, evacuation route planner, monsoon overlay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KSRTC Dashboard&lt;/strong&gt; — Bus route advisories, depot reports, visual driver fatigue assessment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;District Collector Dashboard&lt;/strong&gt; — All 14 Kerala districts with escalation charts and monsoon preparedness scores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System Health&lt;/strong&gt; — Agent status by cluster, API rate limits with circuit breaker states&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other features include crowd-sourced hazard reporting with GPS auto-detection, spatiotemporal forecasting, school zone safety, green corridor management, and RTSP camera integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate Limit Engineering
&lt;/h2&gt;

&lt;p&gt;Free-tier APIs are the backbone. The monitoring interval is 300 seconds across 6 segments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Daily Usage&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open-Meteo&lt;/td&gt;
&lt;td&gt;1,728 calls&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HERE Traffic&lt;/td&gt;
&lt;td&gt;144 calls&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenWeatherMap&lt;/td&gt;
&lt;td&gt;Fallback only&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Claude&lt;/td&gt;
&lt;td&gt;On escalation only&lt;/td&gt;
&lt;td&gt;60 RPM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per-API tracking uses Redis token buckets. If a limit is exhausted, the agent returns confidence=0.0 and vote=HOLD — it never gets API keys revoked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No random numbers for risk scores.&lt;/strong&gt; Every score is derived from real external data. If data is unavailable, the score is 0.0 with confidence 0.0 and vote HOLD. This was a hard rule from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No blocking calls.&lt;/strong&gt; The entire application is async. &lt;code&gt;asyncio.gather()&lt;/code&gt; for parallel agents, &lt;code&gt;httpx.AsyncClient&lt;/code&gt; for HTTP, &lt;code&gt;create_async_engine&lt;/code&gt; for database access. Using &lt;code&gt;time.sleep()&lt;/code&gt; anywhere would be a production-killing bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful degradation everywhere.&lt;/strong&gt; If the database is unreachable, events buffer in a Redis queue and drain with exponential backoff. If an agent fails, it returns a safe default. If Claude fails, attribution is null with a Slack alert. Nothing fails silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Multi-agent consensus is harder than single-model inference. The correlation correction between agents was the most impactful improvement — without it, correlated agents would double-count evidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate limit management is an engineering problem, not an afterthought. Building it into the agent abstraction from the start saved significant debugging later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Causal attribution changes how operators interact with the system. A number like "0.78" means little. "Wet road surface combined with reduced visibility and 23% speed reduction on NH-66 near a school zone active period" — that is actionable.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4jdywcvylrl8vmi448y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4jdywcvylrl8vmi448y.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1lnah0fqioxytb41en5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1lnah0fqioxytb41en5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnn7um4zrcbim29d555i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnn7um4zrcbim29d555i.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Sherin-SEF-AI/SurakshaNet" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/OQG86I1AqTk?si=kArOb8zJN_z1luLu" rel="noopener noreferrer"&gt;Demo Video&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built as a solo project. Open to feedback and contributions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>nextjs</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>7 Open Source Tools I Discovered in 2025 That Feel Illegal to Know 🤫</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Wed, 26 Nov 2025 16:00:57 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/7-open-source-tools-i-discovered-in-2024-that-feel-illegal-to-know-2lgl</link>
      <guid>https://dev.to/sherinjosephroy/7-open-source-tools-i-discovered-in-2024-that-feel-illegal-to-know-2lgl</guid>
      <description>&lt;p&gt;We all have that "Tools" bookmark folder we never look at.&lt;/p&gt;

&lt;p&gt;Last weekend, I did a deep clean. I audited my workflow to find the utilities that actually saved me hours, not just seconds. I ruthlessly cut the bloat and kept the gems.&lt;/p&gt;

&lt;p&gt;If you want to speed up your dev loop in 2025, here is the stack I'm betting on. 👇&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bruno (The Postman Killer) 🐶
Category: API Client Repo: usebruno/bruno&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are tired of Postman asking you to log in or sync to the cloud just to test a GET request, you need Bruno.&lt;/p&gt;

&lt;p&gt;The Killer Feature: It stores your API collections directly in your filesystem (folder structure). This means you can git version control your API tests alongside your code. No more "It works on my machine" sync issues.&lt;/p&gt;

&lt;p&gt;Cost: Open Source.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lazygit 💤
Category: Terminal UI Repo: jesseduffield/lazygit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I know, I know. "Real devs use raw git commands." But when you need to stage specific lines, squash 4 commits, and resolve a merge conflict in 30 seconds, lazygit is a superpower.&lt;/p&gt;

&lt;p&gt;It turns your terminal into a visual dashboard. I haven't typed git add . in six months.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Biome ⚡
Category: Formatter &amp;amp; Linter Repo: biomejs/biome&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I replaced Prettier and ESLint with this. It is written in Rust and it is blazingly fast.&lt;/p&gt;

&lt;p&gt;Why switch? In large monorepos, saving a file used to take 2-3 seconds to format. Biome does it in milliseconds. It handles formatting and linting in a single pass. The speed difference is actually noticeable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PocketBase 🗄️
Category: Backend-as-a-Service Repo: pocketbase/pocketbase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For your side projects, stop over-engineering Kubernetes clusters. PocketBase is an open-source backend in a single Go file.&lt;/p&gt;

&lt;p&gt;It gives you: Realtime database, Auth, File Storage, and an Admin Dashboard. You can drop the binary on a $5 VPS and serve 10k users easily. It is the spiritual successor to Firebase, but you actually own the data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zod 💎
Category: TypeScript Validation Repo: colinhacks/zod&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are trusting your API responses blindly, you are going to have a bad time. Zod allows you to define a schema and validate data at runtime.&lt;/p&gt;

&lt;p&gt;The "Aha" Moment: It infers the static TypeScript type from the schema. You write the validation once, and you get the types for free. It essentially eliminates "undefined is not a function" errors.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ray.so 📸
Category: Sharing Code Link: &lt;a href="//ray.so"&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stop taking screenshots of your VS Code with the messy file tree visible.&lt;/p&gt;

&lt;p&gt;Ray.so creates those beautiful, syntax-highlighted code snippets you see on Twitter/X. If you are writing documentation or sharing a bug fix with a junior dev, presentation matters. It makes your code look readable and professional.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Excalidraw 🎨
Category: Whiteboarding Repo: excalidraw/excalidraw&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best way to document a system isn't a 50-page Google Doc; it's a messy hand-drawn diagram. Excalidraw brings back the "napkin sketch" feel but makes it collaborative.&lt;/p&gt;

&lt;p&gt;Pro Tip: Use the "Libraries" feature to drag and drop AWS/Azure icons instantly.&lt;/p&gt;

&lt;p&gt;The "So What?"&lt;br&gt;
We often get obsessed with learning new frameworks (Next.js vs. Remix vs. Vue), but I've found that the biggest quality-of-life improvements come from optimizing the "boring" stuff—how we commit code, how we test APIs, and how we format files.&lt;/p&gt;

&lt;p&gt;Which "hidden gem" tool are you gatekeeping? Drop it in the comments so I can steal it. 👇&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AutoML Lite: A Simple and Lightweight AutoML Tool</title>
      <dc:creator>Sherin Joseph Roy</dc:creator>
      <pubDate>Thu, 20 Nov 2025 14:19:30 +0000</pubDate>
      <link>https://dev.to/sherinjosephroy/automl-lite-a-simple-and-lightweight-automl-tool-23md</link>
      <guid>https://dev.to/sherinjosephroy/automl-lite-a-simple-and-lightweight-automl-tool-23md</guid>
      <description>&lt;p&gt;Many developers want to try machine learning but feel stuck because most AutoML tools are heavy and slow. AutoML Lite solves this by giving you a clean, fast, and easy way to train and compare models on a normal laptop.&lt;/p&gt;

&lt;p&gt;AutoML Lite trains multiple models, handles preprocessing, and shows performance metrics like accuracy, recall, and F1 score. Supported models include RandomForest, XGBoost, SVM, Logistic Regression, KNN, and Decision Tree. It is built for students, founders, and developers who want quick experiments without complex setup.&lt;/p&gt;

&lt;p&gt;Repo link:&lt;br&gt;
&lt;a href="https://github.com/Sherin-SEF-AI/AutoML-Lite" rel="noopener noreferrer"&gt;https://github.com/Sherin-SEF-AI/AutoML-Lite&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why This Project Matters&lt;/p&gt;

&lt;p&gt;Works on low end laptops&lt;/p&gt;

&lt;p&gt;Reduces confusion around model selection&lt;/p&gt;

&lt;p&gt;Helps you learn how different ML models behave&lt;/p&gt;

&lt;p&gt;Easy to extend with new algorithms&lt;/p&gt;

&lt;p&gt;Great for prototyping and quick testing&lt;/p&gt;

&lt;p&gt;Author&lt;/p&gt;

&lt;p&gt;Sherin Joseph Roy&lt;br&gt;
Co-Founder and builder at DeepMost AI&lt;br&gt;
Works on AI tools, safety tech, developer tools, and open source projects. Passionate about building lightweight and practical ML systems for real world use.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Sherin-SEF-AI" rel="noopener noreferrer"&gt;https://github.com/Sherin-SEF-AI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://sherinjosephroy.link" rel="noopener noreferrer"&gt;https://sherinjosephroy.link&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>opensource</category>
      <category>ai</category>
      <category>development</category>
    </item>
  </channel>
</rss>
