<?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: Vincenzo De Sanctis</title>
    <description>The latest articles on DEV Community by Vincenzo De Sanctis (@vincenzo_desanctis_94502).</description>
    <link>https://dev.to/vincenzo_desanctis_94502</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2300213%2Fae6db63e-f685-4c89-b56a-97d9b7b8e756.jpg</url>
      <title>DEV Community: Vincenzo De Sanctis</title>
      <link>https://dev.to/vincenzo_desanctis_94502</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vincenzo_desanctis_94502"/>
    <language>en</language>
    <item>
      <title>Frames of Truth: An Android App That Reads the Story Behind Your Photos</title>
      <dc:creator>Vincenzo De Sanctis</dc:creator>
      <pubDate>Sun, 11 Oct 2026 20:58:13 +0000</pubDate>
      <link>https://dev.to/vincenzo_desanctis_94502/frames-of-truth-an-android-app-that-reads-the-story-behind-your-photos-1bdl</link>
      <guid>https://dev.to/vincenzo_desanctis_94502/frames-of-truth-an-android-app-that-reads-the-story-behind-your-photos-1bdl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've always been fascinated by the invisible layers of a place. You walk past a building every day and never wonder who lived there, what happened in that street, or why that monument exists. You take a photo, and all of that context stays invisible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frames of Truth&lt;/strong&gt; is my attempt to fix that.&lt;/p&gt;

&lt;p&gt;It's an Android app that takes a photo, reads its GPS coordinates from the EXIF metadata, and uses &lt;strong&gt;open-source AI models&lt;/strong&gt; to tell you the story of the place where the photo was taken. Not the story of what's in the photo — the story of &lt;em&gt;where you were standing&lt;/em&gt; when you took it.&lt;/p&gt;

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

&lt;p&gt;Every smartphone photo carries hidden metadata: when it was taken, with what device, and — if geolocation was enabled — where. That last piece of information is a GPS coordinate. It tells you &lt;em&gt;where the camera was&lt;/em&gt;, not necessarily what the camera was pointing at.&lt;/p&gt;

&lt;p&gt;That distinction matters. If you point your phone at a distant hill and shoot from your backyard, the GPS gives your backyard. The photo has your perspective, not the hill's location.&lt;/p&gt;

&lt;p&gt;Most apps ignore this. They geotag the photo with a generic address and move on. &lt;strong&gt;Frames of Truth&lt;/strong&gt; doesn't. It treats the GPS coordinate as the start of an investigation.&lt;/p&gt;

&lt;p&gt;Here's what happens when you load a photo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app reads the GPS coordinates from the EXIF metadata&lt;/li&gt;
&lt;li&gt;It reverse-geocodes the location (Photon, Nominatim)&lt;/li&gt;
&lt;li&gt;It finds nearby points of interest (Geoapify, Overpass)&lt;/li&gt;
&lt;li&gt;It pulls historical context from Wikipedia and Wikidata&lt;/li&gt;
&lt;li&gt;It sends the photo to a vision model to describe what's in it&lt;/li&gt;
&lt;li&gt;An LLM writes a story in three sections: &lt;strong&gt;Location&lt;/strong&gt;, &lt;strong&gt;Image&lt;/strong&gt;, &lt;strong&gt;Curiosity&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want more, there's a second button that generates a deeper &lt;strong&gt;investigative report&lt;/strong&gt; with six sections: Location, Surroundings, Wikipedia &amp;amp; Wikidata, Recent news (via GDELT), Photo metadata, and an Investigative hypothesis.&lt;/p&gt;

&lt;p&gt;The UI is in 7 languages: Italian, English, French, German, Spanish, Portuguese, Dutch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who is it for?
&lt;/h3&gt;

&lt;p&gt;For anyone who walks past places every day and never wonders what's behind them. For travelers who want context without opening ten tabs. For people who love going outside and want a reason to explore their own neighborhood.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does it get people off the screen?
&lt;/h3&gt;

