This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady. Built with assistance from OpenAI Codex; code, test results and limitations are disclosed below.
What I Built
Sloth Sort takes the first tedious step out of organizing a downloads folder: deciding where each file probably belongs. It uses a small, trainable multinomial naive Bayes model to classify filenames, then writes a plan for human review. It does not move files.
The open AI component is the classifier itself: all of its training examples, tokenization and likelihood calculations are in the source. This is classical machine learning, not an LLM wrapper. Twenty synthetic Portuguese and English training examples define study, finance, work and personal categories. Replace the training JSON and it learns your own categories instead.
Demo
Save the code below as sloth_sort.py, then create synthetic demo files with Python 3.10 or newer:
python -c "from pathlib import Path; p=Path('demo-input'); p.mkdir(exist_ok=True); [(p/n).touch() for n in ['aula_estatistica_homework.pdf','cliente_reuniao_proposta.docx','fatura_bank_invoice.pdf','ferias_family_viagem.jpg','xyz987.bin']]"
Run:
python sloth_sort.py demo-input --output my-plan.csv
The reproducible synthetic demo produces:
| Filename | Suggested folder | Model score |
|---|---|---|
| aula_estatistica_homework.pdf | study | 0.7256 |
| cliente_reuniao_proposta.docx | work | 0.7323 |
| fatura_bank_invoice.pdf | finance | 0.7256 |
| ferias_family_viagem.jpg | personal | 0.7256 |
| xyz987.bin | review | 0.0000 |
These scores are relative probabilities from a tiny model, not calibrated accuracy claims. Unfamiliar words and scores below 0.60 are sent to review. No private files were used in the demo.
Code
The complete MIT-licensed implementation is included below so the prototype is reproducible without a separate service. Save it as sloth_sort.py.
"""Sloth Sort: an offline, trainable filename classifier. MIT licensed."""
import argparse
from collections import Counter
import csv
import json
import math
from pathlib import Path
import re
import unicodedata
LABELS = {
'study': ['aula estatistica exercicio universidade estudo', 'lecture notes probability homework course', 'resumo calculo algebra faculdade', 'assignment exam lesson regression data science', 'atividade disciplina modelagem aprendizado'],
'finance': ['boleto pagamento banco conta fatura', 'invoice payment receipt tax bank statement', 'recibo compra comprovante financeiro', 'expense budget salary credit debit', 'orcamento imposto nota fiscal'],
'work': ['reuniao cliente projeto proposta trabalho', 'meeting client proposal project agenda', 'contrato empresa relatorio equipe', 'sprint roadmap release business report', 'ata pauta planejamento comercial'],
'personal': ['viagem familia ferias foto aniversario', 'holiday family vacation birthday photo', 'receita cozinha lista mercado pessoal', 'travel passport hotel flight ticket', 'musica lazer passeio diario'],
}
def tokens(text):
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode().lower()
return re.findall(r'[a-z]{2,}', text)
class Classifier:
def __init__(self, examples):
self.counts = {label: Counter(tokens(' '.join(rows))) for label, rows in examples.items()}
self.vocab = set().union(*(set(c) for c in self.counts.values()))
self.totals = {k: sum(v.values()) for k, v in self.counts.items()}
def predict(self, name):
words = [w for w in tokens(Path(name).stem) if w in self.vocab]
if not words:
return 'review', 0.0, []
scores = {k: sum(math.log((c[w] + 1) / (self.totals[k] + len(self.vocab))) for w in words) for k, c in self.counts.items()}
top = max(scores, key=scores.get)
weights = {k: math.exp(v - scores[top]) for k, v in scores.items()}
return top, 1 / sum(weights.values()), sorted(set(words))
def plan(folder, model, threshold):
rows = []
for path in sorted(folder.iterdir()):
if not path.is_file() or path.is_symlink():
continue
label, score, evidence = model.predict(path.name)
if score < threshold:
label = 'review'
rows.append({'file': path.name, 'suggested_folder': label, 'model_score': round(score, 4), 'matched_words': ' '.join(evidence)})
return rows
def main():
parser = argparse.ArgumentParser(description='Suggest folders from filenames using local multinomial naive Bayes. Never moves or reads file contents.')
parser.add_argument('folder', type=Path)
parser.add_argument('--training', type=Path, help='JSON mapping category names to lists of example filenames')
parser.add_argument('--threshold', type=float, default=0.60)
parser.add_argument('--output', type=Path, help='Write a NEW CSV plan; existing files are never overwritten')
args = parser.parse_args()
if not args.folder.is_dir():
parser.error('folder must be an existing directory')
if not 0 <= args.threshold <= 1:
parser.error('threshold must be between zero and one')
examples = json.loads(args.training.read_text(encoding='utf-8')) if args.training else LABELS
if not isinstance(examples, dict) or len(examples) < 2 or any(not isinstance(k, str) or not isinstance(v, list) or not v or any(not isinstance(s, str) for s in v) for k, v in examples.items()):
parser.error('training JSON needs at least two categories, each with a nonempty list of strings')
rows = plan(args.folder, Classifier(examples), args.threshold)
if args.output:
with args.output.open('x', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['file', 'suggested_folder', 'model_score', 'matched_words'])
writer.writeheader()
writer.writerows(rows)
print(json.dumps(rows, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
How I Built It
Unicode normalization allows accented Portuguese words to match their unaccented equivalents. A word-frequency model learns each category, applies Laplace smoothing and calculates log likelihoods. It ignores out-of-vocabulary words and abstains when there is no evidence or the score is too low.
The program runs on Python's standard library. No dependency installation, API key or external inference is required. It reads directory-entry names, never document contents. It does not recurse or follow symlinks, and exclusive CSV creation prevents overwriting an existing plan.
Four tests passed locally: known and unknown filenames, custom training, no mutation or recursion, and abstention at a strict threshold. These are smoke and safety checks, not a benchmark. I have not measured real-world accuracy or time saved.
Why Does Open Innovation Matter?
For this narrow task, a small inspectable model makes the tradeoffs visible. The dataset, scoring function and review threshold can all be changed. The program works offline and filenames stay on the machine. There is no inference bill and no vendor account to keep alive.
The lazy payoff is intentionally modest: one local command creates a first-pass sorting plan. A person still reviews ambiguous files and decides what to move. A future version could learn from corrected plans, but that is not implemented here.
Reproducible tests
Save the following as test_sloth_sort.py beside the implementation and run python -m unittest -v.
import tempfile
from pathlib import Path
import unittest
from sloth_sort import Classifier, LABELS, plan
class Tests(unittest.TestCase):
def test_known_and_unseen(self):
model = Classifier(LABELS)
for name, label in [('aula_estatistica_homework.pdf', 'study'), ('fatura_bank_invoice.pdf', 'finance'), ('cliente_reuniao_proposta.docx', 'work'), ('ferias_family_viagem.jpg', 'personal')]:
self.assertEqual(model.predict(name)[0], label)
self.assertEqual(model.predict('xyz987.bin')[0], 'review')
def test_custom_training_changes_prediction(self):
model = Classifier({'cats': ['whiskers feline kitten'], 'dogs': ['puppy canine bark']})
self.assertEqual(model.predict('kitten_whiskers.txt')[0], 'cats')
def test_plan_never_changes_files_or_recurses(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
(root / 'aula_estatistica.pdf').write_bytes(b'private sample')
(root / 'nested').mkdir()
(root / 'nested' / 'invoice.pdf').touch()
before = {str(p.relative_to(root)): p.read_bytes() for p in root.rglob('*') if p.is_file()}
rows = plan(root, Classifier(LABELS), 0.6)
after = {str(p.relative_to(root)): p.read_bytes() for p in root.rglob('*') if p.is_file()}
self.assertEqual(before, after)
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0]['suggested_folder'], 'review')
def test_threshold_abstains(self):
with tempfile.TemporaryDirectory() as d:
(Path(d) / 'aula.pdf').touch()
self.assertEqual(plan(Path(d), Classifier(LABELS), 1.0)[0]['suggested_folder'], 'review')
if __name__ == '__main__':
unittest.main()
License
MIT License
Copyright (c) 2026 Thiago Oliveira Lima
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Top comments (0)