<?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: Shashank Kumar Singh</title>
    <description>The latest articles on DEV Community by Shashank Kumar Singh (@bitshank2338).</description>
    <link>https://dev.to/bitshank2338</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%2F3915859%2Fc4cbcf69-dc76-44ef-8171-efa70efb8666.jpeg</url>
      <title>DEV Community: Shashank Kumar Singh</title>
      <link>https://dev.to/bitshank2338</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bitshank2338"/>
    <language>en</language>
    <item>
      <title>Roam: turn a remembered place into your next walk with local Gemma</title>
      <dc:creator>Shashank Kumar Singh</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:36:07 +0000</pubDate>
      <link>https://dev.to/bitshank2338/roam-turn-a-remembered-place-into-your-next-walk-with-local-gemma-2dgd</link>
      <guid>https://dev.to/bitshank2338/roam-turn-a-remembered-place-into-your-next-walk-with-local-gemma-2dgd</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;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Roam is a map-backed collection of place memories that helps someone decide where to go, then close the tab and actually go there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The starting idea was simple: a photo of a park, a café, a trail, or a workshop is more useful when it comes with somebody's experience of being there. A long feed can make me want to travel without helping me take the first step. I wanted the map, the memory, and a practical plan in one place.&lt;/p&gt;

&lt;p&gt;Roam combines a readable social feed with photography and nearby discovery. There is no signup wall. In the local prototype, someone can attach a photo, speak a short experience, review the transcript, confirm the place, and ask Gemma to help turn it into a memory. Another person can save that place and ask for a visit plan that fits the time they have.&lt;/p&gt;

&lt;p&gt;The important design decision is that AI should help express the experience, not replace it. Saying “I went here and liked the quiet” must not turn into a fabricated review of food, facilities, or an event that the person never mentioned.&lt;/p&gt;

&lt;p&gt;The intended audience is local explorers: people looking for a small outdoor visit, a walk, a café stop, or something nearby worth remembering. Workshops and cultural memories are supported too, but a memory is not a claim that an event is still running.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://roam-bitshank.onrender.com/demo.html" rel="noopener noreferrer"&gt;Watch the narrated Roam demo — 97 seconds, 1080p&lt;/a&gt;&lt;/strong&gt; · &lt;a href="https://roam-bitshank.onrender.com/demo.mp4" rel="noopener noreferrer"&gt;Download MP4&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Dated supplement — 12 October 2026:&lt;/strong&gt; This video was added after the Week 1 deadline of 12:29 PM IST. The original submission was published at 12:06:07 PM IST before the cutoff. The video is an edited walkthrough of captured local application screens, with narration by ElevenLabs' Hope voice. It adds presentation material and does not claim new features or an additional prize category completed during the entry period. The README records the post-deadline addition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://roam-bitshank.onrender.com/" rel="noopener noreferrer"&gt;Open the hosted Roam preview&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Render preview supports browsing, the map, place search, and nearby discovery. It is intentionally read-only: public posting is disabled while private photo storage is being connected, and the hosted preview does not yet have a GPU/Gemma service. Free Render can take time to wake after inactivity.&lt;/p&gt;

