Files
the-daily-epub/config.example.toml
T
thalladaandClaude Fable 5.1 2d857e3e10 Cut weak interest matches and add the rating-driven affinity signal (step 2)
An interest matches an article only when it is in the top three by z and
z >= 1.0, so the Matches line, the stored rows and the weights agree. The
new bounded affinity signal blends each matched interest's rating-derived
weight, gated on attributable ratings like feed affinity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
2026-09-13 05:20:05 +00:00

265 lines
11 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 and Scour interests merged into the system prompt.
profile_path = "data/profile.md"
interests_opml = "data/scour-interests.opml"
[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`, `affinity`) 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
affinity_floor = 15
affinity_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.30
knn = 0.25
affinity = 0.10
heuristic = 0.20
feed = 0.10
social = 0.05
[curation.ranking.weights.utility]
quality = 0.40
fit = 0.20
knn = 0.10
affinity = 0.05
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",
]