Commit: 0f71a91
Parent: 9ec5979

Locales

Mårten Åsberg committed on 2026-05-24 at 07:35
scripts/download-models.ts +53 -6
diff --git a/scripts/download-models.ts b/scripts/download-models.ts
index 69159e8..1719f91 100644
@@ -1,6 +1,34 @@
const BASE_URL =
"https://birdnet-team.github.io/real-time-pwa/models/birdnet";
const BASE_URL = "https://github.com/birdnet-team/real-time-pwa/raw/refs/heads/main/public/models/birdnet";
const OUT_DIR = "static/models/birdnet";
const locales = [
"af",
"ar",
"cs",
"da",
"de",
"en_uk",
"en_us",
"es",
"fi",
"fr",
"hu",
"it",
"ja",
"ko",
"nl",
"no",
"pl",
"pt",
"ro",
"ru",
"sk",
"sl",
"sv",
"th",
"tr",
"uk",
"zh",
];
async function download(url: string, dest: string) {
console.log(`Downloading ${url}`);
@@ -24,9 +52,28 @@ for (const shard of shards) {
await download(`${BASE_URL}/${shard}`, `${OUT_DIR}/${shard}`);
}
await download(
`${BASE_URL}/labels/en_us.txt`,
`${OUT_DIR}/labels/en_us.txt`,
);
await Deno.mkdir(`${OUT_DIR}/labels`, { recursive: true });
for (const locale of locales) {
const url = `${BASE_URL}/labels/${locale}.txt`;
console.log(`Downloading labels: ${url}`);
const text = await fetch(url).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status} for ${url}`);
return r.text();
});
const labels = text
.split("\n")
.filter(Boolean)
.map((line) => {
const idx = line.indexOf("_");
return {
scientificName: idx >= 0 ? line.slice(0, idx) : line,
commonName: idx >= 0 ? line.slice(idx + 1) : line,
};
});
await Deno.writeTextFile(
`${OUT_DIR}/labels/${locale}.json`,
JSON.stringify(labels),
);
}
console.log("Done. Model saved to", OUT_DIR);
src/lib/birdnet.worker.ts +1 -24
diff --git a/src/lib/birdnet.worker.ts b/src/lib/birdnet.worker.ts
index 9a55e1c..93abee4 100644
@@ -2,17 +2,10 @@ import * as tf from "@tensorflow/tfjs";
import type { Prediction, WorkerInMessage, WorkerOutMessage } from "./types";
const MODEL_PATH = "/models/birdnet/model.json";
const LABELS_PATH = "/models/birdnet/labels/en_us.txt";
const WINDOW_SAMPLES = 144000;
const ALPHA = 5.0;
interface BirdLabel {
scientificName: string;
commonName: string;
}
let model: tf.LayersModel | null = null;
let labels: BirdLabel[] = [];
// ── Custom MelSpecLayerSimple ──────────────────────────────────────────────
// Ported from https://github.com/birdnet-team/real-time-pwa (MIT)
@@ -193,18 +186,6 @@ async function init() {
registerStftKernel();
tf.serialization.registerClass(MelSpecLayerSimple);
const labelsText = await fetch(LABELS_PATH).then((r) => r.text());
labels = labelsText
.split("\n")
.filter(Boolean)
.map((line) => {
const idx = line.indexOf("_");
return {
scientificName: idx >= 0 ? line.slice(0, idx) : line,
commonName: idx >= 0 ? line.slice(idx + 1) : line,
};
});
model = await tf.loadLayersModel(MODEL_PATH);
console.log(
"BirdNET model inputs:",
@@ -244,11 +225,7 @@ async function analyze(samples: Float32Array) {
const pooled = sumsExp.map((s) => Math.log(s) / ALPHA);
const predictions: Prediction[] = pooled
.map((confidence, i) => ({
confidence,
commonName: labels[i]?.commonName ?? `Species ${i}`,
scientificName: labels[i]?.scientificName ?? "",
}))
.map((confidence, labelIndex) => ({ confidence, labelIndex }))
.filter((p) => p.confidence > 0.1)
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 10);
src/lib/history.ts +5 -8
diff --git a/src/lib/history.ts b/src/lib/history.ts
index c200487..b2d02f1 100644
@@ -21,7 +21,7 @@ store.subscribe((entries) => {
}
});
let sessionSeen = new Set<string>();
let sessionSeen = new Set<number>();
export function startSession() {
sessionSeen = new Set();
@@ -31,16 +31,13 @@ export function endSession() {
sessionSeen = new Set();
}
export function addDetection(
p: Pick<Prediction, "commonName" | "scientificName" | "confidence">,
) {
export function addDetection(p: Pick<Prediction, "labelIndex" | "confidence">) {
if (p.confidence < 0.7) return;
if (sessionSeen.has(p.scientificName)) return;
sessionSeen.add(p.scientificName);
if (sessionSeen.has(p.labelIndex)) return;
sessionSeen.add(p.labelIndex);
store.update((entries) => [
{
commonName: p.commonName,
scientificName: p.scientificName,
labelIndex: p.labelIndex,
confidence: p.confidence,
detectedAt: new Date().toISOString(),
},
src/lib/labels.ts +69 -0
diff --git a/src/lib/labels.ts b/src/lib/labels.ts
new file mode 100644
index 0000000..b02b679
@@ -0,0 +1,69 @@
import { readable } from "svelte/store";
import type { BirdLabel } from "./types";
const supportedLanguages = [
"af",
"ar",
"cs",
"da",
"de",
"en_uk",
"en_us",
"es",
"fi",
"fr",
"hu",
"it",
"ja",
"ko",
"nl",
"no",
"pl",
"pt",
"ro",
"ru",
"sk",
"sl",
"sv",
"th",
"tr",
"uk",
"zh",
] as const;
function resolveLocale(): Array<(typeof supportedLanguages)[number]> {
if (typeof navigator === "undefined") return ["en_us"];
const navigatorLanguages = navigator.languages.slice(1).map((l) =>
l.replace("-", "_").toLowerCase()
);
const foundLanguages = navigatorLanguages.flatMap(
(n) => supportedLanguages.find((s) => s.startsWith(n)) ?? [],
);
if (!foundLanguages.includes("en_us")) {
foundLanguages.push("en_us");
}
return foundLanguages;
}
async function fetchLabels(
locale: (typeof supportedLanguages)[number],
): Promise<BirdLabel[]> {
const res = await fetch(`/models/birdnet/labels/${locale}.json`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<BirdLabel[]>;
}
export const labelsStore = readable<BirdLabel[]>([], (set) => {
(async () => {
const locales = resolveLocale();
for (const locale of locales) {
try {
const labels = await fetchLabels(locale);
set(labels);
return;
} catch (e) {
console.warn(e);
}
}
})();
});
src/lib/types.ts +7 -4
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 5fa4b15..1c40f4e 100644
@@ -1,12 +1,15 @@
export interface Prediction {
commonName: string;
export interface BirdLabel {
scientificName: string;
commonName: string;
}
export interface Prediction {
labelIndex: number;
confidence: number;
}
export interface HistoryEntry {
commonName: string;
scientificName: string;
labelIndex: number;
confidence: number;
detectedAt: string; // ISO 8601
}
src/routes/+page.svelte +3 -2
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index d8d2663..170e887 100644
@@ -2,6 +2,7 @@
import BirdNetWorker from "$lib/birdnet.worker.ts?worker";
import type { Prediction, WorkerOutMessage } from "$lib/types";
import { addDetection, endSession, startSession } from "$lib/history";
import { labelsStore } from "$lib/labels";
const WINDOW_SAMPLES = 144000;
@@ -108,9 +109,9 @@
{#if predictions.length > 0}
<ol>
{#each predictions as p (p.scientificName)}
{#each predictions as p (p.labelIndex)}
<li style:--pct={p.confidence * 100}>
<span>{p.commonName}</span>
<span>{$labelsStore[p.labelIndex]?.commonName ?? "Unknown"}</span>
<span>{(p.confidence * 100).toFixed(1)}%</span>
</li>
{/each}
src/routes/history/+page.svelte +5 -2
diff --git a/src/routes/history/+page.svelte b/src/routes/history/+page.svelte
index a8d433e..903f951 100644
@@ -1,5 +1,6 @@
<script lang="ts">
import { historyStore } from "$lib/history";
import { labelsStore } from "$lib/labels";
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, {
@@ -21,9 +22,11 @@
<p class="empty">No birds detected yet.</p>
{:else}
<ol>
{#each $historyStore as entry (entry.detectedAt + entry.scientificName)}
{#each $historyStore as entry (entry.detectedAt + entry.labelIndex)}
<li>
<span class="name">{entry.commonName}</span>
<span class="name">{
$labelsStore[entry.labelIndex]?.commonName ?? "Unknown"
}</span>
<span class="when">
<span>{formatDate(entry.detectedAt)}</span>
<span>{formatTime(entry.detectedAt)}</span>