&lt;p&gt;The full Gemma-assisted workflow runs locally with Ollama. These are screenshots of the implemented local prototype, not a claim that the cloud preview currently performs inference.&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%2Fbppqvirxjx6pwpzbmbnc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbppqvirxjx6pwpzbmbnc.png" alt="Roam desktop feed with a map alongside it" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Local feed with clearly labelled fictional example memories and illustrative photography. The example photo is not evidence of an actual visit to the named park.&lt;/em&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%2Fczps2by7n9kwqxvb51lu.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%2Fczps2by7n9kwqxvb51lu.jpg" alt="Local Gemma visit plan" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Gemma generated a plan from example memories, with those examples identified in the UI. Plans enforce a time budget and cite their source memories.&lt;/em&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%2Fwh29u4snjlvxjsttgg0t.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%2Fwh29u4snjlvxjsttgg0t.jpg" alt="A retrieved place overview with honest missing-source information" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For this Misu example, retrieval supplied a restaurant category and mapped cuisine. The interface explicitly says no matching Wikipedia background was found; it does not invent the missing story.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To reproduce the local AI demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Bitshank-2338/roam-touch-grass.git
&lt;span class="nb"&gt;cd &lt;/span&gt;roam-touch-grass
npm ci
ollama pull gemma3:4b
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Node 24.14 or newer, keep Ollama running, and open &lt;code&gt;http://127.0.0.1:4180&lt;/code&gt;. Add your own Geoapify key in a private &lt;code&gt;.env&lt;/code&gt; for the selected map provider; the repository includes the configuration example. Model weights and credentials are not bundled.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Bitshank-2338/roam-touch-grass" rel="noopener noreferrer"&gt;Public source and setup instructions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The application source is MIT licensed. Model weights, map data, fonts, and photographs retain their respective licenses or terms. This submission repository is a clean snapshot of the project built during the challenge window; it excludes operational secrets, private browser identities, user uploads, and private development history.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  A small web stack, with local Gemma at the centre
&lt;/h3&gt;

&lt;p&gt;The frontend uses plain JavaScript and CSS, Leaflet, and a forest-green “Trail R” identity. The backend uses Node.js, Express, Sharp for photo processing, and Ollama for &lt;strong&gt;Gemma 3 4B&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SQLite is the local default. An asynchronous PostgreSQL backend is implemented, and the hosted preview now connects to Tiger Data. The app already has a private S3-compatible media adapter tested against local MinIO; connecting Supabase Storage is the next deployment step.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Photo + reviewed voice transcript + confirmed place
                 |
       server-side source retrieval
                 |
     Gemma selects existing facts / assists fields
                 |
        editable memory in private review
                 |
        approved feed and map contribution
                 |
       source-based, time-bounded visit plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browser speech recognition supplies a transcript for review; Gemma works on that reviewed text. Recognition can depend on the browser provider's online service. “Listen” uses the device's available speech voices, with Stop and cancellation when navigating away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grounding instead of a bigger hallucination
&lt;/h3&gt;

&lt;p&gt;One challenge was expanding a one- or two-line experience without pretending to know more about the user's visit.&lt;/p&gt;

&lt;p&gt;For short notes, the original experience remains verbatim. Roam retrieves place context and asks Gemma to select known &lt;strong&gt;fact IDs&lt;/strong&gt;. Application code attaches the exact retrieved source text, rather than accepting arbitrary model-authored facts. The source metadata is signed so a client cannot simply attach invented attribution.&lt;/p&gt;

&lt;p&gt;When suitable background is absent, Roam provides labelled visit guidance and an explicit limitation. It does not manufacture opening hours, admission fees, venue history, or workshop schedules.&lt;/p&gt;

&lt;p&gt;Nearby discovery also needed correction: a road near the user is not automatically an interesting destination. Retrieval now prioritises named parks, viewpoints, cafés, restaurants, museums, and cultural places. Wikipedia/Wikidata background requires geographic checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anonymous does not mean ownership-free
&lt;/h3&gt;

&lt;p&gt;The server issues a random private browser identity. A recovery export restores ownership and saved memories; it is not a device fingerprint. Clearing browser storage without recovery loses that ownership.&lt;/p&gt;

&lt;p&gt;Local posting requires a recent location receipt and confirmation of the public pin. This is a client claim, not proof of attendance. Photos are decoded, resized, stripped of metadata, and re-encoded before storage.&lt;/p&gt;

&lt;p&gt;Reactions are real, with one heart per browser per post. Replies have moderation states. Trending uses approved, non-demo contributions, duplicate filtering, and independent-browser thresholds. Gemma explains an existing ranking with cited post IDs; it cannot invent a trending venue or change the ranking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safety has an explicit unfinished boundary
&lt;/h3&gt;

&lt;p&gt;The application implements Checking, Needs review, Blocked, and Published states. Pending photos remain private; edits retain the previous approved version while the new revision is checked.&lt;/p&gt;

