Full implementation of a personalized daily newspaper delivered as an EPUB. Articles are pulled from a local self-hosted Miniflux instance, enriched with comments, summarized and filtered by DeepSeek AI, and then assembled into two EPUB editions: standard and optimized for the Xteink X4 e-ink reader. Both are served by the local self-hosted BookOrbit OPDS server in a separate library. Then the X4 edition is futher converted to XTC format and served over a separate OPDS server hosted by the Rust binary. Runs are tracked in a local SQLite database so runs are idempotent per date. Full documentation of the plan is in docs/plans and setup and install instructions are in the README.md file.
6.4 KiB
6.4 KiB
Implementation notes (shared brief for all implementation agents)
Authoritative spec: docs/plans/2026-08-15-the-daily-epub.md. Read it fully before writing code.
This file records implementation-time decisions and verified external facts. Follow both.
Verified external facts (2026-08-15)
- DeepSeek model id is confirmed:
deepseek-v4-flash(version DeepSeek-V4-Flash-0731). Pricing per 1M tokens: $0.0028 cache-hit input, $0.14 cache-miss input, $0.28 output. OpenAI-compatible API athttps://api.deepseek.com/v1, supportsresponse_format: {"type":"json_object"}. - epub-to-xtc-converter (github.com/bigbag/epub-to-xtc-converter) has no global npm bin.
It is invoked as:
node <repo>/cli/index.js convert book.epub -o book.xtch -f xtch -c settings.json(-f xtc= 1-bit,-f xtch= 2-bit grayscale;initsubcommand generates default settings). Therefore config must be fully general:xtc.command = "node",xtc.args = ["/path/to/epub-to-xtc-converter/cli/index.js", "convert"]and the code appends<input.epub> -o <output.xtch> -f <format>(plus-c <settings>). Missing/failed converter is non-fatal (log + continue).- Corrected 2026-08-15 (post-M8, verified by running it).
-cis not optional in practice: the converter validates settings before opening the EPUB and exits 2 withConfiguration errors: - Font path is required. Set font.path in your config file.There is no built-in font default, and the shippedcli/settings.jsonpoints at/usr/share/fonts/Adwaita/AdwaitaSans-Regular.ttf, which most servers do not have. Soxtc.settingsis effectively required wheneverxtc.enabled. Deps also neednpm installinsidecli/(commander, jszip, minimatch, sharp). - That same error also means "I could not read your config."
loadSettingsincli/settings.jsguards withfs.existsSync(configPath), which returnsfalseonEACCESexactly as it does for a missing file, then silently falls back toDEFAULT_SETTINGS(font.path: null). Since/etc/daily-epubis0750 daily-epub:daily-epub, running the converter by hand as any other account reproduces the "Font path is required" error against a valid file. Verify assudo -u daily-epub. - Output size. XTCH is a pre-rendered page bitmap: 480×800 at 2bpp = ~96 KB/page. A
20-article issue rendered at
font.size = 34came to 1,088 pages ≈ 104 MB, in ~13 s.retention_days = 21therefore implies ~2 GB inpublish.xtc_dir.
- Corrected 2026-08-15 (post-M8, verified by running it).
- X4 firmware rendering limits (from the
epub-to-xtc-converteroptimizer's header, which cites papyrix-reader): 464×788 usable viewport, max image decode 2048×3072, baseline JPEG only, no GIF/SVG/WebP, max 1500 CSS rules and simple selectors only (tag,.class,tag.class— no descendant combinators), max word length 200 chars, images under 20 px treated as decorative. These bind the(X4).epubread natively off BookOrbit; they do not bind the.xtch, which CREngine pre-renders to page bitmaps (convertnever callsoptimizeEpub— the two subcommands are independent).epub/x4.rsandstyle-x4.csssatisfy all of them;x4::simplify_xhtmlsoft-hyphenates pastMAX_WORD_CHARSandthe_x4_stylesheet_uses_no_descendant_selectorsguards the selector rule. - Wikipedia Current Events portal pages are created empty a day ahead.
Portal:Current_events/2026_August_15was created 2026-08-14T03:30Z as a 192-byte stub and did not get its first news item until 2026-08-15T13:28Z. The 05:30 America/New_York timer fires at ~09:30Z, so the issue day's own page is always an unpopulated stub — its only<li>elements are thecurrent-events-navbaredit/history/watch links, which the extractor drops, soextract_eventscorrectly returnsNone.world::fetch_with_fallbacktherefore walks back up toMAX_LOOKBACK_DAYSand the section is datelined with the day it actually covers, not the masthead date.
Cross-cutting implementation decisions
- sqlx usage: use runtime queries (
sqlx::query(...).bind(...)) and manual row mapping (orsqlx::FromRowderive withquery_as). Do not use the compile-time checkedquery!/query_as!macros (they require DATABASE_URL/offline data at build time). Migrations viasqlx::migrate!("./migrations")embedded at compile time. - Time:
jiffeverywhere; day boundaries and--dateinterpretation in the configured timezone (America/New_Yorkdefault). Store timestamps in SQLite as RFC3339 UTC strings. - Errors: modules return
thiserrorerror types oranyhow::Result;main.rsusesanyhow. Pipeline stages are best-effort where the spec says so (social, XTC, world briefing, images). - HTTP: one shared
reqwest::Client(rustls, gzip, no cookies, 10s timeouts, UAthe-daily-epub/1.0 (personal rss digest; contact tyler@hallada.net)), passed by clone. - LLM:
async-openaiwith custom base URL. All LLM calls go throughcurate/llm.rsLlmClientwhich tracks token usage into a sharedUsageMeter(input/cached/output tokens, cost usd) and enforcesmax_daily_usd. - Testing: unit tests inline per module; integration tests in
tests/over fixture JSON intests/fixtures/. Never hit the network in tests. LLM stage mockable via--skip-llm(prefilter order used for selection, feed excerpts as summaries). - Style: rustfmt defaults,
cargo clippyclean-ish, nounwrap()outside tests, tracing spans per pipeline stage. - File ownership: waves of agents work in parallel on disjoint files. Do not edit files
outside your assigned set (module wiring in
main.rs/mod.rsis done by the scaffold and the integration wave). If you need a helper from another module that doesn't exist yet, add a// TODO(integration): ...note and code against the stub signature. - Dedupe module: normalize/dedupe (§3.2) lives in
src/dedupe.rs(canonical URL fn + clustering), called from the generate pipeline between ingest and extraction. - World briefing (§3.8) lives in
src/world.rs. - Askama templates in
src/epub/templates/(*.xhtmlaskama templates +style.css,style-x4.css). Askama 0.12+ configured viaaskama.tomlif needed. - Determinism: chapter ids
art-{entry_id}, stable filenames, issue regeneration for the same date replaces prior rows/files (idempotent upsert everywhere).