Turn a screenshot, image(s), PDF, pasted text, or the current conversation into a self-contained, styled HTML blogpost — Gemini analyses the supplied content, then generates a complete HTML5 article (inline responsive CSS, optional MathJax) that expands on the concepts it found. The result is written to disk and opened in your browser.
See one: a full, rendered post — generated from a single screenshot, served exactly as the tool wrote it.
A short demo GIF / video will live here.
Two phases, both Gemini:
- Analyse the inputs (text + images extracted from PDFs, raw images, or pasted text).
- Generate a self-contained blogpost into its own folder
blogposts/<name>/— anindex.htmlplus any referenced images — then open it in the browser.
pip install -r requirements.txtSet a Gemini API key — either export it or drop a .env next to main.py:
export GEMINI_API_KEY=... # or:
echo 'GEMINI_API_KEY=...' > .envOptional .env knobs:
| Variable | Purpose | Default |
|---|---|---|
GEMINI_API_KEY |
required — Gemini API key | — |
COMPETENT_GEMINI_MODELS, STRONG_GEMINI_MODELS |
comma-separated fallback model lists, tried after the preferred model | built-in fallbacks |
BROWSER_PATH |
browser to open the result | /usr/bin/firefox |
BLOGGEN_ENV |
explicit path to a .env file |
— |
The preferred model is gemini-3.6-flash; the env model lists (and a small
built-in list) act as fallbacks.
# From one or more images
python main.py --analyze-only --image-files a.png b.jpg
# From a PDF (extracts its text AND embedded images)
python main.py --analyze-only --text-files paper.pdf
# From a single screenshot (screenshot-specific prompt + hero image)
python main.py --analyze-only --screenshot-path shot.png
# From literal text — write it to a file first
printf '%s' "the text to write up" > /tmp/sb_text.txt
python main.py --analyze-only --raw-text-file /tmp/sb_text.txt
# Mix any of the above in one post
python main.py --analyze-only --text-files notes.pdf --image-files fig1.png --raw-text-file /tmp/sb_text.txtThe tool prints Saved HTML: <path> for the generated file.
Every publish writes blogposts/<name>/source.json and stamps the same record
into the HTML head as <meta name="bloggen:provenance"> (the sidecar is easy to
read; the meta tag survives the folder being moved). It records the working
directory, the git repo + branch + commit if there is one, the input files, the
host and the mode (gemini or agent).
So run bloggen from the project the post is about — that is the signal that makes "which posts came out of my mujoco training?" answerable later. When the origin is more specific than the cwd, or isn't a file at all, name it:
python publish.py --html post.html --source .state/status.html
python main.py --analyze-only --text-files paper.pdf --source https://arxiv.org/abs/…The index page turns each recorded source into a filter chip and a clickable tag on the card. Posts published before this existed simply carry no source; nothing is guessed for them.
build_index.py scans blogposts/ and writes a single static page,
blogposts/index.html, that links to every post:
python build_index.py # regenerate
python build_index.py --open # …and open itIt reads each post's <title>, headings, first paragraphs and full body text,
embeds the whole corpus inline and ranks queries client-side with BM25F —
four weighted fields (title 8× · headings 3× · description 2× · body 1×), prefix
expansion for partial words, accent folding (wohler finds Wöhler) and a
phrase bonus. Works straight off file://: no server, no network, no
dependencies beyond the standard library.
The page is a two-pane layout — a scrollable result list plus a live preview
iframe on wide screens, list-only below 1024 px — and never produces a
page-level scrollbar; only the inner panes scroll. Legacy loose
blogposts/blogpost_*.html files are indexed too, and ones that are
byte-identical to a <slug>/index.html are dropped as duplicates.
Re-run it after publishing; the page is regenerated from scratch each time.
python main.py --installregisters a desktop launcher (and the Claude Code skill). This needs thecli-tools-kitpackage; blogpost generation itself works without it via the CLI flags above.python main.py --install-skill/--uninstall-skillregister a Claude Codebloggenskill (~/.claude/skills/bloggen/SKILL.md).- Running with no flags opens an interactive tkinter content window if the
optional
_shared.guipackage is available; otherwise it points you to the CLI flags.
MIT — see LICENSE.