&lt;p&gt;The ShieldGemma integration and evaluation harness are prepared, but &lt;strong&gt;the actual ShieldGemma models have not been installed or evaluated on the future GPU server&lt;/strong&gt;. Automatic approval stays disabled. Contract tests verify failure handling with controlled responses; those tests are not a substitute for real model evaluation. New video uploads are disabled in this release.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I verified
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;54 automated tests pass&lt;/strong&gt;, with syntax checks passing.&lt;/li&gt;
&lt;li&gt;Coverage includes ownership/recovery, stale moderation decisions, private media, exact source attribution, reactions/comments, speech cancellation, and trend grounding.&lt;/li&gt;
&lt;li&gt;Isolated PostgreSQL/MinIO integration checks cover migration, access denial, interrupted media operations, and persistence.&lt;/li&gt;
&lt;li&gt;Live Geoapify search, named nearby discovery, tiles, and walking routes were checked.&lt;/li&gt;
&lt;li&gt;Real local Gemma produced sourced place context and time-bounded plans.&lt;/li&gt;
&lt;li&gt;Microphone capture and audible summary playback were confirmed on the intended device.&lt;/li&gt;
&lt;li&gt;Render health, search, feed, and trends pass. Tiger Data's provider-usage ledger and trend snapshot survived a Render redeploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks do not establish a completed public launch. Private cloud media, GPU inference, real moderation evaluation, and final Figma repairs remain pending.&lt;/p&gt;

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

&lt;p&gt;Gemma's open weights let the useful part of Roam run on a machine I control. A personal memory does not need to be sent to a proprietary paid inference API just to get help expressing it.&lt;/p&gt;

&lt;p&gt;The model is also replaceable. Retrieval, source validation, identity, and publication decisions live in application code, so the model does not become the only place where the product's rules exist. I can inspect those rules, test them, and change the inference provider without rewriting the social application.&lt;/p&gt;

&lt;p&gt;This is not an entirely offline app: maps and source retrieval need internet, and browser voice recognition may do so too. The local model makes the AI step independently runnable, while the UI states those limits honestly.&lt;/p&gt;

&lt;p&gt;The best outcome for Roam is a short interaction: find a place, understand why someone remembers it, make a realistic plan, and spend the next hour outside.&lt;/p&gt;

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

&lt;p&gt;I developed Roam with Codex assistance, including implementation, test orchestration, and this submission write-up. This post was prepared and published by the agent under my instruction. I am not publishing the raw session because it includes private operational setup.&lt;/p&gt;

&lt;p&gt;Inspiration included &lt;a href="https://dev.to/sbis04/leaf-peep-an-open-model-reads-the-trees-so-you-know-where-to-walk-this-weekend-16hk"&gt;Leaf Peep by Souvik Biswas&lt;/a&gt;, which helped shape the idea of using an open model to encourage an actual outing. Roam's focus is the place-memory and social-discovery workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; local Gemma 3 4B assists memory drafting, selects grounded place facts, and generates plans from cited memories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; the deployed web frontend/backend preview runs on Render, with public posting deliberately gated while durable photo storage is completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Ollama; Google's Gemma and ShieldGemma under their model terms; Geoapify; Leaflet; OpenStreetMap and OpenMapTiles; Wikipedia/Wikidata; Phosphor icons; Outfit and DM Sans; and illustrative Unsplash photography. The Trail R brand imagery was generated with imagegen. Example stories are fictional and are excluded from real trends.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I chose Ollama over cloud AI for my screen-reading assistant (and what I learned)</title>
      <dc:creator>Shashank Kumar Singh</dc:creator>
      <pubDate>Wed, 06 May 2026 11:38:43 +0000</pubDate>
      <link>https://dev.to/bitshank2338/why-i-chose-ollama-over-cloud-ai-for-my-screen-reading-assistant-and-what-i-learned-171m</link>
      <guid>https://dev.to/bitshank2338/why-i-chose-ollama-over-cloud-ai-for-my-screen-reading-assistant-and-what-i-learned-171m</guid>
      <description>&lt;p&gt;When I started building Clicky — a Windows AI assistant that reads your screen and answers out loud — I had to make a fundamental choice: cloud AI or local AI?&lt;/p&gt;

