The OPML file and the profile's ## Interests section become one-time import inputs; the prompt groups by the stored category and the OPML config key is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
260 lines
11 KiB
TOML
260 lines
11 KiB
TOML
# The Daily EPUB — example configuration (spec §3.14).
|
||
#
|
||
# Load order (later wins): built-in defaults ← this file ← `DAILY_EPUB_*` env vars.
|
||
# Nested keys use a double underscore in env vars, e.g.
|
||
# DAILY_EPUB_MINIFLUX__API_KEY=...
|
||
# DAILY_EPUB_PROVIDERS__DEEPSEEK__API_KEY=...
|
||
# DAILY_EPUB_PROVIDERS__ANTHROPIC__API_KEY=...
|
||
# DAILY_EPUB_PROVIDERS__GEMINI__API_KEY=...
|
||
# DAILY_EPUB_VOYAGE__API_KEY=...
|
||
# DAILY_EPUB_SERVER__HMAC_SECRET=...
|
||
# DAILY_EPUB_LOOKBACK_HOURS=30
|
||
# DAILY_EPUB_LLM__EDITOR=gemini # one-off role override, no file edit
|
||
|
||
timezone = "America/New_York"
|
||
lookback_hours = 26
|
||
target_article_count = 20
|
||
retention_days = 21 # EPUBs, by age
|
||
xtc_retention_count = 5 # XTC issues, by count (~80-100 MB each)
|
||
world_briefing = true
|
||
|
||
# SQLite database file. Parent directories are created on demand.
|
||
database_path = "/var/lib/daily-epub/daily-epub.db"
|
||
|
||
# Default output directory for generated artifacts (overridden by `--out`).
|
||
out_dir = "/var/lib/daily-epub/out"
|
||
|
||
# Hand-maintained reader profile loaded into the system prompt.
|
||
profile_path = "data/profile.md"
|
||
|
||
[miniflux]
|
||
base_url = "http://127.0.0.1:8082"
|
||
# Browser-facing Miniflux web UI URL; defaults to base_url.
|
||
# public_url = "https://miniflux.example.com"
|
||
# api_key via DAILY_EPUB_MINIFLUX__API_KEY env
|
||
page_limit = 250
|
||
|
||
# The two LLM roles, each assigned to a provider declared in [providers.*]
|
||
# below. Switching the editor to Gemini is `editor = "gemini"` plus its key in
|
||
# the env file; nothing else changes. An empty editor ("") runs everything on
|
||
# the bulk provider. Every editor call degrades to bulk when the editor's key
|
||
# is missing, its daily ceiling is hit, or the API refuses/fails.
|
||
[llm]
|
||
bulk = "deepseek" # triage, deep assessment, and every fallback
|
||
editor = "anthropic" # lineup, summaries, The Brief, the weekly profile rebuild
|
||
triage_batch_size = 25 # articles per first-pass triage request
|
||
deep_batch_size = 8 # articles per close-reading assessment request
|
||
score_temperature = 0.3 # sent only by providers that take a temperature (kind = "openai")
|
||
editorial_temperature = 0.8 # summaries and The Brief on an openai-kind provider
|
||
|
||
# The provider registry. Any number of entries; a role above names one by its
|
||
# table name. Keys never live here: DAILY_EPUB_PROVIDERS__<NAME>__API_KEY.
|
||
# `max_daily_usd` is a per-provider runaway guard per UTC day (0 = none), not
|
||
# accounting — set hard spend limits in each provider's dashboard as well.
|
||
# Prices are USD per 1M tokens and only feed the guard's arithmetic.
|
||
[providers.deepseek]
|
||
kind = "openai" # openai | anthropic
|
||
base_url = "https://api.deepseek.com/v1"
|
||
model = "deepseek-v4-flash" # DeepSeek-V4-Flash-0731 (confirmed 2026-08-15)
|
||
# api_key via DAILY_EPUB_PROVIDERS__DEEPSEEK__API_KEY env
|
||
max_daily_usd = 2.0
|
||
max_concurrent_requests = 4 # triage and deep-assessment batches in flight
|
||
price_input_per_mtok = 0.14
|
||
price_cache_read_per_mtok = 0.0028 # prefix-cache hits
|
||
price_cache_write_per_mtok = 0.0 # DeepSeek caches implicitly, no write charge
|
||
price_output_per_mtok = 0.28
|
||
|
||
# Claude Opus 5 over the Messages API. Requests carry `output_config.effort`,
|
||
# a cached system block, and `fallbacks = "default"` so a classifier refusal is
|
||
# re-routed server-side; a refusal that still comes back degrades to bulk.
|
||
[providers.anthropic]
|
||
kind = "anthropic"
|
||
base_url = "https://api.anthropic.com"
|
||
model = "claude-opus-5"
|
||
# api_key via DAILY_EPUB_PROVIDERS__ANTHROPIC__API_KEY env
|
||
effort = "high" # low | medium | high | xhigh | max → output_config.effort
|
||
max_daily_usd = 3.0
|
||
max_concurrent_requests = 4 # summaries in flight when this is the summary provider
|
||
price_input_per_mtok = 5.0
|
||
price_cache_read_per_mtok = 0.5
|
||
price_cache_write_per_mtok = 6.25
|
||
price_output_per_mtok = 25.0
|
||
|
||
# Gemini 3.8 Flash through Google's OpenAI-compatible endpoint (beta, verified
|
||
# 2026-09-02). Declared but unreferenced until a role names it.
|
||
[providers.gemini]
|
||
kind = "openai"
|
||
base_url = "https://generativelanguage.googleapis.com/v1beta/openai"
|
||
model = "gemini-3.8-flash"
|
||
# api_key via DAILY_EPUB_PROVIDERS__GEMINI__API_KEY env
|
||
effort = "high" # minimal | low | medium | high → reasoning_effort
|
||
max_daily_usd = 3.0
|
||
max_concurrent_requests = 4
|
||
price_input_per_mtok = 0.75 # promotional through 2026-12-31; $1.50 from 2027-01-01
|
||
price_cache_read_per_mtok = 0.075 # implicit cache hits; $0.15 from 2027-01-01
|
||
price_cache_write_per_mtok = 0.0
|
||
price_output_per_mtok = 3.75 # includes thinking tokens; $7.50 from 2027-01-01
|
||
|
||
# Voyage AI embeddings behind the interest and rated-neighbour signals. Set
|
||
# `enabled = false` (or leave the key unset) and the paper still builds: the
|
||
# learned signals are simply absent, never a penalty.
|
||
[voyage]
|
||
enabled = true
|
||
base_url = "https://api.voyageai.com/v1"
|
||
model = "voyage-4-lite"
|
||
# api_key via DAILY_EPUB_VOYAGE__API_KEY env
|
||
output_dimension = 512 # 256 | 512 | 1024 | 2048
|
||
batch_size = 32
|
||
max_concurrent_requests = 4
|
||
max_input_chars = 60000 # per article, cut on a char boundary
|
||
max_daily_usd = 0.50 # runaway guard ($0.02 / M tokens)
|
||
|
||
[curation]
|
||
max_article_count = 28 # hard ceiling; there is no minimum (§13)
|
||
recent_rejection_days = 7
|
||
recent_rejection_floor = 3.0
|
||
always_include_feeds = [] # miniflux feed ids or site urls
|
||
blocked_domains = []
|
||
# Extra paywalled hosts, merged with the built-in list (nytimes, wsj, ft, …).
|
||
# A short body from one of these is marked "excerpt only" and penalized (§3.3).
|
||
paywall_domains = []
|
||
sections = [
|
||
"Top Stories",
|
||
"Tech & Engineering",
|
||
"Science & Space",
|
||
"AI & Machine Learning",
|
||
"Culture & Essays",
|
||
"Boston & Local",
|
||
"Niche Corner",
|
||
"From the Blogroll",
|
||
]
|
||
|
||
[curation.feedback]
|
||
loved_value = 1.0
|
||
good_value = 0.35
|
||
not_for_me_value = -1.0
|
||
slop_value = -1.0 # AI slop: a full negative; the author penalty is below
|
||
verdicts_in_prompt = 60
|
||
|
||
# Every weight, quota, gate and threshold of the personalized ranker. The
|
||
# learned signals (`knn`, `feed`) contribute nothing until their gates open:
|
||
# the weight ramps linearly from `*_floor` to `*_full` rated articles.
|
||
[curation.ranking]
|
||
triage_max = 800 # eligible articles the triage LLM reads
|
||
deep_keep = 120 # deep-assessment set
|
||
shortlist_keep = 60 # what the editor sees
|
||
assessment_reuse_days = 3
|
||
rating_lookback_days = 180
|
||
rating_half_life_days = 60
|
||
neighbour_k = 5
|
||
negative_coefficient = 0.75
|
||
knn_floor = 8
|
||
knn_full = 25
|
||
feed_floor = 15
|
||
feed_full = 40
|
||
slop_author_penalty = 0.75 # blend and utility × 0.25 for authors with an AI slop verdict
|
||
semantic_min_words = 300
|
||
exploration_slots = 5
|
||
embedding_retention_days = 120 # `features prune`: unrated, unpublished vectors
|
||
telemetry_retention_days = 180 # `features prune`: candidate_runs and article_assessments
|
||
|
||
[curation.ranking.quotas]
|
||
triage = 60
|
||
interest = 20
|
||
knn = 20
|
||
|
||
# Weights need not sum to 1; they are renormalized over the present signals.
|
||
[curation.ranking.weights.preliminary]
|
||
interest = 0.35
|
||
knn = 0.25
|
||
heuristic = 0.20
|
||
feed = 0.10
|
||
social = 0.10
|
||
|
||
[curation.ranking.weights.utility]
|
||
quality = 0.40
|
||
fit = 0.20
|
||
knn = 0.15
|
||
interest = 0.10
|
||
feed = 0.05
|
||
triage = 0.05
|
||
social = 0.03
|
||
heuristic = 0.02
|
||
|
||
[curation.ranking.diversity]
|
||
cluster_threshold = 0.85
|
||
per_cluster_cap = 2
|
||
utility_protected = 10
|
||
|
||
[editorial]
|
||
summary_model = "editor" # editor | bulk — which [llm] role writes the summaries
|
||
summary_input_tokens = 3000 # article text offered per summary
|
||
|
||
[publish]
|
||
# Where both EPUB editions land, and what the OPDS feed lists. BookOrbit is
|
||
# optional — it just watches this folder if you run it.
|
||
# (Renamed from `bookorbit_dir`; the old key is now a hard config error.)
|
||
epub_dir = "/srv/bookorbit/libraries/daily-epub"
|
||
# XTC artifacts. Not listed in OPDS (CrossPoint cannot acquire them); reachable
|
||
# at /files/xtc/<name> for sideloading.
|
||
xtc_dir = "/var/lib/daily-epub/xtc"
|
||
|
||
[xtc]
|
||
enabled = true
|
||
# epub-to-xtc-converter has no global npm bin; it is invoked through node.
|
||
# The code appends: <input.epub> -o <output.xtch> -f <format> -c <settings>
|
||
command = "node"
|
||
args = ["/opt/epub-to-xtc-converter/cli/index.js", "convert"]
|
||
format = "xtch" # xtc (1-bit) | xtch (grayscale)
|
||
# REQUIRED in practice: the converter refuses to start without `font.path`,
|
||
# which can only be given through this file. Start from xtc-settings.example.json.
|
||
settings = "/etc/daily-epub/xtc-settings.json"
|
||
|
||
[server]
|
||
bind = "127.0.0.1:3499" # listen address; keep loopback when trusting proxy IP headers
|
||
public_url = "https://daily.hallada.net" # base URL for EPUB rating links and same-origin checks
|
||
session_days = 30 # sliding lifetime for web login sessions
|
||
login_attempts = 10 # shared login/access-request POSTs per client IP per window
|
||
login_window_minutes = 15 # length of the login throttle window
|
||
jobs_enabled = true # let the dashboard start daily-epub-job@<name>.service
|
||
journal_lines = 300 # job-page journal tail; valid range 10..=5000
|
||
# hmac_secret via DAILY_EPUB_SERVER__HMAC_SECRET env (32+ random bytes)
|
||
# Optional Basic auth for /opds/* and /files/*; without it those existing
|
||
# routes remain public. A signed-in web user can download without Basic auth.
|
||
# basic_auth_user = "daily"
|
||
# basic_auth_pass = "..."
|
||
|
||
[bookorbit]
|
||
enabled = false
|
||
public_url = "https://bookorbit.hallada.net" # what the browser opens
|
||
api_url = "http://127.0.0.1:3498" # where the server talks OPDS; same host
|
||
opds_user = "" # an OPDS user from BookOrbit → Settings → OPDS
|
||
# opds_pass: environment only (DAILY_EPUB_BOOKORBIT__OPDS_PASS)
|
||
|
||
[mail]
|
||
enabled = false
|
||
smtp_host = "" # e.g. email-smtp.us-east-1.amazonaws.com
|
||
smtp_port = 587 # 587 with STARTTLS; commonly 465 with implicit TLS
|
||
smtp_starttls = true # false selects implicit TLS
|
||
smtp_user = ""
|
||
# smtp_pass: environment only (DAILY_EPUB_MAIL__SMTP_PASS)
|
||
from = "" # e.g. The Daily EPUB <daily@hallada.net>
|
||
# notify_to = "operator@example.com" # recipient for access-request notifications
|
||
|
||
[discovery]
|
||
enabled = true # run the feed discovery stage during generate
|
||
max_lookups_per_run = 30 # hosts looked up in Miniflux per run; each host is re-checked at most every 90 days
|
||
skip_hosts = [ # never looked up: the host itself or any subdomain of it
|
||
"news.ycombinator.com",
|
||
"lobste.rs",
|
||
"reddit.com",
|
||
"github.com",
|
||
"gist.github.com",
|
||
"x.com",
|
||
"twitter.com",
|
||
"youtube.com",
|
||
"en.wikipedia.org",
|
||
"arxiv.org",
|
||
"docs.google.com",
|
||
]
|