<?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: Naeem Ahmed</title>
    <description>The latest articles on DEV Community by Naeem Ahmed (@naeem_ahmed_d5539951eede9).</description>
    <link>https://dev.to/naeem_ahmed_d5539951eede9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1822259%2F58d9ad14-ebeb-4003-9c5b-c84f117c0225.gif</url>
      <title>DEV Community: Naeem Ahmed</title>
      <link>https://dev.to/naeem_ahmed_d5539951eede9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naeem_ahmed_d5539951eede9"/>
    <language>en</language>
    <item>
      <title>I Taught a Local Gemma to Clean My Downloads Folder, Offline</title>
      <dc:creator>Naeem Ahmed</dc:creator>
      <pubDate>Sat, 24 Oct 2026 23:03:05 +0000</pubDate>
      <link>https://dev.to/naeem_ahmed_d5539951eede9/i-taught-a-local-gemma-to-clean-my-downloads-folder-offline-7o1</link>
      <guid>https://dev.to/naeem_ahmed_d5539951eede9/i-taught-a-local-gemma-to-clean-my-downloads-folder-offline-7o1</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My Downloads folder is a landfill. &lt;code&gt;IMG_4821.heic&lt;/code&gt;, &lt;code&gt;final_v3.docx&lt;/code&gt;, a &lt;code&gt;LICENSE&lt;/code&gt; with no context, installers from months ago. Every few weeks I promise myself I'll sort it, and every few weeks I don't.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;lazy-tidy&lt;/strong&gt;: a tiny Python script that sorts a messy folder into &lt;code&gt;Documents&lt;/code&gt;, &lt;code&gt;Images&lt;/code&gt;, &lt;code&gt;Videos&lt;/code&gt;, &lt;code&gt;Audio&lt;/code&gt;, &lt;code&gt;Code&lt;/code&gt;, &lt;code&gt;Archives&lt;/code&gt;, &lt;code&gt;Installers&lt;/code&gt; and &lt;code&gt;Other&lt;/code&gt;. It previews by default and only moves files when I pass &lt;code&gt;--apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The "slow and steady" part: it uses the cheapest decision-maker first. Known extensions are sorted instantly with a lookup table. Only files the table can't place (&lt;code&gt;Makefile&lt;/code&gt;, &lt;code&gt;README&lt;/code&gt;, &lt;code&gt;data.bin&lt;/code&gt;, &lt;code&gt;photo.raw&lt;/code&gt;) are handed to a local open-weight model, &lt;strong&gt;Gemma 3 4B running through Ollama&lt;/strong&gt; on my laptop.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/naeemahmedpnl/lazy-tidy" rel="noopener noreferrer"&gt;https://github.com/naeemahmedpnl/lazy-tidy&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Before, a folder with 18 mixed files:&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%2Fmaw8ipigyeim57hwxtnh.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%2Fmaw8ipigyeim57hwxtnh.png" alt=" " width="800" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Preview first, and every line shows who made the decision, &lt;code&gt;[extension]&lt;/code&gt; or &lt;code&gt;[model]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python tidy.py ~/tidy-demo
IMG_4821.heic  -&amp;gt;  Images/      [extension]
LICENSE        -&amp;gt;  Documents/   [model]
Makefile       -&amp;gt;  Code/        [model]
README         -&amp;gt;  Documents/   [model]
data.bin       -&amp;gt;  Archives/    [model]
photo.raw      -&amp;gt;  Images/      [model]
...
Done: 12 by extension, 6 by local model.
Preview only. Re-run with --apply to move files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;--apply&lt;/code&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%2Fyf5rgfd9ze4y4dubg0qz.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%2Fyf5rgfd9ze4y4dubg0qz.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extension first.&lt;/strong&gt; A dictionary maps common extensions to categories. It is instant, free, and never wrong. In my demo it handled 12 of 18 files without touching a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local model for the leftovers.&lt;/strong&gt; Anything unknown goes to Gemma 3 4B via Ollama with a prompt that asks for JSON only:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&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;gemma3:4b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&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;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;prompt&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;json&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;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&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;span class="nf"&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;category&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;Other&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="n"&gt;cat&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CATEGORIES&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails.&lt;/strong&gt; If the model returns something invalid or the call fails, the file goes to &lt;code&gt;Other&lt;/code&gt;. If a name already exists in the destination, the file gets a &lt;code&gt;_dup&lt;/code&gt; suffix instead of being overwritten. Nothing moves unless I pass &lt;code&gt;--apply&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Mattered Here
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It works with no internet.&lt;/strong&gt; I turned Wi-Fi off and proved it with a failing ping before running the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ping &lt;span class="nt"&gt;-c&lt;/span&gt; 2 google.com
&lt;span class="go"&gt;ping: cannot resolve google.com: Unknown host
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;python tidy.py ~/tidy-offline
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Done: 6 by extension, 6 by local model.
python tidy.py ~/tidy-offline  4.067 total
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve files, six of them classified by the model, in about 4 seconds on an M1 Pro, fully offline.&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%2F72aup6q3fnntb0g9xqse.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%2F72aup6q3fnntb0g9xqse.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My file names stay on my machine.&lt;/strong&gt; Downloads contains invoices, resumes and personal photos. Even with just file names, that is information I would rather not send to a server I don't control, just to decide that &lt;code&gt;LICENSE&lt;/code&gt; is a document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing.&lt;/strong&gt; No API key, no per-call billing, no rate limits. I can run it on a schedule forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is swappable.&lt;/strong&gt; The model is one line, &lt;code&gt;MODEL = "gemma3:4b"&lt;/code&gt;. Changing to another open model is a one-line edit, and I can compare how different models handle the awkward files.&lt;/p&gt;

&lt;p&gt;A closed API would have worked for this task too. But for something that touches the contents of my personal folders and should run quietly in the background, running it locally was the better fit, not just a nice-to-have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model only sees file names, never contents.&lt;/strong&gt; My test files were empty &lt;code&gt;touch&lt;/code&gt; files, so this was purely name-based.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It makes mistakes.&lt;/strong&gt; &lt;code&gt;data.bin&lt;/code&gt; went to &lt;code&gt;Archives&lt;/code&gt;, and &lt;code&gt;Other&lt;/code&gt; would have been the better answer. It made the same choice in both runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The small model is a guess on ambiguous names.&lt;/strong&gt; &lt;code&gt;README&lt;/code&gt; and &lt;code&gt;notes_2026&lt;/code&gt; landing in &lt;code&gt;Documents&lt;/code&gt; is reasonable, but it is a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No undo.&lt;/strong&gt; That is exactly why preview is the default. Only top-level files are handled, not subfolders.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Read a short text snippet for ambiguous files, add an undo log, and run it on a schedule so the folder never gets bad again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;One command, a few seconds, no internet, and a folder I had ignored for months is tidy. Do the work once, then let the sloth handle it.&lt;/p&gt;

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