&lt;p&gt;I chose local (Ollama). Here's exactly why, and what I learned the hard way.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/WYY9yJHDaEU" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with cloud AI for a screen assistant
&lt;/h2&gt;

&lt;p&gt;Clicky's core loop is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a screenshot of your screen&lt;/li&gt;
&lt;li&gt;Record your voice question&lt;/li&gt;
&lt;li&gt;Send both to an LLM&lt;/li&gt;
&lt;li&gt;Speak the answer back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where cloud AI becomes a problem. You're sending a &lt;strong&gt;screenshot of your screen&lt;/strong&gt; to a remote server every single time someone presses the hotkey.&lt;/p&gt;

&lt;p&gt;Think about what's on your screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passwords in password managers&lt;/li&gt;
&lt;li&gt;Emails with sensitive info&lt;/li&gt;
&lt;li&gt;Code with API keys&lt;/li&gt;
&lt;li&gt;Personal documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wasn't comfortable sending that to OpenAI servers (or any server) by default. And I couldn't expect users to be comfortable with it either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ollama actually gives you
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ollama.ai" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; lets you run LLMs locally. Pull a model, it runs on your GPU/CPU, responses never leave your machine.&lt;/p&gt;

&lt;p&gt;For Clicky, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero privacy risk&lt;/strong&gt; — screenshots stay on your PC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero cost per query&lt;/strong&gt; — no API bills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero internet required&lt;/strong&gt; — works offline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero rate limits&lt;/strong&gt; — query as fast as your hardware allows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff: you need a decent GPU and ~4–8GB RAM for a usable model. But most modern gaming PCs qualify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models I tested
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;VRAM needed&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.1 8B&lt;/td&gt;
&lt;td&gt;6GB&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral 7B&lt;/td&gt;
&lt;td&gt;5GB&lt;/td&gt;
&lt;td&gt;Great&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-3 mini&lt;/td&gt;
&lt;td&gt;3GB&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B&lt;/td&gt;
&lt;td&gt;2GB&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;Blazing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I use Llama 3.1 8B as the default. It handles screen content, code, and general questions well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ollama HTTP API is dead simple
&lt;/h2&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;httpx&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&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;ask_ollama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenshot_path&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;question&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenshot_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;img_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&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;model&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;llama3.1&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;messages&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="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&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="n"&gt;img_b64&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;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="nf"&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;http://localhost:11434/api/chat&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="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;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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No SDK, no auth, no API key. Just a POST to localhost.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I kept cloud AI as a fallback
&lt;/h2&gt;

&lt;p&gt;Despite going local-first, I built a &lt;code&gt;CompanionManager&lt;/code&gt; class that routes to different providers:&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;CompanionManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_provider&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="k"&gt;if&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OllamaProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openai_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ClaudeProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&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;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claude_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OllamaProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# default
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Power users who want GPT-4V or Claude's vision can switch in settings. But the default is Ollama, zero config.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. Ollama model management is the real UX problem.&lt;/strong&gt;&lt;br&gt;
Users don't want to open a terminal and run &lt;code&gt;ollama pull llama3.1&lt;/code&gt;. I had to build a model-picker UI that shows which models are installed and lets you pull new ones from inside Clicky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. First response latency matters more than throughput.&lt;/strong&gt;&lt;br&gt;
For a voice assistant, you want the first token fast. Streaming responses (even over localhost) made Clicky feel much snappier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. GPU vs CPU is night and day.&lt;/strong&gt;&lt;br&gt;
On CPU only, Llama 3.1 8B takes 15–30 seconds. On a GTX 1080, it's 2–4 seconds. I added a GPU detection check and recommend CPU-only users use Phi-3 mini instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Context window size limits what you can do with screenshots.&lt;/strong&gt;&lt;br&gt;
A 1080p screenshot as base64 JPEG at q=70 is ~200–400KB. Some models handle this fine; others choke. I compress to 768px max width before encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict
&lt;/h2&gt;

