File size: 15,428 Bytes
5023401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import gradio as gr
from transformers import pipeline
import numpy as np
from datetime import datetime

# Load the audio classification model
classifier = pipeline(
    "audio-classification",
    model="dima806/bird_sounds_classification",
    device=-1,
)

# Bird information database (15 species with descriptions)
BIRD_INFO = {
    "Great Tinamou": {
        "habitat": "Tropical and subtropical lowland forests from southern Mexico to northern South America.",
        "song": "A series of tremulous, haunting whistles that echo through the forest - one of the most recognizable sounds of the neotropical lowlands.",
        "range": "Southern Mexico through Central America to northern South America, including Brazil and Peru.",
        "fun_fact": "Despite being a ground-dwelling bird, the Great Tinamou roosts in trees at night. Its eggs are among the most beautiful in the bird world - glossy turquoise-blue.",
    },
    "Plain Chachalaca": {
        "habitat": "Brushy woodland edges, thickets, and riparian areas. The only chachalaca regularly found in the United States (southern Texas).",
        "song": "A loud, raucous CHA-cha-LAC repeated in chorus by groups - unmistakable once you've heard it. Often called at dawn.",
        "range": "Southern Texas through Mexico to Costa Rica.",
        "fun_fact": "Plain Chachalacas are one of the few species in this model's list that can actually be seen in the US - in the Rio Grande Valley of Texas.",
    },
    "Crested Guan": {
        "habitat": "Mountain forests and cloud forests, typically at elevations of 500-2,500 meters.",
        "song": "A variety of honking and trumpeting calls, especially loud during breeding season.",
        "range": "Southern Mexico through Central America to western South America.",
        "fun_fact": "Crested Guans are important seed dispersers for many tropical tree species. They swallow fruits whole and spread seeds through the forest.",
    },
    "Andean Guan": {
        "habitat": "Cloud forests and humid montane forests of the Andes, typically between 1,500 and 3,500 meters elevation.",
        "song": "Deep honking calls that carry through mountain valleys, often given in the early morning.",
        "range": "Andes from Venezuela south through Colombia, Ecuador, Peru, and Bolivia.",
        "fun_fact": "The Andean Guan is a canopy specialist that rarely descends to the ground, moving through the treetops to feed on fruit and leaves.",
    },
    "Little Tinamou": {
        "habitat": "Dense undergrowth of tropical forests. Extremely secretive and almost never seen despite being common.",
        "song": "A long, tremulous whistle that rises and falls - one of the most frequently heard but least seen birds in its range.",
        "range": "Southern Mexico through Central America to South America, as far south as Brazil.",
        "fun_fact": "Little Tinamous are heard far more often than seen. They freeze when threatened and rely on their camouflage, only flushing at the last moment.",
    },
    "Solitary Tinamou": {
        "habitat": "Interior of humid tropical forests, usually on the forest floor.",
        "song": "A mournful, descending whistle that sounds almost electronic - quite eerie when heard in the deep forest.",
        "range": "Central America through northern South America.",
        "fun_fact": "True to its name, the Solitary Tinamou is almost always found alone. Males incubate the eggs and raise the chicks by themselves.",
    },
    "Highland Tinamou": {
        "habitat": "Cloud forests and montane forests, from 1,200 to 3,000 meters elevation.",
        "song": "A clear, descending series of whistles. Sometimes described as sounding like someone playing a slow scale on a flute.",
        "range": "Mountains of Costa Rica and Panama through the Andes to Bolivia.",
        "fun_fact": "Highland Tinamous have been recorded at higher elevations than almost any other tinamou species.",
    },
    "Grey-headed Chachalaca": {
        "habitat": "Dry forests, forest edges, and agricultural areas with scattered trees.",
        "song": "Loud, harsh calls similar to other chachalacas, often given by groups in noisy choruses at dawn and dusk.",
        "range": "Honduras through Central America to northern Colombia.",
        "fun_fact": "Chachalacas get their name from the sound of their call - cha-cha-lac-a - repeated over and over.",
    },
    "Band-tailed Guan": {
        "habitat": "Humid mountain forests and cloud forests.",
        "song": "A series of deep, resonant honking sounds, especially vocal during the breeding season.",
        "range": "Andes from Colombia and Venezuela south to Bolivia.",
        "fun_fact": "Band-tailed Guans travel in small family groups and are surprisingly acrobatic for their size, leaping between branches to reach fruit.",
    },
    "Black-capped Tinamou": {
        "habitat": "Forests from lowlands to lower montane elevations.",
        "song": "A bubbling, accelerating series of whistles - sounds like a bouncing ball slowing to a stop, but in reverse.",
        "range": "Central Peru to Bolivia, in the eastern Andean slopes.",
        "fun_fact": "Like other tinamous, the Black-capped Tinamou can fly but strongly prefers to walk. When it does fly, it's in short, explosive bursts.",
    },
    "Spotted Nothura": {
        "habitat": "Grasslands and open areas, including agricultural fields and pastures.",
        "song": "A series of sharp, staccato whistles, often given from the ground in open grassland.",
        "range": "Central South America - Brazil, Paraguay, Argentina, Uruguay.",
        "fun_fact": "Nothuras are grassland tinamous - unlike their forest-dwelling relatives, they live in open habitats and look somewhat like partridges.",
    },
    "Red-winged Tinamou": {
        "habitat": "Grasslands, scrublands, and agricultural areas in southern South America.",
        "song": "A melodious, flute-like whistle that rises and falls, often heard at dawn and dusk across the pampas.",
        "range": "Southern Brazil, Paraguay, Uruguay, Argentina.",
        "fun_fact": "Named for the rufous-red color visible on its wings in flight - one of the few times you'll see this secretive bird in the open.",
    },
    "Australian Brushturkey": {
        "habitat": "Rainforests, scrublands, and suburban gardens in eastern Australia.",
        "song": "Deep booming calls during breeding. Otherwise relatively quiet compared to other species in this list.",
        "range": "Eastern Australia, from Cape York to southern New South Wales.",
        "fun_fact": "Males build enormous mound nests (up to 4 meters wide) out of decomposing vegetation. The heat from decomposition incubates the eggs - no body heat required.",
    },
    "Dusky Megapode": {
        "habitat": "Tropical forests on islands in the western Pacific, often near volcanic areas.",
        "song": "Loud wailing calls, especially at night. Some species in this family are among the noisiest birds in their habitat.",
        "range": "Islands of Indonesia and Papua New Guinea.",
        "fun_fact": "Some megapodes use volcanic heat to incubate their eggs, burying them in volcanically warmed soil - the only birds known to use geothermal energy for reproduction.",
    },
    "Tataupa Tinamou": {
        "habitat": "Forest edges, secondary growth, and dense undergrowth in tropical and subtropical regions.",
        "song": "A rich, mellow series of whistles. Has been described as one of the most beautiful bird songs in South America.",
        "range": "Eastern South America from Brazil to Argentina.",
        "fun_fact": "The Tataupa Tinamou is more tolerant of disturbed habitats than many of its relatives, which helps it survive in areas affected by deforestation.",
    },
}

