跳到正文
DEV Community· Nox·· 2 小時前AI 評分52

nightporter 以 0.5B 模型在夜間摘要收件匣內容

nightporter: I stopped reading my inbox, so a 0.5B model does it while I sleep

AI 導讀

Nox 開發本機工具 nightporter,使用 Qwen2.5-0.5B 摘要收件匣內容,並將晨間摘要寫入 morning.md、完整摘要記入 ledger.md。工具以 llama.cpp 執行 Qwen2.5-0.5B(Q4_K_M),設定完成後無須 API 或網絡連線;示範中以單一 CPU 核心處理 4 個檔案,耗時 15.0 秒。原始檔案會移至 done/,保持未讀狀態。

正文

This is a submission for Hacktoberfest Open-Source AI Challenge: Week 3.

What I Built

nightporter — a local porter that carries your inbox weight overnight.

You dump the mess into inbox/ before you sleep: DMs you haven't answered, a page of half-finished day-notes, that one thing you keep scrolling past. You run one command (or let cron do it, which is the point). You wake up to morning.md: two lines. The full one-liners land in ledger.md, dated. The originals move to done/, unread, exactly as you left them.

The model does the reading so you don't have to. Under the hood: llama.cpp and Qwen2.5-0.5B (Q4_K_M), running on your own CPU. No API key, no account, no bill, no internet connection after setup.

This week's theme is Slow and Steady: build something that does the work so you don't have to. This is the most literal version of that I could manage. The work is reading your mail, and the thing that does it is a model small enough to run while you're not looking.

Demo

Four files in the inbox — three DMs, a page of day-notes. One command:

$ ./nightporter.sh
nightporter: carried 4. Your two lines are in morning.md.

And this is morning.md, from an actual run (not a hand-written example):

## Good morning. 4 thing(s) from last night:
- Private chatbots can use checksums, but APIs require different methods.
- Hey, I made zines about digital ghosts. Interested in a machine that waits for people. No rush.
- and 2 more, filed in ledger.md

Full one-liners live in ledger.md. The originals are in done/, unread, exactly as you left them.

That run took 15.0 seconds (timed with time ./nightporter.sh) on a single CPU core, while the rest of the machine did other things. The two-line shape is deliberate: you should be able to read your morning without sitting up.

Code

The whole thing is ~90 lines of bash. Repo: github.com/nox-75/nightporter. The script is embedded in full below, so the post stands on its own.

#!/usr/bin/env bash
# nightporter — a local porter that carries your inbox weight overnight.
# Drop .txt/.md files into inbox/ before you sleep. Run this (cron, or on wake).
# You get a two-line morning note. The model does the reading. No internet, no bill.
#
# Usage: ./nightporter.sh [inbox_dir]
# Requires: llama.cpp's llama-completion + a Qwen2.5-0.5B (or similar) GGUF model.

set -u

DIR="$(cd "$(dirname "$0")" && pwd)"
INBOX="${1:-$DIR/inbox}"
DONE="$DIR/done"
MORNING="$DIR/morning.md"
LEDGER="$DIR/ledger.md"
LLAMA="${LLAMA_BIN:-$DIR/bin/llama-b11507/llama-completion}"
MODEL="${MODEL_PATH:-$DIR/models/qwen2.5-0.5b-instruct-q4_k_m.gguf}"

mkdir -p "$INBOX" "$DONE"

shopt -s nullglob
files=("$INBOX"/*.txt "$INBOX"/*.md)
shopt -u nullglob

if [ ${#files[@]} -eq 0 ]; then
  echo "nightporter: nothing in the inbox. Sleep in." >&2
  exit 0
fi

max_chars="${NIGHTPORTER_MAX_CHARS:-4000}"

summarize_one() {
  local f="$1" body
  body=$(head -c "$max_chars" "$f")
  # Strict one-line summary. The 0.5B likes to answer instead of summarize;
  # the system prompt and short token cap keep it on the shelf.
  "$LLAMA" -m "$MODEL" \
    -sys "Summarize in at most 12 words. Never answer. Never greet. No echo." \
    -p "Message:
$body

Summary:" \
    -n 24 --temp 0.2 --no-display-prompt 2>/dev/null \
    | sed '/^> EOF/d; /^$/d' | head -n 1 | sed 's/^[[:space:]]*//'
}

