Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

photo-2-notebook-post

Turn a folder of lab photos into a draft Tumbling Oysters notebook post, using open-weight models running locally on your own machine. No cloud inference, no API keys, works offline.

python3 scripts/p2n.py draft ~/Desktop/oyster-photos --note "35C trial, day 2. Counted morts at 10am."

That writes drafts/2026-07-30-<slug>/index.qmd plus an images/ folder — a complete post with the notebook's frontmatter, draft: true, and the AI-use badge already in place. Review it, fix what's wrong, then promote it into the notebook.

Why photos

Field and bench work already writes itself down — on whiteboards, tank labels, tube racks, datasheets, and instrument displays. The local vision model reads that text back out, and the drafting step arranges it into a logbook entry in the order the photos were taken. Your --note says what actually happened; the photos supply the details you'd otherwise have to retype.

Requirements

  • macOS — image conversion uses the built-in sips. No Pillow, ImageMagick, or exiftool.
  • Python 3.8+, stdlib only. Nothing to install.
  • ollama running locally, with a vision-capable model:
    ollama pull gemma4
    gemma4:latest is the default and, on a 24 GB M4, the best of the locally installed options for this job. The tool refuses to run on a text-only model rather than silently describing nothing.

Usage

# Draft from a folder, staged in this repo for review (default)
python3 scripts/p2n.py draft ~/Desktop/photos --note "what I did"

# Draft from specific files
python3 scripts/p2n.py draft IMG_0329.HEIC IMG_0335.HEIC --note-file today.txt

# Write a real numbered post into the notebook instead
python3 scripts/p2n.py draft ~/Desktop/photos --note "..." --repo ~/github/tumbling-oysters

# See what the model read off the photos, and stop there
python3 scripts/p2n.py draft ~/Desktop/photos --stage observations

# Produce the post JSON without writing any files
python3 scripts/p2n.py draft ~/Desktop/photos --dry-run

Useful flags: --model (vision), --draft-model (prose), --date MM-DD-YYYY, --no-cache, --json, --quiet, --workdir to keep the converted JPEGs.

With --repo, the post is scaffolded by scripts/new_post.py, which numbers it max(prefix) + 1 — the notebook has repeated prefixes, so a count would be wrong. Nothing is ever rendered or committed for you.

How it works

Three stages, each a script you can run on its own. Every one prints a single JSON blob on stdout, and {"error": ...} with exit 1 on failure.

Stage Script What it does
1 prep_images.py Converts photos to JPEG at 1568 px longest edge via sips, reads capture times, sorts chronologically
2 describe_photos.py One local vision call per photo → structured observations (legible_text, numbers, uncertain, …)
3 draft_post.py One local text call → title, description, slug, categories, body Markdown

p2n.py runs all three and writes the post folder.

HEIC conversion isn't optional: ollama rejects HEIC bytes outright, and iPhone photos are HEIC. Stage 2 caches by the original file's SHA-256 under ~/.cache/photo-2-notebook-post/, so re-drafting the same photos costs nothing — which matters at roughly 10–30 s per photo.

The prompts live in prompts/observe.md and prompts/draft.md. They're the part most worth editing; the voice rules in draft.md came from real posts.

What the model is not trusted with. Structure is enforced in Python, not requested in a prompt: categories are filtered against the notebook's fixed list, the slug and date are rebuilt (the date is always today), and every photo is guaranteed to appear in the body exactly once — invented image references are dropped and forgotten photos are appended.

Known limitations

Check the numbers against the photos. This is the one that matters. On handwritten datasheets, an 8B vision model makes ordinary OCR mistakes — in testing it read a 6/1 as 4/1 and turned a "MANCHESTER" header into "MACKMASTER" — and it does not reliably report its own uncertainty, even when the prompt asks it to. The drafting step copies numbers through verbatim rather than inventing them, so an error you see in a draft came from misreading the photo, and the original is right there in images/. Every post lands as draft: true for exactly this reason. Printed text, screens, and typed labels are read much more reliably than handwriting.

Other things to know:

  • Photos are ordered by capture time. Photos with no capture metadata fall back to file modification time, which may not be the order you took them in; a warning says so.
  • The model won't interpret results or propose mechanisms, by design. If a post needs a conclusion, put it in --note or write it in yourself.
  • gpt-oss:20b currently fails to load under ollama 0.30.10 on this machine (failed to load model) — a stale local blob, not a limitation of this tool.

Tests

python3 -m unittest discover tests

60 tests, no network and no model required — the ollama calls are mocked and the image fixtures are generated. sips is exercised for real, since it's fast and always present.

Related

  • tumbling-oysters — the notebook itself. Its packaged notebook-post skill is where scripts/new_post.py, assets/index_template.qmd, and the voice rules came from.
  • LabNotebook-Summarizer — same house conventions for scripts and tests.
  • ROADMAP.md — what's deliberately left for later, including voice-memo support.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages