Context
The Birds page shows species by common name only. Users want to click through to authoritative info (photos, range maps, sounds) — All About Birds, eBird, Wikipedia. We can link reliably, but only if we persist the scientific name: our current species_code is a 6-char slug derived from the scientific name, NOT the eBird code, and common-name-only URLs are ambiguous across regions.
Good news: BirdNET already parses the scientific name from labels.json ("Scientific_Name") and includes it in its prediction output — it is simply dropped before persistence. Wiring it end-to-end unlocks deterministic linking.
Outcome: each detection row on the Birds page gains a small set of external-link icons (All About Birds, eBird, Wikipedia) that resolve deterministically for new rows. Historical rows without a scientific name fall back to common-name-based Wikipedia / All About Birds links where possible.
Architecturally Significant Requirements (ASRs)
Interface (Contract):
Detection pydantic model (platform/orpheus-common/src/orpheus_common/detection/models.py) gains species_scientific: Optional[str] = None. to_dict() / from_dict() round-trip the field.
/api/data/birds/history response detection objects include species_scientific (nullable).
- New
<SpeciesLinks /> React component rendering up to three compact icon links per detection row.
Implementation (Internal Logic):
- Additive SQLite migration: add
species_scientific TEXT column via the existing ensure_schema_updates() hook (auto-runs on startup). No backfill.
- BirdNET agent wiring: parser in
birdnet.py already emits species_scientific (around line 160). Verify the agent main forwards it into the Detection(...) constructor; add a pass-through if missing.
- Frontend slug helpers: lowercase, replace spaces with
- (eBird) or _ (Wikipedia); All About Birds uses its own common-name-based URL format (verify during implementation).
- Fallback logic when
species_scientific is null: render All About Birds + Wikipedia search links keyed on common name; omit the eBird link.
Architectural Constraints
- No backfill. Historical rows keep
species_scientific = NULL. Migration is additive and reversible by dropping the column.
- Must not break existing
Detection consumers — new field is optional with default None.
- Must be Python 3.9 compatible.
- Must not introduce new runtime dependencies on either side.
- External URL formats should be verified against each site's canonical pattern during implementation; keep them in one place (the
<SpeciesLinks /> component) so they're easy to update if a site's URL scheme changes.
Acceptance Criteria
Feature: External Species Links on Birds Page
Scenario: New detection has scientific name and all three links
Given a BirdNET detection arrives with species_scientific="Corvus brachyrhynchos" and species_common="American Crow"
When the detection persists and the Birds page row renders
Then the row shows three icon links: All About Birds, eBird, and Wikipedia
And clicking Wikipedia opens https://en.wikipedia.org/wiki/Corvus_brachyrhynchos
Scenario: Historical detection without scientific name falls back gracefully
Given a historical detection row has species_scientific=NULL and species_common="American Crow"
When the Birds page renders the row
Then All About Birds and a Wikipedia search link are rendered
And the eBird link is NOT rendered (it needs the scientific name)
And the UI does not crash
Scenario: Schema migration on startup is idempotent
Given an existing SQLite detections database with no species_scientific column
When the backend starts up
Then ensure_schema_updates adds the species_scientific TEXT column
And a second startup does not attempt to re-add the column
And old rows read back with species_scientific = None
Definition of Done
Context
The Birds page shows species by common name only. Users want to click through to authoritative info (photos, range maps, sounds) — All About Birds, eBird, Wikipedia. We can link reliably, but only if we persist the scientific name: our current
species_codeis a 6-char slug derived from the scientific name, NOT the eBird code, and common-name-only URLs are ambiguous across regions.Good news: BirdNET already parses the scientific name from
labels.json("Scientific_Name") and includes it in its prediction output — it is simply dropped before persistence. Wiring it end-to-end unlocks deterministic linking.Outcome: each detection row on the Birds page gains a small set of external-link icons (All About Birds, eBird, Wikipedia) that resolve deterministically for new rows. Historical rows without a scientific name fall back to common-name-based Wikipedia / All About Birds links where possible.
Architecturally Significant Requirements (ASRs)
Interface (Contract):
Detectionpydantic model (platform/orpheus-common/src/orpheus_common/detection/models.py) gainsspecies_scientific: Optional[str] = None.to_dict()/from_dict()round-trip the field./api/data/birds/historyresponse detection objects includespecies_scientific(nullable).<SpeciesLinks />React component rendering up to three compact icon links per detection row.Implementation (Internal Logic):
species_scientific TEXTcolumn via the existingensure_schema_updates()hook (auto-runs on startup). No backfill.birdnet.pyalready emitsspecies_scientific(around line 160). Verify the agentmainforwards it into theDetection(...)constructor; add a pass-through if missing.-(eBird) or_(Wikipedia); All About Birds uses its own common-name-based URL format (verify during implementation).species_scientificis null: render All About Birds + Wikipedia search links keyed on common name; omit the eBird link.Architectural Constraints
species_scientific = NULL. Migration is additive and reversible by dropping the column.Detectionconsumers — new field is optional with defaultNone.<SpeciesLinks />component) so they're easy to update if a site's URL scheme changes.Acceptance Criteria
Definition of Done
Detectionmodel updated withspecies_scientific: Optional[str] = Noneand round-trip tests inorpheus-common.ensure_schema_updates(); verified idempotent on an existing DB.species_scientificfrom the parser toDetection(...). Agent test updated./api/data/birds/historyreturnsspecies_scientificin the detection dicts. Backend test updated.<SpeciesLinks />React component + integration intoBirds.tsxdetection rows. Frontend test verifying link URLs for the scientific-present and scientific-NULL cases.