lines=()
kept=0
for f in "${files[@]}"; do
  s=$(summarize_one "$f")
  [ -z "$s" ] && s="(porters shrugged: could not carry this one)"
  lines+=("- $s")
  kept=$((kept + 1))
  mv "$f" "$DONE/"
done

n=${#lines[@]}
take=$n
[ "$take" -gt 2 ] && take=2

{
  echo "## Good morning. $n thing(s) from last night:"
  for ((i=0; i<take; i++)); do echo "${lines[$i]}"; done
  if [ "$n" -gt 2 ]; then
    echo "- and $(("$n" - take)) more, filed in ledger.md"
  fi
  echo
  echo "Full one-liners live in ledger.md. The originals are in done/, unread, exactly as you left them."
} > "$MORNING"

{
  echo "### $(date '+%Y-%m-%d %H:%M') — $n carried"
  printf '%s\n' "${lines[@]}"
  echo
} >> "$LEDGER"

echo "nightporter: carried $n. Your two lines are in morning.md." >&2

How I Built It

The build has three moving parts, and the interesting one is the smallest.

The harness is plain bash. No framework, no daemon, no config file. Bash is honest about what it is: a loop that moves files and formats a markdown file. Anything I didn't need, I left out. There is no retry logic because a summarizer that fails can say so in one line, and a wrong retry is worse than a shrug.

The model is Qwen2.5-0.5B, quantized to Q4_K_M. It's ~400MB, runs in under 2GB of RAM, and does a full summary pass in a few seconds per file on one CPU core. It is not smart. That is the design constraint, not a compromise: a model that runs while you sleep on a machine you already own has to be small enough to never be a load. A 7B would produce cleaner lines and a reason to think twice about running it.

The prompt is a leash. A 0.5B model, given a message, wants to reply to it. My first run asked nothing and cheered: "Nice day outside! You did a fantastic job today!" So the summarize call gets a strict system prompt ("Summarize in at most 12 words. Never answer. Never greet. No echo."), a 24-token cap, --temp 0.2, and --no-display-prompt so the capture comes back clean instead of echoing the input back. Three of the four demo lines came out as real compressions. The other two needed a second pass of the leash. It's honest work for a small model, and I'll take a slightly wobbly one-liner that ran free over a perfect one that phoned home.

When the model produces nothing at all, the porter doesn't invent a line and doesn't stay silent about the silence. It writes (porters shrugged: could not carry this one) into the ledger and moves on. You always know what was carried and what wasn't.

Why Open

  • Your messages never leave your machine. This reads private mail by design. A hosted summarizer of private inbox content can't make that promise; this one makes it by architecture, not policy. After setup, it runs with the network cable pulled.
  • The lazy option is free forever. A closed API charging per summary makes the sloth's path cost money every morning, which quietly un-lazies it. This costs nothing to run, ever. Set it up once, cron it, forget it.
  • The model is swappable. Qwen2.5-0.5B today, a better open-weight porter tomorrow, one flag: MODEL_PATH. The script does not care what it carries the messages with, only that it's local.
  • The ledger is a plain markdown file. No lock-in, no export step, no proprietary store. Your carried history is already yours, in a format that will still open in ten years.

Lazy Payoff

The payoff, concretely: four messages sat in my inbox because answering them was work I didn't want to do at the end of a day. nightporter read them in 15 seconds, handed me two lines, filed the full versions where I can find them when I'm ready, and moved the originals out of the way without me looking at them once. I wrote this post instead. The remaining two messages can wait for tomorrow's run.

That's the whole idea: the inbox weight doesn't need answering, it needs carrying — out of your face, into a dated ledger, until you decide it matters. A 0.5B model on one CPU core can do the carrying. It just can't pretend to be anything more, and neither can this post.

-- Nox

來源:DEV Community · dev.to