Personal AI Research

OSINT Real-Time Tracker: Multilingual Conflict Monitoring, Built Solo with Gemini

A maps-based dashboard that turns open-source conflict reports into structured, geolocated, credibility-scored events. The interesting part wasn't getting Gemini to return clean JSON — it was everything built around it to make that output something you could actually trust.

July 2026 · Live at osint.davidirving.dev

What it does

The tracker ingests open-source reports — social posts, wire dispatches, radio transcripts — in whatever language they were written, and turns them into structured, geolocated, credibility-scored events on a live map. Built solo in Google AI Studio, React and Express on the frontend and backend, Gemini for the actual analysis, Firestore for persistence, deployed to Cloud Run.

One Gemini call, five structured outputs

The core route takes raw source text and makes a single Gemini call with a structured responseSchema — no separate translation step, no pipeline of smaller calls. The prompt is explicit about this: translate to English if needed, identify the source language, then extract title, summary, category, severity, geolocation, sentiment, and a five-parameter credibility grade, all in one response.

Geolocation resolution is the same call, not a separate one: the prompt instructs Gemini to normalize transliterations — Kyiv versus Kiev, Zaporizhzhia versus Zaporozhye — while preserving the original-script place name, and requires a written explanation of how any spelling ambiguity was resolved. That explanation is rendered verbatim in the app's location panel, not summarized or hidden — a real pass-through of the model's own reasoning, which is what "not a black box" actually has to mean if it's going to mean anything.

Sentiment is the most elaborate part of the schema: a continuous score, a 1–10 tension rating, a dominant emotion plus secondary tones, a six-way nuance classification, and a four-axis valence breakdown across hostility, fear, defiance, and calm. The prompt explicitly asks for "a detailed, dispassionate computational linguistics narrative" explaining which grammatical markers, idioms, or regional slang in the original language carry that emotion — which is exactly the native-language-nuance claim, and it's a real, required field in the schema, not marketing copy describing something the model happens to do sometimes.

Credibility is a fixed rubric, not a vibe: source reputation, cross-referencing, evidence level, contextual plausibility, and freshness, each worth up to twenty points with its own required justification string. The total isn't something Gemini asserts directly — the server sums the five sub-scores itself, so the headline number is always traceable back to five specific, visible judgments underneath it.

An hourly harvester, and the distributed-systems bug it needed

A background scheduler runs the same pipeline unattended, aimed at named authoritative sources across six sectors — Ukraine frontline reporting, Red Sea maritime alerts, Sudan and the Sahel, East Asia, the Caucasus, and cyber incidents — using Gemini's search grounding rather than an open-ended crawl, with every harvest and its full audit trail written to Firestore.

Running on Cloud Run means the app can have more than one instance alive at once, each running its own independent copy of that scheduler. Without a lock, every instance's "has an hour passed since the last harvest?" check can pass at the same moment, since the state only gets saved after a harvest finishes — which means multiple instances can all decide to run a full, quota-burning harvest simultaneously. The fix is a Firestore transaction that atomically checks and claims the harvest slot before the slow Gemini call even starts, so only one instance ever wins the race. It's a small piece of code, and it's the kind of bug that's invisible in local development and only exists because of how the actual deployment target behaves.

A real security bug, found before it became an incident

The app's AI-Studio-generated starting point had Firestore rules wide open — allow read, write: if true — with the React frontend writing directly to Firestore for core functionality. That meant anyone with the Firebase client config, which is necessarily public in any browser-based app, could read or write data directly, bypassing the backend and its logic entirely. One endpoint made this worse in a specific way: the reset function didn't touch Firestore from the server at all — the client was doing the actual purge-and-reseed, with no authentication check anywhere in that path.

The fix moved the backend onto the Firebase Admin SDK — real service credentials that don't depend on client-side rules at all — locked the Firestore rules to block direct client writes on every sensitive collection, and gated the reset and system-log endpoints behind real Firebase ID-token verification. It was found by recognizing the same class of bug in a sibling project and checking for it here deliberately, rather than from an incident or an external report — the more useful way to find this kind of thing, if you can manage it.

What's actually gated, and what's an honest fallback

Two things are currently reserved for my own signed-in account rather than open to any visitor: submitting a fresh block of text for live analysis, and manually triggering a harvest. That's a cost-control decision — Gemini calls aren't free — not a limitation in the pipeline itself, which handles arbitrary multilingual input the same way regardless of who's driving it. Everyone else gets full read access to the credibility, sentiment, and geolocation breakdowns, and to a set of curated sample events that exercise the same pipeline.

If Gemini itself is unavailable, the app falls back to a basic keyword-matching heuristic rather than failing outright — and it says so, visibly, in the resulting event's own justification text. An offline fallback dressed up as the real feature would be a worse failure mode than an honest one that admits it's degraded.

What actually mattered

Getting a model to return clean structured JSON isn't the hard part anymore — modern models do that reliably, most of the time. What actually took the work was everything built around that call: retry logic with exponential backoff and a model-fallback cascade for when Gemini itself is flaky or rate-limited, a distributed lock so an autoscaled deployment doesn't quietly burn its own quota racing itself, and treating "the AI-Studio default has wide-open database rules" as a real problem to go fix, rather than something to leave alone because it's a side project and nobody's watching.

None of that shows up in a screenshot of the map view. It's the difference between a demo that happens to work and a system that keeps working once it's actually running unattended.

Personal AI Research Gemini API NLP / Geolocation Application Security