ALL_SPECIES = sorted(set(classifier.model.config.id2label.values()))

# Sighting log (in-memory)
sighting_log = []


def classify_bird(audio):
    if audio is None:
        return "Please upload or record an audio file.", gr.update(), gr.update()

    sr, y = audio

    if y.dtype == np.int16:
        y = y.astype(np.float32) / 32768.0
    elif y.dtype == np.int32:
        y = y.astype(np.float32) / 2147483648.0
    elif y.dtype != np.float32:
        y = y.astype(np.float32)

    if len(y.shape) > 1:
        y = y[:, 0]

    if sr != 16000:
        duration = len(y) / sr
        new_length = int(duration * 16000)
        y = np.interp(
            np.linspace(0, len(y) - 1, new_length),
            np.arange(len(y)),
            y,
        )
        sr = 16000

    results = classifier({"sampling_rate": sr, "raw": y}, top_k=3)

    lines = []
    top_species = results[0]["label"]
    top_score = results[0]["score"]

    if top_score < 0.40:
        lines.append("Not confident - this may not be a recognizable bird song,")
        lines.append("or the species may not be in this model's training data.\n")

    for i, pred in enumerate(results, 1):
        score = pred["score"]
        label = pred["label"]

        # Color-coded confidence
        if score >= 0.70:
            indicator = "HIGH"
        elif score >= 0.40:
            indicator = "MED"
        else:
            indicator = "LOW"

        bar_length = int(score * 20)
        bar = "#" * bar_length + "." * (20 - bar_length)
        lines.append(f"[{indicator}] {i}. {label}")
        lines.append(f"   {bar}  {score:.1%}\n")

    return "\n".join(lines), gr.update(value=top_species), gr.update()


def get_bird_info(species):
    if species in BIRD_INFO:
        info = BIRD_INFO[species]
        text = f"## {species}\n\n"
        text += f"**Habitat:** {info['habitat']}\n\n"
        text += f"**Song:** {info['song']}\n\n"
        text += f"**Range:** {info['range']}\n\n"
        text += f"**Fun fact:** {info['fun_fact']}"
        return text
    else:
        return (
            f"## {species}\n\n"
            f"No detailed description available for this species yet. "
            f"I've written descriptions for 15 of the 50 species in this model. "
            f"Try searching [Xeno-Canto](https://xeno-canto.org/) or the "
            f"[Cornell Lab](https://www.allaboutbirds.org/) for more information."
        )