&lt;p&gt;For a privacy-sensitive app like a screen assistant, &lt;strong&gt;local AI is the right default&lt;/strong&gt;. Cloud AI makes sense as an opt-in for users who want more power and don't mind the tradeoff.&lt;/p&gt;

&lt;p&gt;Ollama made this possible without asking users to do anything complicated. Install Clicky, install Ollama, pull a model — done.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Download Clicky free (Windows):&lt;/strong&gt; &lt;a href="https://clicky.foo" rel="noopener noreferrer"&gt;https://clicky.foo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Article 1 (what it does):&lt;/strong&gt; &lt;a href="https://dev.to/bitshank2338/i-built-a-free-ai-that-reads-my-screen-and-answers-out-loud-no-api-key-runs-offline-with-ollama-15np"&gt;https://dev.to/bitshank2338/i-built-a-free-ai-that-reads-my-screen-and-answers-out-loud-no-api-key-runs-offline-with-ollama-15np&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Article 2 (full technical breakdown):&lt;/strong&gt; &lt;a href="https://dev.to/bitshank2338/how-i-built-a-screen-aware-ai-assistant-in-python-full-stack-breakdown-pyqt6-whisper-ollama-1354"&gt;https://dev.to/bitshank2338/how-i-built-a-screen-aware-ai-assistant-in-python-full-stack-breakdown-pyqt6-whisper-ollama-1354&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What model are you running locally? Let me know in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I built a screen-aware AI assistant in Python – full stack breakdown (PyQt6 + Whisper + Ollama)</title>
      <dc:creator>Shashank Kumar Singh</dc:creator>
      <pubDate>Wed, 06 May 2026 11:35:22 +0000</pubDate>
      <link>https://dev.to/bitshank2338/how-i-built-a-screen-aware-ai-assistant-in-python-full-stack-breakdown-pyqt6-whisper-ollama-1354</link>
      <guid>https://dev.to/bitshank2338/how-i-built-a-screen-aware-ai-assistant-in-python-full-stack-breakdown-pyqt6-whisper-ollama-1354</guid>
      <description>&lt;p&gt;Three months ago I started building Clicky — a Windows AI assistant that reads your screen and answers out loud. Here's the full technical breakdown of every piece.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; PyQt6 system tray → Ctrl+Alt+Space hotkey → screenshot + Whisper STT → Ollama/OpenAI/Claude → edge-tts speaks answer back. Open source, free, no API key needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User presses Ctrl+Alt+Space
        ↓
GlobalHotkey listener (pynput)
        ↓
Screenshot all monitors (mss)
        ↓
Whisper.cpp transcribes audio
        ↓
CompanionManager routes to AI provider
        ↓
Ollama (local) / OpenAI / Claude / Copilot
        ↓
edge-tts speaks answer + arrow overlay on screen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. System tray + hotkey (PyQt6 + pynput)
&lt;/h2&gt;

&lt;p&gt;The app lives in the system tray — no window, zero friction.&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;pynput&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_activate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;QMetaObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;companion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_listening&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueuedConnection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;hotkey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GlobalHotKeys&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;ctrl&amp;gt;+&amp;lt;alt&amp;gt;+&amp;lt;space&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;on_activate&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key trick: &lt;code&gt;QMetaObject.invokeMethod&lt;/code&gt; with &lt;code&gt;Qt.QueuedConnection&lt;/code&gt; — this crosses the thread boundary safely from pynput's background thread into Qt's main thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Screen capture (mss)
&lt;/h2&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;mss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;capture_all_screens&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;mss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mss&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monitors&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="c1"&gt;# skip monitor[0] (all combined)
&lt;/span&gt;            &lt;span class="n"&gt;shot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frombytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bgra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&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;BGRX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# encode as JPEG base64 for vision API
&lt;/span&gt;            &lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JPEG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quality 75 JPEG keeps the payload under API limits while preserving readability.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Speech-to-text (Whisper.cpp)
&lt;/h2&gt;

