
很早之前就想做一套全自动的鸟类观测记录工具。最近终于腾出时间把家里的监控摄像头和 BirdNet-Go 组合到一起做成了一个“能听懂鸟叫”的自动识别系统。这套系统跑起来之后我不用再整天盯着窗外人工记录也能知道附近来过哪些鸟、大概什么时间出现、鸣声活跃程度如何。本文会完整记录这套自动鸟类识别系统的搭建过程包括方案选型、RTSP 音频流采集、BirdNet-Go 推理、结果落库和运行排错。如果你也想把闲置的监控摄像头变成生态观察工具或者想了解 BirdNet-Go 的落地用法这篇文章可以作为一个完整的参考。1. 项目背景与方案概述1.1 BirdNet 与 BirdNet-Go 是什么BirdNET 是一个基于深度学习的鸟类声音识别项目它能够从音频中检测出鸟类的鸣声并给出对应的物种名称和置信度。官方版本通常运行在 PC 或移动端 App 上适合本地录音后上传识别也能通过命令行离线分析。BirdNet-Go 则是 BirdNET 分析能力的 Go 语言移植实现。它的核心思路是加载 BirdNET 官方发布的 TFLite 模型文件在本地对音频片段做推理。相比需要在 Python 环境中安装大量依赖库的方案BirdNet-Go 编译后就是一个独立的可执行文件部署起来更方便CPU 占用也相对可控非常适合在树莓派、旧电脑或者小服务器上长期运行。这里需要区分两个概念BirdNET 是模型与算法生态的总称BirdNet-Go 是其中一个社区实现。两者在模型文件层面是兼容的但推理代码、命令行参数、依赖环境并不完全一样。本文使用的识别引擎是 BirdNet-Go音频采集部分用 FFmpeg 完成。1.2 为什么选择监控摄像头作为音频输入最开始考虑过单独购买 USB 麦克风但实际测试后发现存在几个明显问题第一USB 麦克风需要一台常开的电脑或树莓派靠近部署采集范围有限。第二户外麦克风要解决防水、供电、线缆布线问题成本并不低。第三已经投入使用的监控摄像头往往本身就带音频采集功能如果能复用摄像头的麦克风等于省掉了一套独立拾音硬件。当然并不是所有摄像头都支持音频。买摄像头时需要注意“内置麦克风”或“音频输入”参数有些枪机、半球机虽然支持音频但默认在网页端关闭了声音开关需要去后台把音频编码打开。所以这套系统的最终形态是监控摄像头负责环境拾音通过 RTSP 协议把音频流传给本地服务器FFmpeg 定时截取音频片段BirdNet-Go 负责识别再把结果写入数据库并生成日报。1.3 这套系统可以帮你做什么从应用角度来看搭建完成后可以实现自动记录某个时间段内出现过的鸟类种类。给每次识别结果打上时间戳和置信度便于筛选有效记录。长期积累后可以分析鸟类活动规律比如清晨活跃时段、不同季节的鸟种变化。通过 Webhook 把“发现目标鸟种”的消息推到手机或聊天工具。这套系统不依赖云服务所有音频处理和推理都在本地完成。即使断网只要摄像头和服务器在同一局域网内识别任务仍然可以正常运行。2. 系统架构与整体流程2.1 整体数据链路整个系统可以拆成四个环节先用一张简单的 ASCII 图说明数据流向监控摄像头内置麦克风 | | RTSP音频 视频流 v FFmpeg 拉流并截取音频片段 | | wav / flac 文件 v BirdNet-Go 音频推理 | | JSON 识别结果 v Python 脚本解析结果并写入 SQLite 数据库 | v 日报 / 消息通知从这张图可以看出链路比较短。摄像头只负责采集不需要摄像头本身具备 AI 能力所有智能分析都放在服务器侧完成。因此即使摄像头型号比较老只要支持 RTSP 和音频编码就能纳入这套系统。2.2 核心组成模块整套系统按职责可以拆成以下模块采集模块 负责从监控摄像头的 RTSP 地址拉流并按设定的时长截取音频片段。这里使用 FFmpeg因为它的协议兼容性最好可以处理 H.264/H.265 视频流中嵌套的音频数据。推理模块 负责把音频文件交给 BirdNet-Go 分析。BirdNet-Go 会输出识别到的物种名称、起止时间、置信度。推理过程是一次性吃掉整个音频文件还是切分处理取决于音频时长和模型窗口设置。数据模块 负责保存识别结果。考虑到这是个人项目没有采用 MySQL、PostgreSQL 这样偏重的数据库而是使用 SQLite 单文件存储。历史数据可以按天、按月归档也可以直接读取 sqlite 文件做统计分析。展示与通知模块 负责把每天的识别记录汇总成可读报表或者在识别到特定鸟种时发送通知。通知优先采用 Webhook 方式比如钉钉机器人、Server 酱、企业微信机器人。2.3 为什么把采集和推理分开我在最初设计时考虑过直接让 BirdNet-Go 消费 RTSP 流也就是“实时推流 实时识别”。但这样做有几个问题首先是音频流断流、花屏时不好排查没有中间文件可供复盘其次是模型推理速度跟不上时会丢片段第三是不方便事后校对“这段识别到底是谁的结果”。改成“先落盘、再识别”之后每个音频片段都对应一个独立文件清洗、重跑、分析都变得很容易。虽然多了一些磁盘开销但对于 10 到 30 秒的短音频占用空间完全可以接受。这个设计思路也可以扩展到其他音频识别项目中不局限于鸟类识别。3. 环境准备3.1 硬件条件由于整个推理过程在本地完成硬件的选择会直接影响识别延迟和并发能力。我的实验环境如下监控摄像头支持 RTSP 和内置麦克风音频编码为 AAC。边缘服务器一台 Ubuntu 22.04 的迷你主机4 核 CPU、16 GB 内存。存储系统盘 256 GB足够保存一段时间的音频切片和 SQLite 数据。BirdNet-Go 的推理并不需要 GPU。它在 CPU 上就能运行只是耗时会随 CPU 性能变化。对于个人或小型监测项目一个 4 核以上的 x86 平台就够用了。如果你打算用树莓派建议选择 4GB 内存以上的型号同时把音频窗口调小一些避免推理耗时超过采集周期。3.2 安装 FFmpegFFmpeg 是整个系统中的“搬运工”。在 Ubuntu / Debian 系统上直接使用 apt 安装即可sudo apt update sudo apt install ffmpeg安装完成后验证版本ffmpeg -versionFFmpeg 会附带 ffprobe 工具后面检查摄像头音频流时会用到。如果你使用的是 Windows 系统可以从 FFmpeg 官网下载可执行文件并把 bin 目录加入 PATH。macOS 用户推荐使用 Homebrew 安装brew install ffmpeg版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。3.3 获取 BirdNet-Go 与模型文件BirdNet-Go 是一个开源项目获取方式主要有两种从 GitHub Releases 页面下载对应平台的可执行文件。克隆源码后自行编译。如果你本地已经安装了 Go 环境也可以直接基于源码构建。模型文件需要单独下载。BirdNet-Go 兼容 BirdNET 官方发布的模型和标签文件。以 BirdNET 的 V2.4 模型为例通常会用到下面两个关键文件模型文件BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite标签文件BirdNET_GLOBAL_6K_V2.4_Labels.txt建议从 BirdNET 官方 GitHub 仓库的 Releases 页面下载这两个文件并放到 BirdNet-Go 的模型目录中。下载完模型后BirdNet-Go 的目录结构可以设计成birdnet-go/ ├── birdnet-go # 可执行文件 ├── models/ │ ├── BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite │ └── BirdNET_GLOBAL_6K_V2.4_Labels.txt ├── input/ # 待识别音频 └── output/ # 识别结果 JSON需要提醒的是不同版本 BirdNet-Go 对模型文件路径的指定方式不同。有的版本通过命令行参数--model和--labels指定有的版本默认读取当前目录下的models文件夹。所以拿到压缩包后可以先看 README 中的参数说明避免路径不对导致启动报错。3.4 Python 运行环境为了把识别结果落库、生成日报我使用 Python 3 编写胶水脚本。脚本只依赖 Python 标准库不涉及第三方包所以不需要额外创建虚拟环境。主要用到subprocess调用 ffmpeg 和 birdnet-go。sqlite3写入和查询识别记录。json解析 BirdNet-Go 的输出。time / datetime处理时间戳和定时逻辑。pathlib管理音频文件和结果目录。本文后续的代码都基于这个思路编写。4. 从摄像头获取音频流4.1 确认摄像头音频能力摄像头要进入这套系统前提是必须能输出音频。可以通过 ffprobe 查看 RTSP 流中的媒体信息。假设摄像头 RTSP 地址为rtsp://username:password192.168.1.100:554/Streaming/Channels/101执行ffprobe -rtsp_transport tcp -i rtsp://username:password192.168.1.100:554/Streaming/Channels/101 -show_streams输出中会列出多个流。关注codec_type字段。如果看到类似以下信息说明视频流中确实包含音频codec_typeaudio codec_nameaac sample_rate16000 channels1这里有几个坑需要特别注意。一个是摄像头的 RTSP 地址不一定只有一个端口。很多网络摄像头为了兼容不同手机 App会同时提供主码流和子码流音频通常存在于子码流或主码流中。如果Channels/101没有音频可以试试Channels/201或者摄像头管理后台中的“音频码流”相关选项。另一个是音频采样率。BirdNET 模型训练时使用的采样率通常是 16 kHz 或 48 kHz 的单声道音频。如果摄像头输出的是 8 kHz 采样率识别效果会明显下降。此时需要由 FFmpeg 在截取时强制重采样到 16 kHz。4.2 先做一次人工听音测试在自动采集之前我建议先手动拉取一段 10 秒的音频听一下确认摄像头拾音范围是否覆盖到可能出现鸟鸣的区域。这一步能帮你判断麦克风的位置和灵敏度。拉取一段 10 秒音频转成 wav 格式ffmpeg -rtsp_transport tcp -i rtsp://username:password192.168.1.100:554/Streaming/Channels/101 -t 10 -vn -acodec pcm_s16le -ar 16000 -ac 1 test_10s.wav命令参数说明-rtsp_transport tcp强制使用 TCP 传输。UDP 在弱网下容易丢包音频会出现杂音。-t 10只采集 10 秒。-vn丢弃视频流只保留音频。-acodec pcm_s16le输出为 PCM 16 位小端格式的 wav。-ar 16000强制采样率为 16 kHz。-ac 1强制单声道。如果生成的 wav 文件可以正常播放并且能听到环境声说明音频通路已经打通可以进入识别阶段。4.3 定时截取音频片段的通用命令实际运行中我们不可能一直把摄像头音频录下来。通常的做法是每隔固定时间截取一段 10 到 30 秒的音频。以每 5 分钟分析一次、每次分析 30 秒为例核心命令如下ffmpeg -rtsp_transport tcp -i rtsp://username:password192.168.1.100:554/Streaming/Channels/101 -t 30 -vn -acodec pcm_s16le -ar 16000 -ac 1 -y capture_20250101_080000.wav这里每次调用 ffmpeg 都会重新连接摄像头一次。优点是逻辑简单哪怕某一次摄像头掉线也只是那一次捕获失败不会影响下一次。缺点是有连接延迟一般在一秒以内对于非实时监测场景可以忽略。如果你的摄像头并发连接数有限并且需要更高的采集频率可以考虑让 FFmpeg 常驻运行把音频切成小文件后循环覆盖。但这会引入更复杂的状态管理对本文这种入门部署来说没必要。5. 使用 BirdNet-Go 进行识别5.1 命令行格式与前置准备BirdNet-Go 的用法在不同版本中略有差异。以常见模式为例其调用逻辑类似birdnet-go -i input.wav -o output.json也有部分版本支持如下格式birdnet-go analyze --input input.wav --output output.json具体参数需要以项目 README 为准。遇到不确定时可以先执行不带参数的birdnet-go让程序输出帮助信息。在实际操作中我建议把模型文件、标签文件、音频文件和输出目录都指定完整不依赖默认路径。这样后续写脚本时即使更换服务器只要修改配置常量即可。5.2 单文件识别示例这里先演示一次最简单的单文件识别。假设语音文件是/data/audio/20250101_080000.wav执行./birdnet-go \ -model ./models/BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite \ -labels ./models/BirdNET_GLOBAL_6K_V2.4_Labels.txt \ -i /data/audio/20250101_080000.wav \ -o /data/output/20250101_080000.json命令执行后如果模型加载正常终端会输出类似“processing complete”的日志。随后检查输出 JSON 文件是否已经生成。由于 BirdNet-Go 使用的是 TFLite 模型它对 CPU 指令集有一定要求。个别老 CPU 在加载 FP16 模型时可能报错这时可以换成 FP32 版本模型或者在编译 BirdNet-Go 时开启对应的 TFLite 后端支持。5.3 输出内容解读BirdNet-Go 的输出 JSON 通常会包含多条识别结果每一条对应一个时间窗口内的一个候选物种。大多数版本包含以下关键字段物种名称或代码例如common_name。置信度例如confidence或score。起止时间例如start_time和end_time。音频文件的关联信息。下面用一个结构示例说明字段名随版本变化[ { start_time: 0.0, end_time: 3.0, common_name: 大山雀, scientific_name: Parus cinereus, confidence: 0.82 }, { start_time: 5.0, end_time: 8.0, common_name: 未知, scientific_name: , confidence: 0.12 } ]从结果可以看出系统把一个 30 秒的音频片段切成了多个 3 秒分析窗口并分别预测出最可能的鸟种。我们需要保留置信度较高的记录过滤掉“未知”和低置信度结果这样才能减少误报。6. 实现自动识别任务6.1 总体代码结构环境准备好后就可以写自动识别脚本了。脚本的核心逻辑如下计算当前时间生成音频文件名。使用 FFmpeg 截取最近 30 秒音频。调用 BirdNet-Go 生成 JSON。读取 JSON筛选置信度大于阈值的记录。写入 SQLite 数据库。删除超过保留期限的音频文件避免磁盘写满。下面用一个 Python 脚本实现完整流程。这个脚本可以直接在服务器上后台运行也可以配置到 crontab 中循环执行。6.2 Python 核心脚本创建文件/opt/birdmonitor/monitor.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- 监控摄像头 BirdNet-Go 自动鸟类识别脚本 定时从 RTSP 流截取音频调用 BirdNet-Go 识别并写入 SQLite。 import json import sqlite3 import subprocess import time from datetime import datetime, timedelta from pathlib import Path # 配置区域 RTSP_URL rtsp://username:password192.168.1.100:554/Streaming/Channels/101 AUDIO_DIR Path(/data/audio) OUTPUT_DIR Path(/data/output) DB_PATH /data/birds.sqlite BIRDNET_GO /data/birdnet-go/birdnet-go MODEL_PATH /data/birdnet-go/models/BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite LABELS_PATH /data/birdnet-go/models/BirdNET_GLOBAL_6K_V2.4_Labels.txt CLIP_SECONDS 30 # 每次采集音频长度 SAMPLE_RATE 16000 # 采样率 CONFIDENCE_THRESHOLD 0.5 # 置信度阈值 RETAIN_DAYS 7 # 音频文件保留天数 # def create_table(db_path: str) - None: 初始化 SQLite 数据表。 conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS bird_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, detected_time TEXT NOT NULL, common_name TEXT NOT NULL, scientific_name TEXT, confidence REAL NOT NULL, created_at TEXT NOT NULL ) ) cursor.execute( CREATE INDEX IF NOT EXISTS idx_bird_records_time ON bird_records(detected_time) ) conn.commit() conn.close() def extract_audio(filename: str) - bool: 从 RTSP 流截取音频片段转成 16kHz 单声道 wav。 cmd [ ffmpeg, -rtsp_transport, tcp, -i, RTSP_URL, -t, str(CLIP_SECONDS), -vn, -acodec, pcm_s16le, -ar, str(SAMPLE_RATE), -ac, 1, -y, str(filename) ] result subprocess.run(cmd, stdoutsubprocess.PIPE, stderrsubprocess.PIPE) return result.returncode 0 def run_birdnet(audio_file: Path, output_file: Path) - bool: 调用 BirdNet-Go 识别音频文件生成 JSON 结果。 cmd [ BIRDNET_GO, -model, str(MODEL_PATH), -labels, str(LABELS_PATH), -i, str(audio_file), -o, str(output_file) ] result subprocess.run(cmd, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue) if result.returncode ! 0: print(f[ERROR] BirdNet-Go 执行失败: {result.stderr}) return False return True def parse_results(output_file: Path): 读取 BirdNet-Go 输出 JSON返回过录后的识别记录列表。 with open(output_file, r, encodingutf-8) as f: data json.load(f) records [] if isinstance(data, list): # 兼容 list 格式 for item in data: if not isinstance(item, dict): continue confidence float(item.get(confidence, item.get(score, 0))) common_name item.get(common_name, item.get(species, unknown)) scientific_name item.get(scientific_name, ) if confidence CONFIDENCE_THRESHOLD and common_name.lower() ! unknown: records.append({ common_name: common_name, scientific_name: scientific_name, confidence: confidence }) elif isinstance(data, dict): # 兼容 dict 格式 for key, item in data.items(): if not isinstance(item, dict): continue confidence float(item.get(confidence, item.get(score, 0))) common_name item.get(common_name, item.get(species, unknown)) scientific_name item.get(scientific_name, ) if confidence CONFIDENCE_THRESHOLD and common_name.lower() ! unknown: records.append({ common_name: common_name, scientific_name: scientific_name, confidence: confidence }) return records def save_to_db(records, audio_filename: str, detected_time: str) - None: 将识别记录写入 SQLite。 conn sqlite3.connect(DB_PATH) cursor conn.cursor() for record in records: cursor.execute( INSERT INTO bird_records (filename, detected_time, common_name, scientific_name, confidence, created_at) VALUES (?, ?, ?, ?, ?, ?) , ( audio_filename, detected_time, record[common_name], record[scientific_name], record[confidence], datetime.now().strftime(%Y-%m-%d %H:%M:%S) ) ) conn.commit() conn.close() def clean_old_audio() - None: 删除超出保留时间的音频文件。 deadline datetime.now() - timedelta(daysRETAIN_DAYS) for audio_file in AUDIO_DIR.glob(*.wav): file_time datetime.fromtimestamp(audio_file.stat().st_mtime) if file_time deadline: audio_file.unlink(missing_okTrue) print(f[CLEAN] 删除过期音频: {audio_file.name}) def main() - None: AUDIO_DIR.mkdir(parentsTrue, exist_okTrue) OUTPUT_DIR.mkdir(parentsTrue, exist_okTrue) create_table(DB_PATH) while True: now_str datetime.now().strftime(%Y%m%d_%H%M%S) audio_file AUDIO_DIR / fcapture_{now_str}.wav output_file OUTPUT_DIR / fresult_{now_str}.json print(f[INFO] {now_str} 开始采集音频...) if not extract_audio(str(audio_file)): print([ERROR] FFmpeg 拉流失败等待下一轮...) time.sleep(10) continue if not run_birdnet(audio_file, output_file): print([ERROR] 识别失败等待下一轮...) time.sleep(10) continue records parse_results(output_file) if records: save_to_db(records, audio_file.name, now_str) print(f[INFO] 本轮识别到 {len(records)} 条有效记录) clean_old_audio() # 每隔 5 分钟采集一次实际可按需调整 time.sleep(300) if __name__ __main__: main()这段代码有几个设计点值得说明使用while True加上time.sleep(300)循环而不是依赖 crontab。原因是每次任务需要处理“上一步失败下一步也能自动重试”的状态写成循环更可控。parse_results同时兼容了 JSON 数组和 JSON 对象两种常见输出格式。虽然 BirdNet-Go 不同版本输出结构不完全一样但通过兼容处理可以减少报错。音频文件保留 7 天后自动清理避免长期运行把磁盘写满。置信度阈值写在配置区域方便后续根据实际场景调整。如果发现很多正确鸟种被过滤掉可以降低到 0.4如果误报太多可以提高到 0.6。6.3 运行与验证保存脚本后先不要直接进入死循环建议临时修改CLIP_SECONDS 10并用短任务验证一次。在/opt/birdmonitor下执行python3 monitor.py如果一切正常终端会输出类似下面的日志[INFO] 20250101_080000 开始采集音频... [INFO] 本轮识别到 3 条有效记录随后检查数据库sqlite3 /data/birds.sqlite执行SELECT id, detected_time, common_name, confidence FROM bird_records ORDER BY id DESC LIMIT 10;如果看到置信度超过阈值的鸟种记录说明整套链路已经打通。确认运行没问题后可以用nohup或 systemd 把脚本放到后台长期运行。例如使用 systemd 服务[Unit] DescriptionBird Monitor Service Afternetwork.target [Service] ExecStart/usr/bin/python3 /opt/birdmonitor/monitor.py Restartalways Userbirdmon [Install] WantedBymulti-user.target将文件保存为/etc/systemd/system/birdmonitor.service然后执行sudo systemctl daemon-reload sudo systemctl enable birdmonitor sudo systemctl start birdmonitor使用 systemd 而不是裸nohup的好处是服务异常退出后可以被自动拉起而且开机后会自动启动。7. 结果统计与消息通知自动识别链路跑通后下一步就是让结果“能用”。这里介绍两个增强功能一个是查看日报一个是把关键识别结果推送到手机。7.1 生成每日鸟种汇总在积累几小时数据后可以通过 SQL 统计当天出现的鸟种。下面这条 SQL 可以直接在 sqlite3 中执行SELECT date(detected_time) AS day, common_name, COUNT(*) AS cnt, MAX(confidence) AS max_conf FROM bird_records WHERE day date(now, localtime) GROUP BY day, common_name ORDER BY cnt DESC;如果希望把统计结果写入文件可以在 Python 中封装一个查询函数。这里只展示 SQL 部分核心是用GROUP BY按鸟种聚合用MAX(confidence)查看该鸟种当天最高置信度方便判断记录是否可靠。7.2 通过 Webhook 推送通知如果只记录不通知那就有点像“闷声观察”。实际项目中识别到稀有鸟种后及时通知会更有价值。这里以钉钉机器人或自定义 Webhook 为例。当 Python 脚本从 BirdNet-Go 结果中解析到目标鸟种时可以把结果通过 HTTP POST 发送给 Webhook 地址。代码片段如下import urllib.request WEBHOOK_URL https://your-webhook-url def send_notification(common_name: str, confidence: float, detected_time: str): payload { msgtype: text, text: { content: f发现鸟类{common_name}\n置信度{confidence:.2f}\n时间{detected_time} } } data json.dumps(payload).encode(utf-8) req urllib.request.Request(WEBHOOK_URL, datadata, headers{Content-Type: application/json}) try: urllib.request.urlopen(req, timeout10) except Exception as e: print(f[WARN] 通知发送失败: {e})在save_to_db之前调用send_notification只对置信度高于阈值、且鸟种符合预设名单的记录发送。注意 Webhook 地址属于敏感信息不要把真实地址提交到 Git建议通过环境变量或单独的配置文件读取。8. 常见问题与排查思路实际落地过程中最耗费时间的往往不是识别算法本身而是音频采集链路和安装环境中的各种小问题。下面把典型问题整理成表格。8.1 问题排查表问题现象常见原因解决思路ffprobe 看不到音频流摄像头本身没有麦克风或后台未开启音频检查摄像头型号参数并在后台开启音频编码FFmpeg 拉流报错 401RTSP 用户名密码错误或权限不足核对账户密码确认该账户有视频/音频查看权限拉流断断续续网络不稳定UDP 丢包添加-rtsp_transport tcp参数生成的 wav 文件没有声音音频流采样率低或通道不匹配强制-ar 16000 -ac 1并人工听音确认BirdNet-Go 启动失败模型文件路径不对或模型版本不兼容检查模型文件路径对照 README 确认版本识别结果都是 unknown音频质量差置信度阈值太高先人工听音频再降低阈值到 0.3 观察每次采集周期执行太久推理耗时超过 sleep 间隔调大 sleep 间隔或者缩短录音时长CPU 占用长时间 100%单核运行模型推理优化采集周期关闭其他后台进程数据库记录很多重复同一鸟声被多个时间窗口识别到接受重复并做去重统计或按时间窗口分组摄像头并发连接已达上限同时有 App 和 RTSP 拉流使用子码流减少同时取流的设备数量8.2 音频推断无有效结果的排查步骤如果你发现 BirdNet-Go 能正常运行但始终没有有效记录建议按下面的顺序排查用ffprobe查看原始音频流参数确认 codec_type、sample_rate、channels。使用同一个音频文件跑一次人工识别用播放器听音频里是否真的有鸟鸣。把 BirdNet-Go 识别的 JSON 原始输出打出来看是否存在 species、score 字段。临时把置信度阈值降到 0.1确认低置信度记录是否出现。如果还是空的多半是模型没有正确加载或音频格式不对。尝试换一小段 BirdNET 官方示例音频验证工具链确认是环境问题还是拾音问题。这套排查顺序的核心是“由外到内”先确认输入音频正确再检查模型最后检查脚本解析逻辑。9. 工程化部署建议几小时跑通脚本只是第一步要让系统稳定运行几个星期甚至一个繁殖季还需要关注几个工程细节。9.1 音频采集策略要匹配目标场景鸟类活动有明显的昼夜节律清晨和傍晚通常是鸣声高峰中午相对安静。如果 CPU 资源有限可以在配置中添加一个时间段判断只在早上 5 点到晚上 20 点之间运行采集任务。这样既能覆盖绝大多数鸟种的活动时间也能避免无用计算。采集间隔也需要观察后调整。间隔太短会积累大量重复数据间隔太长容易错过短促的鸟鸣。以 30 秒音频片段为例5 分钟间隔比较适合日常监测如果想重点捕捉求偶期鸣声可以缩短到 1 分钟。9.2 模型推理资源要预留余量BirdNet-Go 进程在启动时需要加载 TFLite 模型加载完成后内存占用会稳定在一个区间。建议在正式部署前观察两个指标峰值内存和平均推理耗时。如果单条 30 秒音频推理耗时超过 30 秒说明设备性能不足以支持实时处理这时应把采集间隔拉长或者缩短音频片段。不要试图同时运行多个 BirdNet-Go 实例来“并发加速”在低端设备上这样做反而会造成 CPU 争抢和频繁换页。9.3 数据表与文件命名要方便回溯BirdNet-Go 分析的结果是否准确有时候需要回到原始音频去复核。所以数据库中的每条记录都应该保留音频文件名且音频文件命名必须包含可读时间信息。例如capture_20250101_080000.wav就比audio_001.wav更有价值。音频文件建议也按日期归档例如/data/audio/2025/01/01/capture_20250101_080000.wav这样回看某一天的数据时不需要跨目录翻找也可以配合备份任务把特定目录上传到 NAS。9.4 避免把 Webhook 明文写入代码通知功能虽然好用但 Webhook 一旦泄露就可能被别人恶意调用。建议把 Webhook 地址、RTSP 密码等敏感配置放在环境变量或独立配置文件中比如/opt/birdmonitor/config.json并在代码中单独读取。示例配置文件{ rtsp_url: rtsp://username:password192.168.1.100:554/Streaming/Channels/101, webhook_url: https://your-webhook-url }Python 脚本读取时import json with open(/opt/birdmonitor/config.json, r) as f: config json.load(f) RTSP_URL config[rtsp_url] WEBHOOK_URL config[webhook_url]这样做有一个额外好处下次更换摄像头或密码时不需要改代码只需要修改配置文件。9.5 留好“假阳性”复盘路径任何自动识别系统都存在误报风险。鸟种识别中常见的假阳性来源包括虫鸣、蛙鸣、树叶摩擦声、车辆喇叭声甚至同一种鸟在远处发出的模糊叫声。不要指望模型输出 100% 准确。建议长期保留置信度在 0.3 到 0.6 之间的“灰色记录”并定期人工复核。积累一定量后你会发现某些鸟种在这个置信度区间内基本全是误报下次就可以直接丢弃某些鸟种则相反真实个体经常被低置信度标记。有了自己的复盘数据阈值调整就不再是拍脑袋。9.6 提醒部署过程中注意权限与合规在部署这套系统时还有几点需要留意。摄像头 RTSP 地址涉及监控权限建议使用最小权限账号只授予该摄像头视频/音频流的查看权限。不要把 RTSP 地址、密码上传到公开代码仓库或博客评论区。同时如果摄像头采集范围内涉及邻居、行人等隐私主体应当提前确认当地对个人区域录音录像的合规要求评估后再长期部署。数据库操作方面任何清理任务在执行前都应先做好数据备份。尤其是删除音频文件、清空 bird_records 表这类操作动手前先确认备份完整避免误删重要观测记录。10. 总结从零开始搭建这套监控摄像头与 BirdNet-Go 结合的系统我个人收获最大的一点是AI 识别并不是整个项目中难度最高的部分音频采集稳定性、数据记录规范和结果复盘才决定了系统能不能长期跑下去。文章写到这里你已经了解了完整的搭建路径用监控摄像头 RTSP 拉流、用 FFmpeg 抽取并重采样音频、用 BirdNet-Go 做鸟类鸣声推理、用 Python 把结果写入 SQLite、再用 Webhook 和统计查询让结果产生价值。如果你手边正好有一台支持音频的监控摄像头不妨先按照第 4 节的方法拉一段环境音频听一听再用一个真实录音文件测试 BirdNet-Go。等到链路跑通后再逐步增加自动采集、消息通知和长期数据库。这套思路不只适用于鸟类也可以迁移到蛙类、蝉鸣甚至其他鸣声动物的生态监测中。动手永远是最好的学习方式。你可以先试着让脚本每小时运行一次积累 24 小时数据然后看看你的摄像头附近到底住着多少种“邻居”。