def add_sighting(species, location, notes):
    if not species:
        return format_log()

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    entry = {
        "species": species,
        "date": timestamp,
        "location": location or "Not specified",
        "notes": notes or "",
    }
    sighting_log.append(entry)
    return format_log()


def format_log():
    if not sighting_log:
        return "No sightings logged yet. Identify a bird and add it to your log!"

    lines = [f"### Sighting Log ({len(sighting_log)} entries)\n"]
    for i, entry in enumerate(sighting_log, 1):
        lines.append(f"**{i}. {entry['species']}**")
        lines.append(f"   {entry['date']}  |  {entry['location']}")
        if entry["notes"]:
            lines.append(f"   Notes: {entry['notes']}")
        lines.append("")

    lines.append("---")
    lines.append(
        "*This log is stored in memory and will reset when the Space restarts. "
        "Copy your log if you want to save it!*"
    )
    return "\n".join(lines)


# Build the interface with tabs
with gr.Blocks(theme=gr.themes.Soft(), title="The Backyard Birder") as demo:
    gr.Markdown(
        """
        # The Backyard Birder
        A multi-feature birding assistant. Identify birds from audio recordings,
        learn about the species, and keep a sighting log.

        **Model:** `dima806/bird_sounds_classification` - 50 species (Tinamous, Guans, Chachalacas, and relatives).
        These are neotropical birds, not typical North American backyard species. The tool demonstrates how
        audio classification pipelines work; with a different model (like BirdNET), it could identify local birds too.

        ---
        """
    )

    # State for passing species between tabs
    current_species = gr.State("")

    with gr.Tabs():
        # Tab 1: Identify
        with gr.Tab("Identify"):
            gr.Markdown("Upload a bird recording to identify the species. Best results with clean recordings of 3+ seconds.")
            with gr.Row():
                with gr.Column():
                    audio_input = gr.Audio(
                        label="Upload or Record Audio",
                        type="numpy",
                    )
                    classify_btn = gr.Button("Identify Bird", variant="primary")
                with gr.Column():
                    classification_output = gr.Textbox(
                        label="Top 3 Predictions",
                        lines=10,
                        interactive=False,
                    )
                    species_display = gr.Textbox(
                        label="Top prediction",
                        visible=False,
                    )

        # Tab 2: Learn
        with gr.Tab("Learn"):
            gr.Markdown("Select a species to learn about it. Descriptions available for 15 of the 50 species.")
            species_dropdown = gr.Dropdown(
                choices=ALL_SPECIES,
                label="Select a Species",
                value="Great Tinamou",
            )
            bird_info_output = gr.Markdown(
                value=get_bird_info("Great Tinamou"),
            )

        # Tab 3: Log
        with gr.Tab("Log Sightings"):
            gr.Markdown("Keep track of what you hear. Add species to your sighting log with location and notes.")
            with gr.Row():
                with gr.Column():
                    log_species = gr.Dropdown(
                        choices=ALL_SPECIES,
                        label="Species",
                        allow_custom_value=True,
                    )
                    log_location = gr.Textbox(
                        label="Location",
                        placeholder="e.g., Backyard, Local park, Trail near school...",
                    )
                    log_notes = gr.Textbox(
                        label="Notes",
                        placeholder="e.g., Heard at dawn, two birds calling back and forth...",
                        lines=2,
                    )
                    log_btn = gr.Button("Add to Log", variant="primary")
                with gr.Column():
                    log_output = gr.Markdown(value=format_log())

    # Wire up events
    classify_btn.click(
        fn=classify_bird,
        inputs=[audio_input],
        outputs=[classification_output, species_dropdown, species_display],
    )

    species_dropdown.change(
        fn=get_bird_info,
        inputs=[species_dropdown],
        outputs=[bird_info_output],
    )

    log_btn.click(
        fn=add_sighting,
        inputs=[log_species, log_location, log_notes],
        outputs=[log_output],
    )

    gr.Markdown(
        """
        ---
        *Riley's Space 3 - AI + Research Level 2*

        **How this works:** The Identify tab uses an audio classification model to predict which species
        is singing. The Learn tab shows hand-written descriptions for 15 species. The Log tab lets you
        track your sightings during this session. This is a multi-model pipeline: audio classification
        feeds species information, and errors in identification propagate to wrong descriptions.
        """
    )

demo.launch()