&lt;p&gt;I use the &lt;code&gt;whisper-cpp&lt;/code&gt; Python bindings — runs on CPU, no GPU needed.&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;whispercpp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Whisper&lt;/span&gt;

&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Whisper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base.en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_path&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="o"&gt;-&amp;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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_path&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;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;base.en&lt;/code&gt; model is 142MB and transcribes ~10s of audio in ~2s on a mid-range CPU. Fast enough to feel instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. AI provider routing
&lt;/h2&gt;

&lt;p&gt;This was the trickiest part — supporting 4 providers with one interface:&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;CompanionManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_provider&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;match&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;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OllamaProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ClaudeProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;copilot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GitHubCopilotProvider&lt;/span&gt;&lt;span class="p"&gt;()&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;ask&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;question&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;screenshots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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="o"&gt;-&amp;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;provider&lt;/span&gt; &lt;span class="o"&gt;=&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;get_provider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Identity questions skip screenshots (avoids vision API refusals)
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_identity_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;screenshots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;screenshots&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;system_prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;is_identity_question()&lt;/code&gt; filter was a fun challenge — vision APIs refuse to identify people in images. So I detect "who is X" patterns with regex and strip the screenshots before sending.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Local AI with Ollama
&lt;/h2&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;httpx&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;complete&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;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&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;model&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;qwen2.5vl:3b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# vision model
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system_prompt&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;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;images&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;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;r&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="nf"&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;http://localhost:11434/api/chat&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="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;r&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Text-to-speech (edge-tts)
&lt;/h2&gt;

&lt;p&gt;Microsoft's neural TTS — free, no API key, sounds great:&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;edge_tts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asyncio&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;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;communicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;edge_tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Communicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US-AriaNeural&lt;/span&gt;&lt;span class="sh"&gt;"&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;communicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/response.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# play with pygame
&lt;/span&gt;    &lt;span class="n"&gt;pygame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;music&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/response.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pygame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;music&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Packaging (PyInstaller + Inno Setup)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# build.bat&lt;/span&gt;
pyinstaller clicky.spec &lt;span class="nt"&gt;--noconfirm&lt;/span&gt;
&lt;span class="c"&gt;# Then Inno Setup builds Setup-Clicky.exe&lt;/span&gt;
iscc installer.iss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.spec&lt;/code&gt; file needs explicit hidden imports for everything dynamic:&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;hiddenimports&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;ai.ollama_bootstrap&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;ui.setup_wizard&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;whispercpp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Thread safety with PyQt6 is non-negotiable&lt;/strong&gt; — never call Qt UI methods from background threads. Use &lt;code&gt;QMetaObject.invokeMethod&lt;/code&gt; or signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whisper base.en is the sweet spot&lt;/strong&gt; — tiny is too inaccurate, small is too slow on CPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vision APIs hate face identification&lt;/strong&gt; — build the filter early, not after your first support ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyInstaller + Ollama = packaging nightmare&lt;/strong&gt; — Ollama runs as a separate process, not bundled. The setup wizard that auto-installs it saved countless support issues.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;macOS port (the hotkey system is the main blocker)&lt;/li&gt;
&lt;li&gt;Plugin system for custom AI skills&lt;/li&gt;
&lt;li&gt;Sub-200ms response time optimization&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full source is on GitHub and it's MIT licensed. If you build something on top of it, let me know.&lt;/p&gt;

&lt;p&gt;📥 Download: &lt;a href="https://clicky.foo" rel="noopener noreferrer"&gt;https://clicky.foo&lt;/a&gt;&lt;br&gt;
⭐ GitHub: &lt;a href="https://github.com/Bitshank-2338/clicky-windows" rel="noopener noreferrer"&gt;https://github.com/Bitshank-2338/clicky-windows&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a free AI that reads my screen and answers out loud – no API key, runs offline with Ollama</title>
      <dc:creator>Shashank Kumar Singh</dc:creator>
      <pubDate>Wed, 06 May 2026 11:15:16 +0000</pubDate>
      <link>https://dev.to/bitshank2338/i-built-a-free-ai-that-reads-my-screen-and-answers-out-loud-no-api-key-runs-offline-with-ollama-15np</link>
      <guid>https://dev.to/bitshank2338/i-built-a-free-ai-that-reads-my-screen-and-answers-out-loud-no-api-key-runs-offline-with-ollama-15np</guid>
      <description>&lt;p&gt;I kept hitting ChatGPT's $20/mo wall. So I built my own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clicky&lt;/strong&gt; is a free, open-source AI assistant for Windows that floats next to your cursor, reads your screen, and speaks the answer back out loud. No copy-paste. No alt-tab. No subscription.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/WYY9yJHDaEU" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I was solving
