I love my Bird Buddy. The Bird Buddy is a camera trap for birds. It’s a bird feeder that senses when something is in front of it, records a movie and takes several pictures. It then uses AI to identify the animal (usually a bird, but sometimes a squirrel) and sends me a notification on my phone, containing a “postcard”, a gallery with the video and some of the pictures it took.
Whenever I find cool pictures, I post them to social media, usually Threads and Bluesky, but there’s a lot of friction in the process. I must download the pictures from the Bird Buddy app, then upload them to the social media app, write a caption, etc. So I decided to automate the process using AI and the Threads and Bluesky APIs.
Gathering the postcards
The first step is to gather the postcards. BirdBuddy doesn’t really have a public API, but it uses GraphQL, and people have reverse-engineered the interface and created a Python library called pybirdbuddy
that can download the postcards from the Bird Buddy app.
The code below shows how to get the pictures from the postcards that were collected in the last 24 hours:
= BirdBuddy(BIRD_BUDDY_USER, BIRD_BUDDY_PASSWORD)
bb
# get the last day
= datetime.datetime.now() - datetime.timedelta(hours=24)
since = await bb.refresh_feed(since=since)
postcards
for card in postcards:
if card.get('__typename') == "FeedItemNewPostcard":
= await bb.sighting_from_postcard(card.get('id'))
sighting = sighting.report.sightings[0].species.name
species
= [{'id': item['id'],
media_items 'date_created': item['createdAt'],
'species': species,
'media_type': item['__typename'],
'image_url': item['contentUrl']}
for item in sigthing.medias
if item['__typename'] == 'MediaImage']
The downside is that this will download all the pictures, and many of the pictures are not very good. I want to filter them out automatically.
Is this picture any good?
To filter the pictures, I use the GPT-4o model with a simple prompt. Here’s the Python code that I use:
def good_bird(image_url):
= OpenAI(api_key=app_config.OPENAI_API_KEY)
openai_client = f"I want to post cute and interesting images of birds to social media.\n"
prompt += "Is this image such a picture? Reply 'Yes' if it is good, otherwise 'No'."
prompt
= openai_client.chat.completions.create(
response ="gpt-4o",
model=[
messages"role": "system", "content": "You are a photography critic and social media content creator"},
{"role": "user", "content": [{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": image_url}}]}],)
{
return 'yes' in response.choices[0].message.content.lower()
Running the list of media_items
through the good_bird
function, I can filter out the pictures that are not good enough to post.
I also use GPT-4o to generate a caption:
def caption(species, image_url):
= OpenAI(api_key=app_config.OPENAI_API_KEY)
openai_client
= f"Generate a caption for this {species} that was captured on a bird feeder camera.\n"
prompt += f"Do not assume the bird's gender.\n"
prompt += f"The caption will be used in a social media post and should be less than 200 characters.\n"
prompt += f"The caption should be suitable for a professional brand, although it can be funny.\n"
prompt += f"Do not use emojis or hashtags. Do not ask for engagement. Do not ask questions.\n"
prompt
= openai_client.chat.completions.create(
response ="gpt-4o",
model=[
messages"role": "system", "content": "You are a photographer and social media content creator"},
{"role": "user", "content": [{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": image_url}}]}],)
{
return response.choices[0].message.content
Now each image has a caption and a flag indicating if it’s good enough to post. In my app, I’m saving all the pictures to a database and posting a few of them every day, but you could post it directly from the code above.
Posting to Threads and Bluesky
To post the pictures, I use the Threads and Bluesky APIs.
The code below shows how to post a picture to Threads. Posting to Threads requires two api calls - one to an endpoint called threads
to create the post and upload the assets another to an endpoint called publish
to actually publish the post. You need a token and a user id to post to Threads, and you can obtain both from the Meta Developers website. Please note that the code below is a simplified version of the actual code, and you should handle errors and exceptions properly.
def post_to_threads(message, image):
= {
payload "access_token": THREADS_TOKEN,
"text": message,
"media_type": "IMAGE",
"image_url": image
}
= f"https://graph.threads.net/v1.0/{THREADS_USER_ID}/threads/"
post_url
= requests.post(post_url, json=payload)
response
if response.status_code == 200:
# get response id
= response.json()
response_json = response_json["id"]
creation_id
= {
publish_payload "access_token": THREADS_TOKEN,
"creation_id": creation_id
}
= f"https://graph.threads.net/v1.0/{app_config.THREADS_USER_ID}/threads_publish/"
publish_url
= requests.post(publish_url, json=publish_payload) response
Posting to Bluesky is similar, but a lot easier. You must have created an app password, your account password can’t be used with the API. The code below shows how to post to Bluesky. In order to post to Bluesky, you should install the package atproto
, which allows you to import a Bluesky Client
class.
def post_to_bluesky(message, image, image_alt_text):
= Client()
client
client.login(BSKY_USER, BSKY_APP_PWD)
= requests.get(image).content
image_data =image_data, image_alt=image_alt_text) client.send_image(message, image
Want to see the posts?
You can see my Bird Buddy posts on Threads and Bluesky. They are all tagged with #Birds
. I hope you enjoy them!
Comments