&lt;p&gt;The app is the opposite of doomscrolling. You &lt;em&gt;have&lt;/em&gt; to go outside to use it. You have to walk somewhere, point your camera at something, and come back with a real photo of a real place. The screen is the shortest part of the experience. The place is the point.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;APK Download&lt;/strong&gt;: &lt;a href="https://github.com/vincenzodesanctis/frames-of-truth/releases" rel="noopener noreferrer"&gt;Latest release on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install it on any Android device (7.0+). On first launch, open the ⚙️ menu and enter your own API keys (Groq, Geoapify — both have free tiers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The main screen with the "Take or choose a photo" prompt&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1c0sjd8dg8qvvbsxstr8.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1c0sjd8dg8qvvbsxstr8.jpg" alt="Main screen" width="800" height="1733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A generated story with the three sections&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rn2jtccbubawo3sslvg.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rn2jtccbubawo3sslvg.jpg" alt="Image analysis" width="800" height="1733"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1vxqex8b1xoygsqlt6r.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1vxqex8b1xoygsqlt6r.jpg" alt=" " width="800" height="1733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The investigative report with collapsible sections&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2uo9ocby366bxrbf371.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2uo9ocby366bxrbf371.jpg" alt="Deep investigation" width="800" height="1733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API key settings modal with the &lt;code&gt;.txt&lt;/code&gt; import feature&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwjau95kto69m2khk9w2.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqwjau95kto69m2khk9w2.jpg" alt="API keys modal" width="800" height="1733"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The full source code is available on GitHub under the MIT license:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/vincenzodesanctis/frames-of-truth" rel="noopener noreferrer"&gt;github.com/vincenzodesanctis/frames-of-truth&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&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;Mobile&lt;/td&gt;
&lt;td&gt;Android (Kotlin) with &lt;strong&gt;Chaquopy&lt;/strong&gt; for Python integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Python (Flask) as a local HTTP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;WebView with HTML/CSS/JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Models&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Open-weight&lt;/strong&gt; via Groq (Qwen 3.8, GPT-OSS) and OpenRouter (Llama 3.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geocoding&lt;/td&gt;
&lt;td&gt;OpenStreetMap (Photon, Nominatim, Overpass, Wikipedia, Wikidata)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXIF/HEIC&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;MetadataExtractor&lt;/strong&gt; (Drew Noakes)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The open-source AI stack
&lt;/h3&gt;

&lt;p&gt;The entire AI pipeline is built on open-weight models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vision analysis&lt;/strong&gt;: Qwen 3.8 via &lt;a href="https://groq.com" rel="noopener noreferrer"&gt;Groq&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Story generation&lt;/strong&gt;: GPT-OSS via Groq&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback narration&lt;/strong&gt;: Llama 3.3 via &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vision fallback&lt;/strong&gt;: Gemini via REST&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The choice of open-weight models wasn't an afterthought — it was the design constraint. I wanted to be able to swap providers without rewriting the app, and to avoid being locked into any single vendor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Python on Android
&lt;/h3&gt;

&lt;p&gt;I used &lt;a href="https://chaquo.com/chaquopy/" rel="noopener noreferrer"&gt;Chaquopy&lt;/a&gt; to run Python 3.8 directly on the phone. Flask serves a local HTTP server on &lt;code&gt;127.0.0.1:5000&lt;/code&gt;, and a WebView loads the UI from there. It's a bit unusual, but it means I can reuse all my Python logic — geocoding, POI discovery, Wikipedia lookups, LLM calls — inside an Android app without rewriting anything in Kotlin.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical challenges I had to solve
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HEIC support&lt;/strong&gt;: Modern Android phones and iPhones save photos as &lt;code&gt;.heic&lt;/code&gt; files by default. Python's PIL library doesn't read HEIC natively. My solution: detect HEIC files on the Kotlin side (reading the magic bytes &lt;code&gt;ftyp&lt;/code&gt;), extract the EXIF metadata &lt;strong&gt;before&lt;/strong&gt; converting, then convert the image to JPEG for the vision model. This way I preserve GPS coordinates even though the file format changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPS redaction on Android 10+&lt;/strong&gt;: Starting with Android 10, the OS redacts EXIF GPS data from any file read through a content URI — unless the app requests the &lt;code&gt;ACCESS_MEDIA_LOCATION&lt;/code&gt; permission. Without it, every photo comes back with &lt;code&gt;lat: null&lt;/code&gt;. With it, the coordinates come through intact. This one took me days to figure out, and required using &lt;code&gt;MediaStore.setRequireOriginal()&lt;/code&gt; on the URI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User-configurable API keys&lt;/strong&gt;: Instead of hardcoding my keys, I built an in-app settings menu with a modal that lets users enter their own keys or import them from a &lt;code&gt;.txt&lt;/code&gt; file. The keys are saved in &lt;code&gt;SharedPreferences&lt;/code&gt; and pushed to the Python layer via a JavaScript Interface bridge. They never leave the device.&lt;/p&gt;

&lt;h3&gt;
  
  
  📴 A note on connectivity
&lt;/h3&gt;

&lt;p&gt;The challenge mentions "an open-weight model that works with no signal." Frames of Truth currently requires an internet connection for AI inference — but the architecture is designed to be &lt;strong&gt;portable to on-device models&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AI pipeline is provider-agnostic. Swapping from Groq to a local model means changing one function call.&lt;/li&gt;
&lt;li&gt;The vision and narration layers are separate, so you can run a small local vision model on the phone and keep the narration online (or vice versa).&lt;/li&gt;
&lt;li&gt;The Python backend runs entirely on the device via Chaquopy. Nothing needs a server.&lt;/li&gt;
&lt;li&gt;The geocoding layer (Photon, Nominatim) can be swapped for a pre-downloaded OpenStreetMap extract for full offline geocoding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason I built it this way is that open-weight models are moving fast. Today they need a GPU in the cloud. Tomorrow they'll run on your phone. Frames of Truth is ready for either world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Building this app on open-weight models wasn't just a cost-saving decision. It changed how I think about software.&lt;/p&gt;

&lt;h3&gt;
  
  
  What open innovation made possible
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No vendor lock-in.&lt;/strong&gt; If Groq changes its pricing tomorrow or shuts down its free tier, I can swap to OpenRouter, Together AI, or a self-hosted model. The app code doesn't change. Only one API call changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Portability.&lt;/strong&gt; Because the AI pipeline is provider-agnostic, the app can eventually run a small local model on the phone itself. Closed APIs make this impossible — you can't run GPT-4 on a phone, but you &lt;em&gt;can&lt;/em&gt; run Llama 3.2 or Gemma 2.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transparency.&lt;/strong&gt; Anyone can read the pipeline end-to-end. They can see exactly which model processes the photo, what prompt is sent, and how the response is structured. There's no black box.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Community-driven improvement.&lt;/strong&gt; The whole project relies on open data and open infrastructure: OpenStreetMap for geocoding, Wikipedia and Wikidata for context, GDELT for news. Every one of these is maintained by a global community of volunteers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost control for indie developers.&lt;/strong&gt; A solo developer can't afford to pay per-token for a closed API on a hobby project. Free tiers on Groq and OpenRouter make this kind of experiment possible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What a closed API wouldn't have allowed
&lt;/h3&gt;

&lt;p&gt;If I had built this on a closed vision API, I would have been locked into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Their pricing (which changes without notice)&lt;/li&gt;
&lt;li&gt;Their rate limits (which often break at the worst moment)&lt;/li&gt;
&lt;li&gt;Their terms of service (which often forbid certain use cases)&lt;/li&gt;
&lt;li&gt;Their model choices (which they can deprecate whenever they want)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With open-weight models, I keep control. That's the whole point of open innovation.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I did not use DevRelay for this project, so there's no agent session to embed. The whole project was built iteratively by hand, testing each layer on a real Android device and debugging via Logcat.&lt;/p&gt;

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

&lt;p&gt;Building this app taught me that open-source AI isn't just a cost-saving alternative to closed models. It's a different way of building software.&lt;/p&gt;

&lt;p&gt;When I can see the model, swap the provider, and read the pipeline end-to-end, I feel like I'm actually in control of the product. I don't wake up one day to find that a service I depend on has changed its terms, or started charging 10x more, or shut down its free tier.&lt;/p&gt;

&lt;p&gt;For an indie project like this, that kind of control is everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The best part of building Frames of Truth was that it made me want to go outside and test it. Every time I took a photo, I learned something new about a place I thought I already knew.&lt;/p&gt;

&lt;p&gt;That's what "Touch Grass" should mean: not abandoning technology, but using it to reconnect with the world. Open-source AI, done right, is a bridge — not a wall.&lt;/p&gt;

&lt;p&gt;If you try the app, let me know what your neighborhood looks like through its eyes. 🌍&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ for the Hacktoberfest Open-Source AI Challenge 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>hacktoberfest</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