&lt;/h2&gt;

&lt;p&gt;Every time I hit an API limit or saw that $20/mo charge, I thought — I have a decent laptop, Ollama runs locally, why am I paying for this?&lt;/p&gt;

&lt;p&gt;But existing local AI setups require you to open a terminal, type prompts, copy-paste context... it's friction. I wanted something that &lt;em&gt;just worked&lt;/em&gt; — press a hotkey, ask a question, get an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Clicky works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;Ctrl+Alt+Space&lt;/strong&gt; anywhere on Windows&lt;/li&gt;
&lt;li&gt;It captures all your screens and starts listening&lt;/li&gt;
&lt;li&gt;You ask your question out loud&lt;/li&gt;
&lt;li&gt;It reads the screen context, queries the AI, and &lt;strong&gt;speaks the answer back&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the entire flow. No window switching. No typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python + PyQt6&lt;/strong&gt; — system tray app, always running in background&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whisper.cpp&lt;/strong&gt; — fast local speech-to-text (runs on CPU)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; — local LLM inference (llama3.2:3b for text, qwen2.5vl:3b for vision)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;edge-tts&lt;/strong&gt; — Microsoft's neural TTS, free and offline-capable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyInstaller&lt;/strong&gt; — packages everything into a single Windows .exe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inno Setup&lt;/strong&gt; — builds the one-click installer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI provider flexibility
&lt;/h2&gt;

&lt;p&gt;Clicky isn't locked to Ollama. It supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Requires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;Free forever&lt;/td&gt;
&lt;td&gt;~4GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI GPT-4o&lt;/td&gt;
&lt;td&gt;Pay per use&lt;/td&gt;
&lt;td&gt;API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Claude&lt;/td&gt;
&lt;td&gt;Pay per use&lt;/td&gt;
&lt;td&gt;API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;Free with Student Pack&lt;/td&gt;
&lt;td&gt;GitHub account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What it can do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Read whatever is on your screen (YouTube, code, emails, docs, anything)&lt;/li&gt;
&lt;li&gt;Search the web and answer instantly&lt;/li&gt;
&lt;li&gt;Point at exact UI elements with a glowing arrow overlay&lt;/li&gt;
&lt;li&gt;Click buttons on your behalf just by asking&lt;/li&gt;
&lt;li&gt;Answer questions about code you're looking at without copy-pasting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup in v1.1.1
&lt;/h2&gt;

&lt;p&gt;The latest version ships with a &lt;strong&gt;first-run setup wizard&lt;/strong&gt; — it detects if Ollama is missing, downloads and installs it automatically, then pulls the right models. One click from installer to working AI.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;📥 &lt;strong&gt;Download free&lt;/strong&gt;: &lt;a href="https://clicky.foo" rel="noopener noreferrer"&gt;https://clicky.foo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐ &lt;strong&gt;GitHub (open source)&lt;/strong&gt;: &lt;a href="https://github.com/Bitshank-2338/clicky-windows" rel="noopener noreferrer"&gt;https://github.com/Bitshank-2338/clicky-windows&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 &lt;strong&gt;Twitter&lt;/strong&gt;: &lt;a href="https://x.com/iamshashank_dev" rel="noopener noreferrer"&gt;https://x.com/iamshashank_dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer questions about the Ollama integration, the PyQt6 architecture, or the PyInstaller packaging pipeline. AMA.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
    </item>
  </channel>
</rss>
