Keep the deployment configuration outside the Git checkout. Required fields are
documented in config.example.toml.
Recommended starting values balance local relevance, seasonal variety, API use, and subscription-backed generation:
| Setting | Recommended | Why |
|---|---|---|
discovery.radius_km |
8 |
Approximately five miles; widen it in sparsely observed areas. |
discovery.species_limit |
50 |
Avoids truncating normal local results without creating an unbounded active set. |
discovery.window |
"last-30-days" |
More reliable than a short window while remaining seasonally relevant. |
schedule.refresh_minutes |
15 |
Keeps observations current at a modest API request rate. |
schedule.generation_minutes |
360 |
Generates at most four new plates per day by default. |
controller.generations_per_cycle |
1 |
Bounds work and recovery impact per invocation. |
research.max_searches_per_day |
5 |
Covers the default generation rate plus one recovery. |
research.max_searches_per_species |
2 |
Allows one normal attempt and one bounded recovery. |
schedule.rotation_minutes |
30 |
A calm starting cadence for an e-paper display. |
display_node.rotation_mode |
"shuffle_bag" |
Shows every active bird once before repeating. |
These are starting points, not service limits. If the controller generates more
often, review and raise max_searches_per_day to match. Cached profiles do not
consume the budget again. Use seed for a broad historical catalog instead of
making the active observation window permanently broad.
Research counters reset at midnight UTC. When a daily or per-species limit is
reached, the controller records the next attempt at that reset instead of
running backoff retries that cannot succeed. An explicit retry never bypasses
the configured research budget.
The controller's workspace_dir must be writable because the Codex image tool
copies its final image there. Keep it separate from configuration, catalog, and
state; the example uses a dedicated workspace directory. catalog_dir and
state_dir must persist across deployments. codex_path must point to a Codex
CLI whose login status reports a ChatGPT-authenticated session. Set the
optional codex_model only after validating that model for profile research,
generation, and review; otherwise the controller inherits the machine-wide
Codex default. Each private run record states the requested value explicitly,
including null when the default was inherited.
Bird Buddy uses controller-private authentication state rather than configured
credentials. Obtain Bird Buddy's permission first, run birdbuddy login, and
verify the selected feeder with birdbuddy status. The controller stores only
the rotating refresh token and fails that provider with a re-login instruction
if the session is revoked. Removing birdbuddy from discovery.sources is the
non-destructive disable path; birdbuddy logout --yes additionally removes local
authentication without deleting accumulated history.
BirdNET-Go uses only the configured birdnet_go_url. Removing birdnet-go
from discovery.sources is its complete disable path; Inky never changes the
detector or downloads its recordings. Provider status reports connection,
response-contract, and taxonomy failures without exposing the configured URL.
BirdNET Analyzer history lives in
state_dir/birdnet-analyzer-detections.json. Back it up with other controller
state, treat the backup as private observation data, and restore it with mode
0600. Removing birdnet-analyzer from discovery.sources disables the
provider without deleting imported history.
eBird Archive history lives in
state_dir/ebird-archive-observations.json. Back it up and restore it with mode
0600; it contains dates and species membership even though raw account,
checklist, and location fields were discarded. Removing ebird-archive from
discovery.sources disables the provider without deleting history. Reimport a
fresh complete Download My Data export to update the snapshot.
The selected feeder's confirmed metadata is always used as a safety net when a
postcard leaves the new-postcard feed before a poll. When all of a cached
postcard's media identifiers are present in confirmed history, the confirmed
species replace its preview classification. Account-level manually added
sightings are separate. Set
discovery.birdbuddy_include_manual_sightings = true only when those sightings
should influence this frame; disable it to exclude them again without deleting
private history.
The first successful sync for the selected feeder after upgrading from pre-linkage Bird Buddy history removes its cached postcard rows that lack media identifiers. Inactive feeder histories remain untouched until that feeder is selected and synced. This avoids keeping an unprovable preview classification; confirmed metadata still supplies conservative presence for records Bird Buddy continues to expose.
Schedules are configured in [schedule]. Conservative starting values are:
- controller HTTP service: always running;
- observation refresh: every 15 minutes;
- generation cycle: every six hours, one candidate per cycle;
- catalog publication: every five minutes when enabled; and
- display cycle: every 30 minutes.
The refresh command does not invoke Codex. generation_minutes,
generations_per_cycle, and max_generation_attempts jointly bound
subscription use. If generation takes longer than its interval, the service
manager does not start a second copy and the generation lock also rejects
manual overlap. Only a candidate that passes the independent Codex review is
published.
max_species_attempts_per_cycle is a separate queue scan cap. A transiently
failing species receives durable exponential backoff and no longer consumes the
successful-generation quota on every cycle. Insufficient licensed references
use the longer insufficient_references_retry_minutes delay because source
availability changes slowly. The retry record also retains species identity so
an explicit retry can requeue work that failed before profile creation. Later
birds continue through the queue. Exhausted factual or visual review remains
terminal and requires retry TAXON_ID.
Discovery requests and validates species-rank iNaturalist results so genera, families, and other aggregate taxa never enter generation. Species context uses iNaturalist first and the Cornell BirdNET Taxonomy API when iNaturalist omits its descriptive context. The fallback is accepted only when the iNaturalist taxon ID and scientific name match exactly. Licensed reference photos remain research-grade CC0 or CC BY iNaturalist observations from distinct observers. The application does not substitute arbitrary web images.
A new species profile may use one tightly bounded Codex web research pass after the structured context and image references are assembled. Research is limited by configured domains, per-species attempts, and a daily total. A validated profile is cached, so image retries do not repeat profile research. Independent quality review may revisit configured source domains to verify the rendered facts rather than trusting the profile's citations.
When that review finds a material disagreement with the cached profile, it
must identify the supported profile field and cite two independent allowed
sources. The reviewer cannot write canonical facts directly. Instead, the
controller permits one fresh source-backed profile research pass within the
same generation limit and research budget. A repeated conflict becomes
terminal. Private runs/*/profile-before-refresh.json and
profile-after-refresh.json files preserve the adjudication evidence.
Conflict output is accepted only when profile_value matches the exact current
profile field. A reviewer must also explicitly identify earlier image
corrections that the current attempt resolves; disappearance alone never turns
a correction into a non-regression invariant.
Published dimensions can differ across reputable sources because their samples or methods differ. A profile uses one compatible, directly supported measurement set and preserves any material sex, age, or season qualifier. The reviewer does not fail that choice merely because another allowed source publishes a different supported set. It reports a profile conflict when the chosen value is unsupported, misquoted, assembled from incompatible endpoints, missing a material qualifier, or presented as broader agreement than its source establishes.
state_dir/runs/*/attempt-history.json is a private, schema-versioned record of
every started image attempt. It includes prompt version, requested model,
generation and review latency, score-axis failures, correction regressions,
explicitly resolved corrections, profile conflicts, and sanitized process-error
types. Terminal failures also retain a copy with their private failed artifacts.
These files can contain source-backed species facts and review diagnostics;
back them up with controller state, never publish them into the reusable catalog.
An explicit retry TAXON_ID preserves a terminal structured profile conflict
in private retry state, even when the review has no image correction. The next
run supplies that conflict to independent review until the current profile is
verified or the source-backed conflict is adjudicated; a generic image failure
message never replaces it. generation-retries.json contains the same private
source-backed facts while a conflict is outstanding, so protect and back it up
with the rest of controller state and never publish it.
Stored conflict sources are revalidated against the current research-domain
allowlist before reuse. If the allowlist has narrowed, that taxon is deferred
with an actionable error rather than placing an unauthorized source into a
search-enabled review prompt.
Use retry TAXON_ID --refresh-research when the authorized domains have changed
or the stored conflict should be adjudicated from scratch. This explicitly
discards outstanding conflict provenance and clears the cached profile and
references so the next run researches them under the current allowlist; any
independent image-correction guidance is preserved.
rotation_mode is configured under [display_node]:
sequential: stable round-robin order;shuffle: existing shuffled-round behavior; removed species are pruned, while new species join on the next refill;shuffle_bag: a separately persisted bag that shows each active species at most once per refill, admits new active species immediately in randomized order, prunes inactive species, and avoids repeating the prior species across a refill when another species is active; orweighted: random selection weighted by current observation count, without immediate repeats when another species is active.
prioritize_latest_detection = true is the default. When the active catalog
contains BirdWeather, BirdNET-Go, or Bird Buddy timestamps, the newest detection
later than the display node's durable watermark is shown once before the
configured rotation resumes. The priority display counts as shown when that
bird is already next in sequence or present in a shuffle pool, which prevents
an immediate duplicate without reordering the other birds. A failed panel
update does not consume the detection, and a first run shows only the current
newest detection rather than replaying the historical window. Set the option to
false to use rotation_mode for every update.
Approved plates use the project's canonical 1200x1600 portrait and 1600x1200 display assets. This geometry remains the catalog contract. PIM774 consumes the display asset unchanged; a PIM773 display node contains the full asset on its 800x480 canvas with paper-colored margins and no crop or stretch. Controller and display state paths remain TOML configuration. Installer bootstrap paths, including the TOML path itself, remain environment variables because the installer must find the configuration before it can load it.
The current observation snapshot, private collection, and generation backlog
are separate state. Use seed to add every distinct discovered taxon to the
private collection and enqueue any missing plate without changing the
configured discovery location or current observation snapshot:
inky-bird-frame seed --config /path/to/config.toml \
--source inaturalist --window last-year --species-limit 500 --dry-run
inky-bird-frame seed --config /path/to/config.toml \
--source inaturalist --window last-year --species-limit 500To seed a historical trip or event, preview an inclusive iNaturalist date range around a command-scoped coordinate before applying the same command:
inky-bird-frame seed --config /path/to/config.toml \
--source inaturalist --latitude 40.7128 --longitude -74.0060 \
--radius-km 11 --start-date 2026-04-01 --end-date 2026-04-03 \
--species-limit 500 --dry-runThe configured source is used unless --source is provided. The live eBird API
cannot query beyond 30 days or guarantee arbitrary coordinate-radius historical
windows, so those ranges use iNaturalist. An imported eBird Archive supports
exact date ranges for the user's own checklists without a location or radius.
--latitude and --longitude must be provided together and do not change the
configured location. The configured radius is used unless --radius-km is
provided. Repeating a seed
is idempotent: collection members and queued taxa are not duplicated. Approved
seeded taxa become active immediately; unapproved taxa remain members while
their missing plates move through generation. Terminal taxa stay blocked until
an explicit retry. Current observations remain ahead of seed-only taxa during
generation.
To migrate an existing local catalog, preview and then explicitly import its currently approved taxa:
inky-bird-frame collection import-approved --config /path/to/config.toml --dry-run
inky-bird-frame collection import-approved --config /path/to/config.toml
inky-bird-frame collection list --config /path/to/config.tomlThe import is a point-in-time trust decision. Later catalog synchronization does not silently add new members. On upgrade, the first seed, collection mutation, or generation cycle also imports taxa already present in a pre-0.2.4 seed queue and records a one-time migration timestamp. Generation performs this step before pending approval recovery. The marker prevents later cycles from re-adding a taxon after an explicit removal. Manage individual taxa with the same previewable commands:
inky-bird-frame collection add 12942 --config /path/to/config.toml --dry-run
inky-bird-frame collection add 12942 --config /path/to/config.toml
inky-bird-frame collection remove 12942 --config /path/to/config.toml --dry-run
inky-bird-frame collection remove 12942 --config /path/to/config.tomlRemoval clears persistent membership but does not suppress a taxon that is also
in the current observation snapshot. The private collection.json stores only
taxon ID, initial membership origin, membership timestamp, and the one-time
queue-migration timestamp. It stores no coordinates, raw observations, names,
or provider counts.
The active catalog contains approved taxa in either current observations or the private collection. When both contain a taxon, current observation count and the newest available BirdWeather, BirdNET-Go, or Bird Buddy detection timestamp take precedence. Collection-only entries omit both fields; weighted rotation uses the display node's existing neutral weight of one instead of fabricated evidence.
status preserves these top-level views for compatibility:
queued: unapproved taxa in the durable seed or human-review queue;deferred: all retained retry records;terminal_blocked: terminal states associated with the durable queue; andfailed: retained failed-artifact paths, which are not necessarily current work.
The compatibility deferred list and nested generation view use the same
captured retry records, while failed comes from the same locked local-state
snapshot. The top-level list intentionally includes every retained retry record;
the nested generation.deferred list remains the authoritative set currently
waiting for its retry time.
Use the nested generation object as the authoritative work view. It combines
the current discovery snapshot with durable queue-only retries using the same
calculation as generation. eligible contains every unapproved, nonterminal
candidate; actionable is the retry-due subset generation can attempt now;
deferred is waiting for next_attempt_at; and terminal_blocked includes live
or queued taxa with incomplete pending, rejected, or failed state. Each list has
a matching count.
generation.complete is true only when the discovery snapshot exists and is no
older than twice schedule.refresh_minutes. Missing or stale discovery remains
visible as generation.discovery.status, preserves the candidates known from
local state, and leaves actionable empty. Corrupt catalog, discovery, or retry
state fails the command instead of being repaired or presented as healthy.
Passing pending candidates remain visible in the separate top-level pending
view and are recovered at the start of a generation cycle. retry TAXON_ID
archives an incomplete or terminal candidate before making it eligible again.
refresh and generate are the scheduled one-shot commands.
controller-cycle runs one observation refresh followed by one generation
cycle in a single invocation and reports the combined JSON result:
inky-bird-frame controller-cycle --config /path/to/config.tomlUse it for a manual end-to-end pass. Scheduled installations keep the separate
refresh and generate commands so each job retains its own interval and
failure notifications; controller-cycle does not send the per-command
notifications those commands emit.
catalog sync copies every species missing from one catalog into another and
rebuilds the destination index:
inky-bird-frame catalog sync --source-catalog /path/to/source \
--catalog /path/to/destinationThe command is add-only by default. It validates both catalogs, refuses a
destination taxon that conflicts with its immutable source version, and reports
published and already-present taxa. Controller bootstrap enables reviewed
migrations with INKY_CATALOG_SYNC_APPLY_REVIEWED_MIGRATIONS=1 instead of a
version-specific command argument, so the current Compose file remains usable
with older rollback images. The equivalent manual option,
--apply-reviewed-migrations, accepts only hash-bound migration ancestry: it
applies a newer reviewed source, permits skipped intermediate releases, and
reports a validated newer destination under retained_newer without
downgrading it. An unrelated, incomplete, or tampered history still fails
closed. Pass --state-dir with the controller state directory when the
destination is live so the copy holds the same lock as generation. The Docker
bootstrap service uses this mode to copy the image's bundled catalog into
persistent storage; see the
Docker controller guide.
Clone this project repository into a controller-only checkout. Install GitHub CLI and authenticate it as the repository owner. The owner must have pull request bypass permission on the base-branch ruleset. GitHub CLI stores its credential outside application configuration; do not use a deploy key because a deploy key cannot exercise the owner's pull request bypass.
[public_catalog]
enabled = true
checkout_dir = "/path/to/inky-bird-frame-source"
repository = "owner/inky-bird-frame"
gh_path = "/path/to/gh"
remote = "origin"
base_branch = "main"
commit_name = "Inky Bird Frame Catalog"
commit_email = "inky-bird-frame@users.noreply.github.com"
[schedule]
catalog_publish_minutes = 5Validate the complete local and remote catalog without committing or pushing:
inky-bird-frame catalog-publish --config /path/to/config.toml --dry-runRun an immediate publication cycle:
inky-bird-frame catalog-publish --config /path/to/config.tomlThe publisher copies only new approved taxa. Existing repository taxa must be
byte-for-byte identical to their local approved versions. It never publishes
the private discovery snapshot, downloaded reference bitmaps, run logs, failed
attempts, or display state. The macOS installer creates the publication
LaunchAgent only when [public_catalog].enabled is true.
The optional owner-only deployment workflow uses the included macOS controller installer. The self-hosted Actions runner dispatches that installer as a one-shot job in the controller user's GUI launchd domain. This is the same security context as the catalog publisher, so the existing keychain-backed GitHub CLI credential remains available without copying it into an Actions secret or environment variable.
The trusted runner reads display connection details from
~/Library/Application Support/Inky Bird Frame/deployment.env:
INKY_BIRD_DISPLAY_HOST=display-node-address
INKY_BIRD_DISPLAY_USER=display-user
INKY_BIRD_DISPLAY_SSH_KEY="$HOME/.ssh/inky-bird-frame-display"
INKY_BIRD_DISPLAY_APP_DIR=/home/display-user/Services/inky-bird-frame
INKY_BIRD_DISPLAY_CONFIG_PATH=/home/display-user/.config/inky-bird-frame/config.toml
INKY_BIRD_DISPLAY_VENV=/home/display-user/.virtualenvs/inky-bird-frameAll six values are deployment-specific and required. Keep this file on the controller. It is not part of the repository.
Tagged GitHub releases are the supported installation points. Patch releases are intended to remain compatible within their minor line. Before version 1.0, a minor release may include a documented configuration or operational migration. Release notes are the source of truth for each upgrade.
Beginning with v1.0.0, the version number describes the compatibility promise:
- A patch release fixes defects, security issues, dependencies, documentation, or catalog content without requiring a configuration or private-state migration.
- A minor release adds backward-compatible behavior. Existing valid 1.x configuration continues to load, new providers and network permissions remain opt-in, and documented command names, options, and machine-readable JSON keep their meaning.
- An incompatible configuration, CLI, or network-interface change waits for a new major release. A deprecated 1.x interface remains available for the rest of the 1.x line; its replacement and eventual removal are announced in the documentation and release notes.
Private controller files are implementation state, not an API for other tools to edit. When a minor release must evolve that state, Inky owns an atomic, restart-safe migration and explains its rollback boundary in the release notes. Keep the pre-upgrade backup until the new release is proven: an older binary is not expected to understand state written by a newer release unless that release explicitly says otherwise.
The public catalog and HTTP interface follow the versioned schema rules in the architecture guide. A 1.x release may add an optional, privacy-reviewed field, command, or route. It does not remove a required field, change a documented field's type or meaning, or reuse a route for a different data class. Human-readable log and error wording may improve without being a compatibility break.
Observation providers remain isolated external dependencies. An upstream API, authorization, or schema change may temporarily degrade that provider, but it must not corrupt saved state or stop the other configured providers. If a security or privacy flaw makes a compatibility promise unsafe, a release may close that boundary earlier; the release notes must identify the exception, impact, and safest upgrade or rollback path.
Before changing versions:
- read every release note between the installed and target versions;
- make a quiesced, private backup using the backup guide;
- confirm the current
doctor controllerandstatusresults so a pre-existing failure is not mistaken for an upgrade regression; and - keep the prior binary, release bundle, or container tag available until the upgraded controller and display have passed their checks.
Upgrade the display node before the controller when release notes announce a
new catalog schema. Otherwise update the controller first, validate its private
configuration, run doctor controller, refresh, and status, then update and
check the display. Do not restore older state over newer state while services
are running. For recovery, stop writers and start the prior release with a
backup created by that release; restore data only when the release notes or the
failure require it.
inky-bird-frame status --config /path/to/config.tomlNormal operation does not require a human approval. The commands below are recovery and operator-override controls for a candidate left pending by an interrupted cycle:
inky-bird-frame approve --config /path/to/config.toml TAXON_ID
inky-bird-frame reject --config /path/to/config.toml TAXON_ID --reason "specific issue"Enable catalog publication to preserve accepted plates for other installations. Each generated image receives an auditable catalog-only PR, while application code continues through the full review and CI policy.
prepare-image fits any image onto the supported plate geometry without
Codex:
inky-bird-frame prepare-image /path/to/image.jpg --output-dir outputIt centers the source on a paper-colored 1200x1600 portrait canvas and
writes <name>-portrait.png plus a rotated 1600x1200 <name>-display.png
into --output-dir (default output). Add --display to also send the
prepared display asset to a locally attached Inky panel, or copy the file to
the display node and use display-image. Prepared images are local output
only; they never enter the approved catalog or rotation.
- Network or source failure: inspect the refresh or generation log and JSON result. Generation refuses a discovery snapshot older than twice the configured refresh interval.
- Unsuitable licensed references: inspect the reference manifest and source pages. The taxon is deferred automatically and later queue items continue.
- Generated image or text defect: the controller feeds review findings into a
targeted edit of the previous attempt automatically. The reviewer keeps its
complete findings for audit but returns only concrete required changes as
correction input. After all configured attempts fail, inspect the retained
artifacts and use
retry TAXON_ID --source-attempt Nwhen a specific attempt is a strong edit base. The command archives that portrait, refreshes cached references and profile data only when--refresh-researchis also supplied, preserves the selected attempt's corrections for the first edit, and keeps the validated retained identity in the generation queue if the original observation later expires. By default, validated research and references are reused. Omit--source-attemptwhen no retained image should be reused. Transient source failures retain the guidance and edit source until generation reaches a successful or terminal quality result. If retry finds an interrupted, fully invalid approval directory, it archives that debris and removes the stale local catalog entry before regeneration; a valid approved plate still requires the explicit replacement command below. When a stronger portrait belongs to an older archived generation run, useretry TAXON_ID --source-run RUN_NAME --source-attempt N.RUN_NAMEis the archive directory name, not a path. The command validates archive containment, the generation-run identity, the selected failed review, and the portrait before preserving the exact archive-relative edit source; it does not move or rewrite the older run. If human inspection accepts some source traits and adjudicates one or more automated findings, add repeatable--correction "..."values to a retry with--source-attemptor--source-candidate. These values replace only the current review corrections; prior human-rejection invariants remain mandatory, and the selected source still passes the same containment and integrity checks. When a human-rejected candidate in the private archive is the strongest edit base, useretry TAXON_ID --source-candidate ARCHIVE_NAME. This selector accepts an archive directory name, not a path, and verifies the candidate's taxon ID, rejected status, human rejection reason, and portrait checksum before changing terminal state. Its rejection reason—not findings from an unrelated later attempt—becomes the targeted correction contract. It may run immediately after--replace-approved; the human rejection remains an invariant while the archived plate becomes the first edit source. - Human rejection after local approval: before public publication, run
retry TAXON_ID --replace-approved --reason "...". The non-empty reason and replacement flag are mandatory. The command archives the approved artifacts with rejection metadata, atomically rebuilds the local catalog index, preserves validated research and references, and makes the taxon eligible for a fresh generation without an edit source, including for a historical-only taxon that is no longer in live discovery. Add--refresh-researchwhen those cached factual inputs are also being rejected. The human rejection reason remains required correction guidance on every attempt. Re-running the same command and reason safely resumes an interrupted migration. This local migration does not replace an immutable public catalog entry; a published correction requires a separate maintainer-reviewed migration. Usecatalog prepare TAXON_IDwith--replace-approvedand a non-empty--reasonagainst the approved source catalog. The command preserves the prior approval timestamp and image hashes in a validatedcatalog_migrationrecord and retains earlier reviewed replacements as history. CI rejects a replacement whose newest record does not match the pull request base, while controller sync may use the recorded ancestry when an installation skips an intermediate release. - Controller unavailable: the current e-paper image remains visible. Display state is not advanced.
- Checksum mismatch: the controller refuses an active on-disk asset that no longer matches its catalog digest. The display also verifies every downloaded image and preserves its current state if that check fails.
- Catalog publication failure: inspect
catalog-publish.logfor the structured command error;catalog-publish.error.logis reserved for process-level diagnostics. Rungh auth status --hostname github.comas the controller service account, then fix authentication, remote divergence, or the reported validation problem. Confirm recovery withcatalog-publish --dry-runbefore rerunningcatalog-publish. Local approval and display rotation continue while public publication is unavailable.
runs/, failed/, and rejected/ under the controller state_dir grow
without automatic pruning so failed and rejected work stays available for
inspection. Check them first when investigating a generation failure. Entries
are safe to delete once they have been reviewed.
catalog-publish-work/ is disposable publisher scratch space, not retained
generation evidence. Each publication invocation removes abandoned
publish-* directories while holding the exclusive publisher lock, before it
creates a new work directory. Files, symbolic links, and unrelated directories
are never removed. The command reports abandoned_work_directories and
cleaned_work_directories. A --dry-run reports abandoned work but leaves it
untouched; the next real publication removes it before Git prunes stale
worktree registrations. If recovery cannot remove a matching directory, the
real publication stops with an actionable error instead of creating more
scratch state and risking further disk exhaustion; --dry-run remains
available for non-destructive diagnosis.
On systemd hosts the services log JSON to journald and rely on the
distribution's default journal rotation. Set SystemMaxUse= in
/etc/systemd/journald.conf and restart systemd-journald to enforce a hard
size cap. macOS LaunchAgents write to log files under the managed support
directory instead.
See notifications.md for provider setup, event filtering,
durable delivery, noise controls, testing, and redacted status commands.