# 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" # 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____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 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 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/ 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: -o -f -c 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 # login POSTs allowed per client IP in one throttle window login_window_minutes = 15 # length of the login throttle window jobs_enabled = true # let the dashboard start daily-epub-job@.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)