NoteGenerator is a CLI workflow for turning PDF courseware into image-based AI notes and writing the result into Notion.
It renders each PDF page to an image, uploads those images to Cloudinary, sends the images to a multimodal model for page-by-page analysis, generates a deck summary, and writes both summary and per-page content into Notion with block-safe rendering.
- Analyze lecture slides from images instead of relying on OCR-only text extraction
- Route files to an existing Notion page automatically when the user does not provide a URL
- Resume long jobs from the failed page after transient network errors
- Keep detailed logs and structured checkpoints for monitoring and recovery
- Write Notion body content and comments using rendering-safe structures
- Expose the whole workflow through a command-line interface suitable for OpenClaw agents
flowchart TD
A["PDF input"] --> B["Render each page to PNG"]
B --> C["Upload page images to Cloudinary"]
C --> D["Send image URLs to Kimi vision model"]
D --> E["Generate page analyses"]
E --> F["Generate deck summary"]
F --> G["Convert body Markdown to Notion blocks"]
E --> H["Convert page analysis to comment-safe rich_text"]
G --> I["Write summary and page body to Notion"]
H --> J["Attach per-page comments to heading blocks"]
I --> K["manifest.json / summary.md / run.log / checkpoint.json"]
J --> K
- Fixed model target:
kimi-k2.5 - Page analysis is image-first
- Cover pages, agenda pages, and transition pages are explicitly prevented from being over-interpreted
- Notion body content is written as blocks
- Per-page analysis is attached as comments on the
第 N 页heading block - Comment-unsafe content such as tables and fenced code blocks is moved into the page body
- Long-running jobs maintain
checkpoint.jsonandheartbeat.json - Multi-file processing is serial and ordered by file modification time
.
├── .env.example
├── docs/
│ └── page_analyzer_notion_manual.md
├── skills/
│ └── notegenerator-openclaw/
│ └── SKILL.md
├── src/notegenerator_cli/
│ ├── cli.py
│ ├── workflow.py
│ ├── notion_utils.py
│ ├── ai_client.py
│ ├── router.py
│ └── install_agent.py
└── tests/
- Python 3.11+
- A Notion integration token with access to the target pages
- A Cloudinary account
- An OpenAI-compatible multimodal endpoint
- Optional: OpenClaw if you want to run the workflow from an agent
git clone git@github.com:BojayL/NoteGenerator.git
cd NoteGenerator
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Copy .env.example to .env and replace placeholders:
cp .env.example .envSupported environment variables:
NOTEGENERATOR_AI_BASE_URLNOTEGENERATOR_AI_API_KEYNOTEGENERATOR_MODELNOTION_API_KEYNOTION_VERSIONCLOUDINARY_URLNOTEGENERATOR_PAGE_DPINOTEGENERATOR_DEFAULT_TELEGRAM_TOKENNOTEGENERATOR_DEFAULT_TELEGRAM_USER_ID
Configuration load order:
- Project
.env - Process environment variables
~/.openclaw/openclaw.jsonBailian defaults~/.config/notion/api_key
Environment check:
notegenerator doctorSingle file with explicit Notion target:
notegenerator ingest /absolute/path/course.pdf "https://www.notion.so/target-page"Batch import with explicit target:
notegenerator batch /path/a.pdf /path/b.pdf --notion-url "https://www.notion.so/target-page"Batch import with automatic routing:
notegenerator batch /path/a.pdf /path/b.pdf --auto-routeRoute without writing:
notegenerator route /absolute/path/course.pdfInspect a run:
notegenerator inspect-run /absolute/path/to/run-dirResume from a failed page:
notegenerator resume-analysis /absolute/path/to/run-dir --start-page 17Resume only a range:
notegenerator resume-analysis /absolute/path/to/run-dir --start-page 17 --end-page 24Finalize an existing run when page analysis is complete but the summary is still missing:
notegenerator finalize-run /absolute/path/to/run-dirOpenClaw inbound entrypoint:
notegenerator openclaw-run --scan-inbound --limit 5Install the companion OpenClaw agent bundle:
notegenerator install-openclaw-agentEach execution creates a dedicated directory under runs/ containing:
run.log: human-readable runtime logevents.jsonl: structured event streamimages/: rendered page PNG filespage_analyses/: page-level analysis outputssummary.md: deck-level summarymanifest.json: final output manifestcheckpoint.json: resumable page stateheartbeat.json: live progress marker, removed automatically when doneerror.json: failure details when a run aborts
If the job fails because of a transient connection issue, do not restart from page 1.
Use inspect-run to read next_page_to_analyze, then resume:
notegenerator inspect-run /absolute/path/to/run-dir
notegenerator resume-analysis /absolute/path/to/run-dir --start-page <next_page>The workflow reuses existing page renders, Cloudinary URLs, and completed page analyses before regenerating the summary and completing the Notion write.
If inspect-run shows:
next_page_to_analyze = nullpage_content_written = truesummary_written = false
then page analysis and page-body/comment writing are already finished, and only the deck summary is missing. In that case, do not rerun ingest or resume-analysis. Use:
notegenerator finalize-run /absolute/path/to/run-dirfinalize-run reuses local page_analyses/, regenerates only the summary, and appends only the missing summary blocks to Notion.
Body content is converted into Notion blocks. The workflow currently writes:
- headings
- paragraphs
- bulleted lists
- numbered lists
- quotes
- callouts
- dividers
- code blocks
- tables
- external images
Per-page analysis is attached to the 第 N 页 heading block as comments.
Only comment-safe rich_text should remain in comments:
- plain text
- inline bold / italic
- inline code
- links
- simple one-level textual lists
The following should be moved to the page body instead:
- Markdown tables
- fenced code blocks
- block-level callouts and quotes
- images
- nested structures
The repository includes support for installing a dedicated OpenClaw agent workspace.
Recommended agent behavior:
- Trigger
openclaw-runoringest - Capture
run_dir - Every 2 minutes, call
inspect-run - If
next_page_to_analyzehas a value, resume from that page - If
next_page_to_analyze = nulland onlysummary_writtenis false, callfinalize-run - Report the PDF path, selected Notion page, run directory, and final result
The reusable OpenClaw-oriented skill is included at:
skills/notegenerator-openclaw/SKILL.md
This section describes a practical end-to-end deployment path for running NoteGenerator from an OpenClaw agent.
Clone the repository, create the virtual environment, and install the package:
git clone git@github.com:BojayL/NoteGenerator.git
cd NoteGenerator
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Create .env from the public template:
cp .env.example .envAt minimum, fill in:
NOTEGENERATOR_AI_API_KEYNOTION_API_KEYCLOUDINARY_URL
If you want the OpenClaw installer to bind a Telegram bot without passing CLI flags every time, also fill in:
NOTEGENERATOR_DEFAULT_TELEGRAM_TOKENNOTEGENERATOR_DEFAULT_TELEGRAM_USER_ID
Validate the environment:
./.venv/bin/notegenerator doctorThis repository does not install OpenClaw itself. Before installing the agent bundle, you should already have:
- a working OpenClaw installation
- a writable
~/.openclaw/openclaw.json - an enabled Telegram channel in OpenClaw if you want Telegram-triggered imports
The CLI can also reuse OpenClaw model defaults from ~/.openclaw/openclaw.json if NOTEGENERATOR_AI_API_KEY is not provided in .env.
The simplest install path is:
cd NoteGenerator
source .venv/bin/activate
notegenerator install-openclaw-agentIf you want to make the deployment explicit, pass the full parameter set:
notegenerator install-openclaw-agent \
--agent-id courseware-notion-router \
--account-id courseware-notion-router \
--display-name "Courseware Importer" \
--persona-theme "Receives PDF courseware, routes it to the right Notion page, and runs the CLI workflow." \
--emoji "📚" \
--telegram-token "$NOTEGENERATOR_DEFAULT_TELEGRAM_TOKEN" \
--telegram-user-id "$NOTEGENERATOR_DEFAULT_TELEGRAM_USER_ID"The installer creates an OpenClaw agent workspace under:
~/.openclaw/agents/<agent_id>/The generated bundle includes:
workspace/AGENTS.mdworkspace/IDENTITY.mdworkspace/TOOLS.mdworkspace/HEARTBEAT.mdworkspace/USER.mdworkspace/SOUL.mdworkspace/skills/courseware-importer/SKILL.md
It also updates ~/.openclaw/openclaw.json by:
- registering the agent
- adding the Telegram account binding
- connecting the agent ID to the Telegram account ID you passed
If ~/.openclaw/agents/pingping/agent/models.json exists locally, the installer copies it into the new agent's agent/ directory as a convenience.
Some OpenClaw setups hot-reload configuration changes and some do not.
If your runtime does not pick up the new agent automatically, restart the relevant OpenClaw process after running the installer.
You should verify that:
- the new agent directory exists
~/.openclaw/openclaw.jsonnow contains your agent and Telegram binding- the Telegram bot token/account ID pair is present under the Telegram accounts section
The generated courseware-importer skill encodes the operational workflow:
- If the user provides a Notion page URL, the agent should run
ingest. - If the user does not provide a page URL, the agent should route or use
openclaw-run --scan-inbound. - Once a run starts, the agent should capture the
run_dir. - Every 2 minutes, the agent should call
inspect-run. - If network instability interrupts analysis, the agent should resume from
next_page_to_analyze. - If multiple files arrive, they should be processed serially in modification-time order.
- When the run is complete,
heartbeat.jsonshould disappear automatically.
There are two common ways to operate it.
Use Telegram as the transport:
- Send one or more PDF files to the Telegram bot bound to the installed agent.
- If you already know the target Notion page, include the URL in the conversation.
- If no URL is provided, let the agent route automatically.
- The agent should process the files with the installed workflow rules.
If your OpenClaw setup writes incoming media to ~/.openclaw/media/inbound, you can run:
cd NoteGenerator
source .venv/bin/activate
notegenerator openclaw-run --scan-inbound --limit 5This is especially useful for debugging the same workflow outside the full agent loop.
During a long run, inspect the active run directory:
notegenerator inspect-run /absolute/path/to/run-dirImportant fields:
uploaded_pagesanalyzed_pagesnext_page_to_analyzenotion_writtenheartbeat_exists
If the run fails due to a transient connection problem, resume from the failed page:
notegenerator resume-analysis /absolute/path/to/run-dir --start-page <next_page>Use the next_page_to_analyze value returned by inspect-run.
After deployment, verify the stack in this order:
notegenerator doctornotegenerator route /absolute/path/to/sample.pdfnotegenerator ingest /absolute/path/to/sample.pdf "https://www.notion.so/target-page"notegenerator inspect-run /absolute/path/to/run-dir
For OpenClaw-specific verification, confirm that:
- the agent workspace was created
- Telegram binding was added
- a sample PDF can be processed through the installed agent
- per-page comments and body blocks render correctly in Notion
NOTEGENERATOR_DEFAULT_TELEGRAM_TOKENandNOTEGENERATOR_DEFAULT_TELEGRAM_USER_IDare only defaults for the installer. You can always override them via CLI flags.- The installed OpenClaw agent is meant to automate the same CLI you can run manually. Debugging the CLI first is usually faster than debugging the full agent loop first.
- If your OpenClaw environment already has a preferred agent naming convention, pass your own
--agent-idand--account-idduring installation.
The page analyzer is constrained by:
docs/page_analyzer_notion_manual.md
That manual exists to keep model output convertible to stable Notion structures and to prevent over-interpretation of low-information pages.
Run tests:
python -m unittest discover -s tests -vUseful implementation entrypoints:
src/notegenerator_cli/cli.pysrc/notegenerator_cli/workflow.pysrc/notegenerator_cli/notion_utils.pysrc/notegenerator_cli/install_agent.py
This public repository intentionally excludes private runtime state and credentials:
.envis not committedruns/is not committed.venv/is not committed- all repository examples use placeholders instead of live API keys