Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ CHAPTERWISE_ADMIN_PASSWORD=change-me
CHAPTERWISE_PUBLIC_URL=http://localhost:8100
# Seconds between automatic scans of the books folder
CHAPTERWISE_SCAN_INTERVAL=600

# --- Telegram (bot) -----------------------------------------------------------
TELEGRAM_BOT_TOKEN=
# The only chat the bot answers. Leave empty, send /start to the bot, and it replies with your ID.
TELEGRAM_CHAT_ID=
CHAPTERWISE_DELIVERY_TIME=07:30
CHAPTERWISE_TIMEZONE=Europe/Rome
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
cancel-in-progress: false
env:
REMOTE: root@${{ secrets.DEPLOY_HOST }}
REMOTE_DIR: /root/chapterwise
REMOTE_DIR: ${{ vars.DEPLOY_DIR || '/root/chapterwise' }}
TAG: sha-${{ github.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -161,5 +161,5 @@ jobs:
sleep 5
done
echo "::error::Health check failed. Recent logs:"
ssh "$REMOTE" "cd $REMOTE_DIR && ./cw.sh ps && ./cw.sh logs --tail 80 migrate web worker" || true
ssh "$REMOTE" "cd $REMOTE_DIR && ./cw.sh ps && ./cw.sh logs --tail 80 migrate web worker bot" || true
exit 1
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ chapterwise splits a book into sections using its own table of contents, summari
same request is never paid for twice. It's built to run next to a self-hosted library such as
[BookOrbit](https://github.com/bookorbit/bookorbit), reading the same books folder read-only.

**Status:** Phase 2. A command-line tool plus a self-hosted web app (library, chapters, summaries)
with a background worker, deployed by CI/CD. Telegram delivery comes next (see [Roadmap](#roadmap)).
**Status:** Phase 3. A command-line tool, a self-hosted web app (library, chapters, summaries) with a
background worker, and a personal Telegram bot that sends one summarized section every morning.
Deployed by CI/CD (see [Roadmap](#roadmap)).

```
books folder (read-only) ──► worker: scan · parse · summarize ──► DeepSeek API
│
Postgres (books, sections, job queue, LLM cache)
│
browser ──► Caddy ──► web: login · library · book · section
│
Telegram ◄──── bot: daily delivery · /books · /reading · /next
```

## Supported formats
Expand Down Expand Up @@ -69,6 +72,7 @@ Three processes from one image or install, sharing one Postgres database:
| `chapterwise migrate` | Create or upgrade the schema (Alembic) |
| `chapterwise serve` | Web UI on port 8100 |
| `chapterwise worker` | Scans the books folder every `CHAPTERWISE_SCAN_INTERVAL` seconds, parses new or changed files, runs summaries |
| `chapterwise bot` | Telegram bot (long polling, so no open port): daily delivery and commands |

- **Job queue:** a Postgres table claimed with `FOR UPDATE SKIP LOCKED`, so there's no Redis to run.
- **Retries:** failed jobs retry with exponential backoff, up to 3 attempts. Jobs interrupted by a
Expand All @@ -87,6 +91,26 @@ chapterwise migrate
chapterwise serve & chapterwise worker
```

## Daily reading on Telegram

Pick a book with `/books` (or **Read daily on Telegram** on its web page). Every morning at
`CHAPTERWISE_DELIVERY_TIME` (07:30 Europe/Rome by default) the bot sends the next section's summary,
with buttons to get the **Next** one right away, **Pause**, or open it on the web.

- **Summaries are prepared ahead:** the next 2 sections are queued for the worker, so the morning
message is ready on time.
- **Missed deliveries catch up:** if the bot was down at 07:30, it sends that day's section on its
first check after coming back. Each section is sent once.
- **Next** sends another section now, and tomorrow's delivery still comes. If that summary isn't
ready yet, it arrives as soon as it is.
- **Commands:** `/books [word]` picks a book, `/reading` shows progress with Pause, Resume and Stop,
and `/next` sends the next section now.
- **Private:** only `TELEGRAM_CHAT_ID` is answered. Until it's set, `/start` replies with your chat
ID to put there.
- **Formatting:** summaries use Telegram's HTML subset, so lists become bullet lines and long
summaries are cut at a paragraph boundary. If Telegram ever rejects the formatting, the message
is resent as plain text.

## Deployment

Pushes to `main` run the tests, build the image to `ghcr.io/aminghuf/chapterwise:sha-<commit>`, then
Expand All @@ -97,13 +121,15 @@ deploy over SSH to the VPS. The deploy writes the image tag, backs up the databa

1. **DNS:** `A read → <server IP>`.
2. **Server:** copy `deploy/server-bootstrap.sh` to the VPS and run `bash server-bootstrap.sh`. It
creates `/root/chapterwise/.env` with generated secrets and asks for the web password and API key.
creates `/root/chapterwise/.env` with generated secrets and asks for the web password, the API key
and the Telegram bot token and chat ID. Re-running it only adds what's missing.
3. **Caddy:** add `deploy/read.aminghuf.com.caddy` to the Caddyfile and reload Caddy.
4. **GitHub secrets** (Settings → Secrets and variables → Actions):
- `DEPLOY_SSH_KEY`: a private key allowed to SSH in as root
- `DEPLOY_HOST`: the server's IP
- `DEPLOY_KNOWN_HOSTS`: the server's host-key line, from `ssh-keyscan -t ed25519 <ip>` checked
against the server
- Optional variable `DEPLOY_DIR`: the server folder to deploy to (default `/root/chapterwise`)

**On the server:** `cd /root/chapterwise && ./cw.sh ps | logs -f worker | backup`.

Expand All @@ -124,6 +150,10 @@ Set these in `.env` or the environment:
| `CHAPTERWISE_ADMIN_PASSWORD` | | The web UI password |
| `CHAPTERWISE_PUBLIC_URL` | `http://localhost:8100` | With `https://`, cookies are marked Secure |
| `CHAPTERWISE_SCAN_INTERVAL` | `600` | Seconds between library scans |
| `TELEGRAM_BOT_TOKEN` | | From @BotFather; the bot idles without it |
| `TELEGRAM_CHAT_ID` | | The only chat the bot answers |
| `CHAPTERWISE_DELIVERY_TIME` | `07:30` | Daily delivery, local time |
| `CHAPTERWISE_TIMEZONE` | `Europe/Rome` | Time zone for the delivery time |

## Development

Expand All @@ -139,7 +169,7 @@ ruff check . && ruff format .

- [x] **Phase 1:** parsers, chunking, cached map-reduce summaries, CLI
- [x] **Phase 2:** Postgres, background worker, books-folder sync, web UI at read.aminghuf.com, CI/CD to the VPS
- [ ] **Phase 3:** personal Telegram bot with scheduled delivery
- [x] **Phase 3:** personal Telegram bot with scheduled delivery
- [ ] **Phase 4:** whole-book summaries, micro-doses (original text plus recall questions), Markdown playbooks as a source

## Notes
Expand Down
10 changes: 10 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name: chapterwise
x-app: &app
image: ${IMAGE:?set by image.env}
restart: unless-stopped
init: true # forwards stop signals and reaps child processes
env_file: .env
environment:
DATABASE_URL: postgresql+psycopg://chapterwise:${POSTGRES_PASSWORD:?}@db:5432/chapterwise
Expand Down Expand Up @@ -76,3 +77,12 @@ services:
depends_on:
migrate:
condition: service_completed_successfully

bot:
<<: *app
command: ["chapterwise", "bot"]
healthcheck:
disable: true
depends_on:
migrate:
condition: service_completed_successfully
11 changes: 11 additions & 0 deletions deploy/server-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ set_if_missing CHAPTERWISE_PUBLIC_URL https://read.aminghuf.com
set_if_missing BOOKS_HOST_PATH /root/bookorbit/books
set_if_missing CHAPTERWISE_MODEL deepseek-flash
set_if_missing TZ Europe/Rome
set_if_missing CHAPTERWISE_TIMEZONE Europe/Rome
set_if_missing CHAPTERWISE_DELIVERY_TIME 07:30
set_if_missing POSTGRES_PASSWORD "$(openssl rand -hex 24)"
set_if_missing CHAPTERWISE_SECRET_KEY "$(openssl rand -hex 32)"

Expand All @@ -31,5 +33,14 @@ if ! grep -q '^DEEPSEEK_API_KEY=' "$ENV"; then
echo "DEEPSEEK_API_KEY=$key" >> "$ENV"
fi

if ! grep -q '^TELEGRAM_BOT_TOKEN=' "$ENV"; then
read -rsp "Telegram bot token from @BotFather (Enter to add later): " token; echo
echo "TELEGRAM_BOT_TOKEN=$token" >> "$ENV"
fi
if ! grep -q '^TELEGRAM_CHAT_ID=' "$ENV"; then
read -rp "Your Telegram chat ID (Enter if unknown: send /start to the bot and it will tell you): " chat
echo "TELEGRAM_CHAT_ID=$chat" >> "$ENV"
fi

[ -d /root/bookorbit/books ] || echo "Warning: /root/bookorbit/books doesn't exist; set BOOKS_HOST_PATH in $ENV"
echo "Ready: $ENV (edit it any time, then run ./cw.sh up -d)"
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "chapterwise"
version = "0.2.0"
version = "0.3.0"
description = "Summarize your books chapter by chapter with an LLM and read them in micro-doses."
readme = "README.md"
license = "MIT"
Expand All @@ -24,8 +24,10 @@ dependencies = [
"pypdf>=5.0",
"python-dotenv>=1.0",
"python-multipart>=0.0.9",
"python-telegram-bot[job-queue]>=21.0",
"sqlalchemy>=2.0",
"typer>=0.12",
"tzdata>=2024.1", # time zones in slim containers
"uvicorn>=0.30",
]

Expand Down
2 changes: 1 addition & 1 deletion src/chapterwise/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Summarize books chapter by chapter with an LLM."""

__version__ = "0.2.0"
__version__ = "0.3.0"
Loading
Loading